Compare commits
51
Commits
2026.8.200
...
2026.9.700
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a1eccf90f4 | ||
|
|
da9091fa38 | ||
|
|
439c700226 | ||
|
|
fa13531668 | ||
|
|
4f11ed7ecf | ||
|
|
c1355970bf | ||
|
|
3167c18bea | ||
|
|
0666a87d15 | ||
|
|
ca7c23a0f6 | ||
|
|
5d23df8da7 | ||
|
|
de310a6604 | ||
|
|
abf32a5e50 | ||
|
|
aa0780dd51 | ||
|
|
b2656599b2 | ||
|
|
01e8670cc1 | ||
|
|
b2e6e9d250 | ||
|
|
d9d619c1dc | ||
|
|
b6228503fc | ||
|
|
4de05caac7 | ||
|
|
5c3ebc4c82 | ||
|
|
5e5c7dcc42 | ||
|
|
84821d2852 | ||
|
|
547da644ce | ||
|
|
757b885f28 | ||
|
|
3762a01d40 | ||
|
|
91b86f29cb | ||
|
|
f88a50d4ab | ||
|
|
c28836933c | ||
|
|
91c8c4aff3 | ||
|
|
d8611d7aea | ||
|
|
7e9a4359cd | ||
|
|
b7f28ab1a3 | ||
|
|
9b368ba761 | ||
|
|
82e13cf7ce | ||
|
|
d104906508 | ||
|
|
a5ff3b55ff | ||
|
|
26225eb8ed | ||
|
|
844138b383 | ||
|
|
d7eb72f417 | ||
|
|
af5acfc946 | ||
|
|
20f76b6a20 | ||
|
|
081b488fe3 | ||
|
|
b37eea43d9 | ||
|
|
89add18273 | ||
|
|
e47c3bda69 | ||
|
|
41d117fe2c | ||
|
|
53ebc5d055 | ||
|
|
a0a0d38fe5 | ||
|
|
52b81b3dcf | ||
|
|
c3d5116885 | ||
|
|
07ccd2d26a |
+2
-1
@@ -9,4 +9,5 @@ __pycache__
|
||||
.nox
|
||||
*.g4
|
||||
.antlr
|
||||
.claude
|
||||
.claude
|
||||
/test/postprocessor/
|
||||
|
||||
Vendored
+80
-2
@@ -13,10 +13,23 @@
|
||||
"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"
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/test/postprocessor"
|
||||
],
|
||||
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||
"linux": {
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/test/postprocessor"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||
@@ -26,6 +39,48 @@
|
||||
"autoAttachChildProcesses": true,
|
||||
"preLaunchTask": "NX Post Support: Compile Debug"
|
||||
},
|
||||
{
|
||||
// Linux workaround: js-debug's extensionHost attach probes localhost and [::1]
|
||||
// in parallel and aborts when [::1] is refused. Start the dev host ourselves
|
||||
// with a fixed inspector port and attach directly to 127.0.0.1.
|
||||
"name": "Run Extension (Linux)",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"address": "127.0.0.1",
|
||||
"port": 9333,
|
||||
"timeout": 30000,
|
||||
"continueOnAttach": true,
|
||||
"sourceMaps": true,
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"preLaunchTask": "NX Post Support: Start Dev Host (Linux)"
|
||||
},
|
||||
{
|
||||
"name": "Debug Extension (Linux, hidden)",
|
||||
"type": "node",
|
||||
"request": "attach",
|
||||
"address": "127.0.0.1",
|
||||
"port": 9333,
|
||||
"timeout": 30000,
|
||||
"continueOnAttach": true,
|
||||
"sourceMaps": true,
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"!**/node_modules/**"
|
||||
],
|
||||
"skipFiles": ["<node_internals>/**"],
|
||||
"preLaunchTask": "NX Post Support: Start Dev Host with debugpy (Linux)",
|
||||
"presentation": {
|
||||
"hidden": true,
|
||||
"group": "",
|
||||
"order": 4
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Python Attach",
|
||||
"type": "debugpy",
|
||||
@@ -46,10 +101,23 @@
|
||||
"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"
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/test/postprocessor"
|
||||
],
|
||||
"cwd": "${env:TEMP}/nx-post-support-vscode-debug",
|
||||
"outFiles": ["${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js"],
|
||||
"linux": {
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}",
|
||||
"${workspaceFolder}",
|
||||
"${workspaceFolder}/test/postprocessor"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
|
||||
"resolveSourceMapLocations": [
|
||||
"${workspaceFolder}/dist/**/*.js",
|
||||
"!**/node_modules/**"
|
||||
]
|
||||
},
|
||||
"sourceMaps": true,
|
||||
"resolveSourceMapLocations": [
|
||||
"${env:TEMP}/nx-post-support-vscode-debug/dist/**/*.js",
|
||||
@@ -92,6 +160,16 @@
|
||||
"group": "",
|
||||
"order": 1
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Debug Extension and Python (Linux)",
|
||||
"configurations": ["Python debug server (hidden)", "Debug Extension (Linux, hidden)"],
|
||||
"stopAll": true,
|
||||
"presentation": {
|
||||
"hidden": false,
|
||||
"group": "",
|
||||
"order": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
Vendored
+48
@@ -15,12 +15,60 @@
|
||||
"${workspaceFolder}",
|
||||
"${env:TEMP}\\nx-post-support-vscode-debug"
|
||||
],
|
||||
"linux": {
|
||||
"command": "npm",
|
||||
"args": ["run", "compile:debug"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "always",
|
||||
"panel": "dedicated",
|
||||
"clear": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "NX Post Support: Start Dev Host (Linux)",
|
||||
"detail": "Opens the Extension Development Host with the inspector on 127.0.0.1:9333 (workaround for js-debug localhost/::1 attach bug).",
|
||||
"type": "shell",
|
||||
"command": "\"$(dirname \"${execPath}\")/bin/$(basename \"${execPath}\")\" --new-window --inspect-brk-extensions=9333 --extensionDevelopmentPath=\"${workspaceFolder}\" \"${workspaceFolder}\" \"${workspaceFolder}/test/postprocessor\"",
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "/bin/bash",
|
||||
"args": ["-c"]
|
||||
}
|
||||
},
|
||||
"dependsOn": "NX Post Support: Compile Debug",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"panel": "dedicated"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "NX Post Support: Start Dev Host with debugpy (Linux)",
|
||||
"detail": "Same as above, but the language server connects to the debugpy listener on 127.0.0.1:5678.",
|
||||
"type": "shell",
|
||||
"command": "\"$(dirname \"${execPath}\")/bin/$(basename \"${execPath}\")\" --new-window --inspect-brk-extensions=9333 --extensionDevelopmentPath=\"${workspaceFolder}\" \"${workspaceFolder}\" \"${workspaceFolder}/test/postprocessor\"",
|
||||
"options": {
|
||||
"shell": {
|
||||
"executable": "/bin/bash",
|
||||
"args": ["-c"]
|
||||
},
|
||||
"env": {
|
||||
"USE_DEBUGPY": "True",
|
||||
"NXPS_DEBUG_HOST": "127.0.0.1",
|
||||
"NXPS_DEBUG_PORT": "5678"
|
||||
}
|
||||
},
|
||||
"dependsOn": "NX Post Support: Compile Debug",
|
||||
"problemMatcher": [],
|
||||
"presentation": {
|
||||
"reveal": "silent",
|
||||
"panel": "dedicated"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+5
-1
@@ -9,6 +9,7 @@ vite.config.js
|
||||
.prettierrc.json
|
||||
esbuild.js
|
||||
.gitea
|
||||
**/.ruff_cache/**
|
||||
.venv/**
|
||||
.nox/**
|
||||
server/.venv/**
|
||||
@@ -18,4 +19,7 @@ server/.nox/**
|
||||
**/requirements.in
|
||||
**/server/src/_debug_server.py
|
||||
server/noxfile.py
|
||||
client/**
|
||||
server/.claude/**
|
||||
server/tests/**
|
||||
client/**
|
||||
dist/**/*.map
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
## Unreleased
|
||||
|
||||
- Reparse only the top-level TCL commands touched by an edit instead of the whole file
|
||||
- Cache the workspace index in the extension storage so restarts skip reparsing unchanged files; the cache is discarded automatically when the server or bundled tclint changes
|
||||
- Speed up TclOO completion, signature help, inlay hints, and Go to Definition by reusing the cached syntax tree and skipping files without classes
|
||||
- Speed up references, document highlights, and call hierarchy with cached definition lookups
|
||||
- Keep background indexing from blocking requests that need a fresh syntax tree
|
||||
- Add Go to Definition for TclOO classes, constructors, and resolved methods, including PSC library definitions
|
||||
- Index PSC layer scripts (including external paths and legacy Windows encoding) and share TclOO class metadata across files for completion, signatures, inlay hints, and highlighting
|
||||
- Add document-local TclOO method completion for `new`/`create` instances, `my`, and statically inferred return chains
|
||||
- Suggest TclOO class names as classes and semantically highlight their declarations and calls
|
||||
- Show signature help and parameter inlay hints for resolved TclOO methods and constructors, including optional and variadic arguments
|
||||
- Add incoming and outgoing call hierarchy for custom TCL procedures and MOM event handlers
|
||||
- Add document highlights for procedure and variable occurrences
|
||||
- Make completion context-aware and prioritize local, current-file, workspace, and built-in symbols
|
||||
- Add command-aware completion for Tcl subcommands, fixed arguments, and valid options
|
||||
- Add semantic argument completion for variables, procedures, namespaces, and local file paths
|
||||
- Add placeholder-based snippets for common Tcl structures and `dict for`
|
||||
- Integrate the NX Tcl Remote Debugger directly into NX Postprocessor Support
|
||||
- Add `nx-tcl` attach configurations and breakpoint support for TCL and DEF files
|
||||
- Support breakpoints, stepping, stack frames, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops
|
||||
- Add generation-safe stack and variable references so delayed VS Code requests cannot address stale NX frames
|
||||
- Add command-buffer breakpoint handling for `LIB_GE_command_buffer_edit_*` source bodies
|
||||
- Extend the README with launch configuration, path mapping, and debugger usage
|
||||
- Prevent truncated TCL inlay hints and add configurable parameter hint modes
|
||||
- Add inlay hints for built-in NX procedures, variadic arguments, and visible ranges
|
||||
- Add inlay hint documentation and navigation to custom procedure definitions
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# NX Postprocessor Support
|
||||
|
||||
A comprehensive VS Code extension providing language support for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
||||
A comprehensive VS Code extension providing language support and remote debugging for NX CAM postprocessor development, including CDL, TCL, and DEF files.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -10,6 +10,12 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
||||
- **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
|
||||
- **Call Hierarchy** - Traces incoming and outgoing calls between custom TCL procedures and MOM event handlers
|
||||
- **Document Highlights** - Highlights all reads, writes, and calls of the symbol under the cursor
|
||||
- **Context-aware Completion** - Prioritizes local symbols and suggests variables, procedures, namespaces, paths, Tcl subcommands, valid argument values, and options based on cursor context
|
||||
- **PSC and TclOO Classes** - Indexes Tcl scripts referenced by PSC layers, including external folders. Classes from indexed files provide method completion, signature help, parameter hints, and class highlighting in other files. PSC script names may omit `.tcl`; relative folders resolve from the PSC directory, and environment-variable folders are supported. Missing scripts are reported in the output channel; encrypted libraries cannot supply static class metadata.
|
||||
- **Tcl Snippets** - Inserts placeholder-based structures for `if`, `foreach`, `proc`, `switch`, `try`, and `dict for`
|
||||
- **NX Tcl Remote Debugger** - Breakpoints, stepping, call stack, scopes, variables, watches, evaluation, logpoints, hit conditions, and Tcl error stops directly in a running NX Post process
|
||||
|
||||
## Supported File Types
|
||||
|
||||
@@ -20,10 +26,14 @@ A comprehensive VS Code extension providing language support for NX CAM postproc
|
||||
## Installation
|
||||
|
||||
1. Install from the VS Code Marketplace
|
||||
2. Install Python 3.11 or higher
|
||||
2. Install Python 3.12 or higher
|
||||
3. Open any `.cdl`, `.tcl`, or `.def` file
|
||||
4. The extension will automatically activate and provide language support
|
||||
|
||||
The former standalone `NX Tcl Remote Debugger` extension is no longer required. Disable or
|
||||
uninstall `local-nx.nx-tcl-debug` before using the integrated debugger because both extensions
|
||||
register the same `nx-tcl` debug type.
|
||||
|
||||
## Configuration
|
||||
|
||||
The extension can be configured through VS Code settings:
|
||||
@@ -38,6 +48,49 @@ TCL files default to unlimited inlay hint length so that VS Code does not
|
||||
truncate later parameter names on a line. An explicit user setting for
|
||||
`editor.inlayHints.maximumLength` still takes precedence.
|
||||
|
||||
## NX Tcl Remote Debugger
|
||||
|
||||
### Add a VS Code attach configuration
|
||||
|
||||
Create `.vscode/launch.json` through **Run and Debug: create a launch.json file** and select
|
||||
**NX Tcl: Attach to NX Post**, or use this configuration:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"type": "nx-tcl",
|
||||
"request": "attach",
|
||||
"name": "Attach to NX Post Tcl",
|
||||
"host": "127.0.0.1",
|
||||
"port": 4711,
|
||||
"connectTimeout": 120000,
|
||||
"stopOnEntry": false,
|
||||
"breakOnError": true,
|
||||
"localRoot": "${workspaceFolder}"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
When VS Code and NX see the source through different roots, set `remoteRoot` to the root used by
|
||||
NX and keep `localRoot` as the corresponding workspace root.
|
||||
|
||||
### Start debugging
|
||||
|
||||
1. Set breakpoints on executable Tcl or DEF commands.
|
||||
2. Start **Attach to NX Post Tcl** in VS Code before running the postprocessor.
|
||||
3. Start postprocessing in NX.
|
||||
4. Use Continue, Step Over, Step Into, Step Out, Pause, Variables, Watch, and the Debug Console as
|
||||
with a normal source debugger.
|
||||
|
||||
Breakpoints remain active and stop at every real invocation. Use a hit condition such as `1` for
|
||||
a one-time stop. Blank lines, comments, declarations, and multiline Tcl commands may be relocated
|
||||
to the nearest executable command; VS Code shows the resolved line. Dynamic
|
||||
`LIB_GE_command_buffer_edit_*` bodies are mapped back to their original source and owning Tcl
|
||||
procedure.
|
||||
|
||||
## Usage
|
||||
|
||||
Simply open any supported file type and enjoy:
|
||||
@@ -48,29 +101,11 @@ Simply open any supported file type and enjoy:
|
||||
- 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.
|
||||
- Incoming and outgoing call hierarchy for custom procedures and MOM event handlers
|
||||
- Document-wide highlights for procedure and variable occurrences
|
||||
- Context-aware completion with local symbols ranked before workspace and built-in symbols, plus semantic arguments, local paths, Tcl subcommands, and options such as `string compare -nocase`
|
||||
- Placeholder-based snippets for common Tcl control structures and procedures
|
||||
- Remote NX Tcl debugging with breakpoints and full stepping
|
||||
|
||||
## Contributing
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
export interface CdlEventHandler {
|
||||
eventName: string
|
||||
parameterNames: string[]
|
||||
toggleOffParameterNames?: string[]
|
||||
}
|
||||
|
||||
function structuralCode(line: string): string {
|
||||
@@ -33,18 +34,6 @@ function structuralCode(line: string): string {
|
||||
return result
|
||||
}
|
||||
|
||||
function braceDelta(line: string): number {
|
||||
let delta = 0
|
||||
for (const character of structuralCode(line)) {
|
||||
if (character === "{") {
|
||||
delta += 1
|
||||
} else if (character === "}") {
|
||||
delta -= 1
|
||||
}
|
||||
}
|
||||
return delta
|
||||
}
|
||||
|
||||
export function cdlEventHandlerAtLine(
|
||||
source: string,
|
||||
declarationLine: number
|
||||
@@ -61,35 +50,44 @@ export function cdlEventHandlerAtLine(
|
||||
}
|
||||
|
||||
const parameterNames: string[] = []
|
||||
const toggleOffParameterNames: string[] = []
|
||||
let currentParameter: string | undefined
|
||||
let eventOpened = false
|
||||
let depth = 0
|
||||
|
||||
for (let lineNumber = declarationLine; lineNumber < lines.length; lineNumber++) {
|
||||
eventLines: for (let lineNumber = declarationLine; lineNumber < lines.length; lineNumber++) {
|
||||
const line = lines[lineNumber]
|
||||
const code = structuralCode(line)
|
||||
|
||||
if (eventOpened && depth === 1) {
|
||||
const parameterMatch = /^\s*PARAM\s+([^\s{]+)/.exec(code)
|
||||
if (parameterMatch) {
|
||||
parameterNames.push(parameterMatch[1])
|
||||
}
|
||||
}
|
||||
|
||||
const delta = braceDelta(line)
|
||||
if (!eventOpened && delta > 0) {
|
||||
eventOpened = true
|
||||
}
|
||||
if (eventOpened) {
|
||||
depth += delta
|
||||
if (depth <= 0) {
|
||||
break
|
||||
const tokens = code.match(/[{}]|[^\s{}]+/g) ?? []
|
||||
for (let index = 0; index < tokens.length; index++) {
|
||||
const token = tokens[index]
|
||||
if (token === "{") {
|
||||
eventOpened = true
|
||||
depth++
|
||||
} else if (token === "}" && eventOpened) {
|
||||
depth--
|
||||
if (depth === 1) currentParameter = undefined
|
||||
if (depth <= 0) break eventLines
|
||||
} else if (depth === 1 && token === "PARAM") {
|
||||
const name = tokens[index + 1]
|
||||
if (name && name !== "{" && name !== "}") {
|
||||
currentParameter = name
|
||||
parameterNames.push(name)
|
||||
index++
|
||||
}
|
||||
} else if (depth === 2 && currentParameter && token === "TOGGLE") {
|
||||
if (tokens[index + 1]?.toLowerCase() === "off") {
|
||||
toggleOffParameterNames.push(currentParameter)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
eventName: eventMatch[1],
|
||||
parameterNames
|
||||
parameterNames,
|
||||
toggleOffParameterNames
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,10 +100,14 @@ function momVariableName(parameterName: string): string {
|
||||
}
|
||||
|
||||
export function createCdlEventHandlerSnippet(handler: CdlEventHandler): string {
|
||||
const toggleOffParameters = new Set(handler.toggleOffParameterNames ?? [])
|
||||
const globals = [
|
||||
...new Set(handler.parameterNames.map((parameter) => momVariableName(parameter)))
|
||||
...new Set(handler.parameterNames.flatMap((parameter) => {
|
||||
const variable = momVariableName(parameter)
|
||||
return toggleOffParameters.has(parameter) ? [variable, `${variable}_defined`] : [variable]
|
||||
}))
|
||||
]
|
||||
const lines = [`proc ${momEventName(handler.eventName)} { } {`]
|
||||
const lines = [`proc ${momEventName(handler.eventName)} {args} {`]
|
||||
|
||||
if (globals.length > 0) {
|
||||
lines.push(...globals.map((variable) => ` global ${variable}`), "")
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import * as vscode from "vscode"
|
||||
import {
|
||||
cdlEventHandlerAtLine,
|
||||
createCdlEventHandlerSnippet
|
||||
} from "./cdlEventHandler"
|
||||
import { cdlEventHandlerAtLine, createCdlEventHandlerSnippet } from "./cdlEventHandler"
|
||||
|
||||
const MACHINE_HEADER_REGEX = /^MACHINE\s+\S+/
|
||||
|
||||
@@ -65,10 +62,7 @@ export function diagnosticHandler(document: vscode.TextDocument) {
|
||||
if (document.languageId === "cdl" || document.languageId === "def") {
|
||||
const text = document.getText()
|
||||
if (!isFirstLineMachine(text)) {
|
||||
const range = new vscode.Range(
|
||||
document.positionAt(0),
|
||||
document.positionAt(text.length)
|
||||
)
|
||||
const range = new vscode.Range(document.positionAt(0), document.positionAt(text.length))
|
||||
const diagnostic = new vscode.Diagnostic(
|
||||
range,
|
||||
"The first line should contain 'MACHINE'.",
|
||||
@@ -82,7 +76,7 @@ export function diagnosticHandler(document: vscode.TextDocument) {
|
||||
|
||||
export function completionHandlerCdl(document: vscode.TextDocument, position: vscode.Position) {
|
||||
const linePrefix = document.lineAt(position).text.substring(0, position.character)
|
||||
const categories = ["MILL", "LATHE", "DRILL"]
|
||||
const categories = ["MILL", "LATHE", "DRILL", "INVALID"]
|
||||
|
||||
if (linePrefix.endsWith("TYPE ")) {
|
||||
return [
|
||||
@@ -193,8 +187,7 @@ export async function definitionCdlEventHandler(
|
||||
.filter(
|
||||
(symbol) =>
|
||||
symbol.kind === vscode.SymbolKind.Function &&
|
||||
(symbol.name === handlerName ||
|
||||
symbol.name.endsWith(`::${handlerName}`))
|
||||
(symbol.name === handlerName || symbol.name.endsWith(`::${handlerName}`))
|
||||
)
|
||||
.map((symbol) => symbol.location)
|
||||
if (indexedLocations.length > 0) {
|
||||
@@ -204,9 +197,7 @@ export async function definitionCdlEventHandler(
|
||||
// The Tcl language server may still be starting; use the file fallback below.
|
||||
}
|
||||
|
||||
const declaration = new RegExp(
|
||||
`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`
|
||||
)
|
||||
const declaration = new RegExp(`^\\s*proc\\s+(?:::)?${escapeRegExp(handlerName)}(?=\\s|\\{)`)
|
||||
const tclFiles = await vscode.workspace.findFiles(
|
||||
"**/*.tcl",
|
||||
"**/{.git,.nox,.venv,dist,node_modules,out}/**"
|
||||
@@ -233,12 +224,7 @@ export async function definitionCdlEventHandler(
|
||||
locations.push(
|
||||
new vscode.Location(
|
||||
uri,
|
||||
new vscode.Range(
|
||||
lineNumber,
|
||||
start,
|
||||
lineNumber,
|
||||
start + handlerName.length
|
||||
)
|
||||
new vscode.Range(lineNumber, start, lineNumber, start + handlerName.length)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,7 +22,12 @@ import {
|
||||
import { getLSClientTraceLevel, getProjectRoot } from "./utilities"
|
||||
import { isVirtualWorkspace } from "./vscodeapi"
|
||||
|
||||
export type IInitOptions = { settings: ISettings[]; globalSettings: ISettings }
|
||||
export type IInitOptions = {
|
||||
settings: ISettings[]
|
||||
globalSettings: ISettings
|
||||
// Folder for the server's persistent index cache; omitted without a workspace.
|
||||
indexCachePath?: string
|
||||
}
|
||||
|
||||
let _disposables: Disposable[] = []
|
||||
|
||||
@@ -86,7 +91,7 @@ async function createServer(
|
||||
}
|
||||
|
||||
// Options to control the language client
|
||||
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.tcl")
|
||||
const tclFileWatcher = workspace.createFileSystemWatcher("**/*.{tcl,psc,def}")
|
||||
const clientOptions: LanguageClientOptions = {
|
||||
// Register the server for python documents
|
||||
documentSelector: isVirtualWorkspace()
|
||||
@@ -114,7 +119,8 @@ export async function restartServer(
|
||||
serverId: string,
|
||||
serverName: string,
|
||||
outputChannel: LogOutputChannel,
|
||||
lsClient?: LanguageClient
|
||||
lsClient?: LanguageClient,
|
||||
indexCachePath?: string
|
||||
): Promise<LanguageClient | undefined> {
|
||||
if (lsClient) {
|
||||
traceInfo(`Server: Stop requested`)
|
||||
@@ -132,7 +138,8 @@ export async function restartServer(
|
||||
outputChannel,
|
||||
{
|
||||
settings: await getExtensionSettings(serverId, true),
|
||||
globalSettings: await getGlobalSettings(serverId, false)
|
||||
globalSettings: await getGlobalSettings(serverId, false),
|
||||
indexCachePath
|
||||
}
|
||||
)
|
||||
traceInfo(`Server: Start requested.`)
|
||||
|
||||
@@ -0,0 +1,650 @@
|
||||
'use strict';
|
||||
|
||||
const path = require('path');
|
||||
const { RuntimeConnection, encodeHex, decodeHex } = require('./protocol');
|
||||
|
||||
const THREAD_ID = 1;
|
||||
|
||||
class NxDebugAdapter {
|
||||
constructor(input = process.stdin, output = process.stdout) {
|
||||
this.input = input;
|
||||
this.output = output;
|
||||
this.inputBuffer = Buffer.alloc(0);
|
||||
this.sequence = 1;
|
||||
this.runtime = null;
|
||||
this.terminated = false;
|
||||
this.explicitDisconnect = false;
|
||||
this.localRoot = '';
|
||||
this.remoteRoot = '';
|
||||
this.nextBreakpointId = 1;
|
||||
// VS Code can expose the same physical file through more than one source
|
||||
// identity (for example, drive mappings or differently cased workspace
|
||||
// roots). SETBPS replaces all breakpoints for a runtime path, so retain
|
||||
// each DAP source group and send their union to NX.
|
||||
this.sourceBreakpointGroups = new Map();
|
||||
this.pendingStartRequest = null;
|
||||
this.resumeInProgress = 0;
|
||||
this.pendingStoppedEvents = [];
|
||||
this.isPaused = false;
|
||||
// DAP frame/variable IDs can outlive a stop in the VS Code UI. Runtime
|
||||
// levels and references, however, are valid only for the current Tcl
|
||||
// pause. Give every stop adapter-owned IDs so a late request can never
|
||||
// accidentally address a same-numbered frame from a later pause.
|
||||
this.nextFrameId = 1;
|
||||
this.frameReferences = new Map();
|
||||
this.runtimeToFrameReference = new Map();
|
||||
this.nextVariablesReference = 1;
|
||||
this.variableReferences = new Map();
|
||||
this.runtimeToVariableReference = new Map();
|
||||
|
||||
input.on('data', (chunk) => this.onData(chunk));
|
||||
input.on('end', () => this.dispose());
|
||||
input.resume();
|
||||
}
|
||||
|
||||
onData(chunk) {
|
||||
this.inputBuffer = Buffer.concat([this.inputBuffer, chunk]);
|
||||
while (true) {
|
||||
const headerEnd = this.inputBuffer.indexOf('\r\n\r\n');
|
||||
if (headerEnd < 0) return;
|
||||
const header = this.inputBuffer.subarray(0, headerEnd).toString('ascii');
|
||||
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
||||
if (!match) throw new Error('missing DAP Content-Length header');
|
||||
const contentLength = Number(match[1]);
|
||||
const messageEnd = headerEnd + 4 + contentLength;
|
||||
if (this.inputBuffer.length < messageEnd) return;
|
||||
const payload = this.inputBuffer.subarray(headerEnd + 4, messageEnd).toString('utf8');
|
||||
this.inputBuffer = this.inputBuffer.subarray(messageEnd);
|
||||
const message = JSON.parse(payload);
|
||||
if (message.type === 'request') {
|
||||
this.handleRequest(message).catch((error) => this.sendErrorResponse(message, error));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
send(message) {
|
||||
message.seq = this.sequence++;
|
||||
const json = JSON.stringify(message);
|
||||
this.output.write(`Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`);
|
||||
}
|
||||
|
||||
sendResponse(request, body = {}) {
|
||||
this.send({
|
||||
type: 'response',
|
||||
request_seq: request.seq,
|
||||
success: true,
|
||||
command: request.command,
|
||||
body,
|
||||
});
|
||||
}
|
||||
|
||||
sendErrorResponse(request, error) {
|
||||
this.send({
|
||||
type: 'response',
|
||||
request_seq: request.seq,
|
||||
success: false,
|
||||
command: request.command,
|
||||
message: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
}
|
||||
|
||||
sendEvent(event, body = {}) {
|
||||
this.send({ type: 'event', event, body });
|
||||
}
|
||||
|
||||
async handleRequest(request) {
|
||||
const args = request.arguments || {};
|
||||
switch (request.command) {
|
||||
case 'initialize':
|
||||
this.sendResponse(request, {
|
||||
supportsConfigurationDoneRequest: true,
|
||||
supportsConditionalBreakpoints: true,
|
||||
supportsHitConditionalBreakpoints: true,
|
||||
supportsLogPoints: true,
|
||||
supportsEvaluateForHovers: true,
|
||||
supportsSetVariable: true,
|
||||
supportsExceptionFilterOptions: true,
|
||||
exceptionBreakpointFilters: [
|
||||
{ filter: 'tclError', label: 'Tcl errors', description: 'Break when a Tcl command returns TCL_ERROR' },
|
||||
],
|
||||
supportsTerminateRequest: true,
|
||||
});
|
||||
break;
|
||||
|
||||
case 'attach':
|
||||
case 'launch':
|
||||
await this.attach(args);
|
||||
this.pendingStartRequest = request;
|
||||
this.sendEvent('initialized');
|
||||
break;
|
||||
|
||||
case 'configurationDone':
|
||||
await this.runtime.request('CONFIGDONE');
|
||||
this.sendResponse(request);
|
||||
if (this.pendingStartRequest) {
|
||||
this.sendResponse(this.pendingStartRequest);
|
||||
this.pendingStartRequest = null;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'setBreakpoints':
|
||||
await this.setBreakpoints(request, args);
|
||||
break;
|
||||
|
||||
case 'setExceptionBreakpoints': {
|
||||
const filters = args.filters || [];
|
||||
await this.runtime.request('SETEXCEPTIONS', filters);
|
||||
this.sendResponse(request, {
|
||||
breakpoints: filters.map(() => ({ verified: true })),
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'threads':
|
||||
this.sendResponse(request, { threads: [{ id: THREAD_ID, name: 'NX Post Tcl' }] });
|
||||
break;
|
||||
|
||||
case 'stackTrace':
|
||||
await this.stackTrace(request);
|
||||
break;
|
||||
|
||||
case 'scopes':
|
||||
await this.scopes(request, args);
|
||||
break;
|
||||
|
||||
case 'variables':
|
||||
await this.variables(request, args);
|
||||
break;
|
||||
|
||||
case 'evaluate':
|
||||
await this.evaluate(request, args);
|
||||
break;
|
||||
|
||||
case 'setVariable':
|
||||
await this.setVariable(request, args);
|
||||
break;
|
||||
|
||||
case 'continue':
|
||||
await this.resume(request, 'CONTINUE', { allThreadsContinued: true });
|
||||
break;
|
||||
|
||||
case 'next':
|
||||
await this.resume(request, 'NEXT');
|
||||
break;
|
||||
|
||||
case 'stepIn':
|
||||
await this.resume(request, 'STEPIN');
|
||||
break;
|
||||
|
||||
case 'stepOut':
|
||||
await this.resume(request, 'STEPOUT');
|
||||
break;
|
||||
|
||||
case 'pause':
|
||||
await this.runtime.request('PAUSE');
|
||||
this.sendResponse(request);
|
||||
break;
|
||||
|
||||
case 'disconnect':
|
||||
case 'terminate':
|
||||
await this.disconnect(request);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.sendResponse(request);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async attach(args) {
|
||||
const host = args.host || '127.0.0.1';
|
||||
const port = Number(args.port || 4711);
|
||||
const timeout = Number(args.connectTimeout || 120000);
|
||||
this.localRoot = args.localRoot ? path.resolve(args.localRoot) : '';
|
||||
this.remoteRoot = args.remoteRoot ? this.normalizePath(args.remoteRoot) : '';
|
||||
this.sourceBreakpointGroups.clear();
|
||||
this.isPaused = false;
|
||||
this.resetStoppedReferences(true);
|
||||
|
||||
this.runtime = new RuntimeConnection();
|
||||
this.runtime.on('event', (name, fields) => this.onRuntimeEvent(name, fields));
|
||||
this.runtime.on('runtimeError', (error) => {
|
||||
this.sendEvent('output', { category: 'stderr', output: `NX: ${error.message}\n` });
|
||||
});
|
||||
this.runtime.on('close', () => {
|
||||
if (!this.explicitDisconnect) this.sendTerminated();
|
||||
});
|
||||
|
||||
const started = Date.now();
|
||||
let lastError;
|
||||
while (Date.now() - started < timeout) {
|
||||
try {
|
||||
await this.runtime.connect(host, port);
|
||||
lastError = null;
|
||||
break;
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
await new Promise((resolve) => setTimeout(resolve, 250));
|
||||
}
|
||||
}
|
||||
if (lastError) {
|
||||
throw new Error(`could not connect to NX at ${host}:${port}: ${lastError.message}`);
|
||||
}
|
||||
await this.runtime.request('CONFIG', [args.stopOnEntry ? 1 : 0, args.breakOnError ? 1 : 0]);
|
||||
}
|
||||
|
||||
async setBreakpoints(request, args) {
|
||||
const sourcePath = args.source && args.source.path ? args.source.path : '';
|
||||
const remotePath = this.toRemotePath(sourcePath);
|
||||
const sourceKey = this.breakpointSourceKey(args.source, sourcePath);
|
||||
const remoteKey = this.breakpointRemoteKey(remotePath);
|
||||
const requested = args.breakpoints || (args.lines || []).map((line) => ({ line }));
|
||||
const records = requested.map((breakpoint) => ({
|
||||
id: this.nextBreakpointId++,
|
||||
line: breakpoint.line,
|
||||
condition: breakpoint.condition || '',
|
||||
hitCondition: breakpoint.hitCondition || '',
|
||||
logMessage: breakpoint.logMessage || '',
|
||||
}));
|
||||
if (!this.isTclSource(sourcePath)) {
|
||||
this.sourceBreakpointGroups.delete(sourceKey);
|
||||
// VS Code forwards breakpoints from every language to every active debug
|
||||
// session. Clear any stale runtime entry left by an older adapter, but do
|
||||
// not make the NX Tcl runtime parse or instrument a Python source file.
|
||||
await this.runtime.request('SETBPS', [encodeHex(remotePath), 0]);
|
||||
this.sendResponse(request, {
|
||||
breakpoints: records.map((breakpoint) => ({
|
||||
id: breakpoint.id,
|
||||
verified: false,
|
||||
line: breakpoint.line,
|
||||
source: args.source,
|
||||
message: 'NX Tcl Remote Debugger supports breakpoints only in .tcl and .def files',
|
||||
})),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.sourceBreakpointGroups.set(sourceKey, { remotePath, remoteKey, records });
|
||||
|
||||
// Collapse exact duplicates shared by aliases, while remembering which
|
||||
// runtime breakpoint represents every DAP breakpoint ID. This keeps hit
|
||||
// counts and log points from firing twice for one Tcl command.
|
||||
const mergedBreakpoints = [];
|
||||
const mergedBySignature = new Map();
|
||||
const representativeById = new Map();
|
||||
for (const group of this.sourceBreakpointGroups.values()) {
|
||||
if (group.remoteKey !== remoteKey) continue;
|
||||
for (const breakpoint of group.records) {
|
||||
const signature = this.breakpointSignature(breakpoint);
|
||||
let representative = mergedBySignature.get(signature);
|
||||
if (!representative) {
|
||||
representative = breakpoint;
|
||||
mergedBySignature.set(signature, representative);
|
||||
mergedBreakpoints.push(representative);
|
||||
}
|
||||
representativeById.set(breakpoint.id, representative.id);
|
||||
}
|
||||
}
|
||||
|
||||
const fields = [encodeHex(remotePath), mergedBreakpoints.length];
|
||||
for (const breakpoint of mergedBreakpoints) {
|
||||
fields.push(
|
||||
breakpoint.id,
|
||||
breakpoint.line,
|
||||
encodeHex(breakpoint.condition),
|
||||
encodeHex(breakpoint.hitCondition),
|
||||
encodeHex(breakpoint.logMessage),
|
||||
);
|
||||
}
|
||||
const runtimeResponse = await this.runtime.request('SETBPS', fields);
|
||||
const responseFields = runtimeResponse.fields || [];
|
||||
const responseCount = Number(responseFields[0] || 0);
|
||||
const responseStride = responseFields.length >= 1 + responseCount * 4 ? 4 : 2;
|
||||
const runtimeBreakpoints = new Map();
|
||||
let responseCursor = 1;
|
||||
for (let index = 0; index < responseCount; index++) {
|
||||
const id = Number(responseFields[responseCursor++]);
|
||||
const line = Number(responseFields[responseCursor++]);
|
||||
let verified = true;
|
||||
let message = '';
|
||||
if (responseStride === 4) {
|
||||
verified = responseFields[responseCursor++] === '1';
|
||||
message = decodeHex(responseFields[responseCursor++]);
|
||||
}
|
||||
runtimeBreakpoints.set(id, { line, verified, message });
|
||||
}
|
||||
this.sendResponse(request, {
|
||||
breakpoints: records.map((breakpoint) => {
|
||||
const representativeId = representativeById.get(breakpoint.id);
|
||||
const runtimeBreakpoint = runtimeBreakpoints.get(representativeId);
|
||||
const result = {
|
||||
id: breakpoint.id,
|
||||
verified: runtimeBreakpoint ? runtimeBreakpoint.verified : false,
|
||||
line: runtimeBreakpoint && runtimeBreakpoint.line ? runtimeBreakpoint.line : breakpoint.line,
|
||||
source: args.source,
|
||||
};
|
||||
if (runtimeBreakpoint && runtimeBreakpoint.message) result.message = runtimeBreakpoint.message;
|
||||
return result;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
async stackTrace(request) {
|
||||
if (!this.isPaused) {
|
||||
this.sendResponse(request, { stackFrames: [], totalFrames: 0 });
|
||||
return;
|
||||
}
|
||||
const { fields } = await this.runtime.request('STACK');
|
||||
const count = Number(fields[0] || 0);
|
||||
const stackFrames = [];
|
||||
let cursor = 1;
|
||||
for (let index = 0; index < count; index++) {
|
||||
const runtimeId = Number(fields[cursor++]);
|
||||
const name = decodeHex(fields[cursor++]);
|
||||
const remoteFile = decodeHex(fields[cursor++]);
|
||||
const line = Number(fields[cursor++]);
|
||||
const column = Number(fields[cursor++]);
|
||||
const localFile = this.toLocalPath(remoteFile);
|
||||
const frame = {
|
||||
id: this.toAdapterFrameId(runtimeId),
|
||||
name: name || '<Tcl>',
|
||||
line: line || 1,
|
||||
column: column || 1,
|
||||
};
|
||||
if (localFile) frame.source = { name: path.basename(localFile), path: localFile };
|
||||
stackFrames.push(frame);
|
||||
}
|
||||
this.sendResponse(request, { stackFrames, totalFrames: stackFrames.length });
|
||||
}
|
||||
|
||||
async scopes(request, args) {
|
||||
const runtimeFrameId = this.frameReferences.get(Number(args.frameId));
|
||||
if (!this.isPaused || runtimeFrameId === undefined) {
|
||||
this.sendResponse(request, { scopes: [] });
|
||||
return;
|
||||
}
|
||||
const { fields } = await this.runtime.request('SCOPES', [runtimeFrameId]);
|
||||
const count = Number(fields[0] || 0);
|
||||
const scopes = [];
|
||||
let cursor = 1;
|
||||
for (let index = 0; index < count; index++) {
|
||||
const variablesReference = this.toAdapterVariablesReference(Number(fields[cursor++]));
|
||||
const name = decodeHex(fields[cursor++]);
|
||||
cursor++; // wire label: expensive
|
||||
const expensive = fields[cursor++] === '1';
|
||||
scopes.push({ name, variablesReference, expensive });
|
||||
}
|
||||
this.sendResponse(request, { scopes });
|
||||
}
|
||||
|
||||
async variables(request, args) {
|
||||
const runtimeReference = this.variableReferences.get(Number(args.variablesReference));
|
||||
if (!this.isPaused || runtimeReference === undefined) {
|
||||
this.sendResponse(request, { variables: [] });
|
||||
return;
|
||||
}
|
||||
const { fields } = await this.runtime.request('VARIABLES', [
|
||||
runtimeReference,
|
||||
args.start || 0,
|
||||
args.count || 0,
|
||||
encodeHex(args.filter || ''),
|
||||
]);
|
||||
const count = Number(fields[0] || 0);
|
||||
const variables = [];
|
||||
let cursor = 1;
|
||||
for (let index = 0; index < count; index++) {
|
||||
variables.push({
|
||||
name: decodeHex(fields[cursor++]),
|
||||
value: decodeHex(fields[cursor++]),
|
||||
type: decodeHex(fields[cursor++]),
|
||||
variablesReference: this.toAdapterVariablesReference(Number(fields[cursor++])),
|
||||
indexedVariables: Number(fields[cursor++]) || undefined,
|
||||
namedVariables: Number(fields[cursor++]) || undefined,
|
||||
});
|
||||
}
|
||||
this.sendResponse(request, { variables });
|
||||
}
|
||||
|
||||
async evaluate(request, args) {
|
||||
// VS Code can refresh Watch/Hover expressions after Continue but before
|
||||
// it has processed the next stopped event. Do not forward that transient
|
||||
// request to the running NX interpreter (which correctly rejects it).
|
||||
if (!this.isPaused) {
|
||||
this.sendResponse(request, {
|
||||
result: '<NX is running; evaluation is available while paused>',
|
||||
type: 'unavailable',
|
||||
variablesReference: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
let runtimeFrameId = 0;
|
||||
if (Number(args.frameId) > 0) {
|
||||
runtimeFrameId = this.frameReferences.get(Number(args.frameId));
|
||||
if (runtimeFrameId === undefined) {
|
||||
this.sendResponse(request, {
|
||||
result: '<stack frame is no longer available>',
|
||||
type: 'unavailable',
|
||||
variablesReference: 0,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
const { fields } = await this.runtime.request('EVALUATE', [runtimeFrameId, encodeHex(args.expression || '')]);
|
||||
this.sendResponse(request, {
|
||||
result: decodeHex(fields[0]),
|
||||
type: decodeHex(fields[1]),
|
||||
variablesReference: this.toAdapterVariablesReference(Number(fields[2])),
|
||||
indexedVariables: Number(fields[3]) || undefined,
|
||||
namedVariables: Number(fields[4]) || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async setVariable(request, args) {
|
||||
const runtimeReference = this.variableReferences.get(Number(args.variablesReference));
|
||||
if (!this.isPaused || runtimeReference === undefined) {
|
||||
throw new Error('variable is no longer available');
|
||||
}
|
||||
const { fields } = await this.runtime.request('SETVARIABLE', [
|
||||
runtimeReference,
|
||||
encodeHex(args.name),
|
||||
encodeHex(args.value),
|
||||
]);
|
||||
this.sendResponse(request, {
|
||||
value: decodeHex(fields[0]),
|
||||
type: decodeHex(fields[1]),
|
||||
variablesReference: this.toAdapterVariablesReference(Number(fields[2])),
|
||||
indexedVariables: Number(fields[3]) || undefined,
|
||||
namedVariables: Number(fields[4]) || undefined,
|
||||
});
|
||||
}
|
||||
|
||||
async resume(request, command, responseBody = {}) {
|
||||
this.isPaused = false;
|
||||
this.resetStoppedReferences();
|
||||
this.resumeInProgress++;
|
||||
try {
|
||||
await this.runtime.request(command);
|
||||
// The runtime can hit the next breakpoint immediately and place its
|
||||
// STOPPED event in the same TCP packet as this response. Always publish
|
||||
// the DAP resume transition before releasing such a queued stop.
|
||||
this.sendResponse(request, responseBody);
|
||||
this.sendEvent('continued', { threadId: THREAD_ID, allThreadsContinued: true });
|
||||
} finally {
|
||||
this.resumeInProgress--;
|
||||
if (this.resumeInProgress === 0) this.flushStoppedEvents();
|
||||
}
|
||||
}
|
||||
|
||||
async disconnect(request) {
|
||||
this.explicitDisconnect = true;
|
||||
this.isPaused = false;
|
||||
this.resetStoppedReferences();
|
||||
if (this.runtime && !this.runtime.closed) {
|
||||
try {
|
||||
await this.runtime.request('DISCONNECT', [], 2000);
|
||||
} catch (_) {
|
||||
// The runtime may close immediately after acknowledging detach.
|
||||
}
|
||||
this.runtime.close();
|
||||
}
|
||||
this.sendResponse(request);
|
||||
this.sendTerminated();
|
||||
}
|
||||
|
||||
onRuntimeEvent(name, fields) {
|
||||
switch (name) {
|
||||
case 'HELLO':
|
||||
this.sendEvent('output', {
|
||||
category: 'console',
|
||||
output: `NX runtime ${decodeHex(fields[0])}, Tcl ${decodeHex(fields[1])}, PID ${fields[2]}\n`,
|
||||
});
|
||||
break;
|
||||
case 'STOPPED':
|
||||
this.isPaused = true;
|
||||
this.resetStoppedReferences();
|
||||
this.publishStoppedEvent({
|
||||
reason: decodeHex(fields[0]) || 'pause',
|
||||
threadId: THREAD_ID,
|
||||
allThreadsStopped: true,
|
||||
text: decodeHex(fields[3]) || undefined,
|
||||
});
|
||||
break;
|
||||
case 'OUTPUT':
|
||||
this.sendEvent('output', { category: 'stdout', output: decodeHex(fields[0]) });
|
||||
break;
|
||||
default:
|
||||
this.sendEvent('output', { category: 'console', output: `NX event ${name}\n` });
|
||||
}
|
||||
}
|
||||
|
||||
publishStoppedEvent(body) {
|
||||
if (this.resumeInProgress > 0) {
|
||||
this.pendingStoppedEvents.push(body);
|
||||
return;
|
||||
}
|
||||
this.sendEvent('stopped', body);
|
||||
}
|
||||
|
||||
flushStoppedEvents() {
|
||||
const queued = this.pendingStoppedEvents;
|
||||
this.pendingStoppedEvents = [];
|
||||
for (const body of queued) this.sendEvent('stopped', body);
|
||||
}
|
||||
|
||||
resetStoppedReferences(resetCounters = false) {
|
||||
this.frameReferences.clear();
|
||||
this.runtimeToFrameReference.clear();
|
||||
this.variableReferences.clear();
|
||||
this.runtimeToVariableReference.clear();
|
||||
if (resetCounters) {
|
||||
this.nextFrameId = 1;
|
||||
this.nextVariablesReference = 1;
|
||||
}
|
||||
}
|
||||
|
||||
toAdapterFrameId(runtimeId) {
|
||||
const normalized = Number(runtimeId);
|
||||
if (!Number.isFinite(normalized) || normalized <= 0) return 0;
|
||||
let adapterId = this.runtimeToFrameReference.get(normalized);
|
||||
if (adapterId !== undefined) return adapterId;
|
||||
adapterId = this.nextFrameId++;
|
||||
this.runtimeToFrameReference.set(normalized, adapterId);
|
||||
this.frameReferences.set(adapterId, normalized);
|
||||
return adapterId;
|
||||
}
|
||||
|
||||
toAdapterVariablesReference(runtimeReference) {
|
||||
const normalized = Number(runtimeReference);
|
||||
if (!Number.isFinite(normalized) || normalized <= 0) return 0;
|
||||
let adapterReference = this.runtimeToVariableReference.get(normalized);
|
||||
if (adapterReference !== undefined) return adapterReference;
|
||||
adapterReference = this.nextVariablesReference++;
|
||||
this.runtimeToVariableReference.set(normalized, adapterReference);
|
||||
this.variableReferences.set(adapterReference, normalized);
|
||||
return adapterReference;
|
||||
}
|
||||
|
||||
isTclSource(sourcePath) {
|
||||
const extension = path.extname(String(sourcePath || '')).toLowerCase();
|
||||
return extension === '.tcl' || extension === '.def';
|
||||
}
|
||||
|
||||
breakpointSourceKey(source = {}, sourcePath = '') {
|
||||
if (source && Number(source.sourceReference) > 0) {
|
||||
return `reference:${source.sourceReference}`;
|
||||
}
|
||||
// Deliberately preserve spelling and case here: they are precisely what
|
||||
// distinguish two VS Code source identities that map to the same runtime
|
||||
// path. Runtime comparison is handled separately below.
|
||||
return `path:${String(sourcePath || '')}`;
|
||||
}
|
||||
|
||||
breakpointRemoteKey(remotePath) {
|
||||
let normalized = this.normalizePath(remotePath);
|
||||
if (process.platform === 'win32' || /^[A-Za-z]:\//.test(normalized) || normalized.startsWith('//')) {
|
||||
normalized = normalized.toLowerCase();
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
breakpointSignature(breakpoint) {
|
||||
return JSON.stringify([
|
||||
Number(breakpoint.line),
|
||||
breakpoint.condition || '',
|
||||
breakpoint.hitCondition || '',
|
||||
breakpoint.logMessage || '',
|
||||
]);
|
||||
}
|
||||
|
||||
normalizePath(value) {
|
||||
const normalized = String(value || '').replace(/\\/g, '/');
|
||||
if (normalized === '/' || /^[A-Za-z]:\/$/.test(normalized)) return normalized;
|
||||
return normalized.replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
samePathPrefix(candidate, prefix) {
|
||||
let normalizedCandidate = this.normalizePath(candidate);
|
||||
let normalizedPrefix = this.normalizePath(prefix);
|
||||
if (process.platform === 'win32') {
|
||||
normalizedCandidate = normalizedCandidate.toLowerCase();
|
||||
normalizedPrefix = normalizedPrefix.toLowerCase();
|
||||
}
|
||||
if (normalizedCandidate === normalizedPrefix) return true;
|
||||
const boundaryPrefix = normalizedPrefix.endsWith('/') ? normalizedPrefix : `${normalizedPrefix}/`;
|
||||
return normalizedCandidate.startsWith(boundaryPrefix);
|
||||
}
|
||||
|
||||
toRemotePath(localPath) {
|
||||
const normalized = this.normalizePath(path.resolve(localPath || '.'));
|
||||
const localRoot = this.normalizePath(this.localRoot);
|
||||
if (localRoot && this.remoteRoot && this.samePathPrefix(normalized, localRoot)) {
|
||||
return this.remoteRoot + normalized.slice(localRoot.length);
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
|
||||
toLocalPath(remotePath) {
|
||||
const normalized = this.normalizePath(remotePath);
|
||||
if (this.remoteRoot && this.localRoot && this.samePathPrefix(normalized, this.remoteRoot)) {
|
||||
const suffix = normalized.slice(this.remoteRoot.length).replace(/^\//, '');
|
||||
return path.join(this.localRoot, ...suffix.split('/'));
|
||||
}
|
||||
return process.platform === 'win32' ? normalized.replace(/\//g, '\\') : normalized;
|
||||
}
|
||||
|
||||
sendTerminated() {
|
||||
if (this.terminated) return;
|
||||
this.terminated = true;
|
||||
this.sendEvent('terminated');
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.runtime) this.runtime.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
new NxDebugAdapter();
|
||||
}
|
||||
|
||||
module.exports = { NxDebugAdapter, THREAD_ID };
|
||||
@@ -0,0 +1,68 @@
|
||||
'use strict';
|
||||
|
||||
const { PassThrough } = require('node:stream');
|
||||
const { NxDebugAdapter } = require('./debugAdapter');
|
||||
|
||||
function frameMessage(message) {
|
||||
const json = JSON.stringify(message);
|
||||
return `Content-Length: ${Buffer.byteLength(json, 'utf8')}\r\n\r\n${json}`;
|
||||
}
|
||||
|
||||
class NxInlineDebugAdapter {
|
||||
constructor() {
|
||||
this.input = new PassThrough();
|
||||
this.output = new PassThrough();
|
||||
this.outputBuffer = Buffer.alloc(0);
|
||||
this.listeners = new Set();
|
||||
this.disposed = false;
|
||||
|
||||
this.onDidSendMessage = (listener, thisArgs, disposables) => {
|
||||
const registration = { listener, thisArgs };
|
||||
this.listeners.add(registration);
|
||||
const disposable = {
|
||||
dispose: () => this.listeners.delete(registration),
|
||||
};
|
||||
if (Array.isArray(disposables)) disposables.push(disposable);
|
||||
return disposable;
|
||||
};
|
||||
|
||||
this.output.on('data', (chunk) => this.onOutput(chunk));
|
||||
this.adapter = new NxDebugAdapter(this.input, this.output);
|
||||
}
|
||||
|
||||
handleMessage(message) {
|
||||
if (!this.disposed) this.input.write(frameMessage(message));
|
||||
}
|
||||
|
||||
onOutput(chunk) {
|
||||
this.outputBuffer = Buffer.concat([this.outputBuffer, chunk]);
|
||||
while (true) {
|
||||
const headerEnd = this.outputBuffer.indexOf('\r\n\r\n');
|
||||
if (headerEnd < 0) return;
|
||||
const header = this.outputBuffer.subarray(0, headerEnd).toString('ascii');
|
||||
const match = /Content-Length:\s*(\d+)/i.exec(header);
|
||||
if (!match) throw new Error('missing DAP Content-Length header from NX adapter');
|
||||
const contentLength = Number(match[1]);
|
||||
const messageEnd = headerEnd + 4 + contentLength;
|
||||
if (this.outputBuffer.length < messageEnd) return;
|
||||
|
||||
const payload = this.outputBuffer.subarray(headerEnd + 4, messageEnd).toString('utf8');
|
||||
this.outputBuffer = this.outputBuffer.subarray(messageEnd);
|
||||
const message = JSON.parse(payload);
|
||||
for (const { listener, thisArgs } of [...this.listeners]) {
|
||||
listener.call(thisArgs, message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispose() {
|
||||
if (this.disposed) return;
|
||||
this.disposed = true;
|
||||
this.adapter.dispose();
|
||||
this.input.end();
|
||||
this.output.destroy();
|
||||
this.listeners.clear();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { NxInlineDebugAdapter, frameMessage };
|
||||
@@ -0,0 +1,116 @@
|
||||
'use strict';
|
||||
|
||||
const net = require('net');
|
||||
const { EventEmitter } = require('events');
|
||||
|
||||
function encodeHex(value) {
|
||||
return Buffer.from(String(value ?? ''), 'utf8').toString('hex') || '-';
|
||||
}
|
||||
|
||||
function decodeHex(value) {
|
||||
if (!value || value === '-') return '';
|
||||
return Buffer.from(value, 'hex').toString('utf8');
|
||||
}
|
||||
|
||||
class RuntimeConnection extends EventEmitter {
|
||||
constructor() {
|
||||
super();
|
||||
this.socket = null;
|
||||
this.buffer = '';
|
||||
this.nextRequestId = 1;
|
||||
this.pending = new Map();
|
||||
this.closed = false;
|
||||
}
|
||||
|
||||
connect(host, port) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const socket = net.createConnection({ host, port });
|
||||
const onError = (error) => {
|
||||
socket.destroy();
|
||||
reject(error);
|
||||
};
|
||||
socket.once('error', onError);
|
||||
socket.once('connect', () => {
|
||||
socket.removeListener('error', onError);
|
||||
this.socket = socket;
|
||||
this.closed = false;
|
||||
socket.setEncoding('utf8');
|
||||
socket.on('data', (chunk) => this.onData(chunk));
|
||||
socket.on('error', (error) => this.emit('runtimeError', error));
|
||||
socket.on('close', () => this.onClose());
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
request(command, fields = [], timeoutMs = 30000) {
|
||||
if (!this.socket || this.closed) {
|
||||
return Promise.reject(new Error('NX runtime is not connected'));
|
||||
}
|
||||
const id = this.nextRequestId++;
|
||||
const normalized = fields.map((field) => String(field ?? ''));
|
||||
if (normalized.some((field) => field.includes('\t') || field.includes('\n'))) {
|
||||
return Promise.reject(new Error('protocol fields must be hex-encoded before transmission'));
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
this.pending.delete(id);
|
||||
reject(new Error(`NX ${command} request timed out`));
|
||||
}, timeoutMs);
|
||||
this.pending.set(id, { resolve, reject, timer, command });
|
||||
this.socket.write(['REQ', id, command, ...normalized].join('\t') + '\n');
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.closed = true;
|
||||
if (this.socket) this.socket.destroy();
|
||||
this.socket = null;
|
||||
}
|
||||
|
||||
onData(chunk) {
|
||||
this.buffer += chunk;
|
||||
let newline;
|
||||
while ((newline = this.buffer.indexOf('\n')) >= 0) {
|
||||
const line = this.buffer.slice(0, newline).replace(/\r$/, '');
|
||||
this.buffer = this.buffer.slice(newline + 1);
|
||||
if (line) this.onLine(line);
|
||||
}
|
||||
}
|
||||
|
||||
onLine(line) {
|
||||
const fields = line.split('\t');
|
||||
if (fields[0] === 'RES') {
|
||||
const id = Number(fields[1]);
|
||||
const pending = this.pending.get(id);
|
||||
if (!pending) return;
|
||||
this.pending.delete(id);
|
||||
clearTimeout(pending.timer);
|
||||
if (fields[2] === 'OK') {
|
||||
pending.resolve({ command: fields[3], fields: fields.slice(4) });
|
||||
} else {
|
||||
pending.reject(new Error(decodeHex(fields[4]) || `NX ${pending.command} failed`));
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (fields[0] === 'EVENT') {
|
||||
this.emit('event', fields[1], fields.slice(2));
|
||||
return;
|
||||
}
|
||||
this.emit('runtimeError', new Error(`unrecognized NX message: ${line}`));
|
||||
}
|
||||
|
||||
onClose() {
|
||||
if (this.closed) return;
|
||||
this.closed = true;
|
||||
for (const pending of this.pending.values()) {
|
||||
clearTimeout(pending.timer);
|
||||
pending.reject(new Error('NX runtime disconnected'));
|
||||
}
|
||||
this.pending.clear();
|
||||
this.emit('close');
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { RuntimeConnection, encodeHex, decodeHex };
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as vscode from "vscode"
|
||||
|
||||
// The adapter is shared with the standalone NX Tcl Remote Debugger. It is
|
||||
// bundled by esbuild into this extension, so no child process or additional
|
||||
// VS Code extension is required.
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { NxInlineDebugAdapter } = require("./inlineAdapter")
|
||||
|
||||
class NxDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
|
||||
createDebugAdapterDescriptor(): vscode.ProviderResult<vscode.DebugAdapterDescriptor> {
|
||||
return new vscode.DebugAdapterInlineImplementation(new NxInlineDebugAdapter())
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDebugConfiguration(
|
||||
folder: vscode.WorkspaceFolder | undefined,
|
||||
config: vscode.DebugConfiguration
|
||||
): vscode.DebugConfiguration {
|
||||
const resolved = { ...config }
|
||||
if (!resolved.type) resolved.type = "nx-tcl"
|
||||
if (!resolved.request) resolved.request = "attach"
|
||||
if (!resolved.name) resolved.name = "Attach to NX Post Tcl"
|
||||
if (!resolved.host) resolved.host = "127.0.0.1"
|
||||
if (!resolved.port) resolved.port = 4711
|
||||
if (!resolved.connectTimeout) resolved.connectTimeout = 120000
|
||||
if (resolved.stopOnEntry === undefined) resolved.stopOnEntry = false
|
||||
if (resolved.breakOnError === undefined) resolved.breakOnError = true
|
||||
if (!resolved.localRoot && folder) resolved.localRoot = folder.uri.fsPath
|
||||
return resolved
|
||||
}
|
||||
|
||||
export function registerNxTclDebugger(context: vscode.ExtensionContext): void {
|
||||
const factory = new NxDebugAdapterFactory()
|
||||
context.subscriptions.push(
|
||||
vscode.debug.registerDebugAdapterDescriptorFactory("nx-tcl", factory),
|
||||
vscode.debug.registerDebugConfigurationProvider("nx-tcl", {
|
||||
resolveDebugConfiguration
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
export { resolveDebugConfiguration }
|
||||
+17
-2
@@ -29,10 +29,13 @@ import { checkIfConfigurationChanged, getInterpreterFromSetting } from "./common
|
||||
import { loadServerDefaults } from "./common/setup"
|
||||
import { getLSClientTraceLevel } from "./common/utilities"
|
||||
import { createOutputChannel, onDidChangeConfiguration, registerCommand } from "./common/vscodeapi"
|
||||
import { registerNxTclDebugger } from "./debugger/register"
|
||||
|
||||
let client: LanguageClient | undefined
|
||||
|
||||
export async function activate(context: vscode.ExtensionContext) {
|
||||
registerNxTclDebugger(context)
|
||||
|
||||
// This is required to get server name and module. This should be
|
||||
// the first thing that we do in this extension.
|
||||
const serverInfo = loadServerDefaults()
|
||||
@@ -70,7 +73,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
traceVerbose(
|
||||
`Using interpreter from ${serverInfo.module}.interpreter: ${interpreter.join(" ")}`
|
||||
)
|
||||
client = await restartServer(serverId, serverName, outputChannel, client)
|
||||
client = await restartServer(
|
||||
serverId,
|
||||
serverName,
|
||||
outputChannel,
|
||||
client,
|
||||
context.storageUri?.fsPath
|
||||
)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -80,7 +89,13 @@ export async function activate(context: vscode.ExtensionContext) {
|
||||
traceVerbose(
|
||||
`Using interpreter from Python extension: ${interpreterDetails.path.join(" ")}`
|
||||
)
|
||||
client = await restartServer(serverId, serverName, outputChannel, client)
|
||||
client = await restartServer(
|
||||
serverId,
|
||||
serverName,
|
||||
outputChannel,
|
||||
client,
|
||||
context.storageUri?.fsPath
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "nx-post-support",
|
||||
"version": "2025.9.200",
|
||||
"version": "2026.9.501",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nx-post-support",
|
||||
"version": "2025.9.200",
|
||||
"version": "2026.9.501",
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.96.0",
|
||||
"@vscode/vsce": "^3.2.1",
|
||||
|
||||
+106
-5
@@ -1,10 +1,16 @@
|
||||
{
|
||||
"name": "nx-post-support",
|
||||
"displayName": "NX Postprocessor Support",
|
||||
"description": "VS Code extension for NX CAM postprocessor development with syntax highlighting, formatting, linting, and auto-completion for CDL, TCL, and DEF files",
|
||||
"version": "2026.8.100",
|
||||
"description": "VS Code extension for NX CAM postprocessor development with language support and remote Tcl debugging for CDL, TCL, and DEF files",
|
||||
"version": "2026.9.600",
|
||||
"publisher": "Christoph",
|
||||
"icon": "images/nx-1.png",
|
||||
"activationEvents": [
|
||||
"onLanguage:tcl",
|
||||
"onLanguage:cdl",
|
||||
"onLanguage:def",
|
||||
"onDebug"
|
||||
],
|
||||
"extensionDependencies": [
|
||||
"ms-python.python"
|
||||
],
|
||||
@@ -23,15 +29,109 @@
|
||||
"UDE",
|
||||
"Postprocessor",
|
||||
"NX CAM",
|
||||
"Siemens NX"
|
||||
"Siemens NX",
|
||||
"debugger",
|
||||
"remote debugging"
|
||||
],
|
||||
"engines": {
|
||||
"vscode": "^1.96.0"
|
||||
},
|
||||
"categories": [
|
||||
"Programming Languages"
|
||||
"Programming Languages",
|
||||
"Debuggers"
|
||||
],
|
||||
"contributes": {
|
||||
"breakpoints": [
|
||||
{
|
||||
"language": "tcl"
|
||||
},
|
||||
{
|
||||
"language": "def"
|
||||
}
|
||||
],
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "nx-tcl",
|
||||
"label": "NX Tcl Remote Debugger",
|
||||
"languages": [
|
||||
"tcl",
|
||||
"def"
|
||||
],
|
||||
"configurationAttributes": {
|
||||
"attach": {
|
||||
"required": [
|
||||
"host",
|
||||
"port"
|
||||
],
|
||||
"properties": {
|
||||
"host": {
|
||||
"type": "string",
|
||||
"default": "127.0.0.1",
|
||||
"description": "Host on which the NX Tcl runtime is listening."
|
||||
},
|
||||
"port": {
|
||||
"type": "number",
|
||||
"default": 4711,
|
||||
"description": "TCP port of the NX Tcl runtime."
|
||||
},
|
||||
"connectTimeout": {
|
||||
"type": "number",
|
||||
"default": 120000,
|
||||
"description": "How long VS Code retries until NX starts the runtime, in milliseconds."
|
||||
},
|
||||
"stopOnEntry": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Stop on the first traced Tcl command."
|
||||
},
|
||||
"breakOnError": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Stop when a traced Tcl command returns TCL_ERROR."
|
||||
},
|
||||
"localRoot": {
|
||||
"type": "string",
|
||||
"description": "Local source root opened in VS Code."
|
||||
},
|
||||
"remoteRoot": {
|
||||
"type": "string",
|
||||
"description": "Corresponding source root on the NX host when it differs from localRoot."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"initialConfigurations": [
|
||||
{
|
||||
"type": "nx-tcl",
|
||||
"request": "attach",
|
||||
"name": "Attach to NX Post Tcl",
|
||||
"host": "127.0.0.1",
|
||||
"port": 4711,
|
||||
"connectTimeout": 120000,
|
||||
"stopOnEntry": false,
|
||||
"breakOnError": true,
|
||||
"localRoot": "${workspaceFolder}"
|
||||
}
|
||||
],
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "NX Tcl: Attach to NX Post",
|
||||
"description": "Attach VS Code to an NX runtime embedded in NX Post.",
|
||||
"body": {
|
||||
"type": "nx-tcl",
|
||||
"request": "attach",
|
||||
"name": "Attach to NX Post Tcl",
|
||||
"host": "127.0.0.1",
|
||||
"port": 4711,
|
||||
"connectTimeout": 120000,
|
||||
"stopOnEntry": false,
|
||||
"breakOnError": true,
|
||||
"localRoot": "${workspaceFolder}"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"languages": [
|
||||
{
|
||||
"id": "tcl",
|
||||
@@ -151,7 +251,8 @@
|
||||
"compile": "node esbuild.js --production",
|
||||
"compile:debug": "node esbuild.js",
|
||||
"watch": "node esbuild.js --watch",
|
||||
"package": "node esbuild.js --production"
|
||||
"package": "node esbuild.js --production",
|
||||
"test:debugger": "node --test test/debugger.test.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/vscode": "^1.96.0",
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
class ExceptionGroup(Exception):
|
||||
"""Minimal backport used by bundled libs on Python < 3.11."""
|
||||
|
||||
def __new__(cls, message, exceptions):
|
||||
obj = super().__new__(cls, message)
|
||||
obj.message = message
|
||||
obj.exceptions = tuple(exceptions)
|
||||
return obj
|
||||
|
||||
def __init__(self, message, exceptions):
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.exceptions = tuple(exceptions)
|
||||
|
||||
def derive(self, exceptions):
|
||||
return self.__class__(self.message, exceptions)
|
||||
@@ -1,12 +0,0 @@
|
||||
lsprotocol-2023.0.1.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
lsprotocol-2023.0.1.dist-info/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
|
||||
lsprotocol-2023.0.1.dist-info/METADATA,sha256=oh7M_V0nCX-lx8MCik5z0_J8Wyd7ApJtdl30wWs4Tb8,2237
|
||||
lsprotocol-2023.0.1.dist-info/RECORD,,
|
||||
lsprotocol-2023.0.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
lsprotocol-2023.0.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81
|
||||
lsprotocol/__init__.py,sha256=zoT6Do2JtGHGb7pOeKpahg4ocXIsSpyowjhOrhUhx8g,94
|
||||
lsprotocol/_hooks.py,sha256=PCTq4Ve_dDd02DMcWQ8afu9gj_oyX0B4nDOomPghqYs,41570
|
||||
lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433
|
||||
lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
lsprotocol/types.py,sha256=nZYiI5ZvHEBkRYjtPd8tpqe0n2NpmdaCZ2RLwHxoMLs,454735
|
||||
lsprotocol/validators.py,sha256=5UMUmWhk52_Ps66_KFydjNkMfLNUsPH3wmV0Buv645s,1420
|
||||
+7
-7
@@ -1,15 +1,14 @@
|
||||
Metadata-Version: 2.1
|
||||
Metadata-Version: 2.4
|
||||
Name: lsprotocol
|
||||
Version: 2023.0.1
|
||||
Summary: Python implementation of the Language Server Protocol.
|
||||
Version: 2025.0.0
|
||||
Summary: Python types for Language Server Protocol.
|
||||
Author-email: Microsoft Corporation <lsprotocol-help@microsoft.com>
|
||||
Maintainer-email: Brett Cannon <brett@python.org>, Karthik Nadig <kanadig@microsoft.com>
|
||||
Requires-Python: >=3.7
|
||||
Requires-Python: >=3.8
|
||||
Description-Content-Type: text/markdown
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
Classifier: Development Status :: 4 - Beta
|
||||
Classifier: License :: OSI Approved :: MIT License
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3.7
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
@@ -17,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
License-File: LICENSE
|
||||
Requires-Dist: attrs>=21.3.0
|
||||
Requires-Dist: cattrs!=23.2.1
|
||||
Project-URL: Issues, https://github.com/microsoft/lsprotocol/issues
|
||||
@@ -24,7 +24,7 @@ Project-URL: Source, https://github.com/microsoft/lsprotocol
|
||||
|
||||
# Language Server Protocol Types implementation for Python
|
||||
|
||||
`lsprotocol` is a python implementation of object types used in the Language Server Protocol (LSP). This repository contains the code generator and the generated types for LSP.
|
||||
`lsprotocol` is a Python implementation of object types used in the Language Server Protocol (LSP). This repository contains the code generator and the generated types for LSP.
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
lsprotocol-2025.0.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
lsprotocol-2025.0.0.dist-info/METADATA,sha256=u3Lb5ZzZi4gH18WQWVPurP9kBvqHDY5U-Utafh8O7Bc,2184
|
||||
lsprotocol-2025.0.0.dist-info/RECORD,,
|
||||
lsprotocol-2025.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
lsprotocol-2025.0.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
lsprotocol-2025.0.0.dist-info/licenses/LICENSE,sha256=ws_MuBL-SCEBqPBFl9_FqZkaaydIJmxHrJG2parhU4M,1141
|
||||
lsprotocol/__init__.py,sha256=zoT6Do2JtGHGb7pOeKpahg4ocXIsSpyowjhOrhUhx8g,94
|
||||
lsprotocol/_hooks.py,sha256=dp-HEi_z7CqTz0cgzCYfT_s32Sr1hcrLlu3ff42lBXI,44624
|
||||
lsprotocol/converters.py,sha256=404tQOVoZL31R9CBrDe6Gx9Nok5cRph3glKlnSx00fo,433
|
||||
lsprotocol/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
lsprotocol/types.py,sha256=LJbn0uKWsPpveLZG2Wswr-1Gn2Tfimue3zV71xVA5SE,476733
|
||||
lsprotocol/validators.py,sha256=5UMUmWhk52_Ps66_KFydjNkMfLNUsPH3wmV0Buv645s,1420
|
||||
@@ -1,7 +1,7 @@
|
||||
# Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
# Licensed under the MIT License.
|
||||
import sys
|
||||
from typing import Any, List, Optional, Tuple, Union
|
||||
from typing import Any, Optional, Sequence, Tuple, Union
|
||||
|
||||
import attrs
|
||||
import cattrs
|
||||
@@ -27,7 +27,7 @@ def _resolve_forward_references() -> None:
|
||||
items = list(filter(_filter, lsp_types.ALL_TYPES_MAP.items()))
|
||||
for _, value in items:
|
||||
if isinstance(value, type):
|
||||
attrs.resolve_types(value, lsp_types.ALL_TYPES_MAP, {}) # type: ignore
|
||||
attrs.resolve_types(value, lsp_types.ALL_TYPES_MAP, {})
|
||||
_resolved_forward_references = True
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
|
||||
def _inlay_hint_label_part_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[str, List[lsp_types.InlayHintLabelPart]]:
|
||||
) -> Union[str, Sequence[lsp_types.InlayHintLabelPart]]:
|
||||
if isinstance(object_, str):
|
||||
return object_
|
||||
|
||||
@@ -431,7 +431,7 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
|
||||
def _completion_list_hook(
|
||||
object_: Any, _: type
|
||||
) -> Optional[Union[lsp_types.CompletionList, List[lsp_types.CompletionItem]]]:
|
||||
) -> Optional[Union[lsp_types.CompletionList, Sequence[lsp_types.CompletionItem]]]:
|
||||
if object_ is None:
|
||||
return None
|
||||
if isinstance(object_, list):
|
||||
@@ -446,8 +446,8 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
) -> Optional[
|
||||
Union[
|
||||
lsp_types.Location,
|
||||
List[lsp_types.Location],
|
||||
List[lsp_types.LocationLink],
|
||||
Sequence[lsp_types.Location],
|
||||
Sequence[lsp_types.LocationLink],
|
||||
]
|
||||
]:
|
||||
if object_ is None:
|
||||
@@ -470,7 +470,7 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
def _symbol_hook(
|
||||
object_: Any, _: type
|
||||
) -> Optional[
|
||||
Union[List[lsp_types.DocumentSymbol], List[lsp_types.SymbolInformation]]
|
||||
Union[Sequence[lsp_types.DocumentSymbol], Sequence[lsp_types.SymbolInformation]]
|
||||
]:
|
||||
if object_ is None:
|
||||
return None
|
||||
@@ -496,8 +496,8 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
Union[
|
||||
OptionalPrimitive,
|
||||
lsp_types.MarkupContent,
|
||||
lsp_types.MarkedString_Type1,
|
||||
List[Union[OptionalPrimitive, lsp_types.MarkedString_Type1]],
|
||||
lsp_types.MarkedStringWithLanguage,
|
||||
Sequence[Union[OptionalPrimitive, lsp_types.MarkedStringWithLanguage]],
|
||||
]
|
||||
]:
|
||||
if object_ is None:
|
||||
@@ -509,14 +509,14 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
(
|
||||
item
|
||||
if isinstance(item, (bool, int, str, float))
|
||||
else converter.structure(item, lsp_types.MarkedString_Type1)
|
||||
else converter.structure(item, lsp_types.MarkedStringWithLanguage)
|
||||
)
|
||||
for item in object_
|
||||
]
|
||||
if "kind" in object_:
|
||||
return converter.structure(object_, lsp_types.MarkupContent)
|
||||
else:
|
||||
return converter.structure(object_, lsp_types.MarkedString_Type1)
|
||||
return converter.structure(object_, lsp_types.MarkedStringWithLanguage)
|
||||
|
||||
def _document_edit_hook(
|
||||
object_: Any, _: type
|
||||
@@ -544,25 +544,25 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
|
||||
def _semantic_tokens_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[OptionalPrimitive, lsp_types.SemanticTokensOptionsFullType1]:
|
||||
) -> Union[OptionalPrimitive, lsp_types.SemanticTokensFullDelta]:
|
||||
if object_ is None:
|
||||
return None
|
||||
if isinstance(object_, (bool, int, str, float)):
|
||||
return object_
|
||||
return converter.structure(object_, lsp_types.SemanticTokensOptionsFullType1)
|
||||
return converter.structure(object_, lsp_types.SemanticTokensFullDelta)
|
||||
|
||||
def _semantic_tokens_capabilities_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[
|
||||
OptionalPrimitive,
|
||||
lsp_types.SemanticTokensClientCapabilitiesRequestsTypeFullType1,
|
||||
lsp_types.ClientSemanticTokensRequestFullDelta,
|
||||
]:
|
||||
if object_ is None:
|
||||
return None
|
||||
if isinstance(object_, (bool, int, str, float)):
|
||||
return object_
|
||||
return converter.structure(
|
||||
object_, lsp_types.SemanticTokensClientCapabilitiesRequestsTypeFullType1
|
||||
object_, lsp_types.ClientSemanticTokensRequestFullDelta
|
||||
)
|
||||
|
||||
def _code_action_kind_hook(
|
||||
@@ -622,29 +622,29 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
def _notebook_sync_option_selector_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[
|
||||
lsp_types.NotebookDocumentSyncOptionsNotebookSelectorType1,
|
||||
lsp_types.NotebookDocumentSyncOptionsNotebookSelectorType2,
|
||||
lsp_types.NotebookDocumentFilterWithNotebook,
|
||||
lsp_types.NotebookDocumentFilterWithCells,
|
||||
]:
|
||||
if "notebook" in object_:
|
||||
return converter.structure(
|
||||
object_, lsp_types.NotebookDocumentSyncOptionsNotebookSelectorType1
|
||||
object_, lsp_types.NotebookDocumentFilterWithNotebook
|
||||
)
|
||||
else:
|
||||
return converter.structure(
|
||||
object_, lsp_types.NotebookDocumentSyncOptionsNotebookSelectorType2
|
||||
object_, lsp_types.NotebookDocumentFilterWithCells
|
||||
)
|
||||
|
||||
def _semantic_token_registration_options_hook(
|
||||
object_: Any, _: type
|
||||
) -> Optional[
|
||||
Union[OptionalPrimitive, lsp_types.SemanticTokensRegistrationOptionsFullType1]
|
||||
Union[OptionalPrimitive, lsp_types.ClientSemanticTokensRequestFullDelta]
|
||||
]:
|
||||
if object_ is None:
|
||||
return None
|
||||
if isinstance(object_, (bool, int, str, float)):
|
||||
return object_
|
||||
return converter.structure(
|
||||
object_, lsp_types.SemanticTokensRegistrationOptionsFullType1
|
||||
object_, lsp_types.ClientSemanticTokensRequestFullDelta
|
||||
)
|
||||
|
||||
def _inline_completion_provider_hook(
|
||||
@@ -659,7 +659,7 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
def _inline_completion_list_hook(
|
||||
object_: Any, _: type
|
||||
) -> Optional[
|
||||
Union[lsp_types.InlineCompletionList, List[lsp_types.InlineCompletionItem]]
|
||||
Union[lsp_types.InlineCompletionList, Sequence[lsp_types.InlineCompletionItem]]
|
||||
]:
|
||||
if object_ is None:
|
||||
return None
|
||||
@@ -682,7 +682,9 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
def _symbol_list_hook(
|
||||
object_: Any, _: type
|
||||
) -> Optional[
|
||||
Union[List[lsp_types.SymbolInformation], List[lsp_types.WorkspaceSymbol]]
|
||||
Union[
|
||||
Sequence[lsp_types.SymbolInformation], Sequence[lsp_types.WorkspaceSymbol]
|
||||
]
|
||||
]:
|
||||
if object_ is None:
|
||||
return None
|
||||
@@ -703,22 +705,71 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
converter.structure(item, lsp_types.SymbolInformation) for item in object_
|
||||
]
|
||||
|
||||
def _notebook_sync_registration_option_selector_hook(
|
||||
def _language_kind_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[
|
||||
lsp_types.NotebookDocumentSyncRegistrationOptionsNotebookSelectorType1,
|
||||
lsp_types.NotebookDocumentSyncRegistrationOptionsNotebookSelectorType2,
|
||||
lsp_types.LanguageKind,
|
||||
OptionalPrimitive,
|
||||
]:
|
||||
if "notebook" in object_:
|
||||
if object_ is None:
|
||||
return None
|
||||
if isinstance(object_, (bool, int, str, float)):
|
||||
return object_
|
||||
return converter.structure(object_, lsp_types.LanguageKind)
|
||||
|
||||
def _text_edit_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[
|
||||
lsp_types.TextEdit, lsp_types.AnnotatedTextEdit, lsp_types.SnippetTextEdit
|
||||
]:
|
||||
if "snippet" in object_:
|
||||
return converter.structure(object_, lsp_types.SnippetTextEdit)
|
||||
if "annotationId" in object_:
|
||||
return converter.structure(object_, lsp_types.AnnotatedTextEdit)
|
||||
return converter.structure(object_, lsp_types.TextEdit)
|
||||
|
||||
def _completion_item_kind_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[lsp_types.CompletionItemKind, OptionalPrimitive]:
|
||||
if object_ is None:
|
||||
return None
|
||||
if isinstance(object_, (bool, int, str, float)):
|
||||
return object_
|
||||
return converter.structure(object_, lsp_types.CompletionItemKind)
|
||||
|
||||
def _relative_pattern_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[OptionalPrimitive, lsp_types.RelativePattern]:
|
||||
if object_ is None:
|
||||
return None
|
||||
if isinstance(object_, (bool, int, str, float)):
|
||||
return object_
|
||||
return converter.structure(object_, lsp_types.RelativePattern)
|
||||
|
||||
def _workspace_folder_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[OptionalPrimitive, lsp_types.WorkspaceFolder]:
|
||||
if object_ is None:
|
||||
return None
|
||||
if isinstance(object_, (bool, int, str, float)):
|
||||
return object_
|
||||
return converter.structure(object_, lsp_types.WorkspaceFolder)
|
||||
|
||||
def _text_document_content_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[
|
||||
OptionalPrimitive,
|
||||
lsp_types.TextDocumentContentRegistrationOptions,
|
||||
lsp_types.TextDocumentContentOptions,
|
||||
]:
|
||||
if object_ is None:
|
||||
return None
|
||||
if "id" in object_:
|
||||
return converter.structure(
|
||||
object_,
|
||||
lsp_types.NotebookDocumentSyncRegistrationOptionsNotebookSelectorType1,
|
||||
object_, lsp_types.TextDocumentContentRegistrationOptions
|
||||
)
|
||||
else:
|
||||
return converter.structure(
|
||||
object_,
|
||||
lsp_types.NotebookDocumentSyncRegistrationOptionsNotebookSelectorType2,
|
||||
)
|
||||
return converter.structure(object_, lsp_types.TextDocumentContentOptions)
|
||||
|
||||
structure_hooks = [
|
||||
(
|
||||
@@ -892,7 +943,7 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
_inlay_hint_provider_hook,
|
||||
),
|
||||
(
|
||||
Union[str, List[lsp_types.InlayHintLabelPart]],
|
||||
Union[str, Sequence[lsp_types.InlayHintLabelPart]],
|
||||
_inlay_hint_label_part_hook,
|
||||
),
|
||||
(
|
||||
@@ -912,22 +963,27 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
_code_action_hook,
|
||||
),
|
||||
(
|
||||
Optional[Union[List[lsp_types.CompletionItem], lsp_types.CompletionList]],
|
||||
Optional[
|
||||
Union[Sequence[lsp_types.CompletionItem], lsp_types.CompletionList]
|
||||
],
|
||||
_completion_list_hook,
|
||||
),
|
||||
(
|
||||
Optional[
|
||||
Union[
|
||||
lsp_types.Location,
|
||||
List[lsp_types.Location],
|
||||
List[lsp_types.LocationLink],
|
||||
Sequence[lsp_types.Location],
|
||||
Sequence[lsp_types.LocationLink],
|
||||
]
|
||||
],
|
||||
_location_hook,
|
||||
),
|
||||
(
|
||||
Optional[
|
||||
Union[List[lsp_types.SymbolInformation], List[lsp_types.DocumentSymbol]]
|
||||
Union[
|
||||
Sequence[lsp_types.SymbolInformation],
|
||||
Sequence[lsp_types.DocumentSymbol],
|
||||
]
|
||||
],
|
||||
_symbol_hook,
|
||||
),
|
||||
@@ -935,8 +991,8 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
Union[
|
||||
lsp_types.MarkupContent,
|
||||
str,
|
||||
lsp_types.MarkedString_Type1,
|
||||
List[Union[str, lsp_types.MarkedString_Type1]],
|
||||
lsp_types.MarkedStringWithLanguage,
|
||||
Sequence[Union[str, lsp_types.MarkedStringWithLanguage]],
|
||||
],
|
||||
_markup_content_hook,
|
||||
),
|
||||
@@ -950,14 +1006,14 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
_document_edit_hook,
|
||||
),
|
||||
(
|
||||
Optional[Union[bool, lsp_types.SemanticTokensOptionsFullType1]],
|
||||
Optional[Union[bool, lsp_types.SemanticTokensFullDelta]],
|
||||
_semantic_tokens_hook,
|
||||
),
|
||||
(
|
||||
Optional[
|
||||
Union[
|
||||
bool,
|
||||
lsp_types.SemanticTokensClientCapabilitiesRequestsTypeFullType1,
|
||||
lsp_types.ClientSemanticTokensRequestFullDelta,
|
||||
]
|
||||
],
|
||||
_semantic_tokens_capabilities_hook,
|
||||
@@ -1012,8 +1068,8 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
),
|
||||
(
|
||||
Union[
|
||||
lsp_types.NotebookDocumentSyncOptionsNotebookSelectorType1,
|
||||
lsp_types.NotebookDocumentSyncOptionsNotebookSelectorType2,
|
||||
lsp_types.NotebookDocumentFilterWithNotebook,
|
||||
lsp_types.NotebookDocumentFilterWithCells,
|
||||
],
|
||||
_notebook_sync_option_selector_hook,
|
||||
),
|
||||
@@ -1027,7 +1083,7 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
_position_encoding_hook,
|
||||
),
|
||||
(
|
||||
Optional[Union[bool, lsp_types.SemanticTokensRegistrationOptionsFullType1]],
|
||||
Optional[Union[bool, lsp_types.ClientSemanticTokensRequestFullDelta]],
|
||||
_semantic_token_registration_options_hook,
|
||||
),
|
||||
(
|
||||
@@ -1037,7 +1093,8 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
(
|
||||
Optional[
|
||||
Union[
|
||||
lsp_types.InlineCompletionList, List[lsp_types.InlineCompletionItem]
|
||||
lsp_types.InlineCompletionList,
|
||||
Sequence[lsp_types.InlineCompletionItem],
|
||||
]
|
||||
],
|
||||
_inline_completion_list_hook,
|
||||
@@ -1049,17 +1106,63 @@ def _register_capabilities_hooks(converter: cattrs.Converter) -> cattrs.Converte
|
||||
(
|
||||
Optional[
|
||||
Union[
|
||||
List[lsp_types.SymbolInformation], List[lsp_types.WorkspaceSymbol]
|
||||
Sequence[lsp_types.SymbolInformation],
|
||||
Sequence[lsp_types.WorkspaceSymbol],
|
||||
]
|
||||
],
|
||||
_symbol_list_hook,
|
||||
),
|
||||
(
|
||||
Union[lsp_types.LanguageKind, str],
|
||||
_language_kind_hook,
|
||||
),
|
||||
(
|
||||
Union[
|
||||
lsp_types.NotebookDocumentSyncRegistrationOptionsNotebookSelectorType1,
|
||||
lsp_types.NotebookDocumentSyncRegistrationOptionsNotebookSelectorType2,
|
||||
lsp_types.TextEdit,
|
||||
lsp_types.AnnotatedTextEdit,
|
||||
lsp_types.SnippetTextEdit,
|
||||
],
|
||||
_notebook_sync_registration_option_selector_hook,
|
||||
_text_edit_hook,
|
||||
),
|
||||
(
|
||||
Optional[Union[lsp_types.CompletionItemKind, int]],
|
||||
_completion_item_kind_hook,
|
||||
),
|
||||
(
|
||||
Union[lsp_types.CompletionItemKind, int],
|
||||
_completion_item_kind_hook,
|
||||
),
|
||||
(
|
||||
Optional[Union[str, lsp_types.RelativePattern]],
|
||||
_relative_pattern_hook,
|
||||
),
|
||||
(
|
||||
Union[str, lsp_types.RelativePattern],
|
||||
_relative_pattern_hook,
|
||||
),
|
||||
(
|
||||
Optional[Union[lsp_types.WorkspaceFolder, str]],
|
||||
_workspace_folder_hook,
|
||||
),
|
||||
(
|
||||
Union[lsp_types.WorkspaceFolder, str],
|
||||
_workspace_folder_hook,
|
||||
),
|
||||
(
|
||||
Optional[
|
||||
Union[
|
||||
lsp_types.TextDocumentContentOptions,
|
||||
lsp_types.TextDocumentContentRegistrationOptions,
|
||||
]
|
||||
],
|
||||
_text_document_content_hook,
|
||||
),
|
||||
(
|
||||
Union[
|
||||
lsp_types.TextDocumentContentOptions,
|
||||
lsp_types.TextDocumentContentRegistrationOptions,
|
||||
],
|
||||
_text_document_content_hook,
|
||||
),
|
||||
]
|
||||
for type_, hook in structure_hooks:
|
||||
@@ -1085,9 +1188,9 @@ def _register_required_structure_hooks(
|
||||
object_: Any, _: type
|
||||
) -> Union[
|
||||
str,
|
||||
lsp_types.TextDocumentFilter_Type1,
|
||||
lsp_types.TextDocumentFilter_Type2,
|
||||
lsp_types.TextDocumentFilter_Type3,
|
||||
lsp_types.TextDocumentFilterLanguage,
|
||||
lsp_types.TextDocumentFilterScheme,
|
||||
lsp_types.TextDocumentFilterPattern,
|
||||
lsp_types.NotebookCellTextDocumentFilter,
|
||||
]:
|
||||
if isinstance(object_, str):
|
||||
@@ -1097,30 +1200,31 @@ def _register_required_structure_hooks(
|
||||
object_, lsp_types.NotebookCellTextDocumentFilter
|
||||
)
|
||||
elif "language" in object_:
|
||||
return converter.structure(object_, lsp_types.TextDocumentFilter_Type1)
|
||||
return converter.structure(object_, lsp_types.TextDocumentFilterLanguage)
|
||||
elif "scheme" in object_:
|
||||
return converter.structure(object_, lsp_types.TextDocumentFilter_Type2)
|
||||
return converter.structure(object_, lsp_types.TextDocumentFilterScheme)
|
||||
else:
|
||||
return converter.structure(object_, lsp_types.TextDocumentFilter_Type3)
|
||||
return converter.structure(object_, lsp_types.TextDocumentFilterPattern)
|
||||
|
||||
def _notebook_filter_hook(
|
||||
object_: Any, _: type
|
||||
) -> Union[
|
||||
str,
|
||||
lsp_types.NotebookDocumentFilter_Type1,
|
||||
lsp_types.NotebookDocumentFilter_Type2,
|
||||
lsp_types.NotebookDocumentFilter_Type3,
|
||||
lsp_types.NotebookDocumentFilterNotebookType,
|
||||
lsp_types.NotebookDocumentFilterScheme,
|
||||
lsp_types.NotebookDocumentFilterPattern,
|
||||
]:
|
||||
if isinstance(object_, str):
|
||||
return str(object_)
|
||||
elif "notebookType" in object_:
|
||||
return converter.structure(object_, lsp_types.NotebookDocumentFilter_Type1)
|
||||
return converter.structure(
|
||||
object_, lsp_types.NotebookDocumentFilterNotebookType
|
||||
)
|
||||
elif "scheme" in object_:
|
||||
return converter.structure(object_, lsp_types.NotebookDocumentFilter_Type2)
|
||||
return converter.structure(object_, lsp_types.NotebookDocumentFilterScheme)
|
||||
else:
|
||||
return converter.structure(object_, lsp_types.NotebookDocumentFilter_Type3)
|
||||
return converter.structure(object_, lsp_types.NotebookDocumentFilterPattern)
|
||||
|
||||
# TODO: Remove the ignore after this issue with attrs is addressed in either attrs or mypy
|
||||
NotebookSelectorItem = attrs.fields(
|
||||
lsp_types.NotebookCellTextDocumentFilter
|
||||
).notebook.type
|
||||
@@ -1133,9 +1237,9 @@ def _register_required_structure_hooks(
|
||||
(Optional[Union[bool, Any]], lambda object_, _type: object_),
|
||||
(
|
||||
Union[
|
||||
lsp_types.TextDocumentFilter_Type1,
|
||||
lsp_types.TextDocumentFilter_Type2,
|
||||
lsp_types.TextDocumentFilter_Type3,
|
||||
lsp_types.TextDocumentFilterLanguage,
|
||||
lsp_types.TextDocumentFilterScheme,
|
||||
lsp_types.TextDocumentFilterPattern,
|
||||
lsp_types.NotebookCellTextDocumentFilter,
|
||||
],
|
||||
_text_document_filter_hook,
|
||||
@@ -1144,20 +1248,26 @@ def _register_required_structure_hooks(
|
||||
(
|
||||
Union[
|
||||
str,
|
||||
lsp_types.NotebookDocumentFilter_Type1,
|
||||
lsp_types.NotebookDocumentFilter_Type2,
|
||||
lsp_types.NotebookDocumentFilter_Type3,
|
||||
lsp_types.NotebookDocumentFilterNotebookType,
|
||||
lsp_types.NotebookDocumentFilterScheme,
|
||||
lsp_types.NotebookDocumentFilterPattern,
|
||||
],
|
||||
_notebook_filter_hook,
|
||||
),
|
||||
(NotebookSelectorItem, _notebook_filter_hook),
|
||||
(
|
||||
Union[lsp_types.LSPObject, List["LSPAny"], str, int, float, bool, None],
|
||||
Union[lsp_types.LSPObject, Sequence["LSPAny"], str, int, float, bool, None],
|
||||
_lsp_object_hook,
|
||||
),
|
||||
(
|
||||
Union[
|
||||
lsp_types.LSPObject, List[lsp_types.LSPAny], str, int, float, bool, None
|
||||
lsp_types.LSPObject,
|
||||
Sequence[lsp_types.LSPAny],
|
||||
str,
|
||||
int,
|
||||
float,
|
||||
bool,
|
||||
None,
|
||||
],
|
||||
_lsp_object_hook,
|
||||
),
|
||||
@@ -1173,10 +1283,10 @@ def _register_required_structure_hooks(
|
||||
(
|
||||
Union[
|
||||
lsp_types.LSPObject,
|
||||
List[
|
||||
Sequence[
|
||||
Union[
|
||||
lsp_types.LSPObject,
|
||||
List["LSPAny"],
|
||||
Sequence["LSPAny"],
|
||||
str,
|
||||
int,
|
||||
float,
|
||||
@@ -1220,7 +1330,7 @@ def _register_custom_property_hooks(converter: cattrs.Converter) -> cattrs.Conve
|
||||
)
|
||||
for a in attrs.fields(cls)
|
||||
}
|
||||
return cattrs.gen.make_dict_unstructure_fn(cls, converter, **attributes)
|
||||
return cattrs.gen.make_dict_unstructure_fn(cls, converter, **attributes) # type: ignore
|
||||
|
||||
def _with_custom_structure(cls: type) -> Any:
|
||||
attributes = {
|
||||
@@ -1230,7 +1340,7 @@ def _register_custom_property_hooks(converter: cattrs.Converter) -> cattrs.Conve
|
||||
)
|
||||
for a in attrs.fields(cls)
|
||||
}
|
||||
return cattrs.gen.make_dict_structure_fn(cls, converter, **attributes)
|
||||
return cattrs.gen.make_dict_structure_fn(cls, converter, **attributes) # type: ignore
|
||||
|
||||
converter.register_unstructure_hook_factory(attrs.has, _with_custom_unstructure)
|
||||
converter.register_structure_hook_factory(attrs.has, _with_custom_structure)
|
||||
|
||||
+3310
-2203
File diff suppressed because it is too large
Load Diff
@@ -1,29 +0,0 @@
|
||||
packaging-26.2.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
packaging-26.2.dist-info/METADATA,sha256=T5y815M0FaR5P3dnyYoralEsgj_IHIczeBVwXyMOyr8,3543
|
||||
packaging-26.2.dist-info/RECORD,,
|
||||
packaging-26.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
packaging-26.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
packaging-26.2.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
|
||||
packaging-26.2.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
||||
packaging-26.2.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
|
||||
packaging/__init__.py,sha256=QhMEdPu2XogrJzV3S0KWS6t7l0I9k8EeDRJl4fnw87s,494
|
||||
packaging/_elffile.py,sha256=-sKkptYqzYw2-x3QByJa5mB4rfPWu1pxkZHRx1WAFCY,3211
|
||||
packaging/_manylinux.py,sha256=Hf6nB0cOrayEs96-p3oIXAgGnFquv20DO5l-o2_Xnv0,9559
|
||||
packaging/_musllinux.py,sha256=Z6swjH3MA7XS3qXnmMN7QPhqP3fnoYI0eQ18e9-HgAE,2707
|
||||
packaging/_parser.py,sha256=Kf2nsDw4c54X82pY8ba4F02Bve6OygGMAjL-Begqcew,11698
|
||||
packaging/_structures.py,sha256=60jRbF78p8z5MKnNd6cAprgOadCJHV0DlmUmRBqFZcs,1109
|
||||
packaging/_tokenizer.py,sha256=tFU2Wr-ZZJdAbkXLEJo7qUQDJaIkfft9DqaifiEND7A,5391
|
||||
packaging/dependency_groups.py,sha256=XZIAVFK9uHG4RCGprmJn3VInUWMesxha_kytJuMO9eY,10218
|
||||
packaging/direct_url.py,sha256=eKmbDiPP1sLV4Mj_kCSZqqknrIyVO9Sr7JpF8KCjp4U,10917
|
||||
packaging/errors.py,sha256=6hfEYXAf8v_IF65-lFadJOMIieBP2xIKtyEXjG1nGIs,2680
|
||||
packaging/licenses/__init__.py,sha256=_Jx0XRiD_58palsWnyLrLuh59ZpGCPIPXLKdZo9OJvQ,7293
|
||||
packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
|
||||
packaging/markers.py,sha256=8fDIUhAF6YMnCNB5FSiwh9pEIusiFzAF73J-0OB8bTk,17055
|
||||
packaging/metadata.py,sha256=crAh0E3GVGVqPlu6EdRFsaG-Y6UYznTUqjuGKRGPv6c,38770
|
||||
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
packaging/pylock.py,sha256=G_1gncTmDbRLY1jo4VDI9Uw-b5IErh_Q9V_BbVJTmD8,33890
|
||||
packaging/requirements.py,sha256=dd1c9aa1gp5NI6btF6UFRQjPn1nxQXnE_T34yDDTEpc,4383
|
||||
packaging/specifiers.py,sha256=Mfp8avQg0lVot17to9lVKBtZD1FsWBTItoGwFUZ3wtg,71514
|
||||
packaging/tags.py,sha256=NQ1weo69_Sjte3xBZ1I_G63CIgCmaN0C24mz-z3hGYo,34224
|
||||
packaging/utils.py,sha256=M7-JMKic2sP1YtV_8aW7eVGB-x3ADuKCiSrsVeCd2Uo,9848
|
||||
packaging/version.py,sha256=Y1aTtxe3sn2xOMa5BdI85-AcHuybbanOVkEvvSRRC8I,38369
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: packaging
|
||||
Version: 26.2
|
||||
Version: 26.3
|
||||
Summary: Core utilities for Python packages
|
||||
Author-email: Donald Stufft <donald@stufft.io>
|
||||
Requires-Python: >=3.8
|
||||
Requires-Python: >=3.9
|
||||
Description-Content-Type: text/x-rst
|
||||
License-Expression: Apache-2.0 OR BSD-2-Clause
|
||||
Classifier: Development Status :: 5 - Production/Stable
|
||||
@@ -11,13 +11,13 @@ Classifier: Intended Audience :: Developers
|
||||
Classifier: Programming Language :: Python
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3 :: Only
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Classifier: Programming Language :: Python :: 3.15
|
||||
Classifier: Programming Language :: Python :: Implementation :: CPython
|
||||
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
||||
Classifier: Programming Language :: Python :: Free Threading :: 4 - Resilient
|
||||
@@ -0,0 +1,31 @@
|
||||
packaging-26.3.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
packaging-26.3.dist-info/METADATA,sha256=cP24n8TUqaBDv3NyuJcrzIg_3f80q1Xpz4DXOHU4R2M,3544
|
||||
packaging-26.3.dist-info/RECORD,,
|
||||
packaging-26.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
packaging-26.3.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
packaging-26.3.dist-info/licenses/LICENSE,sha256=ytHvW9NA1z4HS6YU0m996spceUDD2MNIUuZcSQlobEg,197
|
||||
packaging-26.3.dist-info/licenses/LICENSE.APACHE,sha256=DVQuDIgE45qn836wDaWnYhSdxoLXgpRRKH4RuTjpRZQ,10174
|
||||
packaging-26.3.dist-info/licenses/LICENSE.BSD,sha256=tw5-m3QvHMb5SLNMFqo5_-zpQZY2S8iP8NIYDwAo-sU,1344
|
||||
packaging/__init__.py,sha256=bnWM3QAossrXTHObmtycnwbwVJMjBigLh4v-WdOSGbU,494
|
||||
packaging/_elffile.py,sha256=HwJfVMVz8ahXQMT72QjMHR-wwXfBQ3fr9fCdbNU2h4A,3236
|
||||
packaging/_manylinux.py,sha256=W26EMkxCU4q0zbFI4kMP26NvBEJ7Dv2lW-oIOjX6YTY,10000
|
||||
packaging/_musllinux.py,sha256=Ayj7gnsRcMd99MEH1Jvo3403JiEDSQph6Kytii-crf8,2774
|
||||
packaging/_parser.py,sha256=iKNIEVIJMnSX5FO2NeXbCZ_VkvkzXXEMcxOynNswxQw,12639
|
||||
packaging/_ranges.py,sha256=t7WV8uaSrrNpfFRbhcJMu8tFUvKtxtkFmqgtCHfoFrc,30850
|
||||
packaging/_structures.py,sha256=60jRbF78p8z5MKnNd6cAprgOadCJHV0DlmUmRBqFZcs,1109
|
||||
packaging/_tokenizer.py,sha256=zCXlfA1WTInhUHNHHW4cJkKZypRvsXwOHkr1TvdxPuY,5573
|
||||
packaging/dependency_groups.py,sha256=UwnMVkyjAi37WUgW6dWTuwnN80IIwfuMXsmvtsZwJ4w,11224
|
||||
packaging/direct_url.py,sha256=hDuqldBMu0kojenZ_XfulG6GLLYDEj5-RqaeFx-n0Gs,11645
|
||||
packaging/errors.py,sha256=6hfEYXAf8v_IF65-lFadJOMIieBP2xIKtyEXjG1nGIs,2680
|
||||
packaging/licenses/__init__.py,sha256=WQ0S7uc92-xTtOnuW_xk2cs6rLSUDo68uiYHV85WdAs,7859
|
||||
packaging/licenses/_spdx.py,sha256=WW7DXiyg68up_YND_wpRYlr1SHhiV4FfJLQffghhMxQ,51122
|
||||
packaging/markers.py,sha256=bL6GDTXZjStZnn8Oxz_qsSTgFXEbML6QReZ4ySlA2cw,20554
|
||||
packaging/metadata.py,sha256=k0FJ9CClRJ5vN2qmHW8moII-uCzHxqHIzeWqe-Q9D2M,42059
|
||||
packaging/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
packaging/pylock.py,sha256=G84Kn-Y-Qrh-jO0F2icrrxamiqcEs3GEBeJzWmESRnw,35232
|
||||
packaging/ranges.py,sha256=tHgc6RPl3_ngeQbyBYXFTDAo4ff_VjQYKOedUi3b-rY,83054
|
||||
packaging/requirements.py,sha256=RZR26yEH_jDmsTEAUaW9tEx9O3944Oh7raJoKCy6YvU,7040
|
||||
packaging/specifiers.py,sha256=ZhrTHd9F_7hh0mJnNPTj1mlmApuP23lYa7EFgl2NnVk,52798
|
||||
packaging/tags.py,sha256=DnF6CCp5Yg15zwMhtCDfFVk8SRb82gPCfLnYLgfvXW4,38302
|
||||
packaging/utils.py,sha256=oTdCzZmU7spa37AknRJkCMR4Am6x9Iy5evvy_p8o7MY,12215
|
||||
packaging/version.py,sha256=VYsvtQ_RmMZgpMCC-WuJGcVlkRtrS24H80trYMB0vpc,39040
|
||||
@@ -6,7 +6,7 @@ __title__ = "packaging"
|
||||
__summary__ = "Core utilities for Python packages"
|
||||
__uri__ = "https://github.com/pypa/packaging"
|
||||
|
||||
__version__ = "26.2"
|
||||
__version__ = "26.3"
|
||||
|
||||
__author__ = "Donald Stufft and individual contributors"
|
||||
__email__ = "donald@stufft.io"
|
||||
|
||||
@@ -34,7 +34,7 @@ class EMachine(enum.IntEnum):
|
||||
S390 = 22
|
||||
Arm = 40
|
||||
X8664 = 62
|
||||
AArc64 = 183
|
||||
AArch64 = 183
|
||||
|
||||
|
||||
class ELFFile:
|
||||
@@ -57,8 +57,8 @@ class ELFFile:
|
||||
self.encoding = ident[5] # Data structure encoding (endianness).
|
||||
|
||||
try:
|
||||
# e_fmt: Format for program header.
|
||||
# p_fmt: Format for section header.
|
||||
# e_fmt: Format for the ELF header.
|
||||
# p_fmt: Format for a program header.
|
||||
# p_idx: Indexes to find p_type, p_offset, and p_filesz.
|
||||
e_fmt, self._p_fmt, self._p_idx = {
|
||||
(1, 1): ("<HHIIIIIHHH", "<IIIIIIII", (0, 1, 4)), # 32-bit LSB.
|
||||
@@ -81,8 +81,8 @@ class ELFFile:
|
||||
_,
|
||||
self.flags, # Processor-specific flags.
|
||||
_,
|
||||
self._e_phentsize, # Size of section.
|
||||
self._e_phnum, # Number of sections.
|
||||
self._e_phentsize, # Size of a program header entry.
|
||||
self._e_phnum, # Number of program headers.
|
||||
) = self._read(e_fmt)
|
||||
except struct.error as e:
|
||||
raise ELFInvalid("unable to parse machine and section information") from e
|
||||
|
||||
@@ -7,10 +7,14 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from typing import Generator, Iterator, NamedTuple, Sequence
|
||||
from typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
from ._elffile import EIClass, EIData, ELFFile, EMachine
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import types
|
||||
from collections.abc import Generator, Iterator, Sequence
|
||||
|
||||
EF_ARM_ABIMASK = 0xFF000000
|
||||
EF_ARM_ABI_VER5 = 0x05000000
|
||||
EF_ARM_ABI_FLOAT_HARD = 0x00000400
|
||||
@@ -26,8 +30,6 @@ _ALLOWED_ARCHS = {
|
||||
}
|
||||
|
||||
|
||||
# `os.PathLike` not a generic type until Python 3.9, so sticking with `str`
|
||||
# as the type for `path` until then.
|
||||
@contextlib.contextmanager
|
||||
def _parse_elf(path: str) -> Generator[ELFFile | None, None, None]:
|
||||
try:
|
||||
@@ -136,11 +138,11 @@ def _glibc_version_string_ctypes() -> str | None:
|
||||
# glibc.
|
||||
return None
|
||||
|
||||
# Call gnu_get_libc_version, which returns a string like "2.5"
|
||||
# Call gnu_get_libc_version, which returns a string like "2.5".
|
||||
gnu_get_libc_version.restype = ctypes.c_char_p
|
||||
version_str: str = gnu_get_libc_version()
|
||||
# py2 / py3 compatibility:
|
||||
if not isinstance(version_str, str):
|
||||
# A c_char_p restype comes back as bytes, so decode to text.
|
||||
version_str: str | bytes = gnu_get_libc_version()
|
||||
if isinstance(version_str, bytes):
|
||||
version_str = version_str.decode("ascii")
|
||||
|
||||
return version_str
|
||||
@@ -179,30 +181,44 @@ def _get_glibc_version() -> _GLibCVersion:
|
||||
|
||||
|
||||
# From PEP 513, PEP 600
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _get_manylinux_module() -> types.ModuleType | None:
|
||||
"""Return the ``_manylinux`` C extension module, or None if unavailable.
|
||||
|
||||
The result is cached for the lifetime of the process, since the presence
|
||||
of the module does not change while running.
|
||||
"""
|
||||
try:
|
||||
return __import__("_manylinux")
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
|
||||
def _is_compatible(arch: str, version: _GLibCVersion) -> bool:
|
||||
sys_glibc = _get_glibc_version()
|
||||
if sys_glibc < version:
|
||||
return False
|
||||
# Check for presence of _manylinux module.
|
||||
try:
|
||||
import _manylinux # noqa: PLC0415
|
||||
except ImportError:
|
||||
manylinux_mod = _get_manylinux_module()
|
||||
if manylinux_mod is None:
|
||||
return True
|
||||
if hasattr(_manylinux, "manylinux_compatible"):
|
||||
result = _manylinux.manylinux_compatible(version[0], version[1], arch)
|
||||
if hasattr(manylinux_mod, "manylinux_compatible"):
|
||||
result = manylinux_mod.manylinux_compatible(version[0], version[1], arch)
|
||||
if result is not None:
|
||||
return bool(result)
|
||||
return True
|
||||
if version == _GLibCVersion(2, 5) and hasattr(_manylinux, "manylinux1_compatible"):
|
||||
return bool(_manylinux.manylinux1_compatible)
|
||||
if version == _GLibCVersion(2, 5) and hasattr(
|
||||
manylinux_mod, "manylinux1_compatible"
|
||||
):
|
||||
return bool(manylinux_mod.manylinux1_compatible)
|
||||
if version == _GLibCVersion(2, 12) and hasattr(
|
||||
_manylinux, "manylinux2010_compatible"
|
||||
manylinux_mod, "manylinux2010_compatible"
|
||||
):
|
||||
return bool(_manylinux.manylinux2010_compatible)
|
||||
return bool(manylinux_mod.manylinux2010_compatible)
|
||||
if version == _GLibCVersion(2, 17) and hasattr(
|
||||
_manylinux, "manylinux2014_compatible"
|
||||
manylinux_mod, "manylinux2014_compatible"
|
||||
):
|
||||
return bool(_manylinux.manylinux2014_compatible)
|
||||
return bool(manylinux_mod.manylinux2014_compatible)
|
||||
return True
|
||||
|
||||
|
||||
|
||||
@@ -10,10 +10,13 @@ import functools
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Iterator, NamedTuple, Sequence
|
||||
from typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
from ._elffile import ELFFile
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator, Sequence
|
||||
|
||||
|
||||
class _MuslVersion(NamedTuple):
|
||||
major: int
|
||||
@@ -81,5 +84,5 @@ if __name__ == "__main__": # pragma: no cover
|
||||
print("plat:", plat)
|
||||
print("musl:", _get_musl_version(sys.executable))
|
||||
print("tags:", end=" ")
|
||||
for t in platform_tags(re.sub(r"[.-]", "_", plat.split("-", 1)[-1])):
|
||||
for t in platform_tags([re.sub(r"[.-]", "_", plat.split("-", 1)[-1])]):
|
||||
print(t, end="\n ")
|
||||
|
||||
@@ -7,9 +7,10 @@ the implementation.
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from typing import List, Literal, NamedTuple, Sequence, Tuple, Union
|
||||
from collections.abc import Sequence
|
||||
from typing import Literal, NamedTuple, Union
|
||||
|
||||
from ._tokenizer import DEFAULT_RULES, Tokenizer
|
||||
from ._tokenizer import DEFAULT_RULES, ParserSyntaxError, Tokenizer
|
||||
|
||||
|
||||
class Node:
|
||||
@@ -67,7 +68,14 @@ class Value(Node):
|
||||
__slots__ = ()
|
||||
|
||||
def serialize(self) -> str:
|
||||
return f'"{self}"'
|
||||
value = str(self)
|
||||
if '"' not in value:
|
||||
return f'"{value}"'
|
||||
if "'" not in value:
|
||||
return f"'{value}'"
|
||||
raise ValueError(
|
||||
"Cannot serialize marker value containing both quote characters"
|
||||
)
|
||||
|
||||
|
||||
class Op(Node):
|
||||
@@ -79,9 +87,9 @@ class Op(Node):
|
||||
|
||||
MarkerLogical = Literal["and", "or"]
|
||||
MarkerVar = Union[Variable, Value]
|
||||
MarkerItem = Tuple[MarkerVar, Op, MarkerVar]
|
||||
MarkerItem = tuple[MarkerVar, Op, MarkerVar]
|
||||
MarkerAtom = Union[MarkerItem, Sequence["MarkerAtom"]]
|
||||
MarkerList = List[Union["MarkerList", MarkerAtom, MarkerLogical]]
|
||||
MarkerList = list[Union["MarkerList", MarkerAtom, MarkerLogical]]
|
||||
|
||||
|
||||
class ParsedRequirement(NamedTuple):
|
||||
@@ -264,10 +272,19 @@ def _parse_version_many(tokenizer: Tokenizer) -> str:
|
||||
parsed_specifiers = ""
|
||||
while tokenizer.check("SPECIFIER"):
|
||||
span_start = tokenizer.position
|
||||
parsed_specifiers += tokenizer.read().text
|
||||
specifier = tokenizer.read().text
|
||||
parsed_specifiers += specifier
|
||||
if tokenizer.check("VERSION_PREFIX_TRAIL", peek=True):
|
||||
message = ".* suffix can only be used with `==` or `!=` operators"
|
||||
if specifier.startswith("!=") or (
|
||||
specifier.startswith("==") and not specifier.startswith("===")
|
||||
):
|
||||
message = (
|
||||
".* suffix cannot be used with pre-release, post-release, "
|
||||
"dev or local versions"
|
||||
)
|
||||
tokenizer.raise_syntax_error(
|
||||
".* suffix can only be used with `==` or `!=` operators",
|
||||
message,
|
||||
span_start=span_start,
|
||||
span_end=tokenizer.position + 1,
|
||||
)
|
||||
@@ -354,7 +371,15 @@ def _parse_marker_var(tokenizer: Tokenizer) -> MarkerVar: # noqa: RET503
|
||||
if tokenizer.check("VARIABLE"):
|
||||
return process_env_var(tokenizer.read().text.replace(".", "_"))
|
||||
elif tokenizer.check("QUOTED_STRING"):
|
||||
return process_python_str(tokenizer.read().text)
|
||||
token = tokenizer.read()
|
||||
try:
|
||||
return process_python_str(token.text)
|
||||
except (SyntaxError, ValueError) as exc:
|
||||
raise ParserSyntaxError(
|
||||
"Invalid quoted string",
|
||||
source=tokenizer.source,
|
||||
span=(token.position, token.position + len(token.text)),
|
||||
) from exc
|
||||
else:
|
||||
tokenizer.raise_syntax_error(
|
||||
message="Expected a marker variable or quoted string"
|
||||
|
||||
@@ -0,0 +1,836 @@
|
||||
# This file is dual licensed under the terms of the Apache License, Version
|
||||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
"""Private version-range helpers used by :mod:`packaging.specifiers`."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import enum
|
||||
import functools
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Any,
|
||||
Final,
|
||||
)
|
||||
|
||||
from .version import InvalidVersion, Version
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable, Iterator, Sequence
|
||||
from typing import Union
|
||||
|
||||
# Total-order key for comparing two boundaries (boundary-vs-boundary only).
|
||||
# The post slot may be ``_BOUNDARY_INF`` for an AFTER_POSTS boundary.
|
||||
_BoundaryOrderSuffix = tuple[int, int, int, Union[int, float], int, int]
|
||||
_BoundaryOrderKey = tuple[int, tuple[int, ...], _BoundaryOrderSuffix, float]
|
||||
|
||||
__all__ = [
|
||||
"FULL_RANGE",
|
||||
"bounds_for_spec",
|
||||
"coerce_version",
|
||||
"filter_by_ranges",
|
||||
"intersect_ranges",
|
||||
"intersect_specifier_bounds",
|
||||
"least_version_above",
|
||||
"matches_bounds_only",
|
||||
"range_is_empty",
|
||||
"ranges_are_prerelease_only",
|
||||
"resolve_prereleases",
|
||||
"standard_ranges",
|
||||
"wildcard_ranges",
|
||||
]
|
||||
|
||||
#: The smallest possible PEP 440 version. No valid version is less than this.
|
||||
MIN_VERSION: Final[Version] = Version("0.dev0")
|
||||
|
||||
#: The smallest non-pre-release version, i.e. the nearest non-pre-release at or
|
||||
#: above the ``-inf`` floor.
|
||||
MIN_RELEASE: Final[Version] = Version("0")
|
||||
|
||||
#: Sorts above any real post number and any local label, so a boundary can be
|
||||
#: ordered above the version family it covers when two boundaries are compared.
|
||||
_BOUNDARY_INF: Final[float] = float("inf")
|
||||
|
||||
|
||||
class BoundaryKind(enum.Enum):
|
||||
"""Where a boundary marker sits in the version ordering."""
|
||||
|
||||
AFTER_LOCALS = enum.auto() # after V+local, before V.post0
|
||||
AFTER_POSTS = enum.auto() # after V.postN, before next release
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class BoundaryVersion:
|
||||
"""A point on the version line between two real PEP 440 versions.
|
||||
|
||||
Relative to a base version V::
|
||||
|
||||
V < V+local < AFTER_LOCALS(V) < V.post0 < AFTER_POSTS(V)
|
||||
|
||||
AFTER_LOCALS is the upper bound of ``<=V``, ``==V``, ``!=V`` (no
|
||||
local), and the lower bound of the upper-side range of ``!=V``.
|
||||
AFTER_POSTS is the lower bound of ``>V`` (V final or pre-release),
|
||||
excluding V's post-releases per PEP 440.
|
||||
"""
|
||||
|
||||
__slots__ = (
|
||||
"_cached_dev",
|
||||
"_cached_epoch",
|
||||
"_cached_post",
|
||||
"_cached_pre",
|
||||
"_cached_trimmed_release",
|
||||
"kind",
|
||||
"version",
|
||||
)
|
||||
|
||||
def __init__(self, version: Version, kind: BoundaryKind) -> None:
|
||||
self.version = version
|
||||
self.kind = kind
|
||||
self._cached_trimmed_release = trim_release(version.release)
|
||||
self._cached_epoch = version.epoch
|
||||
self._cached_pre = version.pre
|
||||
self._cached_post = version.post
|
||||
self._cached_dev = version.dev
|
||||
|
||||
def _is_family(self, other: Version) -> bool:
|
||||
"""Is ``other`` a version that this boundary sorts above?"""
|
||||
if other.epoch != self._cached_epoch:
|
||||
return False
|
||||
# Inline release-trim comparison: other.release matches the
|
||||
# trimmed release iff its leading slice is equal and any extra
|
||||
# components are zero. Avoids trim_release's tuple allocation.
|
||||
other_release = other.release
|
||||
trimmed_release = self._cached_trimmed_release
|
||||
trimmed_length = len(trimmed_release)
|
||||
if len(other_release) < trimmed_length:
|
||||
return False
|
||||
if other_release[:trimmed_length] != trimmed_release:
|
||||
return False
|
||||
for i in range(trimmed_length, len(other_release)):
|
||||
if other_release[i] != 0:
|
||||
return False
|
||||
if other.pre != self._cached_pre:
|
||||
return False
|
||||
if self.kind == BoundaryKind.AFTER_LOCALS:
|
||||
# Local family: same public version, any local label.
|
||||
return other.post == self._cached_post and other.dev == self._cached_dev
|
||||
# Post family: V itself + any post-release of V.
|
||||
return other.dev == self._cached_dev or other.post is not None
|
||||
|
||||
def _order_key(self) -> _BoundaryOrderKey:
|
||||
"""Sort key placing this boundary just above the versions it covers.
|
||||
|
||||
It extends ``V``'s comparison key ``(epoch, release, suffix)`` with
|
||||
a trailing ``_BOUNDARY_INF`` local component, so the key sorts after
|
||||
``V`` and every ``V+local`` (whose keys carry a real, finite local
|
||||
segment). ``suffix`` is the 6-int comparison suffix
|
||||
``(pre_rank, pre_n, post_rank, post_n, dev_rank, dev_n)``.
|
||||
|
||||
For an AFTER_POSTS boundary the suffix is replaced with one whose
|
||||
post number is ``_BOUNDARY_INF``, so the key also sorts after every
|
||||
``V.postN``. An AFTER_LOCALS boundary uses ``V``'s suffix unchanged.
|
||||
"""
|
||||
version_key = self.version._key
|
||||
suffix: _BoundaryOrderSuffix = version_key[2]
|
||||
|
||||
if self.kind == BoundaryKind.AFTER_POSTS:
|
||||
suffix = (suffix[0], suffix[1], 1, _BOUNDARY_INF, 1, 0)
|
||||
|
||||
return version_key[0], version_key[1], suffix, _BOUNDARY_INF
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
# Key off the order key so equality matches the ``<`` / ``>`` order:
|
||||
# ``AFTER_POSTS(1.0)`` and ``AFTER_POSTS(1.0.post1)`` are the same point.
|
||||
if isinstance(other, BoundaryVersion):
|
||||
return self._order_key() == other._order_key()
|
||||
return NotImplemented
|
||||
|
||||
def __lt__(self, other: BoundaryVersion | Version) -> bool:
|
||||
if isinstance(other, BoundaryVersion):
|
||||
return self._order_key() < other._order_key()
|
||||
# boundary < other_version iff V < other AND other not in family.
|
||||
# The cheap V >= other path short-circuits before the family check.
|
||||
if not (self.version < other):
|
||||
return False
|
||||
return not self._is_family(other)
|
||||
|
||||
def __gt__(self, other: BoundaryVersion | Version) -> bool:
|
||||
# Defined directly to bypass functools.total_ordering's
|
||||
# NotImplemented round-trip on reflected ``Version < boundary``.
|
||||
if isinstance(other, BoundaryVersion):
|
||||
return self._order_key() > other._order_key()
|
||||
if self.version >= other:
|
||||
return True
|
||||
return self._is_family(other)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
# Keyed to ``__eq__`` (the order key), so equal boundaries hash equal.
|
||||
return hash(self._order_key())
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}({self.version!r}, {self.kind.name})"
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
_VersionOrBoundary = Union[Version, BoundaryVersion, None]
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class LowerBound:
|
||||
"""Lower bound of a version range.
|
||||
|
||||
A version *v* of ``None`` means unbounded below (-inf).
|
||||
At equal versions, ``[v`` sorts before ``(v`` because an inclusive
|
||||
bound starts earlier.
|
||||
"""
|
||||
|
||||
__slots__ = ("_above", "inclusive", "version")
|
||||
|
||||
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
|
||||
self.version = version
|
||||
self.inclusive = inclusive
|
||||
# Pre-bind a predicate "is parsed at or above this lower
|
||||
# bound?" for the hot filter / contains loops. One direct
|
||||
# call per check, no operator-dispatch chain.
|
||||
if version is None:
|
||||
self._above: Callable[[Version], bool] | None = None
|
||||
elif isinstance(version, BoundaryVersion):
|
||||
# >V produces an AFTER_POSTS lower bound; the upper-side
|
||||
# range of !=V produces an AFTER_LOCALS lower bound.
|
||||
if version.kind == BoundaryKind.AFTER_POSTS:
|
||||
self._above = _make_above_after_posts(version.version)
|
||||
else:
|
||||
self._above = _make_above_after_locals(version.version)
|
||||
elif inclusive:
|
||||
self._above = version.__le__
|
||||
else:
|
||||
self._above = version.__lt__
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, LowerBound):
|
||||
return NotImplemented
|
||||
return self.version == other.version and self.inclusive == other.inclusive
|
||||
|
||||
def __lt__(self, other: LowerBound) -> bool:
|
||||
if not isinstance(other, LowerBound):
|
||||
return NotImplemented
|
||||
# -inf < anything (except -inf itself).
|
||||
if self.version is None:
|
||||
return other.version is not None
|
||||
if other.version is None:
|
||||
return False
|
||||
if self.version != other.version:
|
||||
return self.version < other.version
|
||||
# [v < (v: inclusive starts earlier.
|
||||
return self.inclusive and not other.inclusive
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.version, self.inclusive))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
bracket = "[" if self.inclusive else "("
|
||||
return f"<{self.__class__.__name__} {bracket}{self.version!r}>"
|
||||
|
||||
|
||||
@functools.total_ordering
|
||||
class UpperBound:
|
||||
"""Upper bound of a version range.
|
||||
|
||||
A version *v* of ``None`` means unbounded above (+inf).
|
||||
At equal versions, ``v)`` sorts before ``v]`` because an exclusive
|
||||
bound ends earlier.
|
||||
"""
|
||||
|
||||
__slots__ = ("_below", "inclusive", "version")
|
||||
|
||||
def __init__(self, version: _VersionOrBoundary, inclusive: bool) -> None:
|
||||
self.version = version
|
||||
self.inclusive = inclusive
|
||||
# Pre-bind a predicate "is parsed at or below this upper
|
||||
# bound?". See LowerBound for the rationale.
|
||||
if version is None:
|
||||
self._below: Callable[[Version], bool] | None = None
|
||||
elif isinstance(version, BoundaryVersion):
|
||||
# Standard specifiers only ever produce AFTER_LOCALS upper
|
||||
# bounds (from <=V / ==V / !=V with no local).
|
||||
if version.kind == BoundaryKind.AFTER_LOCALS:
|
||||
self._below = _make_below_after_locals(version.version)
|
||||
else:
|
||||
# An AFTER_POSTS upper is not produced by any specifier, but
|
||||
# range algebra reaches it: complementing ``>V`` flips the
|
||||
# ``AFTER_POSTS(V)`` lower into this upper bound.
|
||||
self._below = version.__ge__
|
||||
elif inclusive:
|
||||
self._below = version.__ge__
|
||||
else:
|
||||
self._below = version.__gt__
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, UpperBound):
|
||||
return NotImplemented
|
||||
return self.version == other.version and self.inclusive == other.inclusive
|
||||
|
||||
def __lt__(self, other: UpperBound) -> bool:
|
||||
if not isinstance(other, UpperBound):
|
||||
return NotImplemented
|
||||
# Nothing < +inf (except +inf itself).
|
||||
if self.version is None:
|
||||
return False
|
||||
if other.version is None:
|
||||
return True
|
||||
if self.version != other.version:
|
||||
return self.version < other.version
|
||||
# v) < v]: exclusive ends earlier.
|
||||
return not self.inclusive and other.inclusive
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.version, self.inclusive))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
bracket = "]" if self.inclusive else ")"
|
||||
return f"<{self.__class__.__name__} {self.version!r}{bracket}>"
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
#: A single contiguous interval as a (lower, upper) bound pair.
|
||||
Interval = tuple[LowerBound, UpperBound]
|
||||
|
||||
|
||||
NEG_INF: Final[LowerBound] = LowerBound(None, False)
|
||||
POS_INF: Final[UpperBound] = UpperBound(None, False)
|
||||
FULL_RANGE: Final[tuple[Interval]] = ((NEG_INF, POS_INF),)
|
||||
|
||||
|
||||
def trim_release(release: tuple[int, ...]) -> tuple[int, ...]:
|
||||
"""Strip trailing zeros from a release tuple for normalized comparison."""
|
||||
end = len(release)
|
||||
while end > 1 and release[end - 1] == 0:
|
||||
end -= 1
|
||||
return release if end == len(release) else release[:end]
|
||||
|
||||
|
||||
def _next_prefix_dev0(version: Version) -> Version:
|
||||
"""Smallest version in the next prefix: 1.2 -> 1.3.dev0."""
|
||||
release = (*version.release[:-1], version.release[-1] + 1)
|
||||
return Version.from_parts(epoch=version.epoch, release=release, dev=0)
|
||||
|
||||
|
||||
def _base_dev0(version: Version) -> Version:
|
||||
"""The .dev0 of a version's base release: 1.2 -> 1.2.dev0."""
|
||||
return Version.from_parts(epoch=version.epoch, release=version.release, dev=0)
|
||||
|
||||
|
||||
def coerce_version(version: Version | str) -> Version | None:
|
||||
if not isinstance(version, Version):
|
||||
try:
|
||||
version = Version(version)
|
||||
except InvalidVersion:
|
||||
return None
|
||||
return version
|
||||
|
||||
|
||||
def _make_above_after_posts(version: Version) -> Callable[[Version], bool]:
|
||||
"""Predicate ``parsed > AFTER_POSTS(V)`` for a lower bound.
|
||||
|
||||
Per PEP 440, ``>V`` excludes V's post-releases unless V is itself
|
||||
a post-release. AFTER_POSTS sits above V and every V.postN (with
|
||||
or without local), and just below the next release.
|
||||
"""
|
||||
version_ge = version.__ge__
|
||||
version_epoch = version.epoch
|
||||
version_pre = version.pre
|
||||
version_release_trimmed = trim_release(version.release)
|
||||
trimmed_length = len(version_release_trimmed)
|
||||
|
||||
def above(parsed: Version) -> bool:
|
||||
if version_ge(parsed):
|
||||
return False
|
||||
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
|
||||
# post family.
|
||||
if parsed.epoch != version_epoch:
|
||||
return True
|
||||
parsed_release = parsed.release
|
||||
if len(parsed_release) < trimmed_length:
|
||||
return True
|
||||
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||
return True
|
||||
for i in range(trimmed_length, len(parsed_release)):
|
||||
if parsed_release[i] != 0:
|
||||
return True
|
||||
if parsed.pre != version_pre:
|
||||
return True
|
||||
|
||||
# Same release and pre as V: parsed is in V's post family (V itself,
|
||||
# V+local, or V.postN), which the boundary sits above. A V.devN
|
||||
# (different dev, no post) sorts before V and was already caught by
|
||||
# ``version_ge`` above, so the answer here is always "not above".
|
||||
return False
|
||||
|
||||
return above
|
||||
|
||||
|
||||
def _make_above_after_locals(version: Version) -> Callable[[Version], bool]:
|
||||
"""Predicate ``parsed > AFTER_LOCALS(V)`` for a lower bound.
|
||||
|
||||
Used by the upper-side range of ``!=V`` (when V has no local
|
||||
segment). AFTER_LOCALS sits above V and every ``V+local`` but
|
||||
just below ``V.post0``.
|
||||
"""
|
||||
version_ge = version.__ge__
|
||||
version_epoch = version.epoch
|
||||
version_pre = version.pre
|
||||
version_post = version.post
|
||||
version_dev = version.dev
|
||||
version_release_trimmed = trim_release(version.release)
|
||||
trimmed_length = len(version_release_trimmed)
|
||||
|
||||
def above(parsed: Version) -> bool:
|
||||
if version_ge(parsed):
|
||||
return False
|
||||
# parsed > V cmpkey-wise: above the boundary iff NOT in V's
|
||||
# local family (same public version, any local segment).
|
||||
if parsed.epoch != version_epoch:
|
||||
return True
|
||||
parsed_release = parsed.release
|
||||
if len(parsed_release) < trimmed_length:
|
||||
return True
|
||||
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||
return True
|
||||
for i in range(trimmed_length, len(parsed_release)):
|
||||
if parsed_release[i] != 0:
|
||||
return True
|
||||
if parsed.pre != version_pre:
|
||||
return True
|
||||
if parsed.post != version_post:
|
||||
return True
|
||||
return parsed.dev != version_dev
|
||||
|
||||
return above
|
||||
|
||||
|
||||
def _make_below_after_locals(version: Version) -> Callable[[Version], bool]:
|
||||
"""Predicate ``parsed <= AFTER_LOCALS(V)`` for an upper bound.
|
||||
|
||||
Used by ``<=V``, ``==V``, ``!=V`` (no local). ``parsed`` is at or
|
||||
below the boundary when it is at or below V cmpkey-wise, or when
|
||||
it is in V's local family.
|
||||
"""
|
||||
version_ge = version.__ge__
|
||||
version_epoch = version.epoch
|
||||
version_pre = version.pre
|
||||
version_post = version.post
|
||||
version_dev = version.dev
|
||||
version_release_trimmed = trim_release(version.release)
|
||||
trimmed_length = len(version_release_trimmed)
|
||||
|
||||
def below(parsed: Version) -> bool:
|
||||
if version_ge(parsed):
|
||||
return True
|
||||
# parsed > V cmpkey-wise: below the boundary iff in V's local
|
||||
# family.
|
||||
if parsed.epoch != version_epoch:
|
||||
return False
|
||||
parsed_release = parsed.release
|
||||
if len(parsed_release) < trimmed_length:
|
||||
return False
|
||||
if parsed_release[:trimmed_length] != version_release_trimmed:
|
||||
return False
|
||||
for i in range(trimmed_length, len(parsed_release)):
|
||||
if parsed_release[i] != 0:
|
||||
return False
|
||||
if parsed.pre != version_pre:
|
||||
return False
|
||||
if parsed.post != version_post:
|
||||
return False
|
||||
return parsed.dev == version_dev
|
||||
|
||||
return below
|
||||
|
||||
|
||||
def least_version_above(boundary: BoundaryVersion) -> Version | None:
|
||||
"""Smallest real version strictly above *boundary*, or ``None`` if none exists."""
|
||||
base = boundary.version
|
||||
|
||||
if boundary.kind == BoundaryKind.AFTER_LOCALS:
|
||||
# AFTER_LOCALS(V) sits just below V.post0, so its least successor is
|
||||
# V.post0.dev0 (V.dev(N+1) if V has a dev, V.post(N+1).dev0 if a post).
|
||||
if base.dev is not None:
|
||||
return base.__replace__(dev=base.dev + 1, local=None)
|
||||
next_post = (base.post + 1) if base.post is not None else 0
|
||||
return base.__replace__(post=next_post, dev=0, local=None)
|
||||
|
||||
# AFTER_POSTS(V): a pre-release V steps to the next pre-release's .dev0;
|
||||
# a final-release AFTER_POSTS has no least successor.
|
||||
if base.pre is not None:
|
||||
kind, number = base.pre
|
||||
return base.__replace__(pre=(kind, number + 1), post=None, dev=0, local=None)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def range_is_empty(lower: LowerBound, upper: UpperBound) -> bool:
|
||||
"""True when the range defined by *lower* and *upper* contains no versions.
|
||||
|
||||
A boundary lower sits just below the next real version, so an ordered pair
|
||||
is still empty when the upper excludes that least successor:
|
||||
``(AFTER_POSTS(1.0a1), 1.0a2.dev0)`` holds no version.
|
||||
"""
|
||||
if upper.version is None:
|
||||
return False
|
||||
|
||||
if lower.version is None:
|
||||
# Nothing sorts below MIN_VERSION, so an exclusive upper at or below it
|
||||
# leaves an empty floor interval such as ``(-inf, 0.dev0)``.
|
||||
return (
|
||||
not upper.inclusive
|
||||
and isinstance(upper.version, Version)
|
||||
and upper.version <= MIN_VERSION
|
||||
)
|
||||
|
||||
if isinstance(lower.version, BoundaryVersion):
|
||||
successor = least_version_above(lower.version)
|
||||
if successor is not None:
|
||||
if upper.version == successor:
|
||||
return not upper.inclusive
|
||||
return upper.version < successor
|
||||
|
||||
if lower.version == upper.version:
|
||||
return not (lower.inclusive and upper.inclusive)
|
||||
|
||||
return lower.version > upper.version
|
||||
|
||||
|
||||
def intersect_ranges(
|
||||
left: Sequence[Interval],
|
||||
right: Sequence[Interval],
|
||||
) -> list[Interval]:
|
||||
"""Intersect two sorted, non-overlapping range lists (two-pointer merge)."""
|
||||
result: list[Interval] = []
|
||||
left_index = right_index = 0
|
||||
while left_index < len(left) and right_index < len(right):
|
||||
left_lower, left_upper = left[left_index]
|
||||
right_lower, right_upper = right[right_index]
|
||||
|
||||
lower = max(left_lower, right_lower)
|
||||
upper = min(left_upper, right_upper)
|
||||
|
||||
if not range_is_empty(lower, upper):
|
||||
result.append((lower, upper))
|
||||
|
||||
# Advance whichever side has the smaller upper bound.
|
||||
if left_upper < right_upper:
|
||||
left_index += 1
|
||||
else:
|
||||
right_index += 1
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def filter_by_ranges(
|
||||
ranges: Sequence[Interval],
|
||||
iterable: Iterable[Any],
|
||||
key: Callable[[Any], Version | str] | None,
|
||||
prereleases: bool | None,
|
||||
region: Sequence[Interval] = (),
|
||||
) -> Iterator[Any]:
|
||||
"""Filter *iterable* against precomputed version *ranges*.
|
||||
|
||||
With ``prereleases=None``, the PEP 440 default applies: pre-releases are
|
||||
excluded unless no final matches, in which case buffered pre-releases come
|
||||
out at the end. A pre-release inside the opt-in ``region`` is the exception:
|
||||
it is force-admitted in place, as ``prereleases=True`` would yield it. A
|
||||
force-admitted pre-release is not a final, so it never suppresses the buffer.
|
||||
"""
|
||||
if prereleases is None:
|
||||
prerelease_buffer: list[Any] = []
|
||||
found_final = False
|
||||
|
||||
if len(ranges) == 1:
|
||||
# Hot path: most specifiers and small SpecifierSets reduce to
|
||||
# a single contiguous range.
|
||||
lower, upper = ranges[0]
|
||||
above = lower._above
|
||||
below = upper._below
|
||||
for item in iterable:
|
||||
parsed = coerce_version(item if key is None else key(item))
|
||||
if parsed is None:
|
||||
continue
|
||||
if above is not None and not above(parsed):
|
||||
continue
|
||||
if below is not None and not below(parsed):
|
||||
continue
|
||||
if not parsed.is_prerelease:
|
||||
found_final = True
|
||||
yield item
|
||||
elif region and matches_bounds_only(region, parsed):
|
||||
yield item
|
||||
elif not found_final:
|
||||
prerelease_buffer.append(item)
|
||||
if not found_final:
|
||||
yield from prerelease_buffer
|
||||
return
|
||||
|
||||
for item in iterable:
|
||||
parsed = coerce_version(item if key is None else key(item))
|
||||
if parsed is None:
|
||||
continue
|
||||
for lower, upper in ranges:
|
||||
above = lower._above
|
||||
if above is not None and not above(parsed):
|
||||
break
|
||||
below = upper._below
|
||||
if below is None or below(parsed):
|
||||
if not parsed.is_prerelease:
|
||||
found_final = True
|
||||
yield item
|
||||
elif region and matches_bounds_only(region, parsed):
|
||||
yield item
|
||||
elif not found_final:
|
||||
prerelease_buffer.append(item)
|
||||
break
|
||||
if not found_final:
|
||||
yield from prerelease_buffer
|
||||
return
|
||||
|
||||
exclude_prereleases = prereleases is False
|
||||
|
||||
if len(ranges) == 1:
|
||||
# Hot path: most specifiers and small SpecifierSets reduce to
|
||||
# a single contiguous range.
|
||||
lower, upper = ranges[0]
|
||||
above = lower._above
|
||||
below = upper._below
|
||||
for item in iterable:
|
||||
parsed = coerce_version(item if key is None else key(item))
|
||||
if parsed is None:
|
||||
continue
|
||||
if exclude_prereleases and parsed.is_prerelease:
|
||||
continue
|
||||
if above is not None and not above(parsed):
|
||||
continue
|
||||
if below is None or below(parsed):
|
||||
yield item
|
||||
return
|
||||
|
||||
for item in iterable:
|
||||
parsed = coerce_version(item if key is None else key(item))
|
||||
if parsed is None:
|
||||
continue
|
||||
if exclude_prereleases and parsed.is_prerelease:
|
||||
continue
|
||||
for lower, upper in ranges:
|
||||
above = lower._above
|
||||
if above is not None and not above(parsed):
|
||||
break
|
||||
below = upper._below
|
||||
if below is None or below(parsed):
|
||||
yield item
|
||||
break
|
||||
|
||||
|
||||
def _nearest_release_above_prerelease(version: Version) -> Version:
|
||||
"""Smallest non-pre-release at or above a pre-release *version*."""
|
||||
if version.pre is not None:
|
||||
# An a/b/rc pre-release drops to its final release, which outranks
|
||||
# every post-release of that pre-release (1.0a1.post0 -> 1.0).
|
||||
return version.__replace__(pre=None, post=None, dev=None, local=None)
|
||||
|
||||
# A dev-only release keeps its post-release (1.0.post0.dev0 -> 1.0.post0,
|
||||
# whose final 1.0 sorts below it).
|
||||
return version.__replace__(dev=None, local=None)
|
||||
|
||||
|
||||
def _lowest_release_at_or_above(value: Version | BoundaryVersion | None) -> Version:
|
||||
"""Smallest non-pre-release version at or above *value*.
|
||||
|
||||
``None`` is the ``-inf`` floor, whose nearest non-pre-release is
|
||||
:data:`MIN_RELEASE`.
|
||||
"""
|
||||
if value is None:
|
||||
return MIN_RELEASE
|
||||
if isinstance(value, BoundaryVersion):
|
||||
inner_version = value.version
|
||||
if inner_version.is_prerelease:
|
||||
return _nearest_release_above_prerelease(inner_version)
|
||||
# AFTER_LOCALS(1.0) -> nearest non-pre is 1.0.post0
|
||||
# AFTER_LOCALS(1.0.post0) -> nearest non-pre is 1.0.post1
|
||||
next_post = (inner_version.post + 1) if inner_version.post is not None else 0
|
||||
return inner_version.__replace__(post=next_post, local=None)
|
||||
|
||||
if not value.is_prerelease:
|
||||
return value
|
||||
|
||||
return _nearest_release_above_prerelease(value)
|
||||
|
||||
|
||||
def ranges_are_prerelease_only(ranges: Sequence[Interval]) -> bool:
|
||||
"""True when every range in *ranges* contains only pre-releases.
|
||||
|
||||
Used to detect unsatisfiable specifier sets when ``prereleases=False``:
|
||||
if every range is pre-release-only, every contained version is excluded.
|
||||
"""
|
||||
for lower, upper in ranges:
|
||||
nearest = _lowest_release_at_or_above(lower.version)
|
||||
if upper.version is None or nearest < upper.version:
|
||||
return False
|
||||
if nearest == upper.version and upper.inclusive:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def wildcard_ranges(op: str, base: Version) -> list[Interval]:
|
||||
"""Ranges for ==V.* and !=V.*.
|
||||
|
||||
==1.2.* -> [1.2.dev0, 1.3.dev0); !=1.2.* -> complement.
|
||||
"""
|
||||
lower = _base_dev0(base)
|
||||
upper = _next_prefix_dev0(base)
|
||||
if op == "==":
|
||||
return [(LowerBound(lower, True), UpperBound(upper, False))]
|
||||
# !=
|
||||
return [
|
||||
(NEG_INF, UpperBound(lower, False)),
|
||||
(LowerBound(upper, True), POS_INF),
|
||||
]
|
||||
|
||||
|
||||
def standard_ranges(op: str, version: Version, has_local: bool) -> list[Interval]:
|
||||
"""Ranges for the standard PEP 440 operators (no wildcard, no ===).
|
||||
|
||||
*has_local* indicates whether the spec string included a ``+local``
|
||||
segment; relevant only for ``==`` / ``!=`` to decide whether the
|
||||
upper bound includes V's local family.
|
||||
"""
|
||||
if op == ">=":
|
||||
return [(LowerBound(version, True), POS_INF)]
|
||||
|
||||
if op == "<=":
|
||||
return [
|
||||
(
|
||||
NEG_INF,
|
||||
UpperBound(BoundaryVersion(version, BoundaryKind.AFTER_LOCALS), True),
|
||||
)
|
||||
]
|
||||
|
||||
if op == ">":
|
||||
if version.dev is not None:
|
||||
# >V.devN: dev versions have no post-releases, so the
|
||||
# next real version is V.dev(N+1).
|
||||
lower_bound = version.__replace__(dev=version.dev + 1, local=None)
|
||||
return [(LowerBound(lower_bound, True), POS_INF)]
|
||||
if version.post is not None:
|
||||
# >V.postN: next real version is V.post(N+1).dev0.
|
||||
lower_bound = version.__replace__(post=version.post + 1, dev=0, local=None)
|
||||
return [(LowerBound(lower_bound, True), POS_INF)]
|
||||
# >V (final or pre-release V): exclude V itself, V+local, and
|
||||
# every V.postN per PEP 440.
|
||||
return [
|
||||
(
|
||||
LowerBound(BoundaryVersion(version, BoundaryKind.AFTER_POSTS), False),
|
||||
POS_INF,
|
||||
)
|
||||
]
|
||||
|
||||
if op == "<":
|
||||
# <V excludes pre-releases of V when V is not a pre-release.
|
||||
# V.dev0 is the earliest pre-release of V.
|
||||
bound = (
|
||||
version if version.is_prerelease else version.__replace__(dev=0, local=None)
|
||||
)
|
||||
if bound <= MIN_VERSION:
|
||||
return []
|
||||
return [(NEG_INF, UpperBound(bound, False))]
|
||||
|
||||
# ==, !=: local versions of V match when the spec has no local segment.
|
||||
after_locals = BoundaryVersion(version, BoundaryKind.AFTER_LOCALS)
|
||||
upper = version if has_local else after_locals
|
||||
|
||||
if op == "==":
|
||||
return [(LowerBound(version, True), UpperBound(upper, True))]
|
||||
|
||||
if op == "!=":
|
||||
return [
|
||||
(NEG_INF, UpperBound(version, False)),
|
||||
(LowerBound(upper, False), POS_INF),
|
||||
]
|
||||
|
||||
if op == "~=":
|
||||
prefix = version.__replace__(release=version.release[:-1])
|
||||
return [
|
||||
(LowerBound(version, True), UpperBound(_next_prefix_dev0(prefix), False))
|
||||
]
|
||||
|
||||
raise ValueError(f"Unknown operator: {op!r}") # pragma: no cover
|
||||
|
||||
|
||||
def bounds_for_spec(op: str, version_str: str, version: Version) -> list[Interval]:
|
||||
"""Ranges for one specifier's ``(op, version_str)``.
|
||||
|
||||
Dispatches between the wildcard and standard builders. ``version`` is the
|
||||
parsed ``version_str`` (its base, without the trailing ``.*``, for
|
||||
wildcards). ``===`` is not handled here; its match is a literal string
|
||||
compared in :mod:`packaging.specifiers`.
|
||||
"""
|
||||
if version_str.endswith(".*"):
|
||||
return wildcard_ranges(op, version)
|
||||
|
||||
return standard_ranges(op, version, "+" in version_str)
|
||||
|
||||
|
||||
def intersect_specifier_bounds(
|
||||
per_specifier_ranges: Iterable[Sequence[Interval]],
|
||||
) -> Sequence[Interval]:
|
||||
"""Intersect each specifier's ranges into a single sequence.
|
||||
|
||||
Short-circuits once the running intersection is empty, since no later
|
||||
specifier can revive it. Callers must pass at least one specifier.
|
||||
"""
|
||||
result: Sequence[Interval] | None = None
|
||||
for sub in per_specifier_ranges:
|
||||
if result is None:
|
||||
result = sub
|
||||
else:
|
||||
result = intersect_ranges(result, sub)
|
||||
if not result:
|
||||
break
|
||||
|
||||
if result is None: # pragma: no cover - callers guard non-empty input
|
||||
raise RuntimeError("intersect_specifier_bounds called with no specifiers")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def matches_bounds_only(ranges: Sequence[Interval], version: Version) -> bool:
|
||||
"""Whether ``version`` falls within any of ``ranges``.
|
||||
|
||||
The pure bounds membership test, for a single already-parsed version with
|
||||
no pre-release policy applied. ``ranges`` are sorted and non-overlapping,
|
||||
so a version below one range's lower bound is below every later range too.
|
||||
"""
|
||||
for lower, upper in ranges:
|
||||
above = lower._above
|
||||
if above is not None and not above(version):
|
||||
return False
|
||||
|
||||
below = upper._below
|
||||
if below is None or below(version):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def resolve_prereleases(
|
||||
configured: bool | None, autodetected: bool | None
|
||||
) -> bool | None:
|
||||
"""Resolve a specifier's effective default pre-release policy.
|
||||
|
||||
An explicit ``configured`` value wins; otherwise an autodetected ``True``
|
||||
propagates and anything else falls back to the PEP 440 default (``None``).
|
||||
"""
|
||||
if configured is not None:
|
||||
return configured
|
||||
|
||||
if autodetected:
|
||||
return True
|
||||
|
||||
return None
|
||||
@@ -3,13 +3,18 @@ from __future__ import annotations
|
||||
import contextlib
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import Generator, Mapping, NoReturn
|
||||
from typing import TYPE_CHECKING, NoReturn
|
||||
|
||||
from .specifiers import Specifier
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Generator, Mapping
|
||||
|
||||
|
||||
@dataclass
|
||||
class Token:
|
||||
__slots__ = ("name", "position", "text")
|
||||
|
||||
name: str
|
||||
text: str
|
||||
position: int
|
||||
@@ -84,7 +89,7 @@ DEFAULT_RULES: dict[str, re.Pattern[str]] = {
|
||||
"VERSION_PREFIX_TRAIL": re.compile(r"\.\*"),
|
||||
"VERSION_LOCAL_LABEL_TRAIL": re.compile(r"\+[a-z0-9]+(?:[-_\.][a-z0-9]+)*"),
|
||||
"WS": re.compile(r"[ \t]+"),
|
||||
"END": re.compile(r"$"),
|
||||
"END": re.compile(r"\Z"),
|
||||
}
|
||||
|
||||
|
||||
@@ -95,6 +100,8 @@ class Tokenizer:
|
||||
matches.
|
||||
"""
|
||||
|
||||
__slots__ = ("next_token", "position", "rules", "source")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
source: str,
|
||||
@@ -135,7 +142,7 @@ class Tokenizer:
|
||||
def expect(self, name: str, *, expected: str) -> Token:
|
||||
"""Expect a certain token name next, failing with a syntax error otherwise.
|
||||
|
||||
The token is *not* read.
|
||||
The token is read and returned.
|
||||
"""
|
||||
if not self.check(name):
|
||||
raise self.raise_syntax_error(f"Expected {expected}")
|
||||
|
||||
@@ -4,7 +4,7 @@ import re
|
||||
from collections.abc import Mapping, Sequence
|
||||
|
||||
from .errors import _ErrorCollector
|
||||
from .requirements import Requirement
|
||||
from .requirements import InvalidRequirement, Requirement
|
||||
|
||||
__all__ = [
|
||||
"CyclicDependencyGroup",
|
||||
@@ -28,12 +28,16 @@ def __dir__() -> list[str]:
|
||||
class DuplicateGroupNames(ValueError):
|
||||
"""
|
||||
The same dependency groups were defined twice, with different non-normalized names.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
|
||||
class CyclicDependencyGroup(ValueError):
|
||||
"""
|
||||
The dependency group includes form a cycle.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
def __init__(self, requested_group: str, group: str, include_group: str) -> None:
|
||||
@@ -50,6 +54,10 @@ class CyclicDependencyGroup(ValueError):
|
||||
f"{requested_group}: {reason}"
|
||||
)
|
||||
|
||||
# Support pickling; ``args`` does not match ``__init__``'s signature.
|
||||
def __reduce__(self) -> tuple[type[CyclicDependencyGroup], tuple[str, str, str]]:
|
||||
return (self.__class__, (self.requested_group, self.group, self.include_group))
|
||||
|
||||
|
||||
# in the PEP 735 spec, the tables in dependency group lists were described as
|
||||
# "Dependency Object Specifiers", but the only defined type of object was a
|
||||
@@ -58,6 +66,8 @@ class InvalidDependencyGroupObject(ValueError):
|
||||
"""
|
||||
A member of a dependency group was identified as a dict, but was not in a valid
|
||||
format.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
|
||||
@@ -67,6 +77,12 @@ class InvalidDependencyGroupObject(ValueError):
|
||||
|
||||
|
||||
class DependencyGroupInclude:
|
||||
"""
|
||||
A reference to another dependency group by name.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
__slots__ = ("include_group",)
|
||||
|
||||
def __init__(self, include_group: str) -> None:
|
||||
@@ -91,6 +107,8 @@ class DependencyGroupResolver:
|
||||
|
||||
:param dependency_groups: A mapping, as provided via pyproject
|
||||
``[dependency-groups]``.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -227,9 +245,10 @@ class DependencyGroupResolver:
|
||||
for item in raw_group:
|
||||
if isinstance(item, str):
|
||||
# packaging.requirements.Requirement parsing ensures that this is a
|
||||
# valid PEP 508 Dependency Specifier
|
||||
# raises InvalidRequirement on failure
|
||||
elements.append(Requirement(item))
|
||||
# valid PEP 508 Dependency Specifier. Collect InvalidRequirement
|
||||
# if it throws that.
|
||||
with errors.collect(InvalidRequirement):
|
||||
elements.append(Requirement(item))
|
||||
elif isinstance(item, Mapping):
|
||||
if tuple(item.keys()) != ("include-group",):
|
||||
errors.error(
|
||||
@@ -239,10 +258,22 @@ class DependencyGroupResolver:
|
||||
)
|
||||
else:
|
||||
include_group = item["include-group"]
|
||||
elements.append(DependencyGroupInclude(include_group=include_group))
|
||||
if not isinstance(include_group, str):
|
||||
msg = (
|
||||
"Dependency group include-group value is not a string: "
|
||||
f"{item!r}"
|
||||
)
|
||||
errors.error(TypeError(msg))
|
||||
else:
|
||||
elements.append(
|
||||
DependencyGroupInclude(include_group=include_group)
|
||||
)
|
||||
else:
|
||||
errors.error(TypeError(f"Invalid dependency group item: {item!r}"))
|
||||
|
||||
if errors.errors:
|
||||
return ()
|
||||
|
||||
self._parsed_groups[group] = tuple(elements)
|
||||
return self._parsed_groups[group]
|
||||
|
||||
@@ -261,6 +292,8 @@ def resolve_dependency_groups(
|
||||
:param dependency_groups: the parsed contents of the ``[dependency-groups]`` table
|
||||
from ``pyproject.toml``
|
||||
:param groups: the name of the group(s) to resolve
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
resolver = DependencyGroupResolver(dependency_groups)
|
||||
return tuple(str(r) for group in groups for r in resolver.resolve(group))
|
||||
|
||||
@@ -9,6 +9,7 @@ from typing import TYPE_CHECKING, Any, Protocol, TypeVar
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import sys
|
||||
from collections.abc import Collection
|
||||
from urllib.parse import SplitResult
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import Self
|
||||
@@ -83,7 +84,7 @@ _PEP610_USER_PASS_ENV_VARS_REGEX = re.compile(
|
||||
def _strip_auth_from_netloc(netloc: str, safe_user_passwords: Collection[str]) -> str:
|
||||
if "@" not in netloc:
|
||||
return netloc
|
||||
user_pass, netloc_no_user_pass = netloc.split("@", 1)
|
||||
user_pass, netloc_no_user_pass = netloc.rsplit("@", 1)
|
||||
if user_pass in safe_user_passwords:
|
||||
return netloc
|
||||
if _PEP610_USER_PASS_ENV_VARS_REGEX.match(user_pass):
|
||||
@@ -109,8 +110,15 @@ def _strip_url(url: str, safe_user_passwords: Collection[str]) -> str:
|
||||
)
|
||||
|
||||
|
||||
def _file_url_has_absolute_path(parsed_url: SplitResult) -> bool:
|
||||
return parsed_url.path.startswith("/")
|
||||
|
||||
|
||||
class DirectUrlValidationError(Exception):
|
||||
"""Raised when when input data is not spec-compliant."""
|
||||
"""Raised when when input data is not spec-compliant.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
context: str | None = None
|
||||
message: str
|
||||
@@ -146,6 +154,8 @@ class _DirectUrlRequiredKeyError(DirectUrlValidationError):
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class VcsInfo:
|
||||
"""The version control information of a :class:`DirectUrl`."""
|
||||
|
||||
vcs: str
|
||||
commit_id: str
|
||||
requested_revision: str | None = None
|
||||
@@ -173,6 +183,8 @@ class VcsInfo:
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class ArchiveInfo:
|
||||
"""The archive information of a :class:`DirectUrl`."""
|
||||
|
||||
hashes: Mapping[str, str] | None = None
|
||||
|
||||
def __init__(
|
||||
@@ -219,6 +231,8 @@ class ArchiveInfo:
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class DirInfo:
|
||||
"""The local directory information of a :class:`DirectUrl`."""
|
||||
|
||||
editable: bool | None = None
|
||||
|
||||
def __init__(
|
||||
@@ -237,7 +251,10 @@ class DirInfo:
|
||||
|
||||
@dataclasses.dataclass(frozen=True, init=False)
|
||||
class DirectUrl:
|
||||
"""A class representing a direct URL."""
|
||||
"""A class representing a direct URL.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
url: str
|
||||
archive_info: ArchiveInfo | None = None
|
||||
@@ -277,11 +294,18 @@ class DirectUrl:
|
||||
raise DirectUrlValidationError(
|
||||
"Exactly one of vcs_info, archive_info, dir_info must be present"
|
||||
)
|
||||
if direct_url.dir_info is not None and not direct_url.url.startswith("file://"):
|
||||
raise DirectUrlValidationError(
|
||||
"URL scheme must be file:// when dir_info is present",
|
||||
context="url",
|
||||
)
|
||||
if direct_url.dir_info is not None:
|
||||
parsed_url = urllib.parse.urlsplit(direct_url.url)
|
||||
if parsed_url.scheme != "file":
|
||||
raise DirectUrlValidationError(
|
||||
"URL scheme must be file:// when dir_info is present",
|
||||
context="url",
|
||||
)
|
||||
if not _file_url_has_absolute_path(parsed_url):
|
||||
raise DirectUrlValidationError(
|
||||
"File URL must be absolute when dir_info is present",
|
||||
context="url",
|
||||
)
|
||||
# XXX subdirectory must be relative, can we, should we validate that here?
|
||||
return direct_url
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
license_ref_allowed = re.compile("^[A-Za-z0-9.-]*$")
|
||||
license_ref_allowed = re.compile("^[A-Za-z0-9.-]+$")
|
||||
|
||||
NormalizedLicenseExpression = NewType("NormalizedLicenseExpression", str)
|
||||
"""
|
||||
@@ -64,7 +64,7 @@ class InvalidLicenseExpression(ValueError):
|
||||
>>> canonicalize_license_expression("invalid")
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
packaging.licenses.InvalidLicenseExpression: Invalid license expression: 'invalid'
|
||||
packaging.licenses.InvalidLicenseExpression: Unknown license: 'invalid'
|
||||
"""
|
||||
|
||||
|
||||
@@ -148,9 +148,18 @@ def canonicalize_license_expression(
|
||||
|
||||
# Take a final pass to check for unknown licenses/exceptions.
|
||||
normalized_tokens = []
|
||||
for token in tokens:
|
||||
last_license_start = False
|
||||
for index, token in enumerate(tokens):
|
||||
if token in {"or", "and", "with", "(", ")"}:
|
||||
if token == "with" and (
|
||||
not last_license_start
|
||||
or index + 1 == len(tokens)
|
||||
or tokens[index + 1] in {"or", "and", "with", "(", ")"}
|
||||
):
|
||||
message = f"Invalid license expression: {raw_license_expression!r}"
|
||||
raise InvalidLicenseExpression(message)
|
||||
normalized_tokens.append(token.upper())
|
||||
last_license_start = False
|
||||
continue
|
||||
|
||||
if normalized_tokens and normalized_tokens[-1] == "WITH":
|
||||
@@ -159,6 +168,7 @@ def canonicalize_license_expression(
|
||||
raise InvalidLicenseExpression(message)
|
||||
|
||||
normalized_tokens.append(EXCEPTIONS[token]["id"])
|
||||
last_license_start = False
|
||||
else:
|
||||
if token.endswith("+"):
|
||||
final_token = token[:-1]
|
||||
@@ -168,15 +178,17 @@ def canonicalize_license_expression(
|
||||
suffix = ""
|
||||
|
||||
if final_token.startswith("licenseref-"):
|
||||
if not license_ref_allowed.match(final_token):
|
||||
message = f"Invalid licenseref: {final_token!r}"
|
||||
license_ref_id = final_token[len("licenseref-") :]
|
||||
if suffix or not license_ref_allowed.match(license_ref_id):
|
||||
message = f"Invalid licenseref: {token!r}"
|
||||
raise InvalidLicenseExpression(message)
|
||||
normalized_tokens.append(license_refs[final_token] + suffix)
|
||||
normalized_tokens.append(license_refs[final_token])
|
||||
else:
|
||||
if final_token not in LICENSES:
|
||||
message = f"Unknown license: {final_token!r}"
|
||||
raise InvalidLicenseExpression(message)
|
||||
normalized_tokens.append(LICENSES[final_token]["id"] + suffix)
|
||||
last_license_start = True
|
||||
|
||||
normalized_expression = " ".join(normalized_tokens)
|
||||
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import operator
|
||||
import os
|
||||
import platform
|
||||
import sys
|
||||
from typing import AbstractSet, Callable, Literal, Mapping, TypedDict, Union, cast
|
||||
from collections.abc import Set as AbstractSet
|
||||
from typing import TYPE_CHECKING, Callable, Literal, TypedDict, Union, cast
|
||||
|
||||
from ._parser import MarkerAtom, MarkerList, Op, Value, Variable
|
||||
from ._parser import parse_marker as _parse_marker
|
||||
@@ -16,6 +18,9 @@ from ._tokenizer import ParserSyntaxError
|
||||
from .specifiers import InvalidSpecifier, Specifier
|
||||
from .utils import canonicalize_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Mapping
|
||||
|
||||
__all__ = [
|
||||
"Environment",
|
||||
"EvaluateContext",
|
||||
@@ -40,6 +45,8 @@ Valid values for the ``context`` passed to :meth:`Marker.evaluate` are:
|
||||
* ``"metadata"`` (for core metadata; default)
|
||||
* ``"lock_file"`` (for lock files)
|
||||
* ``"requirement"`` (i.e. all other situations)
|
||||
|
||||
.. versionadded:: 25.0
|
||||
"""
|
||||
|
||||
MARKERS_ALLOWING_SET = {"extras", "dependency_groups"}
|
||||
@@ -68,8 +75,17 @@ class UndefinedComparison(ValueError):
|
||||
"""
|
||||
|
||||
|
||||
class UndefinedEnvironmentName(ValueError):
|
||||
"""Raised when evaluating a marker that references a missing environment key."""
|
||||
class UndefinedEnvironmentName(KeyError):
|
||||
"""Raised when evaluating a marker that references a missing environment key.
|
||||
|
||||
Subclasses :class:`KeyError` so that code catching the bare ``KeyError`` that
|
||||
a missing environment lookup historically produced keeps working.
|
||||
|
||||
.. versionchanged:: 26.3
|
||||
Now subclasses :class:`KeyError` (was :class:`ValueError`) and is raised by
|
||||
:meth:`Marker.evaluate` for missing environment keys, where a bare
|
||||
``KeyError`` was raised before.
|
||||
"""
|
||||
|
||||
|
||||
class Environment(TypedDict):
|
||||
@@ -152,16 +168,30 @@ class Environment(TypedDict):
|
||||
def _normalize_extras(
|
||||
result: MarkerList | MarkerAtom | str,
|
||||
) -> MarkerList | MarkerAtom | str:
|
||||
if isinstance(result, list):
|
||||
return [_normalize_extras(r) for r in result]
|
||||
if not isinstance(result, tuple):
|
||||
return result
|
||||
|
||||
lhs, op, rhs = result
|
||||
if isinstance(lhs, Variable) and lhs.value == "extra":
|
||||
if isinstance(lhs, Variable) and lhs.value == "extra" and isinstance(rhs, Value):
|
||||
normalized_extra = canonicalize_name(rhs.value)
|
||||
rhs = Value(normalized_extra)
|
||||
elif isinstance(rhs, Variable) and rhs.value == "extra":
|
||||
elif isinstance(rhs, Variable) and rhs.value == "extra" and isinstance(lhs, Value):
|
||||
normalized_extra = canonicalize_name(lhs.value)
|
||||
lhs = Value(normalized_extra)
|
||||
elif (
|
||||
isinstance(rhs, Variable)
|
||||
and rhs.value in MARKERS_ALLOWING_SET
|
||||
and isinstance(lhs, Value)
|
||||
):
|
||||
# PEP 685 (extras) / PEP 735 (dependency_groups): the set-valued membership
|
||||
# literal must also be normalized. evaluate() already canonicalizes both
|
||||
# operands for these keys (see _normalize), so normalizing the literal at
|
||||
# parse time keeps __str__/__eq__/__hash__ consistent with evaluate() -- e.g.
|
||||
# Marker('"Foo" in extras') and Marker('"foo" in extras') must compare and
|
||||
# hash equal (the membership variable is always the right-hand operand).
|
||||
lhs = Value(canonicalize_name(lhs.value))
|
||||
return lhs, op, rhs
|
||||
|
||||
|
||||
@@ -178,16 +208,14 @@ def _format_marker(
|
||||
) -> str:
|
||||
assert isinstance(marker, (list, tuple, str))
|
||||
|
||||
# Sometimes we have a structure like [[...]] which is a single item list
|
||||
# where the single item is itself it's own list. In that case we want skip
|
||||
# the rest of this function so that we don't get extraneous () on the
|
||||
# outside.
|
||||
# Unwrap a redundant [[...]] wrapper, but keep the nesting context so a
|
||||
# nested group keeps the parentheses its and/or precedence needs.
|
||||
if (
|
||||
isinstance(marker, list)
|
||||
and len(marker) == 1
|
||||
and isinstance(marker[0], (list, tuple))
|
||||
):
|
||||
return _format_marker(marker[0])
|
||||
return _format_marker(marker[0], first=first)
|
||||
|
||||
if isinstance(marker, list):
|
||||
inner = (_format_marker(m, first=False) for m in marker)
|
||||
@@ -251,6 +279,15 @@ def _normalize(
|
||||
return lhs, rhs
|
||||
|
||||
|
||||
def _lookup_environment(
|
||||
environment: dict[str, str | AbstractSet[str]], key: str
|
||||
) -> str | AbstractSet[str]:
|
||||
try:
|
||||
return environment[key]
|
||||
except KeyError:
|
||||
raise UndefinedEnvironmentName(key) from None
|
||||
|
||||
|
||||
def _evaluate_markers(
|
||||
markers: MarkerList, environment: dict[str, str | AbstractSet[str]]
|
||||
) -> bool:
|
||||
@@ -264,14 +301,20 @@ def _evaluate_markers(
|
||||
|
||||
if isinstance(lhs, Variable):
|
||||
environment_key = lhs.value
|
||||
lhs_value = environment[environment_key]
|
||||
lhs_value = _lookup_environment(environment, environment_key)
|
||||
rhs_value = rhs.value
|
||||
else:
|
||||
lhs_value = lhs.value
|
||||
environment_key = rhs.value
|
||||
rhs_value = environment[environment_key]
|
||||
rhs_value = _lookup_environment(environment, environment_key)
|
||||
|
||||
assert isinstance(lhs_value, str), "lhs must be a string"
|
||||
if not isinstance(lhs_value, str):
|
||||
raise UndefinedComparison(
|
||||
f"Set-valued marker {environment_key!r} can only be used "
|
||||
f'with the membership form (e.g. "<name>" in '
|
||||
f"{environment_key}); it cannot appear on the left-hand "
|
||||
f"side of {op.serialize()!r}."
|
||||
)
|
||||
lhs_value, rhs_value = _normalize(lhs_value, rhs_value, key=environment_key)
|
||||
groups[-1].append(_eval_op(lhs_value, op, rhs_value, key=environment_key))
|
||||
elif marker == "or":
|
||||
@@ -292,10 +335,14 @@ def _format_full_version(info: sys._version_info) -> str:
|
||||
return version
|
||||
|
||||
|
||||
def default_environment() -> Environment:
|
||||
"""Return the default marker environment for the current Python process.
|
||||
@functools.cache
|
||||
def _cached_default_environment() -> Environment:
|
||||
"""Build the default marker environment for the current Python process.
|
||||
|
||||
This is the base environment used by :meth:`Marker.evaluate`.
|
||||
The values are derived from process-constant data (the running interpreter
|
||||
and the host platform), so this is cached and built only once. The result is
|
||||
shared between callers and must never be mutated; :func:`default_environment`
|
||||
returns a fresh copy.
|
||||
"""
|
||||
iver = _format_full_version(sys.implementation.version)
|
||||
implementation_name = sys.implementation.name
|
||||
@@ -314,6 +361,22 @@ def default_environment() -> Environment:
|
||||
}
|
||||
|
||||
|
||||
def default_environment() -> Environment:
|
||||
"""Return the default marker environment for the current Python process.
|
||||
|
||||
This is the base environment used by :meth:`Marker.evaluate`. A fresh copy
|
||||
is returned on every call so callers may freely mutate the result; a shallow
|
||||
copy suffices because all values are immutable strings.
|
||||
|
||||
.. versionchanged:: 26.3
|
||||
The environment is computed once per process and cached, since it is
|
||||
derived from process-constant data. Patching ``platform``/``sys``/``os``
|
||||
after the first call has no effect; pass an explicit ``environment`` to
|
||||
:meth:`Marker.evaluate` to evaluate against different values.
|
||||
"""
|
||||
return cast("Environment", dict(_cached_default_environment()))
|
||||
|
||||
|
||||
class Marker:
|
||||
"""Represents a parsed dependency marker expression.
|
||||
|
||||
@@ -421,11 +484,19 @@ class Marker:
|
||||
raise TypeError(f"Cannot restore Marker from {state!r}")
|
||||
|
||||
def __and__(self, other: Marker) -> Marker:
|
||||
"""Combine this marker with another using ``and``.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
return self._from_markers([self._markers, "and", other._markers])
|
||||
|
||||
def __or__(self, other: Marker) -> Marker:
|
||||
"""Combine this marker with another using ``or``.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
if not isinstance(other, Marker):
|
||||
return NotImplemented
|
||||
return self._from_markers([self._markers, "or", other._markers])
|
||||
@@ -454,19 +525,23 @@ class Marker:
|
||||
is missing from the evaluation environment.
|
||||
:returns: ``True`` if the marker matches, otherwise ``False``.
|
||||
|
||||
.. versionchanged:: 25.0
|
||||
Added the ``context`` parameter, which influences which marker names
|
||||
are considered valid.
|
||||
"""
|
||||
current_environment = cast(
|
||||
"dict[str, str | AbstractSet[str]]", default_environment()
|
||||
)
|
||||
if context == "lock_file":
|
||||
current_environment.update(
|
||||
extras=frozenset(), dependency_groups=frozenset()
|
||||
)
|
||||
current_environment |= {
|
||||
"extras": frozenset(),
|
||||
"dependency_groups": frozenset(),
|
||||
}
|
||||
elif context == "metadata":
|
||||
current_environment["extra"] = ""
|
||||
|
||||
if environment is not None:
|
||||
current_environment.update(environment)
|
||||
current_environment |= environment
|
||||
if "extra" in current_environment:
|
||||
# The API used to allow setting extra to None. We need to handle
|
||||
# this case for backwards compatibility. Also skip running
|
||||
@@ -479,6 +554,16 @@ class Marker:
|
||||
)
|
||||
|
||||
|
||||
def _pep440_python_full_version(python_full_version: str) -> str:
|
||||
"""
|
||||
Work around platform.python_version() returning something that is not PEP 440
|
||||
compliant for non-tagged Python builds.
|
||||
"""
|
||||
if python_full_version.endswith("+"):
|
||||
return f"{python_full_version}local"
|
||||
return python_full_version
|
||||
|
||||
|
||||
def _repair_python_full_version(
|
||||
env: dict[str, str | AbstractSet[str]],
|
||||
) -> dict[str, str | AbstractSet[str]]:
|
||||
@@ -487,6 +572,5 @@ def _repair_python_full_version(
|
||||
compliant for non-tagged Python builds.
|
||||
"""
|
||||
python_full_version = cast("str", env["python_full_version"])
|
||||
if python_full_version.endswith("+"):
|
||||
env["python_full_version"] = f"{python_full_version}local"
|
||||
env["python_full_version"] = _pep440_python_full_version(python_full_version)
|
||||
return env
|
||||
|
||||
@@ -6,6 +6,7 @@ import email.parser
|
||||
import email.policy
|
||||
import keyword
|
||||
import pathlib
|
||||
import re
|
||||
import typing
|
||||
from typing import (
|
||||
Any,
|
||||
@@ -22,6 +23,7 @@ from .errors import ExceptionGroup, _ErrorCollector
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .licenses import NormalizedLicenseExpression
|
||||
from .version import Version
|
||||
|
||||
T = typing.TypeVar("T")
|
||||
|
||||
@@ -42,7 +44,10 @@ def __dir__() -> list[str]:
|
||||
|
||||
|
||||
class InvalidMetadata(ValueError):
|
||||
"""A metadata field contains invalid data."""
|
||||
"""A metadata field contains invalid data.
|
||||
|
||||
.. versionadded:: 23.2
|
||||
"""
|
||||
|
||||
field: str
|
||||
"""The name of the field that contains invalid data."""
|
||||
@@ -51,6 +56,10 @@ class InvalidMetadata(ValueError):
|
||||
self.field = field
|
||||
super().__init__(message)
|
||||
|
||||
# Support pickling; ``args`` does not match ``__init__``'s signature.
|
||||
def __reduce__(self) -> tuple[type[InvalidMetadata], tuple[str, str]]:
|
||||
return (self.__class__, (self.field, self.args[0]))
|
||||
|
||||
|
||||
# The RawMetadata class attempts to make as few assumptions about the underlying
|
||||
# serialization formats as possible. The idea is that as long as a serialization
|
||||
@@ -125,11 +134,17 @@ class RawMetadata(TypedDict, total=False):
|
||||
|
||||
# Metadata 2.4 - PEP 639
|
||||
license_expression: str
|
||||
""".. versionadded:: 24.2"""
|
||||
license_files: list[str]
|
||||
""".. versionadded:: 24.2"""
|
||||
|
||||
# Metadata 2.5 - PEP 794
|
||||
import_names: list[str]
|
||||
""".. versionadded:: 26.0"""
|
||||
import_namespaces: list[str]
|
||||
""".. versionadded:: 26.0"""
|
||||
|
||||
# Metadata 2.6 - PEP 808 (no new fields, behavior change for Dynamic)
|
||||
|
||||
|
||||
# 'keywords' is special as it's a string in the core metadata spec, but we
|
||||
@@ -225,13 +240,19 @@ def _get_payload(msg: email.message.Message, source: bytes | str) -> str:
|
||||
# and we don't need to deal with it.
|
||||
if isinstance(source, str):
|
||||
payload = msg.get_payload()
|
||||
assert isinstance(payload, str)
|
||||
# A multipart payload makes get_payload() return a list of messages
|
||||
# rather than a str; route it to ``unparsed``.
|
||||
if not isinstance(payload, str):
|
||||
raise ValueError("payload is not a string") # noqa: TRY004
|
||||
return payload
|
||||
# If our source is a bytes, then we're managing the encoding and we need
|
||||
# to deal with it.
|
||||
else:
|
||||
bpayload = msg.get_payload(decode=True)
|
||||
assert isinstance(bpayload, bytes)
|
||||
# A multipart payload makes get_payload(decode=True) return None;
|
||||
# route it to ``unparsed``.
|
||||
if not isinstance(bpayload, bytes):
|
||||
raise ValueError("payload in an invalid encoding") # noqa: TRY004
|
||||
try:
|
||||
return bpayload.decode("utf8", "strict")
|
||||
except UnicodeDecodeError as exc:
|
||||
@@ -287,11 +308,19 @@ _EMAIL_TO_RAW_MAPPING = {
|
||||
_RAW_TO_EMAIL_MAPPING = {raw: email for email, raw in _EMAIL_TO_RAW_MAPPING.items()}
|
||||
|
||||
|
||||
# A bare "\r" makes the email generator raise ``HeaderWriteError``, and on
|
||||
# CPython releases without the CVE-2024-6923 fix any ``str.splitlines``
|
||||
# boundary ends the header line, so fold all of them, not just "\n".
|
||||
_LINE_BOUNDARY_RE = re.compile(r"\r\n|[\n\r\v\f\x1c\x1d\x1e\x85\u2028\u2029]")
|
||||
|
||||
|
||||
# This class is for writing RFC822 messages
|
||||
class RFC822Policy(email.policy.EmailPolicy):
|
||||
"""
|
||||
This is :class:`email.policy.EmailPolicy`, but with a simple ``header_store_parse``
|
||||
implementation that handles multi-line values, and some nice defaults.
|
||||
|
||||
.. versionadded:: 26.0
|
||||
"""
|
||||
|
||||
utf8 = True
|
||||
@@ -300,7 +329,7 @@ class RFC822Policy(email.policy.EmailPolicy):
|
||||
|
||||
def header_store_parse(self, name: str, value: str) -> tuple[str, str]:
|
||||
size = len(name) + 2
|
||||
value = value.replace("\n", "\n" + " " * size)
|
||||
value = _LINE_BOUNDARY_RE.sub("\n" + " " * size, value)
|
||||
return (name, value)
|
||||
|
||||
|
||||
@@ -310,6 +339,8 @@ class RFC822Message(email.message.EmailMessage):
|
||||
This is :class:`email.message.EmailMessage` with two small changes: it defaults to
|
||||
our `RFC822Policy`, and it correctly writes unicode when being called
|
||||
with `bytes()`.
|
||||
|
||||
.. versionadded:: 26.0
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
@@ -511,8 +542,20 @@ _NOT_FOUND = object()
|
||||
|
||||
|
||||
# Keep the two values in sync.
|
||||
_VALID_METADATA_VERSIONS = ["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
||||
_MetadataVersion = Literal["1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5"]
|
||||
_VALID_METADATA_VERSIONS = [
|
||||
"1.0",
|
||||
"1.1",
|
||||
"1.2",
|
||||
"2.1",
|
||||
"2.2",
|
||||
"2.3",
|
||||
"2.4",
|
||||
"2.5",
|
||||
"2.6",
|
||||
]
|
||||
_MetadataVersion = Literal[
|
||||
"1.0", "1.1", "1.2", "2.1", "2.2", "2.3", "2.4", "2.5", "2.6"
|
||||
]
|
||||
|
||||
_REQUIRED_ATTRS = frozenset(["metadata_version", "name", "version"])
|
||||
|
||||
@@ -572,9 +615,7 @@ class _Validator(Generic[T]):
|
||||
def _invalid_metadata(
|
||||
self, msg: str, cause: Exception | None = None
|
||||
) -> InvalidMetadata:
|
||||
exc = InvalidMetadata(
|
||||
self.raw_name, msg.format_map({"field": repr(self.raw_name)})
|
||||
)
|
||||
exc = InvalidMetadata(self.raw_name, msg)
|
||||
exc.__cause__ = cause
|
||||
return exc
|
||||
|
||||
@@ -586,67 +627,82 @@ class _Validator(Generic[T]):
|
||||
|
||||
def _process_name(self, value: str) -> str:
|
||||
if not value:
|
||||
raise self._invalid_metadata("{field} is a required field")
|
||||
raise self._invalid_metadata(f"{self.raw_name!r} is a required field")
|
||||
# Validate the name as a side-effect.
|
||||
try:
|
||||
utils.canonicalize_name(value, validate=True)
|
||||
except utils.InvalidName as exc:
|
||||
raise self._invalid_metadata(
|
||||
f"{value!r} is invalid for {{field}}", cause=exc
|
||||
f"{value!r} is invalid for {self.raw_name!r}", cause=exc
|
||||
) from exc
|
||||
else:
|
||||
return value
|
||||
|
||||
def _process_version(self, value: str) -> version_module.Version:
|
||||
def _process_version(self, value: str) -> Version:
|
||||
if not value:
|
||||
raise self._invalid_metadata("{field} is a required field")
|
||||
raise self._invalid_metadata(f"{self.raw_name!r} is a required field")
|
||||
try:
|
||||
return version_module.parse(value)
|
||||
except version_module.InvalidVersion as exc:
|
||||
raise self._invalid_metadata(
|
||||
f"{value!r} is invalid for {{field}}", cause=exc
|
||||
f"{value!r} is invalid for {self.raw_name!r}", cause=exc
|
||||
) from exc
|
||||
|
||||
def _process_summary(self, value: str) -> str:
|
||||
"""Check the field contains no newlines."""
|
||||
if "\n" in value:
|
||||
raise self._invalid_metadata("{field} must be a single line")
|
||||
"""Check the field contains no line breaks."""
|
||||
if _LINE_BOUNDARY_RE.search(value):
|
||||
raise self._invalid_metadata(f"{self.raw_name!r} must be a single line")
|
||||
return value
|
||||
|
||||
def _process_description_content_type(self, value: str) -> str:
|
||||
content_types = {"text/plain", "text/x-rst", "text/markdown"}
|
||||
invalid_msg = (
|
||||
f"{self.raw_name!r} must be one of {list(content_types)}, not {value!r}"
|
||||
)
|
||||
message = email.message.EmailMessage()
|
||||
message["content-type"] = value
|
||||
try:
|
||||
message["content-type"] = value
|
||||
# The email parser can raise IndexError on malformed RFC 2231
|
||||
# parameters such as "text/plain; x*".
|
||||
except (ValueError, IndexError) as exc:
|
||||
msg = f"{value!r} is not a valid content type for {self.raw_name!r}"
|
||||
raise self._invalid_metadata(msg, cause=exc) from exc
|
||||
content_type_header = message["content-type"]
|
||||
if content_type_header.defects:
|
||||
defect = content_type_header.defects[0]
|
||||
msg = (
|
||||
f"{value!r} is not a valid content type for {self.raw_name!r}: {defect}"
|
||||
)
|
||||
raise self._invalid_metadata(msg, cause=defect) from defect
|
||||
|
||||
content_type, parameters = (
|
||||
# Defaults to `text/plain` if parsing failed.
|
||||
message.get_content_type().lower(),
|
||||
message["content-type"].params,
|
||||
content_type_header.params,
|
||||
)
|
||||
# Check if content-type is valid or defaulted to `text/plain` and thus was
|
||||
# not parseable.
|
||||
if content_type not in content_types or content_type not in value.lower():
|
||||
raise self._invalid_metadata(
|
||||
f"{{field}} must be one of {list(content_types)}, not {value!r}"
|
||||
)
|
||||
raise self._invalid_metadata(invalid_msg)
|
||||
|
||||
charset = parameters.get("charset", "UTF-8")
|
||||
if charset != "UTF-8":
|
||||
if charset.lower() != "utf-8":
|
||||
raise self._invalid_metadata(
|
||||
f"{{field}} can only specify the UTF-8 charset, not {charset!r}"
|
||||
f"{self.raw_name!r} can only specify the UTF-8 charset, not {charset!r}"
|
||||
)
|
||||
|
||||
markdown_variants = {"GFM", "CommonMark"}
|
||||
variant = parameters.get("variant", "GFM") # Use an acceptable default.
|
||||
if content_type == "text/markdown" and variant not in markdown_variants:
|
||||
raise self._invalid_metadata(
|
||||
f"valid Markdown variants for {{field}} are {list(markdown_variants)}, "
|
||||
f"not {variant!r}",
|
||||
f"valid Markdown variants for {self.raw_name!r} are "
|
||||
f"{list(markdown_variants)}, not {variant!r}",
|
||||
)
|
||||
return value
|
||||
|
||||
def _process_dynamic(self, value: list[str]) -> list[str]:
|
||||
for dynamic_field in map(str.lower, value):
|
||||
dynamic_fields = list(map(str.lower, value))
|
||||
for dynamic_field in dynamic_fields:
|
||||
if dynamic_field in {"name", "version", "metadata-version"}:
|
||||
raise self._invalid_metadata(
|
||||
f"{dynamic_field!r} is not allowed as a dynamic field"
|
||||
@@ -655,7 +711,7 @@ class _Validator(Generic[T]):
|
||||
raise self._invalid_metadata(
|
||||
f"{dynamic_field!r} is not a valid dynamic field"
|
||||
)
|
||||
return list(map(str.lower, value))
|
||||
return dynamic_fields
|
||||
|
||||
def _process_provides_extra(
|
||||
self,
|
||||
@@ -667,7 +723,7 @@ class _Validator(Generic[T]):
|
||||
normalized_names.append(utils.canonicalize_name(name, validate=True))
|
||||
except utils.InvalidName as exc:
|
||||
raise self._invalid_metadata(
|
||||
f"{name!r} is invalid for {{field}}", cause=exc
|
||||
f"{name!r} is invalid for {self.raw_name!r}", cause=exc
|
||||
) from exc
|
||||
else:
|
||||
return normalized_names
|
||||
@@ -677,7 +733,7 @@ class _Validator(Generic[T]):
|
||||
return specifiers.SpecifierSet(value)
|
||||
except specifiers.InvalidSpecifier as exc:
|
||||
raise self._invalid_metadata(
|
||||
f"{value!r} is invalid for {{field}}", cause=exc
|
||||
f"{value!r} is invalid for {self.raw_name!r}", cause=exc
|
||||
) from exc
|
||||
|
||||
def _process_requires_dist(
|
||||
@@ -690,7 +746,7 @@ class _Validator(Generic[T]):
|
||||
reqs.append(requirements.Requirement(req))
|
||||
except requirements.InvalidRequirement as exc:
|
||||
raise self._invalid_metadata(
|
||||
f"{req!r} is invalid for {{field}}", cause=exc
|
||||
f"{req!r} is invalid for {self.raw_name!r}", cause=exc
|
||||
) from exc
|
||||
else:
|
||||
return reqs
|
||||
@@ -700,7 +756,7 @@ class _Validator(Generic[T]):
|
||||
return licenses.canonicalize_license_expression(value)
|
||||
except ValueError as exc:
|
||||
raise self._invalid_metadata(
|
||||
f"{value!r} is invalid for {{field}}", cause=exc
|
||||
f"{value!r} is invalid for {self.raw_name!r}", cause=exc
|
||||
) from exc
|
||||
|
||||
def _process_license_files(self, value: list[str]) -> list[str]:
|
||||
@@ -708,23 +764,24 @@ class _Validator(Generic[T]):
|
||||
for path in value:
|
||||
if ".." in path:
|
||||
raise self._invalid_metadata(
|
||||
f"{path!r} is invalid for {{field}}, "
|
||||
f"{path!r} is invalid for {self.raw_name!r}, "
|
||||
"parent directory indicators are not allowed"
|
||||
)
|
||||
if "*" in path:
|
||||
raise self._invalid_metadata(
|
||||
f"{path!r} is invalid for {{field}}, paths must be resolved"
|
||||
f"{path!r} is invalid for {self.raw_name!r}, paths must be resolved"
|
||||
)
|
||||
if (
|
||||
pathlib.PurePosixPath(path).is_absolute()
|
||||
or pathlib.PureWindowsPath(path).is_absolute()
|
||||
):
|
||||
raise self._invalid_metadata(
|
||||
f"{path!r} is invalid for {{field}}, paths must be relative"
|
||||
f"{path!r} is invalid for {self.raw_name!r}, paths must be relative"
|
||||
)
|
||||
if pathlib.PureWindowsPath(path).as_posix() != path:
|
||||
raise self._invalid_metadata(
|
||||
f"{path!r} is invalid for {{field}}, paths must use '/' delimiter"
|
||||
f"{path!r} is invalid for {self.raw_name!r}, "
|
||||
"paths must use '/' delimiter"
|
||||
)
|
||||
paths.append(path)
|
||||
return paths
|
||||
@@ -736,17 +793,17 @@ class _Validator(Generic[T]):
|
||||
for identifier in name.split("."):
|
||||
if not identifier.isidentifier():
|
||||
raise self._invalid_metadata(
|
||||
f"{name!r} is invalid for {{field}}; "
|
||||
f"{name!r} is invalid for {self.raw_name!r}; "
|
||||
f"{identifier!r} is not a valid identifier"
|
||||
)
|
||||
elif keyword.iskeyword(identifier):
|
||||
raise self._invalid_metadata(
|
||||
f"{name!r} is invalid for {{field}}; "
|
||||
f"{name!r} is invalid for {self.raw_name!r}; "
|
||||
f"{identifier!r} is a keyword"
|
||||
)
|
||||
if semicolon and private.lstrip() != "private":
|
||||
raise self._invalid_metadata(
|
||||
f"{import_name!r} is invalid for {{field}}; "
|
||||
f"{import_name!r} is invalid for {self.raw_name!r}; "
|
||||
"the only valid option is 'private'"
|
||||
)
|
||||
return value
|
||||
@@ -761,6 +818,12 @@ class Metadata:
|
||||
metadata fields instead of only using built-in types. Any invalid metadata
|
||||
will cause :exc:`InvalidMetadata` to be raised (with a
|
||||
:py:attr:`~BaseException.__cause__` attribute as appropriate).
|
||||
|
||||
.. versionadded:: 23.2
|
||||
|
||||
.. versionchanged:: 24.0
|
||||
Optional attributes now return None when the field is absent instead of
|
||||
raising.
|
||||
"""
|
||||
|
||||
_raw: RawMetadata
|
||||
@@ -769,8 +832,9 @@ class Metadata:
|
||||
def from_raw(cls, data: RawMetadata, *, validate: bool = True) -> Metadata:
|
||||
"""Create an instance from :class:`RawMetadata`.
|
||||
|
||||
If *validate* is true, all metadata will be validated. All exceptions
|
||||
related to validation will be gathered and raised as an :class:`ExceptionGroup`.
|
||||
If *validate* is true, all metadata will be validated and all related
|
||||
exceptions will be gathered and raised as an :class:`ExceptionGroup`;
|
||||
otherwise, validation happens per attribute when it is accessed.
|
||||
"""
|
||||
ins = cls()
|
||||
ins._raw = data.copy() # Mutations occur due to caching enriched values.
|
||||
@@ -829,20 +893,32 @@ class Metadata:
|
||||
raw, unparsed = parse_email(data)
|
||||
|
||||
if validate:
|
||||
with _ErrorCollector().on_exit("unparsed") as collector:
|
||||
with _ErrorCollector().on_exit("invalid or unparsed metadata") as collector:
|
||||
for unparsed_key in unparsed:
|
||||
if unparsed_key in _EMAIL_TO_RAW_MAPPING:
|
||||
message = f"{unparsed_key!r} has invalid data"
|
||||
else:
|
||||
message = f"unrecognized field: {unparsed_key!r}"
|
||||
collector.error(InvalidMetadata(unparsed_key, message))
|
||||
try:
|
||||
validated = cls.from_raw(raw, validate=validate)
|
||||
except ExceptionGroup as exc_group:
|
||||
# The no-branch pragmas cover arcs only seen by Python 3.9.
|
||||
for exc in exc_group.exceptions: # pragma: no branch
|
||||
# A required field reported above as unparsed is absent
|
||||
# from `raw`, so skip from_raw's duplicate "missing"
|
||||
# complaint.
|
||||
if not (
|
||||
isinstance(exc, InvalidMetadata)
|
||||
and exc.field in unparsed
|
||||
and _EMAIL_TO_RAW_MAPPING.get(exc.field) not in raw
|
||||
):
|
||||
collector.error(exc)
|
||||
else:
|
||||
if not collector.errors: # pragma: no branch
|
||||
return validated
|
||||
|
||||
try:
|
||||
return cls.from_raw(raw, validate=validate)
|
||||
except ExceptionGroup as exc_group:
|
||||
raise ExceptionGroup(
|
||||
"invalid or unparsed metadata", exc_group.exceptions
|
||||
) from None
|
||||
return cls.from_raw(raw, validate=validate)
|
||||
|
||||
metadata_version: _Validator[_MetadataVersion] = _Validator()
|
||||
""":external:ref:`core-metadata-metadata-version`
|
||||
@@ -853,7 +929,7 @@ class Metadata:
|
||||
""":external:ref:`core-metadata-name`
|
||||
(required; validated using :func:`~packaging.utils.canonicalize_name` and its
|
||||
*validate* parameter)"""
|
||||
version: _Validator[version_module.Version] = _Validator()
|
||||
version: _Validator[Version] = _Validator()
|
||||
""":external:ref:`core-metadata-version` (required)"""
|
||||
dynamic: _Validator[list[str] | None] = _Validator(
|
||||
added="2.2",
|
||||
@@ -889,9 +965,15 @@ class Metadata:
|
||||
license_expression: _Validator[NormalizedLicenseExpression | None] = _Validator(
|
||||
added="2.4"
|
||||
)
|
||||
""":external:ref:`core-metadata-license-expression`"""
|
||||
""":external:ref:`core-metadata-license-expression`
|
||||
|
||||
.. versionadded:: 24.2
|
||||
"""
|
||||
license_files: _Validator[list[str] | None] = _Validator(added="2.4")
|
||||
""":external:ref:`core-metadata-license-file`"""
|
||||
""":external:ref:`core-metadata-license-file`
|
||||
|
||||
.. versionadded:: 24.2
|
||||
"""
|
||||
classifiers: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||
""":external:ref:`core-metadata-classifier`"""
|
||||
requires_dist: _Validator[list[requirements.Requirement] | None] = _Validator(
|
||||
@@ -919,9 +1001,15 @@ class Metadata:
|
||||
obsoletes_dist: _Validator[list[str] | None] = _Validator(added="1.2")
|
||||
""":external:ref:`core-metadata-obsoletes-dist`"""
|
||||
import_names: _Validator[list[str] | None] = _Validator(added="2.5")
|
||||
""":external:ref:`core-metadata-import-name`"""
|
||||
""":external:ref:`core-metadata-import-name`
|
||||
|
||||
.. versionadded:: 26.0
|
||||
"""
|
||||
import_namespaces: _Validator[list[str] | None] = _Validator(added="2.5")
|
||||
""":external:ref:`core-metadata-import-namespace`"""
|
||||
""":external:ref:`core-metadata-import-namespace`
|
||||
|
||||
.. versionadded:: 26.0
|
||||
"""
|
||||
requires: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||
"""``Requires`` (deprecated)"""
|
||||
provides: _Validator[list[str] | None] = _Validator(added="1.1")
|
||||
@@ -932,6 +1020,8 @@ class Metadata:
|
||||
def as_rfc822(self) -> RFC822Message:
|
||||
"""
|
||||
Return an RFC822 message with the metadata.
|
||||
|
||||
.. versionadded:: 26.0
|
||||
"""
|
||||
message = RFC822Message()
|
||||
self._write_metadata(message)
|
||||
|
||||
@@ -14,9 +14,14 @@ from typing import (
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
from urllib.parse import urlparse
|
||||
from urllib.parse import unquote, urlparse
|
||||
|
||||
from .markers import Environment, Marker, default_environment
|
||||
from .markers import (
|
||||
Environment,
|
||||
Marker,
|
||||
_pep440_python_full_version,
|
||||
default_environment,
|
||||
)
|
||||
from .specifiers import SpecifierSet
|
||||
from .tags import create_compatible_tags_selector, sys_tags
|
||||
from .utils import (
|
||||
@@ -45,6 +50,7 @@ __all__ = [
|
||||
"PackageVcs",
|
||||
"PackageWheel",
|
||||
"Pylock",
|
||||
"PylockSelectError",
|
||||
"PylockUnsupportedVersionError",
|
||||
"PylockValidationError",
|
||||
"is_valid_pylock_path",
|
||||
@@ -99,7 +105,10 @@ def _get(d: Mapping[str, Any], expected_type: type[_T], key: str) -> _T | None:
|
||||
"""Get a value from the dictionary and verify it's the expected type."""
|
||||
if (value := d.get(key)) is None:
|
||||
return None
|
||||
if not isinstance(value, expected_type):
|
||||
if not isinstance(value, expected_type) or (
|
||||
# Special case: bool is a subclass of int, but TOML distinguishes the two
|
||||
expected_type is int and isinstance(value, bool)
|
||||
):
|
||||
raise PylockValidationError(
|
||||
f"Unexpected type {type(value).__name__} "
|
||||
f"(expected {expected_type.__name__})",
|
||||
@@ -255,7 +264,8 @@ def _url_name(url: str | None) -> str | None:
|
||||
if not url:
|
||||
return None
|
||||
url_path = urlparse(url).path
|
||||
return url_path.rsplit("/", 1)[-1]
|
||||
# The last path component is percent-encoded, so decode it to the file name
|
||||
return unquote(url_path.rsplit("/", 1)[-1])
|
||||
|
||||
|
||||
def _validate_hashes(hashes: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
@@ -306,7 +316,10 @@ class PylockUnsupportedVersionError(PylockValidationError):
|
||||
|
||||
|
||||
class PylockSelectError(Exception):
|
||||
"""Base exception for errors raised by :meth:`Pylock.select`."""
|
||||
"""Base exception for errors raised by :meth:`Pylock.select`.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
|
||||
@dataclass(frozen=True, init=False)
|
||||
@@ -460,7 +473,10 @@ class PackageSdist:
|
||||
|
||||
@property
|
||||
def filename(self) -> str:
|
||||
"""Get the filename of the sdist."""
|
||||
"""Get the filename of the sdist.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
filename = self.name or _path_name(self.path) or _url_name(self.url)
|
||||
if not filename:
|
||||
raise PylockValidationError("Cannot determine sdist filename")
|
||||
@@ -737,6 +753,7 @@ class Pylock:
|
||||
tags: Sequence[Tag] | None = None,
|
||||
extras: Collection[str] | None = None,
|
||||
dependency_groups: Collection[str] | None = None,
|
||||
prefer_sdist_predicate: Callable[[NormalizedName], bool] | None = None,
|
||||
) -> Iterator[
|
||||
tuple[
|
||||
Package,
|
||||
@@ -758,11 +775,23 @@ class Pylock:
|
||||
The *dependency_groups* parameter represents the groups to install. If
|
||||
unspecified, the default groups are used.
|
||||
|
||||
The *prefer_sdist_predicate* parameter is called for packages with a source
|
||||
distribution. If it returns ``True``, the source distribution is selected
|
||||
before attempting wheel compatibility. If no source distribution is
|
||||
available, wheel selection proceeds as usual without calling the predicate.
|
||||
|
||||
This method must be used on valid Pylock instances (i.e. one obtained
|
||||
from :meth:`Pylock.from_dict` or if constructed manually, after calling
|
||||
:meth:`Pylock.validate`).
|
||||
|
||||
.. versionadded:: 26.1
|
||||
|
||||
.. versionchanged:: 26.3
|
||||
Added the *prefer_sdist_predicate* parameter.
|
||||
"""
|
||||
compatible_tags_selector = create_compatible_tags_selector(tags or sys_tags())
|
||||
compatible_tags_selector = create_compatible_tags_selector(
|
||||
tags if tags is not None else sys_tags()
|
||||
)
|
||||
|
||||
# #. Gather the extras and dependency groups to install and set ``extras`` and
|
||||
# ``dependency_groups`` for marker evaluation, respectively.
|
||||
@@ -782,7 +811,7 @@ class Pylock:
|
||||
),
|
||||
),
|
||||
)
|
||||
env_python_full_version = (
|
||||
env_python_full_version = _pep440_python_full_version(
|
||||
environment["python_full_version"]
|
||||
if environment
|
||||
else default_environment()["python_full_version"]
|
||||
@@ -870,6 +899,15 @@ class Pylock:
|
||||
elif package.archive is not None:
|
||||
yield package, package.archive
|
||||
|
||||
# - Else if source preference selects an available
|
||||
# :ref:`pylock-packages-sdist`:
|
||||
elif (
|
||||
package.sdist is not None
|
||||
and prefer_sdist_predicate is not None
|
||||
and prefer_sdist_predicate(package.name)
|
||||
):
|
||||
yield package, package.sdist
|
||||
|
||||
# - Else if there are entries for :ref:`pylock-packages-wheels`:
|
||||
elif package.wheels:
|
||||
# #. Look for the appropriate wheel file based on
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,14 +3,17 @@
|
||||
# for complete details.
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Iterator
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from ._parser import parse_requirement as _parse_requirement
|
||||
from ._tokenizer import ParserSyntaxError
|
||||
from .markers import Marker, _normalize_extra_values
|
||||
from .specifiers import SpecifierSet
|
||||
from .specifiers import InvalidSpecifier, SpecifierSet
|
||||
from .utils import canonicalize_name
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
__all__ = [
|
||||
"InvalidRequirement",
|
||||
"Requirement",
|
||||
@@ -24,6 +27,8 @@ def __dir__() -> list[str]:
|
||||
class InvalidRequirement(ValueError):
|
||||
"""
|
||||
An invalid requirement was found, users should refer to PEP 508.
|
||||
|
||||
.. versionadded:: 16.1
|
||||
"""
|
||||
|
||||
|
||||
@@ -34,6 +39,18 @@ class Requirement:
|
||||
URL, and extras. Raises InvalidRequirement on a badly-formed requirement
|
||||
string.
|
||||
|
||||
.. versionadded:: 16.1
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Added equality (``__eq__``) and hashing (``__hash__``) so requirements
|
||||
can be compared and stored in sets / dicts.
|
||||
|
||||
.. versionchanged:: 23.2
|
||||
Equality and hashing began canonicalizing requirement names, so
|
||||
requirements whose names differ only by normalization (e.g.
|
||||
``Requirement("Foo")`` vs ``Requirement("foo")``) now compare and hash
|
||||
equal.
|
||||
|
||||
Instances are safe to serialize with :mod:`pickle`. They use a stable
|
||||
format so the same pickle can be loaded in future packaging releases.
|
||||
|
||||
@@ -43,6 +60,16 @@ class Requirement:
|
||||
be unpickled with future releases. Backward compatibility with pickles
|
||||
from packaging < 26.2 is supported but may be removed in a future
|
||||
release.
|
||||
|
||||
.. versionchanged:: 26.3
|
||||
|
||||
The dedicated pickle support introduced in 26.2 did not preserve the
|
||||
specifier's explicit :attr:`~packaging.specifiers.SpecifierSet.prereleases`
|
||||
override; it is now included again.
|
||||
|
||||
Equality and hashing normalize requirement names, extras, and
|
||||
equivalent specifiers. The string representation still preserves the
|
||||
parsed name and extras spelling.
|
||||
"""
|
||||
|
||||
# TODO: Can we test whether something is contained within a requirement?
|
||||
@@ -50,6 +77,8 @@ class Requirement:
|
||||
# the thing as well as the version? What about the markers?
|
||||
# TODO: Can we normalize the name and extra name?
|
||||
|
||||
__slots__ = ("extras", "marker", "name", "specifier", "url")
|
||||
|
||||
def __init__(self, requirement_string: str) -> None:
|
||||
try:
|
||||
parsed = _parse_requirement(requirement_string)
|
||||
@@ -58,8 +87,11 @@ class Requirement:
|
||||
|
||||
self.name: str = parsed.name
|
||||
self.url: str | None = parsed.url or None
|
||||
self.extras: set[str] = set(parsed.extras or [])
|
||||
self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
|
||||
self.extras: set[str] = set(parsed.extras)
|
||||
try:
|
||||
self.specifier: SpecifierSet = SpecifierSet(parsed.specifier)
|
||||
except InvalidSpecifier as e:
|
||||
raise InvalidRequirement(str(e)) from e
|
||||
self.marker: Marker | None = None
|
||||
if parsed.marker is not None:
|
||||
self.marker = Marker.__new__(Marker)
|
||||
@@ -83,29 +115,44 @@ class Requirement:
|
||||
if self.marker:
|
||||
yield f"; {self.marker}"
|
||||
|
||||
def __getstate__(self) -> str:
|
||||
# Return the requirement string for compactness and stability.
|
||||
# Re-parsed on load to reconstruct all fields.
|
||||
return str(self)
|
||||
def __getstate__(self) -> tuple[str, bool | None]:
|
||||
# Return the requirement string for compactness and stability, paired
|
||||
# with the specifier's explicit prereleases override, which is not
|
||||
# captured by the string form. Re-parsed on load to reconstruct all
|
||||
# other fields.
|
||||
return (str(self), self.specifier._prereleases)
|
||||
|
||||
def __setstate__(self, state: object) -> None:
|
||||
if isinstance(state, str):
|
||||
# New format (26.2+): just the requirement string.
|
||||
try:
|
||||
tmp = Requirement(state)
|
||||
except InvalidRequirement as exc:
|
||||
raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
|
||||
self.name = tmp.name
|
||||
self.url = tmp.url
|
||||
self.extras = tmp.extras
|
||||
self.specifier = tmp.specifier
|
||||
self.marker = tmp.marker
|
||||
return
|
||||
if isinstance(state, dict):
|
||||
# Format (26.2): just the requirement string.
|
||||
requirement_string: str = state
|
||||
prereleases: bool | None = None
|
||||
elif (
|
||||
isinstance(state, tuple)
|
||||
and len(state) == 2
|
||||
and isinstance(state[0], str)
|
||||
and (state[1] is None or isinstance(state[1], bool))
|
||||
):
|
||||
# New format (26.3+): (requirement string, specifier prereleases).
|
||||
requirement_string, prereleases = state
|
||||
elif isinstance(state, dict) and state.keys() >= set(self.__slots__):
|
||||
# Old format (packaging <= 26.1, no __slots__): plain __dict__.
|
||||
self.__dict__.update(state)
|
||||
for key in self.__slots__:
|
||||
setattr(self, key, state[key])
|
||||
return
|
||||
raise TypeError(f"Cannot restore Requirement from {state!r}")
|
||||
else:
|
||||
raise TypeError(f"Cannot restore Requirement from {state!r}")
|
||||
|
||||
try:
|
||||
tmp = Requirement(requirement_string)
|
||||
except InvalidRequirement as exc:
|
||||
raise TypeError(f"Cannot restore Requirement from {state!r}") from exc
|
||||
self.name = tmp.name
|
||||
self.url = tmp.url
|
||||
self.extras = tmp.extras
|
||||
self.specifier = tmp.specifier
|
||||
self.specifier._prereleases = prereleases
|
||||
self.marker = tmp.marker
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "".join(self._iter_parts(self.name))
|
||||
@@ -114,15 +161,31 @@ class Requirement:
|
||||
return f"<{self.__class__.__name__}({str(self)!r})>"
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash(tuple(self._iter_parts(canonicalize_name(self.name))))
|
||||
# Mirror __eq__ by hashing the canonical specifier object rather than
|
||||
# its raw string. ``_iter_parts`` yields ``str(self.specifier)``, which
|
||||
# is non-canonical, so trailing-zero-equivalent requirements such as
|
||||
# ``foo==1.0.0`` and ``foo==1.0.0.0`` (which compare equal) would
|
||||
# otherwise hash differently, breaking the hash/__eq__ invariant.
|
||||
return hash(
|
||||
(
|
||||
canonicalize_name(self.name),
|
||||
frozenset(canonicalize_name(e) for e in self.extras),
|
||||
self.specifier,
|
||||
self.url,
|
||||
self.marker,
|
||||
)
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, Requirement):
|
||||
return NotImplemented
|
||||
|
||||
# Extras must be normalized before comparison as per PEP 685.
|
||||
self_extras = frozenset(canonicalize_name(e) for e in self.extras)
|
||||
other_extras = frozenset(canonicalize_name(e) for e in other.extras)
|
||||
return (
|
||||
canonicalize_name(self.name) == canonicalize_name(other.name)
|
||||
and self.extras == other.extras
|
||||
and self_extras == other_extras
|
||||
and self.specifier == other.specifier
|
||||
and self.url == other.url
|
||||
and self.marker == other.marker
|
||||
|
||||
+344
-830
File diff suppressed because it is too large
Load Diff
+181
-60
@@ -12,13 +12,10 @@ import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import sysconfig
|
||||
from collections.abc import Iterable, Iterator, Sequence
|
||||
from importlib.machinery import EXTENSION_SUFFIXES
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Iterable,
|
||||
Iterator,
|
||||
Sequence,
|
||||
Tuple,
|
||||
TypeVar,
|
||||
cast,
|
||||
)
|
||||
@@ -26,15 +23,17 @@ from typing import (
|
||||
from . import _manylinux, _musllinux
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import AbstractSet
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Set as AbstractSet
|
||||
|
||||
|
||||
__all__ = [
|
||||
"INTERPRETER_SHORT_NAMES",
|
||||
"AppleVersion",
|
||||
"InvalidTag",
|
||||
"PythonVersion",
|
||||
"Tag",
|
||||
"TooManyTagsError",
|
||||
"UnsortedTagsError",
|
||||
"android_platforms",
|
||||
"compatible_tags",
|
||||
@@ -47,6 +46,7 @@ __all__ = [
|
||||
"mac_platforms",
|
||||
"parse_tag",
|
||||
"platform_tags",
|
||||
"pure_python_tags",
|
||||
"sys_tags",
|
||||
]
|
||||
|
||||
@@ -58,7 +58,18 @@ def __dir__() -> list[str]:
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
PythonVersion = Sequence[int]
|
||||
AppleVersion = Tuple[int, int]
|
||||
"""
|
||||
A sequence of integers describing a Python version, e.g. ``(3, 13)``.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
|
||||
AppleVersion = tuple[int, int]
|
||||
"""
|
||||
A ``(major, minor)`` integer pair describing an Apple OS version.
|
||||
|
||||
.. versionadded:: 24.2
|
||||
"""
|
||||
_T = TypeVar("_T")
|
||||
|
||||
INTERPRETER_SHORT_NAMES: dict[str, str] = {
|
||||
@@ -82,6 +93,25 @@ _32_BIT_INTERPRETER = _compute_32_bit_interpreter()
|
||||
class UnsortedTagsError(ValueError):
|
||||
"""
|
||||
Raised when a tag component is not in sorted order per PEP 425.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
"""
|
||||
|
||||
|
||||
class InvalidTag(ValueError):
|
||||
"""
|
||||
Raised when an interpreter component is not an identifier, a tag component
|
||||
is empty, or a tag does not have exactly three components.
|
||||
|
||||
.. versionadded:: 26.3
|
||||
"""
|
||||
|
||||
|
||||
class TooManyTagsError(ValueError):
|
||||
"""
|
||||
Raised when a compressed tag set exceeds the configured limit.
|
||||
|
||||
.. versionadded:: 26.3
|
||||
"""
|
||||
|
||||
|
||||
@@ -200,7 +230,9 @@ class Tag:
|
||||
raise TypeError(f"Cannot restore Tag from {state!r}")
|
||||
|
||||
|
||||
def parse_tag(tag: str, *, validate_order: bool = False) -> frozenset[Tag]:
|
||||
def parse_tag(
|
||||
tag: str, *, validate_order: bool = False, limit: int | None = None
|
||||
) -> frozenset[Tag]:
|
||||
"""
|
||||
Parses the provided tag (e.g. `py3-none-any`) into a frozenset of
|
||||
:class:`Tag` instances.
|
||||
@@ -212,29 +244,70 @@ def parse_tag(tag: str, *, validate_order: bool = False) -> frozenset[Tag]:
|
||||
If **validate_order** is true, compressed tag set components are checked
|
||||
to be in sorted order as required by PEP 425.
|
||||
|
||||
If **limit** is not ``None``, the compressed tag set can generate at most
|
||||
that many tags.
|
||||
|
||||
:param str tag: The tag to parse, e.g. ``"py3-none-any"``.
|
||||
:param bool validate_order: Check whether compressed tag set components
|
||||
are in sorted order.
|
||||
:param int | None limit: The maximum number of tags to parse.
|
||||
:raises UnsortedTagsError: If **validate_order** is true and any compressed tag
|
||||
set component is not in sorted order.
|
||||
:raises InvalidTag: If the interpreter field is not an identifier; if the
|
||||
interpreter, ABI, or platform field (or any member of a compressed tag
|
||||
set) is empty; or if the tag does not have exactly three components.
|
||||
:raises TooManyTagsError: If **limit** is not ``None`` and the compressed tag
|
||||
set would generate more than **limit** tags.
|
||||
:raises ValueError: If **limit** is negative.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
The *validate_order* parameter.
|
||||
|
||||
.. versionadded:: 26.3
|
||||
Raises :class:`InvalidTag` when an interpreter component is not an
|
||||
identifier, a tag component is empty, or a tag does not have exactly
|
||||
three components.
|
||||
Added the *limit* parameter. Raises :class:`TooManyTagsError` if the compressed
|
||||
tag set would generate more than *limit* tags.
|
||||
"""
|
||||
tags = set()
|
||||
interpreters, abis, platforms = tag.split("-")
|
||||
if validate_order:
|
||||
for component in (interpreters, abis, platforms):
|
||||
parts = component.split(".")
|
||||
if parts != sorted(parts):
|
||||
raise UnsortedTagsError(
|
||||
f"Tag component {component!r} is not in sorted order per PEP 425"
|
||||
)
|
||||
for interpreter in interpreters.split("."):
|
||||
for abi in abis.split("."):
|
||||
for platform_ in platforms.split("."):
|
||||
tags.add(Tag(interpreter, abi, platform_))
|
||||
return frozenset(tags)
|
||||
|
||||
if limit is not None and limit < 0:
|
||||
raise ValueError("limit must be non-negative")
|
||||
|
||||
component_parts = [component.split(".") for component in tag.split("-")]
|
||||
for parts in component_parts:
|
||||
if "" in parts:
|
||||
component = ".".join(parts)
|
||||
raise InvalidTag(f"Tag {tag!r} has an empty component: {component!r}")
|
||||
if validate_order and parts != sorted(parts):
|
||||
component = ".".join(parts)
|
||||
raise UnsortedTagsError(
|
||||
f"Tag component {component!r} is not in sorted order per PEP 425"
|
||||
)
|
||||
|
||||
tag_count = 1
|
||||
for parts in component_parts:
|
||||
tag_count *= len(parts)
|
||||
|
||||
if limit is not None and tag_count > limit:
|
||||
raise TooManyTagsError(
|
||||
f"Compressed tag set would generate {tag_count} tags, exceeding "
|
||||
f"limit {limit}"
|
||||
)
|
||||
|
||||
try:
|
||||
interpreters, abis, platforms = component_parts
|
||||
except ValueError as exc:
|
||||
raise InvalidTag(f"Tag {tag!r} must have exactly three components") from exc
|
||||
for interpreter in interpreters:
|
||||
if not interpreter.isidentifier():
|
||||
raise InvalidTag(f"Tag {tag!r} has an invalid interpreter: {interpreter!r}")
|
||||
return frozenset(
|
||||
Tag(interpreter, abi, platform_)
|
||||
for interpreter in interpreters
|
||||
for abi in abis
|
||||
for platform_ in platforms
|
||||
)
|
||||
|
||||
|
||||
def _get_config_var(name: str, warn: bool = False) -> int | str | None:
|
||||
@@ -355,6 +428,8 @@ def cpython_tags(
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
if not python_version:
|
||||
python_version = sys.version_info[:2]
|
||||
@@ -364,8 +439,10 @@ def cpython_tags(
|
||||
if abis is None:
|
||||
abis = _cpython_abis(python_version, warn) if len(python_version) > 1 else []
|
||||
abis = list(abis)
|
||||
# 'abi3' and 'none' are explicitly handled later.
|
||||
for explicit_abi in ("abi3", "none"):
|
||||
threading = _is_threaded_cpython(abis)
|
||||
# Stable ABIs and 'none' are explicitly handled later.
|
||||
explicit_abis = ("abi3", "abi3t", "none") if threading else ("abi3", "none")
|
||||
for explicit_abi in explicit_abis:
|
||||
try:
|
||||
abis.remove(explicit_abi)
|
||||
except ValueError: # noqa: PERF203
|
||||
@@ -376,10 +453,8 @@ def cpython_tags(
|
||||
for platform_ in platforms:
|
||||
yield Tag(interpreter, abi, platform_)
|
||||
|
||||
threading = _is_threaded_cpython(abis)
|
||||
use_abi3 = _abi3_applies(python_version, threading)
|
||||
use_abi3t = _abi3t_applies(python_version, threading)
|
||||
|
||||
if use_abi3:
|
||||
yield from (Tag(interpreter, "abi3", platform_) for platform_ in platforms)
|
||||
if use_abi3t:
|
||||
@@ -417,7 +492,7 @@ def _generic_abi() -> list[str]:
|
||||
# => graalpy_38_native
|
||||
|
||||
ext_suffix = _get_config_var("EXT_SUFFIX", warn=True)
|
||||
if not isinstance(ext_suffix, str) or ext_suffix[0] != ".":
|
||||
if not isinstance(ext_suffix, str) or not ext_suffix.startswith("."):
|
||||
raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')")
|
||||
parts = ext_suffix.split(".")
|
||||
if len(parts) < 3:
|
||||
@@ -426,7 +501,10 @@ def _generic_abi() -> list[str]:
|
||||
soabi = parts[1]
|
||||
if soabi.startswith("cpython"):
|
||||
# non-windows
|
||||
abi = "cp" + soabi.split("-")[1]
|
||||
cpython_parts = soabi.split("-")
|
||||
if len(cpython_parts) < 2 or not cpython_parts[1]:
|
||||
raise SystemError("invalid sysconfig.get_config_var('EXT_SUFFIX')")
|
||||
abi = "cp" + cpython_parts[1]
|
||||
elif soabi.startswith("cp"):
|
||||
# windows
|
||||
abi = soabi.split("-")[0]
|
||||
@@ -469,6 +547,8 @@ def generic_tags(
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
if not interpreter:
|
||||
interp_name = interpreter_name()
|
||||
@@ -498,6 +578,30 @@ def _py_interpreter_range(py_version: PythonVersion) -> Iterator[str]:
|
||||
yield f"py{_version_nodot((py_version[0], minor))}"
|
||||
|
||||
|
||||
def pure_python_tags(
|
||||
python_version: PythonVersion | None = None,
|
||||
) -> Iterator[Tag]:
|
||||
"""
|
||||
Yields the pure-Python tags compatible with ``python_version``.
|
||||
|
||||
The tags use the ``"none"`` ABI and ``"any"`` platform, so their
|
||||
generation does not depend on the running platform.
|
||||
|
||||
.. versionadded:: 26.3
|
||||
|
||||
:param Sequence python_version: A one- or two-item sequence representing the
|
||||
compatible version of Python. Defaults to
|
||||
``sys.version_info[:2]``.
|
||||
:raises ValueError: If ``python_version`` is an empty sequence.
|
||||
"""
|
||||
if python_version is None:
|
||||
python_version = sys.version_info[:2]
|
||||
elif not python_version:
|
||||
raise ValueError("python_version must contain at least one item")
|
||||
for version in _py_interpreter_range(python_version):
|
||||
yield Tag(version, "none", "any")
|
||||
|
||||
|
||||
def compatible_tags(
|
||||
python_version: PythonVersion | None = None,
|
||||
interpreter: str | None = None,
|
||||
@@ -520,6 +624,8 @@ def compatible_tags(
|
||||
``"cp38"``. Defaults to the current interpreter.
|
||||
:param Iterable platforms: Iterable of compatible platforms. Defaults to the
|
||||
platforms compatible with the current system.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
if not python_version:
|
||||
python_version = sys.version_info[:2]
|
||||
@@ -529,8 +635,7 @@ def compatible_tags(
|
||||
yield Tag(version, "none", platform_)
|
||||
if interpreter:
|
||||
yield Tag(interpreter, "none", "any")
|
||||
for version in _py_interpreter_range(python_version):
|
||||
yield Tag(version, "none", "any")
|
||||
yield from pure_python_tags(python_version)
|
||||
|
||||
|
||||
def _mac_arch(arch: str, is_32bit: bool = _32_BIT_INTERPRETER) -> str:
|
||||
@@ -548,12 +653,12 @@ def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
|
||||
if cpu_arch == "x86_64":
|
||||
if version < (10, 4):
|
||||
return []
|
||||
formats.extend(["intel", "fat64", "fat32"])
|
||||
formats.extend(["intel", "fat64", "fat3"])
|
||||
|
||||
elif cpu_arch == "i386":
|
||||
if version < (10, 4):
|
||||
return []
|
||||
formats.extend(["intel", "fat32", "fat"])
|
||||
formats.extend(["intel", "fat3", "fat"])
|
||||
|
||||
elif cpu_arch == "ppc64":
|
||||
# TODO: Need to care about 32-bit PPC for ppc64 through 10.2?
|
||||
@@ -564,7 +669,7 @@ def _mac_binary_formats(version: AppleVersion, cpu_arch: str) -> list[str]:
|
||||
elif cpu_arch == "ppc":
|
||||
if version > (10, 6):
|
||||
return []
|
||||
formats.extend(["fat32", "fat"])
|
||||
formats.extend(["fat3", "fat"])
|
||||
|
||||
if cpu_arch in {"arm64", "x86_64"}:
|
||||
formats.append("universal2")
|
||||
@@ -598,29 +703,33 @@ def mac_platforms(
|
||||
- On Windows, platform compatibility is statically specified
|
||||
- On Linux, code must be run on the system itself to determine
|
||||
compatibility
|
||||
"""
|
||||
version_str, _, cpu_arch = platform.mac_ver()
|
||||
if version is None:
|
||||
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
|
||||
if version == (10, 16):
|
||||
# When built against an older macOS SDK, Python will report macOS 10.16
|
||||
# instead of the real version.
|
||||
version_str = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-sS",
|
||||
"-c",
|
||||
"import platform; print(platform.mac_ver()[0])",
|
||||
],
|
||||
check=True,
|
||||
env={"SYSTEM_VERSION_COMPAT": "0"},
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
).stdout
|
||||
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
|
||||
|
||||
if arch is None:
|
||||
arch = _mac_arch(cpu_arch)
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
if version is None or arch is None:
|
||||
version_str, _, cpu_arch = platform.mac_ver()
|
||||
if version is None:
|
||||
version = cast("AppleVersion", tuple(map(int, version_str.split(".")[:2])))
|
||||
if version == (10, 16):
|
||||
# When built against an older macOS SDK, Python will report macOS 10.16
|
||||
# instead of the real version.
|
||||
version_str = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"-sS",
|
||||
"-c",
|
||||
"import platform; print(platform.mac_ver()[0])",
|
||||
],
|
||||
check=True,
|
||||
env={"SYSTEM_VERSION_COMPAT": "0"},
|
||||
stdout=subprocess.PIPE,
|
||||
text=True,
|
||||
).stdout
|
||||
version = cast(
|
||||
"AppleVersion", tuple(map(int, version_str.split(".")[:2]))
|
||||
)
|
||||
if arch is None:
|
||||
arch = _mac_arch(cpu_arch)
|
||||
|
||||
if (10, 0) <= version < (11, 0):
|
||||
# Prior to Mac OS 11, each yearly release of Mac OS bumped the
|
||||
@@ -642,7 +751,6 @@ def mac_platforms(
|
||||
for binary_format in binary_formats:
|
||||
yield f"macosx_{major_version}_{minor_version}_{binary_format}"
|
||||
|
||||
if version >= (11, 0):
|
||||
# Mac OS 11 on x86_64 is compatible with binaries from previous releases.
|
||||
# Arm64 support was introduced in 11.0, so no Arm binaries from previous
|
||||
# releases exist.
|
||||
@@ -681,6 +789,8 @@ def ios_platforms(
|
||||
.. note::
|
||||
Behavior of this method is undefined if invoked on non-iOS platforms
|
||||
without providing explicit version and multiarch arguments.
|
||||
|
||||
.. versionadded:: 24.2
|
||||
"""
|
||||
if version is None:
|
||||
# if iOS is the current platform, ios_ver *must* be defined. However,
|
||||
@@ -740,6 +850,8 @@ def android_platforms(
|
||||
e.g. ``arm64_v8a``. Defaults to the current system's ABI , as returned by
|
||||
``sysconfig.get_platform``. Hyphens and periods will be replaced with
|
||||
underscores.
|
||||
|
||||
.. versionadded:: 25.0
|
||||
"""
|
||||
if platform.system() != "Android" and (api_level is None or abi is None):
|
||||
raise TypeError(
|
||||
@@ -776,10 +888,10 @@ def _linux_platforms(is_32bit: bool = _32_BIT_INTERPRETER) -> Iterator[str]:
|
||||
linux = "linux_armv8l"
|
||||
_, arch = linux.split("_", 1)
|
||||
archs = {"armv8l": ["armv8l", "armv7l"]}.get(arch, [arch])
|
||||
yield from _manylinux.platform_tags(archs)
|
||||
yield from _musllinux.platform_tags(archs)
|
||||
for arch in archs:
|
||||
yield f"linux_{arch}"
|
||||
yield from _manylinux.platform_tags(archs)
|
||||
yield from _musllinux.platform_tags(archs)
|
||||
|
||||
|
||||
def _emscripten_platforms() -> Iterator[str]:
|
||||
@@ -798,6 +910,8 @@ def _generic_platforms() -> Iterator[str]:
|
||||
def platform_tags() -> Iterator[str]:
|
||||
"""
|
||||
Yields the :attr:`~Tag.platform` tags for the running interpreter.
|
||||
|
||||
.. versionadded:: 21.1
|
||||
"""
|
||||
if platform.system() == "Darwin":
|
||||
return mac_platforms()
|
||||
@@ -821,6 +935,8 @@ def interpreter_name() -> str:
|
||||
be returned when appropriate.
|
||||
|
||||
This typically acts as the prefix to the :attr:`~Tag.interpreter` tag.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
name = sys.implementation.name
|
||||
return INTERPRETER_SHORT_NAMES.get(name) or name
|
||||
@@ -833,6 +949,8 @@ def interpreter_version(*, warn: bool = False) -> str:
|
||||
This typically acts as the suffix to the :attr:`~Tag.interpreter` tag.
|
||||
|
||||
:param bool warn: Whether warnings should be logged. Defaults to ``False``.
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
version = _get_config_var("py_version_nodot", warn=warn)
|
||||
return str(version) if version else _version_nodot(sys.version_info[:2])
|
||||
@@ -867,15 +985,18 @@ def sys_tags(*, warn: bool = False) -> Iterator[Tag]:
|
||||
|
||||
.. versionchanged:: 21.3
|
||||
Added the `pp3-none-any` tag (:issue:`311`).
|
||||
.. versionchanged:: 27.0
|
||||
.. versionchanged:: 26.1
|
||||
Added the `abi3t` tag (:issue:`1099`).
|
||||
.. versionchanged:: 26.3
|
||||
Native ``linux_*`` platform tags are now ordered before ``manylinux``
|
||||
and ``musllinux`` tags (:issue:`160`).
|
||||
"""
|
||||
|
||||
interp_name = interpreter_name()
|
||||
if interp_name == "cp":
|
||||
yield from cpython_tags(warn=warn)
|
||||
else:
|
||||
yield from generic_tags()
|
||||
yield from generic_tags(warn=warn)
|
||||
|
||||
if interp_name == "pp":
|
||||
interp = "pp3"
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import NewType, Tuple, Union, cast
|
||||
from typing import NewType, Union, cast
|
||||
|
||||
from .tags import Tag, UnsortedTagsError, parse_tag
|
||||
from .tags import InvalidTag, Tag, UnsortedTagsError, parse_tag
|
||||
from .version import InvalidVersion, Version, _TrimmedRelease
|
||||
|
||||
__all__ = [
|
||||
@@ -28,29 +28,42 @@ def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
BuildTag = Union[Tuple[()], Tuple[int, str]]
|
||||
BuildTag = Union[tuple[()], tuple[int, str]]
|
||||
"""
|
||||
A wheel build tag: an empty tuple, or a ``(build number, build tag suffix)`` pair.
|
||||
|
||||
.. versionadded:: 20.9
|
||||
"""
|
||||
|
||||
NormalizedName = NewType("NormalizedName", str)
|
||||
"""
|
||||
A :class:`typing.NewType` of :class:`str`, representing a normalized name.
|
||||
|
||||
.. versionadded:: 20.4
|
||||
"""
|
||||
|
||||
|
||||
class InvalidName(ValueError):
|
||||
"""
|
||||
An invalid distribution name; users should refer to the packaging user guide.
|
||||
|
||||
.. versionadded:: 23.2
|
||||
"""
|
||||
|
||||
|
||||
class InvalidWheelFilename(ValueError):
|
||||
"""
|
||||
An invalid wheel filename was found, users should refer to PEP 427.
|
||||
|
||||
.. versionadded:: 20.9
|
||||
"""
|
||||
|
||||
|
||||
class InvalidSdistFilename(ValueError):
|
||||
"""
|
||||
An invalid sdist filename was found, users should refer to the packaging user guide.
|
||||
|
||||
.. versionadded:: 20.9
|
||||
"""
|
||||
|
||||
|
||||
@@ -58,9 +71,12 @@ class InvalidSdistFilename(ValueError):
|
||||
_validate_regex = re.compile(
|
||||
r"[a-z0-9]|[a-z0-9][a-z0-9._-]*[a-z0-9]", re.IGNORECASE | re.ASCII
|
||||
)
|
||||
_normalized_regex = re.compile(r"[a-z0-9]|[a-z0-9]([a-z0-9-](?!--))*[a-z0-9]", re.ASCII)
|
||||
_normalized_regex = re.compile(r"[a-z0-9]+(?:-[a-z0-9]+)*", re.ASCII)
|
||||
# PEP 427: The build number must start with a digit.
|
||||
_build_tag_regex = re.compile(r"(\d+)(.*)", re.ASCII)
|
||||
# PEP 427: Valid characters for an escaped project name in a wheel filename.
|
||||
# Requires at least one character so an empty project name is rejected.
|
||||
_wheel_name_regex = re.compile(r"^[\w._]+\Z", re.UNICODE)
|
||||
|
||||
|
||||
def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
|
||||
@@ -87,6 +103,14 @@ def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
|
||||
'oslo-concurrency'
|
||||
>>> canonicalize_name("requests")
|
||||
'requests'
|
||||
|
||||
.. versionadded:: 16.2
|
||||
|
||||
.. versionchanged:: 20.4
|
||||
The return type was changed to :class:`NormalizedName`.
|
||||
|
||||
.. versionchanged:: 23.2
|
||||
Added the *validate* keyword parameter.
|
||||
"""
|
||||
if validate and not _validate_regex.fullmatch(name):
|
||||
raise InvalidName(f"name is invalid: {name!r}")
|
||||
@@ -102,16 +126,27 @@ def canonicalize_name(name: str, *, validate: bool = False) -> NormalizedName:
|
||||
|
||||
def is_normalized_name(name: str) -> bool:
|
||||
"""
|
||||
Check if a name is already normalized (i.e. :func:`canonicalize_name` would
|
||||
roundtrip to the same value).
|
||||
Check if a name is a normalized project name (i.e. a valid name that
|
||||
:func:`canonicalize_name` would roundtrip to the same value).
|
||||
|
||||
The roundtrip only characterizes normalized names for *valid* names. A name
|
||||
must start and end with an ASCII letter or digit, which
|
||||
:func:`canonicalize_name` does not enforce: it leaves a leading or trailing
|
||||
hyphen in place, so such a name roundtrips without being normalized.
|
||||
|
||||
:param str name: The name to check.
|
||||
|
||||
>>> from packaging.utils import is_normalized_name
|
||||
>>> from packaging.utils import canonicalize_name, is_normalized_name
|
||||
>>> is_normalized_name("requests")
|
||||
True
|
||||
>>> is_normalized_name("Django")
|
||||
False
|
||||
>>> canonicalize_name("_not_legal")
|
||||
'-not-legal'
|
||||
>>> is_normalized_name("-not-legal") # roundtrips, but not a valid name
|
||||
False
|
||||
|
||||
.. versionadded:: 23.2
|
||||
"""
|
||||
return _normalized_regex.fullmatch(name) is not None
|
||||
|
||||
@@ -145,6 +180,14 @@ def canonicalize_version(
|
||||
|
||||
>>> canonicalize_version('1.4.0.0.0')
|
||||
'1.4'
|
||||
|
||||
.. versionadded:: 17.1
|
||||
|
||||
.. versionchanged:: 21.0
|
||||
The return type was narrowed to :class:`str`.
|
||||
|
||||
.. versionchanged:: 22.0
|
||||
Added the *strip_trailing_zero* keyword parameter.
|
||||
"""
|
||||
if isinstance(version, str):
|
||||
try:
|
||||
@@ -196,8 +239,18 @@ def parse_wheel_filename(
|
||||
>>> not build
|
||||
True
|
||||
|
||||
.. versionadded:: 20.9
|
||||
|
||||
.. versionchanged:: 23.2
|
||||
Raises :class:`InvalidWheelFilename` when the version component is invalid.
|
||||
|
||||
.. versionadded:: 26.1
|
||||
The *validate_order* parameter.
|
||||
|
||||
.. versionchanged:: 26.3
|
||||
Raises :class:`InvalidWheelFilename` when an interpreter component is
|
||||
not an identifier, a tag set component is empty, or the project name is
|
||||
empty.
|
||||
"""
|
||||
if not filename.endswith(".whl"):
|
||||
raise InvalidWheelFilename(
|
||||
@@ -214,7 +267,7 @@ def parse_wheel_filename(
|
||||
parts = filename.split("-", dashes - 2)
|
||||
name_part = parts[0]
|
||||
# See PEP 427 for the rules on escaping the project name.
|
||||
if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None:
|
||||
if "__" in name_part or _wheel_name_regex.match(name_part) is None:
|
||||
raise InvalidWheelFilename(f"Invalid project name: {filename!r}")
|
||||
name = canonicalize_name(name_part)
|
||||
|
||||
@@ -243,6 +296,10 @@ def parse_wheel_filename(
|
||||
f"Invalid wheel filename (compressed tag set components must be in "
|
||||
f"sorted order per PEP 425): {filename!r}"
|
||||
) from None
|
||||
except InvalidTag:
|
||||
raise InvalidWheelFilename(
|
||||
f"Invalid wheel filename (invalid tag component): {filename!r}"
|
||||
) from None
|
||||
return (name, version, build, tags)
|
||||
|
||||
|
||||
@@ -255,8 +312,10 @@ def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
|
||||
|
||||
:param str filename: The name of the sdist file.
|
||||
:raises InvalidSdistFilename: If the filename does not end
|
||||
with an sdist extension (``.zip`` or ``.tar.gz``), or if it does not
|
||||
contain a dash separating the name and the version of the distribution.
|
||||
with an sdist extension (``.zip`` or ``.tar.gz``), if it does not
|
||||
contain a dash separating the name and the version of the distribution,
|
||||
if the project name is empty, or if the version portion is not a valid
|
||||
version.
|
||||
|
||||
>>> from packaging.utils import parse_sdist_filename
|
||||
>>> from packaging.version import Version
|
||||
@@ -266,6 +325,17 @@ def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
|
||||
>>> ver == Version('1.0')
|
||||
True
|
||||
|
||||
.. versionadded:: 20.9
|
||||
|
||||
.. versionchanged:: 21.0
|
||||
Added support for ``.zip`` source distributions.
|
||||
|
||||
.. versionchanged:: 23.2
|
||||
Raises :class:`InvalidSdistFilename` when the version component is invalid.
|
||||
|
||||
.. versionchanged:: 26.3
|
||||
Raises :class:`InvalidSdistFilename` on an empty project name.
|
||||
|
||||
.. _Source distribution format: https://packaging.python.org/specifications/source-distribution-format/#source-distribution-file-name
|
||||
"""
|
||||
if filename.endswith(".tar.gz"):
|
||||
@@ -283,6 +353,10 @@ def parse_sdist_filename(filename: str) -> tuple[NormalizedName, Version]:
|
||||
name_part, sep, version_part = file_stem.rpartition("-")
|
||||
if not sep:
|
||||
raise InvalidSdistFilename(f"Invalid sdist filename: {filename!r}")
|
||||
if not name_part:
|
||||
raise InvalidSdistFilename(
|
||||
f"Invalid sdist filename (empty project name): {filename!r}"
|
||||
)
|
||||
|
||||
name = canonicalize_name(name_part)
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ from typing import (
|
||||
Literal,
|
||||
NamedTuple,
|
||||
SupportsInt,
|
||||
Tuple,
|
||||
TypedDict,
|
||||
Union,
|
||||
)
|
||||
@@ -67,13 +66,13 @@ def __dir__() -> list[str]:
|
||||
return __all__
|
||||
|
||||
|
||||
LocalType = Tuple[Union[int, str], ...]
|
||||
LocalType = tuple[Union[int, str], ...]
|
||||
|
||||
CmpLocalType = Tuple[Tuple[int, str], ...]
|
||||
CmpSuffix = Tuple[int, int, int, int, int, int]
|
||||
CmpLocalType = tuple[tuple[int, str], ...]
|
||||
CmpSuffix = tuple[int, int, int, int, int, int]
|
||||
CmpKey = Union[
|
||||
Tuple[int, Tuple[int, ...], CmpSuffix],
|
||||
Tuple[int, Tuple[int, ...], CmpSuffix, CmpLocalType],
|
||||
tuple[int, tuple[int, ...], CmpSuffix],
|
||||
tuple[int, tuple[int, ...], CmpSuffix, CmpLocalType],
|
||||
]
|
||||
VersionComparisonMethod = Callable[[CmpKey, CmpKey], bool]
|
||||
|
||||
@@ -288,10 +287,15 @@ def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | Non
|
||||
return value
|
||||
if isinstance(value, tuple) and len(value) == 2:
|
||||
letter, number = value
|
||||
letter = normalize_pre(letter)
|
||||
if letter in {"a", "b", "rc"} and isinstance(number, int) and number >= 0:
|
||||
# The letter must be a string before it can be normalized.
|
||||
if (
|
||||
isinstance(letter, str)
|
||||
and (normalized := normalize_pre(letter)) in {"a", "b", "rc"}
|
||||
and isinstance(number, int)
|
||||
and number >= 0
|
||||
):
|
||||
# type checkers can't infer the Literal type here on letter
|
||||
return (letter, number) # type: ignore[return-value]
|
||||
return (normalized, number) # type: ignore[return-value]
|
||||
msg = f"pre must be a tuple of ('a'|'b'|'rc', non-negative int), got {value}"
|
||||
raise InvalidVersion(msg)
|
||||
|
||||
@@ -411,9 +415,16 @@ class Version(_BaseVersion):
|
||||
If the ``version`` does not conform to PEP 440 in any way then this
|
||||
exception will be raised.
|
||||
"""
|
||||
if _SIMPLE_VERSION_INDICATORS.issuperset(version):
|
||||
try:
|
||||
is_simple = _SIMPLE_VERSION_INDICATORS.issuperset(version)
|
||||
except TypeError:
|
||||
raise InvalidVersion(f"Invalid version: {version!r}") from None
|
||||
|
||||
if is_simple:
|
||||
try:
|
||||
self._release = tuple(map(int, version.split(".")))
|
||||
except AttributeError:
|
||||
raise InvalidVersion(f"Invalid version: {version!r}") from None
|
||||
except ValueError:
|
||||
# Empty parts (from "1..2", ".1", etc.) are invalid versions.
|
||||
# Any other ValueError (e.g. int str-digits limit) should
|
||||
@@ -433,7 +444,10 @@ class Version(_BaseVersion):
|
||||
return
|
||||
|
||||
# Validate the version and parse it into pieces
|
||||
match = self._regex.fullmatch(version)
|
||||
try:
|
||||
match = self._regex.fullmatch(version)
|
||||
except TypeError:
|
||||
raise InvalidVersion(f"Invalid version: {version!r}") from None
|
||||
if not match:
|
||||
raise InvalidVersion(f"Invalid version: {version!r}")
|
||||
self._epoch = int(match.group("epoch")) if match.group("epoch") else 0
|
||||
@@ -1041,6 +1055,8 @@ class Version(_BaseVersion):
|
||||
|
||||
>>> Version("1.2.3").major
|
||||
1
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
return self.release[0] if len(self.release) >= 1 else 0
|
||||
|
||||
@@ -1052,6 +1068,8 @@ class Version(_BaseVersion):
|
||||
2
|
||||
>>> Version("1").minor
|
||||
0
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
return self.release[1] if len(self.release) >= 2 else 0
|
||||
|
||||
@@ -1063,6 +1081,8 @@ class Version(_BaseVersion):
|
||||
3
|
||||
>>> Version("1").micro
|
||||
0
|
||||
|
||||
.. versionadded:: 20.0
|
||||
"""
|
||||
return self.release[2] if len(self.release) >= 3 else 0
|
||||
|
||||
@@ -1079,6 +1099,7 @@ class _TrimmedRelease(Version):
|
||||
self._post = version._post
|
||||
self._local = version._local
|
||||
self._key_cache = version._key_cache
|
||||
self._hash_cache = version._hash_cache
|
||||
return
|
||||
super().__init__(version) # pragma: no cover
|
||||
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
pygls-1.3.1.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
pygls-1.3.1.dist-info/LICENSE.txt,sha256=b0kVxr8adbxhHDGM8t6T3jWLMbQJ7QLrngwkWnnWCl8,11367
|
||||
pygls-1.3.1.dist-info/METADATA,sha256=ZZmXz51Jk7TTtWO1hLfSWQynv6rDCM798YWVbloCqHk,4726
|
||||
pygls-1.3.1.dist-info/RECORD,,
|
||||
pygls-1.3.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
pygls-1.3.1.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
|
||||
pygls/__init__.py,sha256=rdTb3X-53tCjgNpS5dRsxv0ukMDjEly2pVEOPTnjx5k,1488
|
||||
pygls/capabilities.py,sha256=3tl-cqu82QpxHz-Z3NtlU6z-gbPqIvyvZ6gyZ-dr0-0,16756
|
||||
pygls/client.py,sha256=loVwqagoY0BnS6RwfpYtXwhg0NiIE6Tvbj2DzpOF7GA,6292
|
||||
pygls/constants.py,sha256=0YsX4Egp9jVLuAu9v8T_ThrObrVKGP916-X-9bkmId8,1470
|
||||
pygls/exceptions.py,sha256=skJKYaJCXI5At2eS1pNdQ6r3_B9Z-SMlyqr0_dwUVEw,6302
|
||||
pygls/feature_manager.py,sha256=7C--ra3GaG44LoZd8MaZ6Jh_8uxQMZo5jzykGwT7Fdg,8494
|
||||
pygls/lsp/__init__.py,sha256=pp9PCQhGzgPPFfWID47cdz_Q0LpZjT2kFPgTS-N7iVM,5236
|
||||
pygls/lsp/client.py,sha256=8JVgyjXXOblPlDhLAvNbukK-qrQNf__757elz9XYkCo,76358
|
||||
pygls/progress.py,sha256=Ml8vgJ9ueFC4YUqwvrgHatX8l_O3odhvmoPOLIcMNok,2789
|
||||
pygls/protocol/__init__.py,sha256=YI5xMBILWYKexx343wUDrHUVPuJFu9A48VSf0ieXYJM,1822
|
||||
pygls/protocol/json_rpc.py,sha256=01nqJoCNDmZQ78tyFxPfA9_kEBHyuBe-7ZUXCqwmu4g,20124
|
||||
pygls/protocol/language_server.py,sha256=b6X30DCLN4sdG85OhLVdrBJ9cweL4eKVgsc10bnMzXk,20109
|
||||
pygls/protocol/lsp_meta.py,sha256=kX1nL7XGVIYWp6UUyT3yDFnhcvpoCU_3mdyzpDTtUyQ,1593
|
||||
pygls/py.typed,sha256=ZfGKUcVseOxYpg6BU9EuhkP4dErsepCA4apkj_9YnYc,65
|
||||
pygls/server.py,sha256=T-k2wsP0W5a6KHmE9Afrv2PTy3qACEuCPGSJPmzC30w,20753
|
||||
pygls/uris.py,sha256=lknA_8hNYfs47mOFQxnFYW2TWH-8iL68ghTVD0Cccsc,5764
|
||||
pygls/workspace/__init__.py,sha256=tD6ahYMIPVsDdsWJ32EhF8wK-IJy6IQGInkI-BmIAxY,2883
|
||||
pygls/workspace/position_codec.py,sha256=UDv1kXFMyCDnu-iGhEbylCJP74L0TAL2hFvhoFrSpOY,8019
|
||||
pygls/workspace/text_document.py,sha256=8LcxsQeawuPNDrNDm3A2oMjWxYA3YxXm3NiHyQJQp8M,9031
|
||||
pygls/workspace/workspace.py,sha256=Zpd96kvVM7po7cI_XLenRAq8Wp3mjlMBX_YxO6Ecvs8,11556
|
||||
+21
-29
@@ -1,27 +1,26 @@
|
||||
Metadata-Version: 2.1
|
||||
Metadata-Version: 2.4
|
||||
Name: pygls
|
||||
Version: 1.3.1
|
||||
Version: 2.1.1
|
||||
Summary: A pythonic generic language server (pronounced like 'pie glass')
|
||||
Home-page: https://github.com/openlawlibrary/pygls
|
||||
License: Apache-2.0
|
||||
License-Expression: Apache-2.0
|
||||
License-File: LICENSE.txt
|
||||
Author: Open Law Library
|
||||
Author-email: info@openlawlib.org
|
||||
Maintainer: Tom BH
|
||||
Maintainer-email: tom@tombh.co.uk
|
||||
Requires-Python: >=3.8
|
||||
Classifier: License :: OSI Approved :: Apache Software License
|
||||
Requires-Python: >=3.9
|
||||
Classifier: Programming Language :: Python :: 3
|
||||
Classifier: Programming Language :: Python :: 3.8
|
||||
Classifier: Programming Language :: Python :: 3.9
|
||||
Classifier: Programming Language :: Python :: 3.10
|
||||
Classifier: Programming Language :: Python :: 3.11
|
||||
Classifier: Programming Language :: Python :: 3.12
|
||||
Classifier: Programming Language :: Python :: 3.13
|
||||
Classifier: Programming Language :: Python :: 3.14
|
||||
Provides-Extra: ws
|
||||
Requires-Dist: attrs (>=24.3.0)
|
||||
Requires-Dist: cattrs (>=23.1.2)
|
||||
Requires-Dist: lsprotocol (==2023.0.1)
|
||||
Requires-Dist: websockets (>=11.0.3) ; extra == "ws"
|
||||
Project-URL: Documentation, https://pygls.readthedocs.io/en/latest
|
||||
Project-URL: Repository, https://github.com/openlawlibrary/pygls
|
||||
Requires-Dist: lsprotocol (==2025.0.0)
|
||||
Requires-Dist: websockets (>=13.0) ; extra == "ws"
|
||||
Description-Content-Type: text/markdown
|
||||
|
||||
[](https://pypi.org/project/pygls/)   [](https://pygls.readthedocs.io/en/latest/)
|
||||
@@ -32,27 +31,22 @@ _pygls_ (pronounced like "pie glass") is a pythonic generic implementation of th
|
||||
|
||||
## Quickstart
|
||||
```python
|
||||
from pygls.server import LanguageServer
|
||||
from lsprotocol.types import (
|
||||
TEXT_DOCUMENT_COMPLETION,
|
||||
CompletionItem,
|
||||
CompletionList,
|
||||
CompletionParams,
|
||||
)
|
||||
from pygls.lsp.server import LanguageServer
|
||||
from lsprotocol import types
|
||||
|
||||
server = LanguageServer("example-server", "v0.1")
|
||||
|
||||
@server.feature(TEXT_DOCUMENT_COMPLETION)
|
||||
def completions(params: CompletionParams):
|
||||
@server.feature(types.TEXT_DOCUMENT_COMPLETION)
|
||||
def completions(params: types.CompletionParams):
|
||||
items = []
|
||||
document = server.workspace.get_document(params.text_document.uri)
|
||||
document = server.workspace.get_text_document(params.text_document.uri)
|
||||
current_line = document.lines[params.position.line].strip()
|
||||
if current_line.endswith("hello."):
|
||||
items = [
|
||||
CompletionItem(label="world"),
|
||||
CompletionItem(label="friend"),
|
||||
types.CompletionItem(label="world"),
|
||||
types.CompletionItem(label="friend"),
|
||||
]
|
||||
return CompletionList(is_incomplete=False, items=items)
|
||||
return types.CompletionList(is_incomplete=False, items=items)
|
||||
|
||||
server.start_io()
|
||||
```
|
||||
@@ -79,12 +73,10 @@ There are also other Language Servers with "general" in their descriptons, or at
|
||||
* https://github.com/jose-elias-alvarez/null-ls.nvim (Neovim only)
|
||||
|
||||
## Tests
|
||||
All Pygls sub-tasks require the Poetry `poe` plugin: https://github.com/nat-n/poethepoet
|
||||
|
||||
* `poetry install --all-extras`
|
||||
* `poetry run poe test`
|
||||
* `poetry run poe test-pyodide`
|
||||
All Pygls sub-tasks require the `uv`: https://docs.astral.sh/uv/getting-started/installation
|
||||
|
||||
* `uv run --all-extras poe test`
|
||||
* `uv run --all-extras poe test-pyodide`
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
pygls-2.1.1.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
pygls-2.1.1.dist-info/METADATA,sha256=irBCbsPjhAwIG_0lZcboG7qJQ7R8OwGyI4heqlbWW4I,4531
|
||||
pygls-2.1.1.dist-info/RECORD,,
|
||||
pygls-2.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
pygls-2.1.1.dist-info/WHEEL,sha256=kJCRJT_g0adfAJzTx2GUMmS80rTJIVHRCfG0DQgLq3o,88
|
||||
pygls-2.1.1.dist-info/licenses/LICENSE.txt,sha256=b0kVxr8adbxhHDGM8t6T3jWLMbQJ7QLrngwkWnnWCl8,11367
|
||||
pygls/__init__.py,sha256=pK-9Xb-tbL9uXGziKDv8Av3tQCJuMiktoqKdkV80V4w,1553
|
||||
pygls/capabilities.py,sha256=bVu1cR9pTDypI9pEMGE2i87wGmMot6CdfC6gwjH2p-o,17596
|
||||
pygls/cli.py,sha256=EC99J1HzlitNY6DB-9CmjKbE0NsDN427YkT9Num4TCU,2319
|
||||
pygls/client.py,sha256=hlhN5nqrI-CV64Cu0yCtiawF2w9vORLSlW0uVMHSSkI,7585
|
||||
pygls/constants.py,sha256=0YsX4Egp9jVLuAu9v8T_ThrObrVKGP916-X-9bkmId8,1470
|
||||
pygls/exceptions.py,sha256=MXJO0-qgqck6RiXYBrtROEeXtAw0AddQRnBu8nQMxOk,6724
|
||||
pygls/feature_manager.py,sha256=ozsF68bUYCFKyv0ycB9pISDEmUns7LEDflTdkRRKXSw,8895
|
||||
pygls/io_.py,sha256=HeP3Q0j7fg0KvZtKDxEf1WZJ7C5O4_CpZ_S0P1DSJOA,9244
|
||||
pygls/lsp/__init__.py,sha256=pp9PCQhGzgPPFfWID47cdz_Q0LpZjT2kFPgTS-N7iVM,5236
|
||||
pygls/lsp/_base_client.py,sha256=kFEuMlXk1skc6Y8x02EIwOod1oD5Qs8pMfsIW1oiZMo,81733
|
||||
pygls/lsp/_base_server.py,sha256=HVvEndKfPamr5FrGILD5naZIQDuOT3H4SbOhoZfMImE,17854
|
||||
pygls/lsp/_capabilities.py,sha256=YIgOkdqcUl9tTZ4pz40ZmuGaDjWnauM4L5MOz2Oz9Gs,121441
|
||||
pygls/lsp/client.py,sha256=zah7RZWnFSF5GYoomsbdz7VR2xuZS1krCaBzFJIHw5Y,160
|
||||
pygls/lsp/server.py,sha256=2rkJ6Chq_pC7DonZ2j1MTsIBTij1N1MOkFbkRlidxvs,4174
|
||||
pygls/progress.py,sha256=Ml8vgJ9ueFC4YUqwvrgHatX8l_O3odhvmoPOLIcMNok,2789
|
||||
pygls/protocol/__init__.py,sha256=XYUCEBA9Cl4sF1gtmjruHWVcKRdYN-LYsdbqIuqQCkg,1718
|
||||
pygls/protocol/json_rpc.py,sha256=wKy8Knv3xbszSe5PFjq7wauDQBfWtXJ4MHdeBp3q1P8,23796
|
||||
pygls/protocol/language_server.py,sha256=qLFRnlEaDrycm0nJKzXI0tI21E-m_Ewk9GlyPMZcGjc,15495
|
||||
pygls/py.typed,sha256=ZfGKUcVseOxYpg6BU9EuhkP4dErsepCA4apkj_9YnYc,65
|
||||
pygls/server.py,sha256=MvRmgoGqEBPQGrwcvZl021CHGhWeDH9NvdeFaCDKye0,9627
|
||||
pygls/uris.py,sha256=wAjK_kL-gOWcSe86BnBlsDPZ47aTm3S1EuCr9gmcMEU,5800
|
||||
pygls/workspace/__init__.py,sha256=AxU6NydTtfHIih_Xd8lSUm8AydhPopVXKGRuSGnzOok,274
|
||||
pygls/workspace/position_codec.py,sha256=CFRzod1pr-2vdpyGV2wNk2cxq6cqJo8WNtLH3KktfNs,9381
|
||||
pygls/workspace/text_document.py,sha256=4SKRRF9gRRtljtghiPHJYram2_h-4zj2DE0MyjxZYkE,12303
|
||||
pygls/workspace/workspace.py,sha256=QgeN0URYraABW6W_qATVYdStVhMCw5LGN0DFuPmx5fo,10112
|
||||
@@ -1,4 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: poetry-core 1.9.0
|
||||
Generator: poetry-core 2.3.1
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
@@ -21,5 +21,7 @@ import sys
|
||||
|
||||
IS_WIN = os.name == "nt"
|
||||
IS_PYODIDE = "pyodide" in sys.modules
|
||||
IS_WASI = sys.platform == "wasi"
|
||||
IS_WASM = IS_PYODIDE or IS_WASI
|
||||
|
||||
pygls = "pygls"
|
||||
|
||||
@@ -14,31 +14,23 @@
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from functools import reduce
|
||||
from typing import Any, Dict, List, Optional, Set, Union, TypeVar
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Set, TypeVar, Union
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.lsp._capabilities import get_capability as get_capability
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
def get_capability(
|
||||
client_capabilities: types.ClientCapabilities, field: str, default: Any = None
|
||||
) -> Any:
|
||||
"""Check if ClientCapabilities has some nested value without raising
|
||||
AttributeError.
|
||||
e.g. get_capability('text_document.synchronization.will_save')
|
||||
"""
|
||||
try:
|
||||
value = reduce(getattr, field.split("."), client_capabilities)
|
||||
except AttributeError:
|
||||
return default
|
||||
|
||||
# If we reach the desired leaf value but it's None, return the default.
|
||||
return default if value is None else value
|
||||
_SUPPORTED_ENCODINGS = frozenset(
|
||||
[
|
||||
types.PositionEncodingKind.Utf8,
|
||||
types.PositionEncodingKind.Utf16,
|
||||
types.PositionEncodingKind.Utf32,
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class ServerCapabilitiesBuilder:
|
||||
@@ -54,6 +46,9 @@ class ServerCapabilitiesBuilder:
|
||||
commands: List[str],
|
||||
text_document_sync_kind: types.TextDocumentSyncKind,
|
||||
notebook_document_sync: Optional[types.NotebookDocumentSyncOptions] = None,
|
||||
position_encoding: Union[
|
||||
types.PositionEncodingKind, str
|
||||
] = types.PositionEncodingKind.Utf16,
|
||||
):
|
||||
self.client_capabilities = client_capabilities
|
||||
self.features = features
|
||||
@@ -63,12 +58,37 @@ class ServerCapabilitiesBuilder:
|
||||
self.notebook_document_sync = notebook_document_sync
|
||||
|
||||
self.server_cap = types.ServerCapabilities()
|
||||
self.server_cap.position_encoding = position_encoding
|
||||
|
||||
def _provider_options(self, feature: str, default: T) -> Optional[Union[T, Any]]:
|
||||
if feature in self.features:
|
||||
return self.feature_options.get(feature, default)
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def choose_position_encoding(
|
||||
cls, client_capabilities: types.ClientCapabilities
|
||||
) -> Union[types.PositionEncodingKind, str]:
|
||||
server_encoding: Union[types.PositionEncodingKind, str] = (
|
||||
types.PositionEncodingKind.Utf16
|
||||
)
|
||||
|
||||
if (general := client_capabilities.general) is None:
|
||||
return server_encoding
|
||||
|
||||
if (encodings := general.position_encodings) is None:
|
||||
return server_encoding
|
||||
|
||||
# We match client preference where this an overlap between its and our supported encodings.
|
||||
for client_encoding in encodings:
|
||||
if client_encoding in _SUPPORTED_ENCODINGS:
|
||||
server_encoding = client_encoding
|
||||
return server_encoding
|
||||
|
||||
logger.warning(f"Unknown `PositionEncoding`s: {encodings}")
|
||||
|
||||
return server_encoding
|
||||
|
||||
def _with_text_document_sync(self):
|
||||
open_close = (
|
||||
types.TEXT_DOCUMENT_DID_OPEN in self.features
|
||||
@@ -145,7 +165,7 @@ class ServerCapabilitiesBuilder:
|
||||
|
||||
def _with_type_definition(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_TYPE_DEFINITION, default=types.TypeDefinitionOptions()
|
||||
types.TEXT_DOCUMENT_TYPE_DEFINITION, default=True
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.type_definition_provider = value
|
||||
@@ -161,9 +181,7 @@ class ServerCapabilitiesBuilder:
|
||||
return self
|
||||
|
||||
def _with_implementation(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_IMPLEMENTATION, default=types.ImplementationOptions()
|
||||
)
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_IMPLEMENTATION, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.implementation_provider = value
|
||||
return self
|
||||
@@ -201,6 +219,7 @@ class ServerCapabilitiesBuilder:
|
||||
types.TEXT_DOCUMENT_CODE_LENS, default=types.CodeLensOptions()
|
||||
)
|
||||
if value is not None:
|
||||
value.resolve_provider = types.CODE_LENS_RESOLVE in self.features
|
||||
self.server_cap.code_lens_provider = value
|
||||
return self
|
||||
|
||||
@@ -209,6 +228,7 @@ class ServerCapabilitiesBuilder:
|
||||
types.TEXT_DOCUMENT_DOCUMENT_LINK, default=types.DocumentLinkOptions()
|
||||
)
|
||||
if value is not None:
|
||||
value.resolve_provider = types.DOCUMENT_LINK_RESOLVE in self.features
|
||||
self.server_cap.document_link_provider = value
|
||||
return self
|
||||
|
||||
@@ -241,9 +261,25 @@ class ServerCapabilitiesBuilder:
|
||||
return self
|
||||
|
||||
def _with_rename(self):
|
||||
value = self._provider_options(types.TEXT_DOCUMENT_RENAME, default=True)
|
||||
if value is not None:
|
||||
self.server_cap.rename_provider = value
|
||||
server_supports_rename = types.TEXT_DOCUMENT_RENAME in self.features
|
||||
if server_supports_rename is False:
|
||||
return self
|
||||
|
||||
client_prepare_support = get_capability(
|
||||
self.client_capabilities, "text_document.rename.prepare_support", False
|
||||
)
|
||||
|
||||
# From the spec:
|
||||
# > RenameOptions may only be specified if the client states that it supports
|
||||
# > prepareSupport in its initial initialize request.
|
||||
if not client_prepare_support:
|
||||
self.server_cap.rename_provider = server_supports_rename
|
||||
|
||||
else:
|
||||
self.server_cap.rename_provider = types.RenameOptions(
|
||||
prepare_provider=types.TEXT_DOCUMENT_PREPARE_RENAME in self.features
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
def _with_folding_range(self):
|
||||
@@ -302,12 +338,12 @@ class ServerCapabilitiesBuilder:
|
||||
self.server_cap.semantic_tokens_provider = value
|
||||
return self
|
||||
|
||||
full_support: Union[bool, types.SemanticTokensOptionsFullType1] = (
|
||||
full_support: Union[bool, types.SemanticTokensFullDelta] = (
|
||||
types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL in self.features
|
||||
)
|
||||
|
||||
if types.TEXT_DOCUMENT_SEMANTIC_TOKENS_FULL_DELTA in self.features:
|
||||
full_support = types.SemanticTokensOptionsFullType1(delta=True)
|
||||
full_support = types.SemanticTokensFullDelta(delta=True)
|
||||
|
||||
options = types.SemanticTokensOptions(
|
||||
legend=value,
|
||||
@@ -364,7 +400,7 @@ class ServerCapabilitiesBuilder:
|
||||
value = self._provider_options(method_name, default=None)
|
||||
setattr(file_operations, capability_name, value)
|
||||
|
||||
self.server_cap.workspace = types.ServerCapabilitiesWorkspaceType(
|
||||
self.server_cap.workspace = types.WorkspaceOptions(
|
||||
workspace_folders=types.WorkspaceFoldersServerCapabilities(
|
||||
supported=True,
|
||||
change_notifications=True,
|
||||
@@ -391,30 +427,12 @@ class ServerCapabilitiesBuilder:
|
||||
self.server_cap.inline_value_provider = value
|
||||
return self
|
||||
|
||||
def _with_position_encodings(self):
|
||||
self.server_cap.position_encoding = types.PositionEncodingKind.Utf16
|
||||
|
||||
general = self.client_capabilities.general
|
||||
if general is None:
|
||||
return self
|
||||
|
||||
encodings = general.position_encodings
|
||||
if encodings is None:
|
||||
return self
|
||||
|
||||
if types.PositionEncodingKind.Utf16 in encodings:
|
||||
return self
|
||||
|
||||
if types.PositionEncodingKind.Utf32 in encodings:
|
||||
self.server_cap.position_encoding = types.PositionEncodingKind.Utf32
|
||||
return self
|
||||
|
||||
if types.PositionEncodingKind.Utf8 in encodings:
|
||||
self.server_cap.position_encoding = types.PositionEncodingKind.Utf8
|
||||
return self
|
||||
|
||||
logger.warning(f"Unknown `PositionEncoding`s: {encodings}")
|
||||
|
||||
def _with_inline_completion_provider(self):
|
||||
value = self._provider_options(
|
||||
types.TEXT_DOCUMENT_INLINE_COMPLETION, default=None
|
||||
)
|
||||
if value is not None:
|
||||
self.server_cap.inline_completion_provider = value
|
||||
return self
|
||||
|
||||
def _build(self):
|
||||
@@ -455,6 +473,6 @@ class ServerCapabilitiesBuilder:
|
||||
._with_workspace_capabilities()
|
||||
._with_diagnostic_provider()
|
||||
._with_inline_value_provider()
|
||||
._with_position_encodings()
|
||||
._with_inline_completion_provider()
|
||||
._build()
|
||||
)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
############################################################################
|
||||
# Copyright(c) Open Law Library. All rights reserved. #
|
||||
# See ThirdPartyNotices.txt in the project root for additional notices. #
|
||||
# #
|
||||
# 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. #
|
||||
############################################################################
|
||||
"""A simple cli wrapper for pygls servers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from pygls.server import JsonRPCServer
|
||||
|
||||
|
||||
def start_server(server: JsonRPCServer, args: list[str] | None = None):
|
||||
"""A helper function that implements a simple cli wrapper for a pygls server
|
||||
allowing the user to select between the supported transports."""
|
||||
|
||||
name = type(server).__name__
|
||||
parser = argparse.ArgumentParser(description=f"start a {name} instance")
|
||||
parser.add_argument("--tcp", action="store_true", help="start a TCP server")
|
||||
parser.add_argument("--ws", action="store_true", help="start a WebSocket server")
|
||||
parser.add_argument("--host", default="127.0.0.1", help="bind to this address")
|
||||
parser.add_argument("--port", type=int, default=8888, help="bind to this port")
|
||||
|
||||
arguments = parser.parse_args(args)
|
||||
|
||||
if arguments.tcp:
|
||||
server.start_tcp(arguments.host, arguments.port)
|
||||
elif arguments.ws:
|
||||
server.start_ws(arguments.host, arguments.port)
|
||||
else:
|
||||
server.start_io()
|
||||
+105
-66
@@ -14,63 +14,30 @@
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import typing
|
||||
from threading import Event
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
from typing import Union
|
||||
|
||||
from cattrs import Converter
|
||||
|
||||
from pygls.exceptions import PyglsError, JsonRpcException
|
||||
from pygls.exceptions import JsonRpcException, PyglsError
|
||||
from pygls.io_ import run_async, run_websocket
|
||||
from pygls.protocol import JsonRPCProtocol, default_converter
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import List
|
||||
from typing import Optional
|
||||
from typing import Type
|
||||
|
||||
from cattrs import Converter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def aio_readline(stop_event, reader, message_handler):
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
|
||||
# Initialize message buffer
|
||||
message = []
|
||||
content_length = 0
|
||||
|
||||
while not stop_event.is_set():
|
||||
# Read a header line
|
||||
header = await reader.readline()
|
||||
if not header:
|
||||
break
|
||||
message.append(header)
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = await reader.readexactly(content_length)
|
||||
if not body:
|
||||
break
|
||||
message.append(body)
|
||||
|
||||
# Pass message to protocol
|
||||
message_handler(b"".join(message))
|
||||
|
||||
# Reset the buffer
|
||||
message = []
|
||||
content_length = 0
|
||||
|
||||
|
||||
class JsonRPCClient:
|
||||
"""Base JSON-RPC client."""
|
||||
|
||||
@@ -79,14 +46,14 @@ class JsonRPCClient:
|
||||
protocol_cls: Type[JsonRPCProtocol] = JsonRPCProtocol,
|
||||
converter_factory: Callable[[], Converter] = default_converter,
|
||||
):
|
||||
# Strictly speaking `JsonRPCProtocol` wants a `LanguageServer`, not a
|
||||
# `JsonRPCClient`. However there similar enough for our purposes, which is
|
||||
# that this client will mostly be used in testing contexts.
|
||||
# Strictly speaking, `JsonRPCProtocol` wants a `JsonRPCServer`, not a
|
||||
# `JsonRPCClient`. However they're similar enough for our purposes, which
|
||||
# is that this client will mostly be used in testing contexts.
|
||||
self.protocol = protocol_cls(self, converter_factory()) # type: ignore
|
||||
|
||||
self._server: Optional[asyncio.subprocess.Process] = None
|
||||
self._stop_event = Event()
|
||||
self._async_tasks: List[asyncio.Task] = []
|
||||
self._async_tasks: List[asyncio.Task[Any]] = []
|
||||
|
||||
@property
|
||||
def stopped(self) -> bool:
|
||||
@@ -128,39 +95,112 @@ class JsonRPCClient:
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
self.protocol.connection_made(server.stdin) # type: ignore
|
||||
# Keep mypy happy
|
||||
if server.stdout is None:
|
||||
raise RuntimeError("Server process is missing a stdout stream")
|
||||
|
||||
# Keep mypy happy
|
||||
if server.stdin is None:
|
||||
raise RuntimeError("Server process is missing a stdin stream")
|
||||
|
||||
self.protocol.set_writer(server.stdin)
|
||||
connection = asyncio.create_task(
|
||||
aio_readline(self._stop_event, server.stdout, self.protocol.data_received)
|
||||
run_async(
|
||||
stop_event=self._stop_event,
|
||||
reader=server.stdout,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
notify_exit = asyncio.create_task(self._server_exit())
|
||||
|
||||
self._server = server
|
||||
self._async_tasks.extend([connection, notify_exit])
|
||||
|
||||
async def _server_exit(self):
|
||||
if self._server is not None:
|
||||
await self._server.wait()
|
||||
logger.debug(
|
||||
"Server process %s exited with return code: %s",
|
||||
self._server.pid,
|
||||
self._server.returncode,
|
||||
async def start_tcp(self, host: str, port: int):
|
||||
"""Start communicating with a server over TCP."""
|
||||
reader, writer = await asyncio.open_connection(host, port)
|
||||
|
||||
self.protocol.set_writer(writer)
|
||||
connection = asyncio.create_task(
|
||||
run_async(
|
||||
stop_event=self._stop_event,
|
||||
reader=reader,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
|
||||
self._async_tasks.extend([connection])
|
||||
|
||||
async def start_ws(self, host: str, port: int):
|
||||
"""Start communicating with a server over WebSockets."""
|
||||
|
||||
try:
|
||||
from websockets.asyncio.client import connect
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"Run `pip install pygls[ws]` to install dependencies required for websockets."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
uri = f"ws://{host}:{port}"
|
||||
websocket = await connect(uri)
|
||||
connection = asyncio.create_task(
|
||||
run_websocket(
|
||||
stop_event=self._stop_event,
|
||||
websocket=websocket,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
self._async_tasks.extend([connection])
|
||||
|
||||
# Yield control to the event loop, gives the run_websocket task chance to spin up.
|
||||
await asyncio.sleep(0)
|
||||
|
||||
async def _server_exit(self):
|
||||
"""Cleanup handler that runs when the server process managed by the client exits"""
|
||||
if self._server is None:
|
||||
return
|
||||
|
||||
await self._server.wait()
|
||||
|
||||
pid = self._server.pid
|
||||
returncode = self._server.returncode
|
||||
|
||||
reason = f"Server process {pid} exited with return code: {returncode}"
|
||||
logger.debug(reason)
|
||||
|
||||
# Cancel any pending requests
|
||||
for id_, fut in self.protocol._request_futures.items():
|
||||
if not fut.done():
|
||||
fut.set_exception(RuntimeError(reason))
|
||||
logger.debug("Cancelled pending request '%s': %s", id_, reason)
|
||||
|
||||
try:
|
||||
await self.server_exit(self._server)
|
||||
self._stop_event.set()
|
||||
except Exception:
|
||||
logger.exception("Error in server_exit handler")
|
||||
|
||||
self._stop_event.set()
|
||||
|
||||
async def server_exit(self, server: asyncio.subprocess.Process):
|
||||
"""Called when the server process exits."""
|
||||
|
||||
def _report_server_error(
|
||||
self, error: Exception, source: Union[PyglsError, JsonRpcException]
|
||||
self, error: Exception, source: type[PyglsError] | type[JsonRpcException]
|
||||
):
|
||||
try:
|
||||
self.report_server_error(error, source)
|
||||
except Exception:
|
||||
logger.error("Unable to report error", exc_info=True)
|
||||
logger.exception("Unable to report error")
|
||||
|
||||
def report_server_error(
|
||||
self, error: Exception, source: Union[PyglsError, JsonRpcException]
|
||||
self, error: Exception, source: type[PyglsError] | type[JsonRpcException]
|
||||
):
|
||||
"""Called when the server does something unexpected e.g. respond with malformed
|
||||
JSON."""
|
||||
@@ -169,8 +209,7 @@ class JsonRPCClient:
|
||||
self._stop_event.set()
|
||||
|
||||
if self._server is not None and self._server.returncode is None:
|
||||
logger.debug("Terminating server process: %s", self._server.pid)
|
||||
self._server.terminate()
|
||||
await self._server.wait()
|
||||
|
||||
if len(self._async_tasks) > 0:
|
||||
await asyncio.gather(*self._async_tasks)
|
||||
|
||||
@@ -16,7 +16,10 @@
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
from typing import Any
|
||||
from typing import Set
|
||||
from typing import Type
|
||||
from lsprotocol.types import ResponseError
|
||||
@@ -25,14 +28,23 @@ from lsprotocol.types import ResponseError
|
||||
class JsonRpcException(Exception):
|
||||
"""A class used as a base class for json rpc exceptions."""
|
||||
|
||||
def __init__(self, message=None, code=None, data=None):
|
||||
message = message or getattr(self.__class__, "MESSAGE")
|
||||
CODE = -32603
|
||||
MESSAGE = ""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
message: str | None = None,
|
||||
code: int | None = None,
|
||||
data: Any | None = None,
|
||||
):
|
||||
message = message or self.MESSAGE
|
||||
|
||||
super().__init__(message)
|
||||
self.message = message
|
||||
self.code = code or getattr(self.__class__, "CODE")
|
||||
self.message: str = message
|
||||
self.code: int = code or self.CODE
|
||||
self.data = data
|
||||
|
||||
def __eq__(self, other):
|
||||
def __eq__(self, other: Any):
|
||||
return (
|
||||
isinstance(other, self.__class__)
|
||||
and self.code == other.code
|
||||
@@ -43,7 +55,7 @@ class JsonRpcException(Exception):
|
||||
return hash((self.code, self.message))
|
||||
|
||||
@staticmethod
|
||||
def from_error(error):
|
||||
def from_error(error: ResponseError):
|
||||
for exc_class in _EXCEPTIONS:
|
||||
if exc_class.supports_code(error.code):
|
||||
return exc_class(
|
||||
@@ -53,7 +65,16 @@ class JsonRpcException(Exception):
|
||||
return JsonRpcException(code=error.code, message=error.message, data=error.data)
|
||||
|
||||
@classmethod
|
||||
def supports_code(cls, code):
|
||||
def of(cls, exc: Any):
|
||||
"""Default ``of`` implementation that raises a ``JsonRpcException`` derived from
|
||||
the given exception
|
||||
"""
|
||||
return cls(
|
||||
message=f"{cls.MESSAGE}: {exc}",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def supports_code(cls, code: int):
|
||||
# Defaults to UnknownErrorCode
|
||||
return getattr(cls, "CODE", -32001) == code
|
||||
|
||||
@@ -91,7 +112,7 @@ class JsonRpcMethodNotFound(JsonRpcException):
|
||||
MESSAGE = "Method Not Found"
|
||||
|
||||
@classmethod
|
||||
def of(cls, method):
|
||||
def of(cls, method: str):
|
||||
return cls(message=cls.MESSAGE + ": " + method)
|
||||
|
||||
|
||||
|
||||
@@ -55,13 +55,21 @@ def get_help_attrs(f):
|
||||
)
|
||||
|
||||
|
||||
def has_ls_param_or_annotation(f, annotation):
|
||||
"""Returns true if callable has first parameter named `ls` or type of
|
||||
annotation"""
|
||||
def has_ls_param_or_annotation(f, actual_type):
|
||||
"""Returns true if the given callable's first parameter is
|
||||
|
||||
- named `ls`
|
||||
- has a type annotation compatible with the given type
|
||||
"""
|
||||
try:
|
||||
sig = inspect.signature(f)
|
||||
first_p = next(itertools.islice(sig.parameters.values(), 0, 1))
|
||||
return first_p.name == PARAM_LS or get_type_hints(f)[first_p.name] == annotation
|
||||
|
||||
if first_p.name == PARAM_LS:
|
||||
return True
|
||||
|
||||
expected_type = get_type_hints(f)[first_p.name]
|
||||
return issubclass(actual_type, expected_type)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
@@ -80,6 +88,10 @@ def wrap_with_server(f, server):
|
||||
async def wrapped(*args, **kwargs):
|
||||
return await f(server, *args, **kwargs)
|
||||
|
||||
# Used by `workspace/executeCommand` to access the original function's
|
||||
# signature. Mirrors how functools.partial works.
|
||||
wrapped.func = f # type: ignore[attr-defined]
|
||||
|
||||
else:
|
||||
wrapped = functools.partial(f, server)
|
||||
if is_thread_function(f):
|
||||
@@ -196,7 +208,8 @@ class FeatureManager:
|
||||
raise TypeError(
|
||||
(
|
||||
f'Options of method "{feature_name}"'
|
||||
f" should be instance of type {options_type}"
|
||||
f" is instance of type {type(options)}"
|
||||
f" which is not a subtype of {options_type}"
|
||||
)
|
||||
)
|
||||
self._feature_options[feature_name] = options
|
||||
|
||||
@@ -0,0 +1,296 @@
|
||||
############################################################################
|
||||
# Copyright(c) Open Law Library. All rights reserved. #
|
||||
# See ThirdPartyNotices.txt in the project root for additional notices. #
|
||||
# #
|
||||
# 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. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import typing
|
||||
|
||||
from pygls.exceptions import JsonRpcException
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import logging
|
||||
import threading
|
||||
from collections.abc import Awaitable
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from typing import Any, BinaryIO, Callable, Protocol
|
||||
|
||||
from websockets.asyncio.client import ClientConnection
|
||||
from websockets.asyncio.server import ServerConnection
|
||||
|
||||
from pygls.protocol import JsonRPCProtocol
|
||||
|
||||
class Reader(Protocol):
|
||||
"""An synchronous reader."""
|
||||
|
||||
def readline(self) -> bytes: ...
|
||||
|
||||
def read(self, n: int) -> bytes: ...
|
||||
|
||||
class Writer(Protocol):
|
||||
"""An synchronous writer."""
|
||||
|
||||
def close(self) -> None: ...
|
||||
|
||||
def write(self, data: bytes) -> None: ...
|
||||
|
||||
class AsyncReader(typing.Protocol):
|
||||
"""An asynchronous reader."""
|
||||
|
||||
def readline(self) -> Awaitable[bytes]: ...
|
||||
|
||||
def readexactly(self, n: int) -> Awaitable[bytes]: ...
|
||||
|
||||
class AsyncWriter(typing.Protocol):
|
||||
"""An asynchronous writer."""
|
||||
|
||||
def close(self) -> Awaitable[None]: ...
|
||||
|
||||
def write(self, data: bytes) -> Awaitable[None]: ...
|
||||
|
||||
|
||||
class StdinAsyncReader:
|
||||
"""Read from stdin asynchronously."""
|
||||
|
||||
def __init__(self, stdin: BinaryIO, executor: ThreadPoolExecutor | None = None):
|
||||
self.stdin = stdin
|
||||
self._loop: asyncio.AbstractEventLoop | None = None
|
||||
self.executor = executor
|
||||
|
||||
@property
|
||||
def loop(self):
|
||||
if self._loop is None:
|
||||
self._loop = asyncio.get_running_loop()
|
||||
|
||||
return self._loop
|
||||
|
||||
def readline(self) -> Awaitable[bytes]:
|
||||
return self.loop.run_in_executor(self.executor, self.stdin.readline)
|
||||
|
||||
def readexactly(self, n: int) -> Awaitable[bytes]:
|
||||
return self.loop.run_in_executor(self.executor, self.stdin.read, n)
|
||||
|
||||
|
||||
class StdoutWriter:
|
||||
"""Align a stdout stream with pygls' writer interface."""
|
||||
|
||||
def __init__(self, stdout: BinaryIO):
|
||||
self._stdout = stdout
|
||||
|
||||
def close(self):
|
||||
self._stdout.close()
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
self._stdout.write(data)
|
||||
self._stdout.flush()
|
||||
|
||||
|
||||
class WebSocketWriter:
|
||||
"""Align a websocket connection with pygls' writer interface"""
|
||||
|
||||
def __init__(self, ws: ServerConnection | ClientConnection):
|
||||
self._ws = ws
|
||||
|
||||
def close(self) -> Awaitable[None]:
|
||||
return self._ws.close()
|
||||
|
||||
def write(self, data: bytes) -> Awaitable[None]:
|
||||
return self._ws.send(data)
|
||||
|
||||
|
||||
async def run_async(
|
||||
stop_event: threading.Event,
|
||||
reader: AsyncReader,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run a main message processing loop, asynchronously
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
reader
|
||||
The reader to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
"""
|
||||
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
content_length = 0
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
|
||||
while not stop_event.is_set():
|
||||
# Read a header line
|
||||
header = await reader.readline()
|
||||
if not header:
|
||||
break
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = await reader.readexactly(content_length)
|
||||
if not body:
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(body, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
finally:
|
||||
# Reset
|
||||
content_length = 0
|
||||
|
||||
|
||||
def run(
|
||||
stop_event: threading.Event,
|
||||
reader: Reader,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run a main message processing loop, synchronously
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
reader
|
||||
The reader to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
|
||||
error_handler
|
||||
Function to call when an error is encountered.
|
||||
"""
|
||||
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
content_length = 0
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
|
||||
while not stop_event.is_set():
|
||||
# Read a header line
|
||||
header = reader.readline()
|
||||
if not header:
|
||||
break
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = reader.read(content_length)
|
||||
if not body:
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(body, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
finally:
|
||||
# Reset
|
||||
content_length = 0
|
||||
|
||||
|
||||
async def run_websocket(
|
||||
websocket: ClientConnection | ServerConnection,
|
||||
stop_event: threading.Event,
|
||||
protocol: JsonRPCProtocol,
|
||||
logger: logging.Logger | None = None,
|
||||
error_handler: Callable[[Exception, type[JsonRpcException]], Any] | None = None,
|
||||
):
|
||||
"""Run the main message processing loop, over websockets.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
stop_event
|
||||
A ``threading.Event`` used to break the main loop
|
||||
|
||||
websocket
|
||||
The websocket to read messages from
|
||||
|
||||
protocol
|
||||
The protocol instance that should handle the messages
|
||||
|
||||
logger
|
||||
The logger instance to use
|
||||
|
||||
error_handler
|
||||
Function to call when an error is encountered.
|
||||
"""
|
||||
|
||||
logger = logger or logging.getLogger(__name__)
|
||||
protocol.set_writer(WebSocketWriter(websocket), include_headers=False)
|
||||
|
||||
try:
|
||||
from websockets.exceptions import ConnectionClosed
|
||||
except ImportError:
|
||||
logger.exception(
|
||||
"Run `pip install pygls[ws]` to install dependencies required for websockets."
|
||||
)
|
||||
return
|
||||
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
logger.debug("waiting for a message...")
|
||||
data = await websocket.recv(decode=False)
|
||||
except ConnectionClosed:
|
||||
logger.debug("Websocket connection closed.")
|
||||
stop_event.set()
|
||||
break
|
||||
|
||||
try:
|
||||
message = json.loads(data, object_hook=protocol.structure_message)
|
||||
protocol.handle_message(message)
|
||||
except Exception as exc:
|
||||
logger.exception("Unable to handle message")
|
||||
if error_handler:
|
||||
error_handler(exc, JsonRpcException)
|
||||
|
||||
logger.debug("Exiting main loop")
|
||||
await websocket.close()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,463 @@
|
||||
############################################################################
|
||||
# Copyright(c) Open Law Library. All rights reserved. #
|
||||
# See ThirdPartyNotices.txt in the project root for additional notices. #
|
||||
# #
|
||||
# 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. #
|
||||
############################################################################
|
||||
|
||||
# GENERATED FROM scripts/generate_code.py -- DO NOT EDIT
|
||||
# flake8: noqa
|
||||
from __future__ import annotations
|
||||
|
||||
from lsprotocol import types
|
||||
from pygls.protocol import LanguageServerProtocol
|
||||
from pygls.protocol import default_converter
|
||||
from pygls.server import JsonRPCServer
|
||||
import typing
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from cattrs import Converter
|
||||
from concurrent.futures import Future
|
||||
from typing import Any
|
||||
from typing import Callable
|
||||
from typing import Optional
|
||||
from typing import Sequence
|
||||
|
||||
|
||||
class BaseLanguageServer(JsonRPCServer):
|
||||
|
||||
protocol: LanguageServerProtocol
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
protocol_cls: type[LanguageServerProtocol] = LanguageServerProtocol,
|
||||
converter_factory: Callable[[], Converter] = default_converter,
|
||||
max_workers: int | None = None,
|
||||
):
|
||||
super().__init__(protocol_cls, converter_factory, max_workers)
|
||||
|
||||
def client_register_capability(
|
||||
self,
|
||||
params: types.RegistrationParams,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`client/registerCapability` request.
|
||||
|
||||
The `client/registerCapability` request is sent from the server to the client to register a new capability
|
||||
handler on the client side.
|
||||
"""
|
||||
return self.protocol.send_request("client/registerCapability", params, callback)
|
||||
|
||||
async def client_register_capability_async(
|
||||
self,
|
||||
params: types.RegistrationParams,
|
||||
) -> None:
|
||||
"""Make a :lsp:`client/registerCapability` request.
|
||||
|
||||
The `client/registerCapability` request is sent from the server to the client to register a new capability
|
||||
handler on the client side.
|
||||
"""
|
||||
return await self.protocol.send_request_async("client/registerCapability", params)
|
||||
|
||||
def client_unregister_capability(
|
||||
self,
|
||||
params: types.UnregistrationParams,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`client/unregisterCapability` request.
|
||||
|
||||
The `client/unregisterCapability` request is sent from the server to the client to unregister a previously registered capability
|
||||
handler on the client side.
|
||||
"""
|
||||
return self.protocol.send_request("client/unregisterCapability", params, callback)
|
||||
|
||||
async def client_unregister_capability_async(
|
||||
self,
|
||||
params: types.UnregistrationParams,
|
||||
) -> None:
|
||||
"""Make a :lsp:`client/unregisterCapability` request.
|
||||
|
||||
The `client/unregisterCapability` request is sent from the server to the client to unregister a previously registered capability
|
||||
handler on the client side.
|
||||
"""
|
||||
return await self.protocol.send_request_async("client/unregisterCapability", params)
|
||||
|
||||
def window_show_document(
|
||||
self,
|
||||
params: types.ShowDocumentParams,
|
||||
callback: Optional[Callable[[types.ShowDocumentResult], None]] = None,
|
||||
) -> Future[types.ShowDocumentResult]:
|
||||
"""Make a :lsp:`window/showDocument` request.
|
||||
|
||||
A request to show a document. This request might open an
|
||||
external program depending on the value of the URI to open.
|
||||
For example a request to open `https://code.visualstudio.com/`
|
||||
will very likely open the URI in a WEB browser.
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return self.protocol.send_request("window/showDocument", params, callback)
|
||||
|
||||
async def window_show_document_async(
|
||||
self,
|
||||
params: types.ShowDocumentParams,
|
||||
) -> types.ShowDocumentResult:
|
||||
"""Make a :lsp:`window/showDocument` request.
|
||||
|
||||
A request to show a document. This request might open an
|
||||
external program depending on the value of the URI to open.
|
||||
For example a request to open `https://code.visualstudio.com/`
|
||||
will very likely open the URI in a WEB browser.
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("window/showDocument", params)
|
||||
|
||||
def window_show_message_request(
|
||||
self,
|
||||
params: types.ShowMessageRequestParams,
|
||||
callback: Optional[Callable[[Optional[types.MessageActionItem]], None]] = None,
|
||||
) -> Future[Optional[types.MessageActionItem]]:
|
||||
"""Make a :lsp:`window/showMessageRequest` request.
|
||||
|
||||
The show message request is sent from the server to the client to show a message
|
||||
and a set of options actions to the user.
|
||||
"""
|
||||
return self.protocol.send_request("window/showMessageRequest", params, callback)
|
||||
|
||||
async def window_show_message_request_async(
|
||||
self,
|
||||
params: types.ShowMessageRequestParams,
|
||||
) -> Optional[types.MessageActionItem]:
|
||||
"""Make a :lsp:`window/showMessageRequest` request.
|
||||
|
||||
The show message request is sent from the server to the client to show a message
|
||||
and a set of options actions to the user.
|
||||
"""
|
||||
return await self.protocol.send_request_async("window/showMessageRequest", params)
|
||||
|
||||
def window_work_done_progress_create(
|
||||
self,
|
||||
params: types.WorkDoneProgressCreateParams,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`window/workDoneProgress/create` request.
|
||||
|
||||
The `window/workDoneProgress/create` request is sent from the server to the client to initiate progress
|
||||
reporting from the server.
|
||||
"""
|
||||
return self.protocol.send_request("window/workDoneProgress/create", params, callback)
|
||||
|
||||
async def window_work_done_progress_create_async(
|
||||
self,
|
||||
params: types.WorkDoneProgressCreateParams,
|
||||
) -> None:
|
||||
"""Make a :lsp:`window/workDoneProgress/create` request.
|
||||
|
||||
The `window/workDoneProgress/create` request is sent from the server to the client to initiate progress
|
||||
reporting from the server.
|
||||
"""
|
||||
return await self.protocol.send_request_async("window/workDoneProgress/create", params)
|
||||
|
||||
def workspace_apply_edit(
|
||||
self,
|
||||
params: types.ApplyWorkspaceEditParams,
|
||||
callback: Optional[Callable[[types.ApplyWorkspaceEditResult], None]] = None,
|
||||
) -> Future[types.ApplyWorkspaceEditResult]:
|
||||
"""Make a :lsp:`workspace/applyEdit` request.
|
||||
|
||||
A request sent from the server to the client to modified certain resources.
|
||||
"""
|
||||
return self.protocol.send_request("workspace/applyEdit", params, callback)
|
||||
|
||||
async def workspace_apply_edit_async(
|
||||
self,
|
||||
params: types.ApplyWorkspaceEditParams,
|
||||
) -> types.ApplyWorkspaceEditResult:
|
||||
"""Make a :lsp:`workspace/applyEdit` request.
|
||||
|
||||
A request sent from the server to the client to modified certain resources.
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/applyEdit", params)
|
||||
|
||||
def workspace_code_lens_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/codeLens/refresh` request.
|
||||
|
||||
A request to refresh all code actions
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/codeLens/refresh", params, callback)
|
||||
|
||||
async def workspace_code_lens_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/codeLens/refresh` request.
|
||||
|
||||
A request to refresh all code actions
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/codeLens/refresh", params)
|
||||
|
||||
def workspace_configuration(
|
||||
self,
|
||||
params: types.ConfigurationParams,
|
||||
callback: Optional[Callable[[Sequence[Optional[Any]]], None]] = None,
|
||||
) -> Future[Sequence[Optional[Any]]]:
|
||||
"""Make a :lsp:`workspace/configuration` request.
|
||||
|
||||
The 'workspace/configuration' request is sent from the server to the client to fetch a certain
|
||||
configuration setting.
|
||||
|
||||
This pull model replaces the old push model were the client signaled configuration change via an
|
||||
event. If the server still needs to react to configuration changes (since the server caches the
|
||||
result of `workspace/configuration` requests) the server should register for an empty configuration
|
||||
change event and empty the cache if such an event is received.
|
||||
"""
|
||||
return self.protocol.send_request("workspace/configuration", params, callback)
|
||||
|
||||
async def workspace_configuration_async(
|
||||
self,
|
||||
params: types.ConfigurationParams,
|
||||
) -> Sequence[Optional[Any]]:
|
||||
"""Make a :lsp:`workspace/configuration` request.
|
||||
|
||||
The 'workspace/configuration' request is sent from the server to the client to fetch a certain
|
||||
configuration setting.
|
||||
|
||||
This pull model replaces the old push model were the client signaled configuration change via an
|
||||
event. If the server still needs to react to configuration changes (since the server caches the
|
||||
result of `workspace/configuration` requests) the server should register for an empty configuration
|
||||
change event and empty the cache if such an event is received.
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/configuration", params)
|
||||
|
||||
def workspace_diagnostic_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/diagnostic/refresh` request.
|
||||
|
||||
The diagnostic refresh request definition.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/diagnostic/refresh", params, callback)
|
||||
|
||||
async def workspace_diagnostic_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/diagnostic/refresh` request.
|
||||
|
||||
The diagnostic refresh request definition.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/diagnostic/refresh", params)
|
||||
|
||||
def workspace_folding_range_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/foldingRange/refresh` request.
|
||||
|
||||
@since 3.18.0
|
||||
@proposed
|
||||
"""
|
||||
return self.protocol.send_request("workspace/foldingRange/refresh", params, callback)
|
||||
|
||||
async def workspace_folding_range_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/foldingRange/refresh` request.
|
||||
|
||||
@since 3.18.0
|
||||
@proposed
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/foldingRange/refresh", params)
|
||||
|
||||
def workspace_inlay_hint_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/inlayHint/refresh` request.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/inlayHint/refresh", params, callback)
|
||||
|
||||
async def workspace_inlay_hint_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/inlayHint/refresh` request.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/inlayHint/refresh", params)
|
||||
|
||||
def workspace_inline_value_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/inlineValue/refresh` request.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/inlineValue/refresh", params, callback)
|
||||
|
||||
async def workspace_inline_value_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/inlineValue/refresh` request.
|
||||
|
||||
@since 3.17.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/inlineValue/refresh", params)
|
||||
|
||||
def workspace_semantic_tokens_refresh(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/semanticTokens/refresh` request.
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return self.protocol.send_request("workspace/semanticTokens/refresh", params, callback)
|
||||
|
||||
async def workspace_semantic_tokens_refresh_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/semanticTokens/refresh` request.
|
||||
|
||||
@since 3.16.0
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/semanticTokens/refresh", params)
|
||||
|
||||
def workspace_text_document_content_refresh(
|
||||
self,
|
||||
params: types.TextDocumentContentRefreshParams,
|
||||
callback: Optional[Callable[[None], None]] = None,
|
||||
) -> Future[None]:
|
||||
"""Make a :lsp:`workspace/textDocumentContent/refresh` request.
|
||||
|
||||
The `workspace/textDocumentContent` request is sent from the server to the client to refresh
|
||||
the content of a specific text document.
|
||||
|
||||
@since 3.18.0
|
||||
@proposed
|
||||
"""
|
||||
return self.protocol.send_request("workspace/textDocumentContent/refresh", params, callback)
|
||||
|
||||
async def workspace_text_document_content_refresh_async(
|
||||
self,
|
||||
params: types.TextDocumentContentRefreshParams,
|
||||
) -> None:
|
||||
"""Make a :lsp:`workspace/textDocumentContent/refresh` request.
|
||||
|
||||
The `workspace/textDocumentContent` request is sent from the server to the client to refresh
|
||||
the content of a specific text document.
|
||||
|
||||
@since 3.18.0
|
||||
@proposed
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/textDocumentContent/refresh", params)
|
||||
|
||||
def workspace_workspace_folders(
|
||||
self,
|
||||
params: None,
|
||||
callback: Optional[Callable[[Optional[Sequence[types.WorkspaceFolder]]], None]] = None,
|
||||
) -> Future[Optional[Sequence[types.WorkspaceFolder]]]:
|
||||
"""Make a :lsp:`workspace/workspaceFolders` request.
|
||||
|
||||
The `workspace/workspaceFolders` is sent from the server to the client to fetch the open workspace folders.
|
||||
"""
|
||||
return self.protocol.send_request("workspace/workspaceFolders", params, callback)
|
||||
|
||||
async def workspace_workspace_folders_async(
|
||||
self,
|
||||
params: None,
|
||||
) -> Optional[Sequence[types.WorkspaceFolder]]:
|
||||
"""Make a :lsp:`workspace/workspaceFolders` request.
|
||||
|
||||
The `workspace/workspaceFolders` is sent from the server to the client to fetch the open workspace folders.
|
||||
"""
|
||||
return await self.protocol.send_request_async("workspace/workspaceFolders", params)
|
||||
|
||||
def cancel_request(self, params: types.CancelParams) -> None:
|
||||
"""Send a :lsp:`$/cancelRequest` notification.
|
||||
|
||||
|
||||
"""
|
||||
self.protocol.notify("$/cancelRequest", params)
|
||||
|
||||
def log_trace(self, params: types.LogTraceParams) -> None:
|
||||
"""Send a :lsp:`$/logTrace` notification.
|
||||
|
||||
|
||||
"""
|
||||
self.protocol.notify("$/logTrace", params)
|
||||
|
||||
def progress(self, params: types.ProgressParams) -> None:
|
||||
"""Send a :lsp:`$/progress` notification.
|
||||
|
||||
|
||||
"""
|
||||
self.protocol.notify("$/progress", params)
|
||||
|
||||
def telemetry_event(self, params: typing.Optional[typing.Any]) -> None:
|
||||
"""Send a :lsp:`telemetry/event` notification.
|
||||
|
||||
The telemetry event notification is sent from the server to the client to ask
|
||||
the client to log telemetry data.
|
||||
"""
|
||||
self.protocol.notify("telemetry/event", params)
|
||||
|
||||
def text_document_publish_diagnostics(self, params: types.PublishDiagnosticsParams) -> None:
|
||||
"""Send a :lsp:`textDocument/publishDiagnostics` notification.
|
||||
|
||||
Diagnostics notification are sent from the server to the client to signal
|
||||
results of validation runs.
|
||||
"""
|
||||
self.protocol.notify("textDocument/publishDiagnostics", params)
|
||||
|
||||
def window_log_message(self, params: types.LogMessageParams) -> None:
|
||||
"""Send a :lsp:`window/logMessage` notification.
|
||||
|
||||
The log message notification is sent from the server to the client to ask
|
||||
the client to log a particular message.
|
||||
"""
|
||||
self.protocol.notify("window/logMessage", params)
|
||||
|
||||
def window_show_message(self, params: types.ShowMessageParams) -> None:
|
||||
"""Send a :lsp:`window/showMessage` notification.
|
||||
|
||||
The show message notification is sent from a server to a client to ask
|
||||
the client to display a particular message in the user interface.
|
||||
"""
|
||||
self.protocol.notify("window/showMessage", params)
|
||||
File diff suppressed because it is too large
Load Diff
+4
-1959
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.exceptions import FeatureRequestError
|
||||
|
||||
from ._base_server import BaseLanguageServer
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Callable
|
||||
from typing import TypeVar
|
||||
|
||||
from pygls.server import ServerErrors
|
||||
from pygls.progress import Progress
|
||||
from pygls.workspace import Workspace
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
|
||||
class LanguageServer(BaseLanguageServer):
|
||||
"""The default LanguageServer
|
||||
|
||||
This class can be extended and it can be passed as a first argument to
|
||||
registered commands/features.
|
||||
|
||||
.. |ServerInfo| replace:: :class:`~lsprotocol.types.ServerInfo`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name
|
||||
Name of the server, used to populate |ServerInfo| which is sent to
|
||||
the client during initialization
|
||||
|
||||
version
|
||||
Version of the server, used to populate |ServerInfo| which is sent to
|
||||
the client during initialization
|
||||
|
||||
protocol_cls
|
||||
The :class:`~pygls.protocol.LanguageServerProtocol` class definition, or any
|
||||
subclass of it.
|
||||
|
||||
max_workers
|
||||
Maximum number of workers for ``ThreadPool`` and ``ThreadPoolExecutor``
|
||||
|
||||
text_document_sync_kind
|
||||
Text document synchronization method
|
||||
|
||||
None
|
||||
No synchronization
|
||||
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Full`
|
||||
Send entire document text with each update
|
||||
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental`
|
||||
Send only the region of text that changed with each update
|
||||
|
||||
notebook_document_sync
|
||||
Advertise :lsp:`NotebookDocument` support to the client.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
version: str,
|
||||
text_document_sync_kind: types.TextDocumentSyncKind = types.TextDocumentSyncKind.Incremental,
|
||||
notebook_document_sync: types.NotebookDocumentSyncOptions | None = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
self.name = name
|
||||
self.version = version
|
||||
self._text_document_sync_kind = text_document_sync_kind
|
||||
self._notebook_document_sync = notebook_document_sync
|
||||
self.process_id: int | None = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def client_capabilities(self) -> types.ClientCapabilities:
|
||||
"""The client's capabilities."""
|
||||
return self.protocol.client_capabilities
|
||||
|
||||
@property
|
||||
def server_capabilities(self) -> types.ServerCapabilities:
|
||||
"""The server's capabilities."""
|
||||
return self.protocol.server_capabilities
|
||||
|
||||
@property
|
||||
def workspace(self) -> Workspace:
|
||||
"""Returns in-memory workspace."""
|
||||
return self.protocol.workspace
|
||||
|
||||
@property
|
||||
def work_done_progress(self) -> Progress:
|
||||
"""Gets the object to manage client's progress bar."""
|
||||
return self.protocol.progress
|
||||
|
||||
def report_server_error(self, error: Exception, source: ServerErrors):
|
||||
"""
|
||||
Sends error to the client for displaying.
|
||||
|
||||
By default this function does not handle LSP request errors. This is because LSP requests
|
||||
require direct responses and so already have a mechanism for including unexpected errors
|
||||
in the response body.
|
||||
|
||||
All other errors are "out of band" in the sense that the client isn't explicitly waiting
|
||||
for them. For example diagnostics are returned as notifications, not responses to requests,
|
||||
and so can seemingly be sent at random. Also for example consider JSON RPC serialization
|
||||
and deserialization, if a payload cannot be parsed then the whole request/response cycle
|
||||
cannot be completed and so one of these "out of band" error messages is sent.
|
||||
|
||||
These "out of band" error messages are not a requirement of the LSP spec. Pygls simply
|
||||
offers this behaviour as a recommended default. It is perfectly reasonble to override this
|
||||
default.
|
||||
"""
|
||||
|
||||
if source == FeatureRequestError:
|
||||
return
|
||||
|
||||
self.window_show_message(
|
||||
types.ShowMessageParams(
|
||||
message=f"Error in server: {error}",
|
||||
type=types.MessageType.Error,
|
||||
)
|
||||
)
|
||||
@@ -1,7 +1,6 @@
|
||||
import json
|
||||
from typing import Any
|
||||
|
||||
from collections import namedtuple
|
||||
from typing import Any
|
||||
|
||||
from lsprotocol import converters
|
||||
|
||||
@@ -12,7 +11,6 @@ from pygls.protocol.json_rpc import (
|
||||
JsonRPCResponseMessage,
|
||||
)
|
||||
from pygls.protocol.language_server import LanguageServerProtocol, lsp_method
|
||||
from pygls.protocol.lsp_meta import LSPMeta, call_user_feature
|
||||
|
||||
|
||||
def _dict_to_object(d: Any):
|
||||
@@ -68,8 +66,6 @@ __all__ = (
|
||||
"JsonRPCRequestMessage",
|
||||
"JsonRPCResponseMessage",
|
||||
"JsonRPCNotification",
|
||||
"LSPMeta",
|
||||
"call_user_feature",
|
||||
"_dict_to_object",
|
||||
"_params_field_structure_hook",
|
||||
"_result_field_structure_hook",
|
||||
|
||||
@@ -15,54 +15,90 @@
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import enum
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
import uuid
|
||||
import traceback
|
||||
import typing
|
||||
import uuid
|
||||
from concurrent.futures import Future
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
Union,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pygls.server import LanguageServer, WebSocketTransportAdapter
|
||||
|
||||
from typing import Any, Callable, Protocol, Type, Union, runtime_checkable
|
||||
|
||||
import attrs
|
||||
from cattrs.errors import ClassValidationError
|
||||
|
||||
from lsprotocol.types import (
|
||||
CANCEL_REQUEST,
|
||||
EXIT,
|
||||
WORKSPACE_EXECUTE_COMMAND,
|
||||
ResponseError,
|
||||
ResponseErrorMessage,
|
||||
)
|
||||
|
||||
from pygls.exceptions import (
|
||||
FeatureNotificationError,
|
||||
FeatureRequestError,
|
||||
JsonRpcException,
|
||||
JsonRpcInternalError,
|
||||
JsonRpcInvalidParams,
|
||||
JsonRpcMethodNotFound,
|
||||
JsonRpcRequestCancelled,
|
||||
FeatureNotificationError,
|
||||
FeatureRequestError,
|
||||
)
|
||||
from pygls.feature_manager import FeatureManager, is_thread_function
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
|
||||
from cattrs import Converter
|
||||
|
||||
from pygls.io_ import AsyncWriter, Writer
|
||||
from pygls.server import JsonRPCServer
|
||||
|
||||
MessageHandler = Union[Callable[[Any], Any],]
|
||||
MessageCallback = Callable[[Future[Any]], None]
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# cattrs needs access to this type definition so we cannot include it in the
|
||||
# TYPE_CHECKING block above
|
||||
MsgId = Union[str, int]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RPCNotification(Protocol):
|
||||
method: str
|
||||
jsonrpc: str
|
||||
params: Any
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RPCRequest(Protocol):
|
||||
id: MsgId
|
||||
method: str
|
||||
jsonrpc: str
|
||||
params: Any
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RPCResponse(Protocol):
|
||||
id: MsgId
|
||||
jsonrpc: str
|
||||
result: Any
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RPCError(Protocol):
|
||||
id: MsgId
|
||||
jsonrpc: str
|
||||
error: Any
|
||||
|
||||
|
||||
RPCMessage = Union[RPCNotification, RPCResponse, RPCRequest, RPCError]
|
||||
|
||||
|
||||
@attrs.define
|
||||
class JsonRPCNotification:
|
||||
@@ -81,7 +117,7 @@ class JsonRPCRequestMessage:
|
||||
Used as a fallback for unknown types.
|
||||
"""
|
||||
|
||||
id: Union[int, str]
|
||||
id: MsgId
|
||||
method: str
|
||||
jsonrpc: str
|
||||
params: Any
|
||||
@@ -93,13 +129,13 @@ class JsonRPCResponseMessage:
|
||||
Used as a fallback for unknown types.
|
||||
"""
|
||||
|
||||
id: Union[int, str]
|
||||
id: MsgId
|
||||
jsonrpc: str
|
||||
result: Any
|
||||
|
||||
|
||||
class JsonRPCProtocol(asyncio.Protocol):
|
||||
"""Json RPC protocol implementation using on top of `asyncio.Protocol`.
|
||||
class JsonRPCProtocol:
|
||||
"""Json RPC protocol implementation
|
||||
|
||||
Specification of the protocol can be found here:
|
||||
https://www.jsonrpc.org/specification
|
||||
@@ -109,86 +145,156 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
CHARSET = "utf-8"
|
||||
CONTENT_TYPE = "application/vscode-jsonrpc"
|
||||
|
||||
MESSAGE_PATTERN = re.compile(
|
||||
rb"^(?:[^\r\n]+\r\n)*"
|
||||
+ rb"Content-Length: (?P<length>\d+)\r\n"
|
||||
+ rb"(?:[^\r\n]+\r\n)*\r\n"
|
||||
+ rb"(?P<body>{.*)",
|
||||
re.DOTALL,
|
||||
)
|
||||
|
||||
VERSION = "2.0"
|
||||
|
||||
def __init__(self, server: LanguageServer, converter):
|
||||
def __init__(self, server: JsonRPCServer, converter: Converter):
|
||||
self._server = server
|
||||
self._converter = converter
|
||||
|
||||
self._shutdown = False
|
||||
|
||||
# Book keeping for in-flight requests
|
||||
self._request_futures: Dict[str, Future[Any]] = {}
|
||||
self._result_types: Dict[str, Any] = {}
|
||||
self._ctx_msg_id: contextvars.ContextVar[MsgId | None] = contextvars.ContextVar(
|
||||
"msg_id", default=None
|
||||
)
|
||||
self._request_futures: dict[MsgId, Future[Any]] = {}
|
||||
self._result_types: dict[MsgId, Any] = {}
|
||||
|
||||
self.fm = FeatureManager(server, converter)
|
||||
self.transport: Optional[
|
||||
Union[asyncio.WriteTransport, WebSocketTransportAdapter]
|
||||
] = None
|
||||
self._message_buf: List[bytes] = []
|
||||
|
||||
self._send_only_body = False
|
||||
self.writer: AsyncWriter | Writer | None = None
|
||||
self._include_headers = False
|
||||
|
||||
def __call__(self):
|
||||
return self
|
||||
|
||||
def _execute_notification(self, handler, *params):
|
||||
"""Executes notification message handler."""
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
future = asyncio.ensure_future(handler(*params))
|
||||
future.add_done_callback(self._execute_notification_callback)
|
||||
else:
|
||||
if is_thread_function(handler):
|
||||
self._server.thread_pool.apply_async(handler, (*params,))
|
||||
else:
|
||||
handler(*params)
|
||||
@property
|
||||
def msg_id(self) -> MsgId | None:
|
||||
"""Returns the id of the current context (if it exists)."""
|
||||
ctx = contextvars.copy_context()
|
||||
return ctx.get(self._ctx_msg_id)
|
||||
|
||||
def _execute_notification_callback(self, future):
|
||||
"""Success callback used for coroutine notification message."""
|
||||
if future.exception():
|
||||
try:
|
||||
raise future.exception()
|
||||
except Exception:
|
||||
error = JsonRpcInternalError.of(sys.exc_info())
|
||||
logger.exception('Exception occurred in notification: "%s"', error)
|
||||
def _execute_handler(
|
||||
self,
|
||||
msg_id: MsgId,
|
||||
handler: MessageHandler,
|
||||
callback: MessageCallback,
|
||||
args: tuple[Any, ...] | None = None,
|
||||
kwargs: dict[str, Any] | None = None,
|
||||
):
|
||||
"""Execute the given message handler.
|
||||
|
||||
# Revisit. Client does not support response with msg_id = None
|
||||
# https://stackoverflow.com/questions/31091376/json-rpc-2-0-allow-notifications-to-have-an-error-response
|
||||
# self._send_response(None, error=error)
|
||||
Parameters
|
||||
----------
|
||||
msg_id
|
||||
The id of the message being handled
|
||||
|
||||
def _execute_request(self, msg_id, handler, params):
|
||||
"""Executes request message handler."""
|
||||
handler
|
||||
The request handler to call
|
||||
|
||||
callback
|
||||
An optional callback function to call upon completion of the handler
|
||||
|
||||
args
|
||||
Positional arguments to pass to the handler
|
||||
|
||||
kwargs
|
||||
Keyword arguments to pass to the handler
|
||||
"""
|
||||
future: Future[Any]
|
||||
args = args or tuple()
|
||||
kwargs = kwargs or {}
|
||||
|
||||
if asyncio.iscoroutinefunction(handler):
|
||||
future = asyncio.ensure_future(handler(params))
|
||||
future = asyncio.ensure_future(handler(*args, **kwargs))
|
||||
self._request_futures[msg_id] = future
|
||||
future.add_done_callback(partial(self._execute_request_callback, msg_id))
|
||||
future.add_done_callback(callback)
|
||||
|
||||
elif is_thread_function(handler):
|
||||
future = self._server.thread_pool.submit(handler, *args, **kwargs)
|
||||
self._request_futures[msg_id] = future
|
||||
future.add_done_callback(callback)
|
||||
|
||||
elif inspect.isgeneratorfunction(handler):
|
||||
future = Future()
|
||||
self._request_futures[msg_id] = future
|
||||
future.add_done_callback(callback)
|
||||
|
||||
try:
|
||||
self._run_generator(
|
||||
future=None, gen=handler(*args, **kwargs), result_future=future
|
||||
)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
else:
|
||||
# Can't be canceled
|
||||
if is_thread_function(handler):
|
||||
self._server.thread_pool.apply_async(
|
||||
handler,
|
||||
(params,),
|
||||
callback=partial(
|
||||
self._send_response,
|
||||
msg_id,
|
||||
),
|
||||
error_callback=partial(self._execute_request_err_callback, msg_id),
|
||||
)
|
||||
else:
|
||||
self._send_response(msg_id, handler(params))
|
||||
# While a future is not necessary for a synchronous function, it allows us to use a single
|
||||
# pattern across all handler types
|
||||
future = Future()
|
||||
future.add_done_callback(callback)
|
||||
|
||||
try:
|
||||
result = handler(*args, **kwargs)
|
||||
future.set_result(result)
|
||||
except Exception as exc:
|
||||
future.set_exception(exc)
|
||||
|
||||
def _run_generator(
|
||||
self,
|
||||
future: Future[Any] | None,
|
||||
*,
|
||||
gen: Generator[Any, Any, Any],
|
||||
result_future: Future[Any],
|
||||
):
|
||||
"""Run the next portion of the given generator.
|
||||
|
||||
Generator handlers are designed to ``yield`` to other handlers that are executed
|
||||
separately before their results are sent back into the generator allowing
|
||||
execution to continue.
|
||||
|
||||
Generator handlers are primarily used in the implementation of pygls' builtin
|
||||
feature handlers.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
future
|
||||
The future that contains the result of the previously executed handler, if any
|
||||
|
||||
gen
|
||||
The generator to run
|
||||
|
||||
result_future
|
||||
The future to send the final result to once the generator stops.
|
||||
"""
|
||||
|
||||
if result_future.cancelled():
|
||||
return
|
||||
|
||||
try:
|
||||
value = future.result() if future is not None else None
|
||||
handler, args, kwargs = gen.send(value)
|
||||
|
||||
self._execute_handler(
|
||||
str(uuid.uuid4()),
|
||||
handler,
|
||||
args=args,
|
||||
kwargs=kwargs,
|
||||
callback=partial(
|
||||
self._run_generator, gen=gen, result_future=result_future
|
||||
),
|
||||
)
|
||||
except StopIteration as result:
|
||||
result_future.set_result(result.value)
|
||||
|
||||
except Exception as exc:
|
||||
result_future.set_exception(exc)
|
||||
|
||||
def _send_handler_result(self, future: Future[Any], *, msg_id: MsgId):
|
||||
"""Callback function that sends the result of the given future to the client.
|
||||
|
||||
Used to respond to request messages.
|
||||
"""
|
||||
self._request_futures.pop(msg_id, None)
|
||||
|
||||
def _execute_request_callback(self, msg_id, future):
|
||||
"""Success callback used for coroutine request message."""
|
||||
try:
|
||||
if not future.cancelled():
|
||||
self._send_response(msg_id, result=future.result())
|
||||
@@ -199,30 +305,41 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
f'Request with id "{msg_id}" is canceled'
|
||||
).to_response_error(),
|
||||
)
|
||||
self._request_futures.pop(msg_id, None)
|
||||
except JsonRpcException as exc:
|
||||
logger.exception('Exception occurred for message "%s"', msg_id)
|
||||
self._send_response(msg_id, error=exc.to_response_error())
|
||||
self._server._report_server_error(exc, FeatureRequestError)
|
||||
|
||||
except Exception:
|
||||
error = JsonRpcInternalError.of(sys.exc_info())
|
||||
logger.exception('Exception occurred for message "%s": %s', msg_id, error)
|
||||
logger.exception('Exception occurred for message "%s"', msg_id)
|
||||
self._send_response(msg_id, error=error.to_response_error())
|
||||
self._server._report_server_error(error, FeatureRequestError)
|
||||
|
||||
def _execute_request_err_callback(self, msg_id, exc):
|
||||
"""Error callback used for coroutine request message."""
|
||||
exc_info = (type(exc), exc, None)
|
||||
error = JsonRpcInternalError.of(exc_info)
|
||||
logger.exception('Exception occurred for message "%s": %s', msg_id, error)
|
||||
self._send_response(msg_id, error=error.to_response_error())
|
||||
def _check_handler_result(self, future: Future[Any]):
|
||||
"""Check the result of the future to see if an error occurred.
|
||||
|
||||
def _get_handler(self, feature_name):
|
||||
"""Returns builtin or used defined feature by name if exists."""
|
||||
try:
|
||||
return self.fm.builtin_features[feature_name]
|
||||
except KeyError:
|
||||
Used when handling notification messages
|
||||
"""
|
||||
if not future.cancelled() and (exc := future.exception()) is not None:
|
||||
try:
|
||||
return self.fm.features[feature_name]
|
||||
except KeyError:
|
||||
raise JsonRpcMethodNotFound.of(feature_name)
|
||||
raise exc
|
||||
except Exception:
|
||||
error = JsonRpcInternalError.of(sys.exc_info())
|
||||
self._server._report_server_error(error, FeatureNotificationError)
|
||||
|
||||
def _handle_cancel_notification(self, msg_id):
|
||||
def _get_handler(self, feature_name: str) -> MessageHandler:
|
||||
"""Returns builtin or used defined feature by name if exists."""
|
||||
|
||||
if (handler := self.fm.builtin_features.get(feature_name)) is not None:
|
||||
return handler
|
||||
|
||||
if (handler := self.fm.features.get(feature_name)) is not None:
|
||||
return handler
|
||||
|
||||
raise JsonRpcMethodNotFound.of(feature_name)
|
||||
|
||||
def _handle_cancel_notification(self, msg_id: MsgId):
|
||||
"""Handles a cancel notification from the client."""
|
||||
future = self._request_futures.pop(msg_id, None)
|
||||
|
||||
@@ -234,7 +351,7 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
if future.cancel():
|
||||
logger.info('Cancelled request with id "%s"', msg_id)
|
||||
|
||||
def _handle_notification(self, method_name, params):
|
||||
def _handle_notification(self, method_name: str, params: Any):
|
||||
"""Handles a notification from the client."""
|
||||
if method_name == CANCEL_REQUEST:
|
||||
self._handle_cancel_notification(params.id)
|
||||
@@ -242,29 +359,45 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
try:
|
||||
handler = self._get_handler(method_name)
|
||||
self._execute_notification(handler, params)
|
||||
except (KeyError, JsonRpcMethodNotFound):
|
||||
logger.warning('Ignoring notification for unknown method "%s"', method_name)
|
||||
self._execute_handler(
|
||||
msg_id=str(uuid.uuid4()),
|
||||
handler=handler,
|
||||
args=(params,),
|
||||
callback=self._check_handler_result,
|
||||
)
|
||||
except JsonRpcMethodNotFound:
|
||||
logger.warning("Ignoring notification for unknown method %r", method_name)
|
||||
except Exception as error:
|
||||
logger.exception(
|
||||
'Failed to handle notification "%s": %s',
|
||||
"Failed to handle notification %r: %s",
|
||||
method_name,
|
||||
params,
|
||||
exc_info=True,
|
||||
)
|
||||
self._server._report_server_error(error, FeatureNotificationError)
|
||||
|
||||
def _handle_request(self, msg_id, method_name, params):
|
||||
def _handle_request(self, msg_id: MsgId, method_name: str, params: Any):
|
||||
"""Handles a request from the client."""
|
||||
try:
|
||||
handler = self._get_handler(method_name)
|
||||
|
||||
# workspace/executeCommand is a special case
|
||||
if method_name == WORKSPACE_EXECUTE_COMMAND:
|
||||
handler(params, msg_id)
|
||||
else:
|
||||
self._execute_request(msg_id, handler, params)
|
||||
# Set the request id within the current context.
|
||||
self._ctx_msg_id.set(msg_id)
|
||||
self._execute_handler(
|
||||
msg_id=msg_id,
|
||||
handler=handler,
|
||||
args=(params,),
|
||||
callback=partial(self._send_handler_result, msg_id=msg_id),
|
||||
)
|
||||
|
||||
except JsonRpcMethodNotFound as error:
|
||||
logger.warning(
|
||||
"Failed to handle request %r, unknown method %r",
|
||||
msg_id,
|
||||
method_name,
|
||||
)
|
||||
self._send_response(msg_id, None, error.to_response_error())
|
||||
self._server._report_server_error(error, FeatureRequestError)
|
||||
except JsonRpcException as error:
|
||||
logger.exception(
|
||||
"Failed to handle request %s %s %s",
|
||||
@@ -287,7 +420,12 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
self._send_response(msg_id, None, err)
|
||||
self._server._report_server_error(error, FeatureRequestError)
|
||||
|
||||
def _handle_response(self, msg_id, result=None, error=None):
|
||||
def _handle_response(
|
||||
self,
|
||||
msg_id: MsgId,
|
||||
result: Any | None = None,
|
||||
error: ResponseError | None = None,
|
||||
):
|
||||
"""Handles a response from the client."""
|
||||
future = self._request_futures.pop(msg_id, None)
|
||||
|
||||
@@ -302,7 +440,7 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
logger.debug('Received result for message "%s": %s', msg_id, result)
|
||||
future.set_result(result)
|
||||
|
||||
def _serialize_message(self, data):
|
||||
def _serialize_message(self, data: Any) -> dict[str, Any]:
|
||||
"""Function used to serialize data sent to the client."""
|
||||
|
||||
if hasattr(data, "__attrs_attrs__"):
|
||||
@@ -313,7 +451,7 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
return data.__dict__
|
||||
|
||||
def _deserialize_message(self, data):
|
||||
def structure_message(self, data: dict[str, Any]):
|
||||
"""Function used to deserialize data recevied from the client."""
|
||||
|
||||
if "jsonrpc" not in data:
|
||||
@@ -330,7 +468,8 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
return self._converter.structure(data, request_type)
|
||||
else:
|
||||
response_type = (
|
||||
self._result_types.pop(data["id"]) or JsonRPCResponseMessage
|
||||
self._result_types.pop(data["id"], None)
|
||||
or JsonRPCResponseMessage
|
||||
)
|
||||
return self._converter.structure(data, response_type)
|
||||
|
||||
@@ -347,7 +486,7 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
logger.error("Unable to deserialize message\n%s", traceback.format_exc())
|
||||
raise JsonRpcInternalError() from exc
|
||||
|
||||
def _procedure_handler(self, message):
|
||||
def handle_message(self, message: RPCMessage):
|
||||
"""Delegates message to handlers depending on message type."""
|
||||
|
||||
if message.jsonrpc != JsonRPCProtocol.VERSION:
|
||||
@@ -358,27 +497,31 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
logger.warning("Server shutting down. No more requests!")
|
||||
return
|
||||
|
||||
if hasattr(message, "method"):
|
||||
if hasattr(message, "id"):
|
||||
logger.debug("Request message received.")
|
||||
self._handle_request(message.id, message.method, message.params)
|
||||
else:
|
||||
logger.debug("Notification message received.")
|
||||
self._handle_notification(message.method, message.params)
|
||||
else:
|
||||
if hasattr(message, "error"):
|
||||
logger.debug("Error message received.")
|
||||
self._handle_response(message.id, None, message.error)
|
||||
else:
|
||||
logger.debug("Response message received.")
|
||||
self._handle_response(message.id, message.result)
|
||||
# Run each handler within its own context.
|
||||
ctx = contextvars.copy_context()
|
||||
|
||||
def _send_data(self, data):
|
||||
if isinstance(message, RPCRequest):
|
||||
logger.debug("Request %r received", message.method)
|
||||
ctx.run(self._handle_request, message.id, message.method, message.params)
|
||||
|
||||
elif isinstance(message, RPCNotification):
|
||||
logger.debug("Notification %r received", message.method)
|
||||
ctx.run(self._handle_notification, message.method, message.params)
|
||||
|
||||
elif isinstance(message, RPCResponse):
|
||||
logger.debug("Response message received.")
|
||||
ctx.run(self._handle_response, message.id, message.result)
|
||||
|
||||
else:
|
||||
logger.debug("Error message received.")
|
||||
ctx.run(self._handle_response, message.id, None, message.error)
|
||||
|
||||
def _send_data(self, data: Any):
|
||||
"""Sends data to the client."""
|
||||
if not data:
|
||||
return
|
||||
|
||||
if self.transport is None:
|
||||
if self.writer is None:
|
||||
logger.error("Unable to send data, no available transport!")
|
||||
return
|
||||
|
||||
@@ -386,31 +529,49 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
body = json.dumps(data, default=self._serialize_message)
|
||||
logger.info("Sending data: %s", body)
|
||||
|
||||
if self._send_only_body:
|
||||
# Mypy/Pyright seem to think `write()` wants `"bytes | bytearray | memoryview"`
|
||||
# But runtime errors with anything but `str`.
|
||||
self.transport.write(body) # type: ignore
|
||||
return
|
||||
if self._include_headers:
|
||||
header = (
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
|
||||
)
|
||||
data = header + body
|
||||
else:
|
||||
data = body
|
||||
|
||||
header = (
|
||||
f"Content-Length: {len(body)}\r\n"
|
||||
f"Content-Type: {self.CONTENT_TYPE}; charset={self.CHARSET}\r\n\r\n"
|
||||
).encode(self.CHARSET)
|
||||
res = self.writer.write(data.encode(self.CHARSET))
|
||||
if inspect.isawaitable(res):
|
||||
asyncio.ensure_future(res)
|
||||
|
||||
self.transport.write(header + body.encode(self.CHARSET))
|
||||
except BrokenPipeError:
|
||||
logger.exception("Error sending data. BrokenPipeError", exc_info=True)
|
||||
raise
|
||||
except Exception as error:
|
||||
logger.exception("Error sending data", exc_info=True)
|
||||
self._server._report_server_error(error, JsonRpcInternalError)
|
||||
|
||||
def _send_response(
|
||||
self, msg_id, result=None, error: Union[ResponseError, None] = None
|
||||
self,
|
||||
msg_id: MsgId,
|
||||
result: Any | None = None,
|
||||
error: Union[ResponseError, None] = None,
|
||||
):
|
||||
"""Sends a JSON RPC response to the client.
|
||||
"""Send a JSON-RPC response
|
||||
|
||||
Args:
|
||||
msg_id(str): Id from request
|
||||
result(any): Result returned by handler
|
||||
error(any): Error returned by handler
|
||||
.. important::
|
||||
|
||||
You should only set ``result`` OR ``error``.
|
||||
If both are set, then the ``result`` value will be ignored.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
msg_id
|
||||
The id of the message to respond to
|
||||
|
||||
result
|
||||
The result to send in the event of a success
|
||||
|
||||
error
|
||||
The error to send in the event of a failure
|
||||
"""
|
||||
|
||||
if error is not None:
|
||||
@@ -424,70 +585,51 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
self._send_data(response)
|
||||
|
||||
def connection_lost(self, exc):
|
||||
"""Method from base class, called when connection is lost, in which case we
|
||||
want to shutdown the server's process as well.
|
||||
"""
|
||||
logger.error("Connection to the client is lost! Shutting down the server.")
|
||||
sys.exit(1)
|
||||
|
||||
def connection_made( # type: ignore # see: https://github.com/python/typeshed/issues/3021
|
||||
def set_writer(
|
||||
self,
|
||||
transport: asyncio.Transport,
|
||||
writer: AsyncWriter | Writer,
|
||||
include_headers: bool = True,
|
||||
):
|
||||
"""Method from base class, called when connection is established"""
|
||||
self.transport = transport
|
||||
"""Set the writer object to use when sending data
|
||||
|
||||
def data_received(self, data: bytes):
|
||||
try:
|
||||
self._data_received(data)
|
||||
except Exception as error:
|
||||
logger.exception("Error receiving data", exc_info=True)
|
||||
self._server._report_server_error(error, JsonRpcInternalError)
|
||||
Parameters
|
||||
----------
|
||||
writer
|
||||
The writer object
|
||||
|
||||
def _data_received(self, data: bytes):
|
||||
"""Method from base class, called when server receives the data"""
|
||||
logger.debug("Received %r", data)
|
||||
include_headers
|
||||
Flag indicating if headers like ``Content-Length`` should be included when
|
||||
sending data. (Default ``True``)
|
||||
"""
|
||||
self.writer = writer
|
||||
self._include_headers = include_headers
|
||||
|
||||
while len(data):
|
||||
# Append the incoming chunk to the message buffer
|
||||
self._message_buf.append(data)
|
||||
|
||||
# Look for the body of the message
|
||||
message = b"".join(self._message_buf)
|
||||
found = JsonRPCProtocol.MESSAGE_PATTERN.fullmatch(message)
|
||||
|
||||
body = found.group("body") if found else b""
|
||||
length = int(found.group("length")) if found else 1
|
||||
|
||||
if len(body) < length:
|
||||
# Message is incomplete; bail until more data arrives
|
||||
return
|
||||
|
||||
# Message is complete;
|
||||
# extract the body and any remaining data,
|
||||
# and reset the buffer for the next message
|
||||
body, data = body[:length], body[length:]
|
||||
self._message_buf = []
|
||||
|
||||
# Parse the body
|
||||
self._procedure_handler(
|
||||
json.loads(
|
||||
body.decode(self.CHARSET), object_hook=self._deserialize_message
|
||||
)
|
||||
)
|
||||
|
||||
def get_message_type(self, method: str) -> Optional[Type]:
|
||||
def get_message_type(self, method: str) -> Type[Any] | None:
|
||||
"""Return the type definition of the message associated with the given method."""
|
||||
return None
|
||||
|
||||
def get_result_type(self, method: str) -> Optional[Type]:
|
||||
def get_result_type(self, method: str) -> Type[Any] | None:
|
||||
"""Return the type definition of the result associated with the given method."""
|
||||
return None
|
||||
|
||||
def notify(self, method: str, params=None):
|
||||
"""Sends a JSON RPC notification to the client."""
|
||||
def notify(self, method: str, params: Any | None = None):
|
||||
"""Send a JSON-RPC notification.
|
||||
|
||||
.. note::
|
||||
|
||||
Notifications are "fire-and-forget", there is no way for the recipient to
|
||||
respond directly to a notification. If you expect a response to this message,
|
||||
use ``send_request``.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
method
|
||||
The method name of the message to send
|
||||
|
||||
params
|
||||
The payload of the message
|
||||
|
||||
"""
|
||||
logger.debug("Sending notification: '%s' %s", method, params)
|
||||
|
||||
notification_type = self.get_message_type(method) or JsonRPCNotification
|
||||
@@ -497,15 +639,35 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
self._send_data(notification)
|
||||
|
||||
def send_request(self, method, params=None, callback=None, msg_id=None):
|
||||
"""Sends a JSON RPC request to the client.
|
||||
def send_request(
|
||||
self,
|
||||
method: str,
|
||||
params: Any | None = None,
|
||||
callback: Callable[[Any], None] | None = None,
|
||||
msg_id: MsgId | None = None,
|
||||
) -> Future[Any]:
|
||||
"""Send a JSON-RPC request
|
||||
|
||||
Args:
|
||||
method(str): The method name of the message to send
|
||||
params(any): The payload of the message
|
||||
Parameters
|
||||
----------
|
||||
method
|
||||
The method name of the message to send
|
||||
|
||||
Returns:
|
||||
Future that will be resolved once a response has been received
|
||||
params
|
||||
The payload of the message
|
||||
|
||||
callback
|
||||
If set, the given callback will be called with the result of the future
|
||||
when it resolves
|
||||
|
||||
msg_id
|
||||
Send the request using the given id, if ``None``, an id will be automatically
|
||||
generated
|
||||
|
||||
Returns
|
||||
-------
|
||||
Future[Any]
|
||||
A future that will resolve once a response has been received
|
||||
"""
|
||||
|
||||
if msg_id is None:
|
||||
@@ -521,12 +683,12 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
jsonrpc=JsonRPCProtocol.VERSION,
|
||||
)
|
||||
|
||||
future = Future() # type: ignore[var-annotated]
|
||||
future: Future[Any] = Future()
|
||||
# If callback function is given, call it when result is received
|
||||
if callback:
|
||||
|
||||
def wrapper(future: Future):
|
||||
result = future.result()
|
||||
def wrapper(fut: Future[Any]):
|
||||
result = fut.result()
|
||||
logger.info("Client response for %s received: %s", params, result)
|
||||
callback(result)
|
||||
|
||||
@@ -539,22 +701,35 @@ class JsonRPCProtocol(asyncio.Protocol):
|
||||
|
||||
return future
|
||||
|
||||
def send_request_async(self, method, params=None, msg_id=None):
|
||||
"""Calls `send_request` and wraps `concurrent.futures.Future` with
|
||||
`asyncio.Future` so it can be used with `await` keyword.
|
||||
def send_request_async(
|
||||
self, method: str, params: Any | None = None, msg_id: MsgId | None = None
|
||||
):
|
||||
"""Send a JSON-RPC request, asynchronously.
|
||||
|
||||
Args:
|
||||
method(str): The method name of the message to send
|
||||
params(any): The payload of the message
|
||||
msg_id(str|int): Optional, message id
|
||||
This method calls `send_request`, wrapping the resulting future with
|
||||
``asyncio.wrap_future`` so it can be used in an ``async def`` function and
|
||||
awaited with the ``await`` keyword.
|
||||
|
||||
Returns:
|
||||
`asyncio.Future` that can be awaited
|
||||
Parameters
|
||||
----------
|
||||
method
|
||||
The method name of the message to send
|
||||
|
||||
params
|
||||
The payload of the message
|
||||
|
||||
callback
|
||||
If set, the given callback will be called with the result of the future
|
||||
when it resolves
|
||||
|
||||
msg_id
|
||||
Send the request using the given id, if ``None``, an id will be automatically
|
||||
generated
|
||||
|
||||
Returns
|
||||
-------
|
||||
`asyncio.Future` that can be awaited
|
||||
"""
|
||||
return asyncio.wrap_future(
|
||||
self.send_request(method, params=params, msg_id=msg_id)
|
||||
)
|
||||
|
||||
def thread(self):
|
||||
"""Decorator that mark function to execute it in a thread."""
|
||||
return self.fm.thread()
|
||||
|
||||
@@ -15,88 +15,34 @@
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import json
|
||||
import logging
|
||||
import sys
|
||||
from concurrent.futures import Future
|
||||
import typing
|
||||
from functools import lru_cache
|
||||
from itertools import zip_longest
|
||||
from typing import (
|
||||
Callable,
|
||||
List,
|
||||
Optional,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.capabilities import ServerCapabilitiesBuilder
|
||||
from pygls.lsp import ConfigCallbackType, ShowDocumentCallbackType
|
||||
from lsprotocol.types import (
|
||||
CLIENT_REGISTER_CAPABILITY,
|
||||
CLIENT_UNREGISTER_CAPABILITY,
|
||||
EXIT,
|
||||
INITIALIZE,
|
||||
INITIALIZED,
|
||||
METHOD_TO_TYPES,
|
||||
NOTEBOOK_DOCUMENT_DID_CHANGE,
|
||||
NOTEBOOK_DOCUMENT_DID_CLOSE,
|
||||
NOTEBOOK_DOCUMENT_DID_OPEN,
|
||||
LOG_TRACE,
|
||||
SET_TRACE,
|
||||
SHUTDOWN,
|
||||
TEXT_DOCUMENT_DID_CHANGE,
|
||||
TEXT_DOCUMENT_DID_CLOSE,
|
||||
TEXT_DOCUMENT_DID_OPEN,
|
||||
TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS,
|
||||
WINDOW_LOG_MESSAGE,
|
||||
WINDOW_SHOW_DOCUMENT,
|
||||
WINDOW_SHOW_MESSAGE,
|
||||
WINDOW_WORK_DONE_PROGRESS_CANCEL,
|
||||
WORKSPACE_APPLY_EDIT,
|
||||
WORKSPACE_CONFIGURATION,
|
||||
WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS,
|
||||
WORKSPACE_EXECUTE_COMMAND,
|
||||
WORKSPACE_SEMANTIC_TOKENS_REFRESH,
|
||||
)
|
||||
from lsprotocol.types import (
|
||||
ApplyWorkspaceEditParams,
|
||||
Diagnostic,
|
||||
DidChangeNotebookDocumentParams,
|
||||
DidChangeTextDocumentParams,
|
||||
DidChangeWorkspaceFoldersParams,
|
||||
DidCloseNotebookDocumentParams,
|
||||
DidCloseTextDocumentParams,
|
||||
DidOpenNotebookDocumentParams,
|
||||
DidOpenTextDocumentParams,
|
||||
ExecuteCommandParams,
|
||||
InitializeParams,
|
||||
InitializeResult,
|
||||
LogMessageParams,
|
||||
LogTraceParams,
|
||||
MessageType,
|
||||
PublishDiagnosticsParams,
|
||||
RegistrationParams,
|
||||
SetTraceParams,
|
||||
ShowDocumentParams,
|
||||
ShowMessageParams,
|
||||
TraceValues,
|
||||
UnregistrationParams,
|
||||
WorkspaceApplyEditResponse,
|
||||
WorkspaceEdit,
|
||||
InitializeResultServerInfoType,
|
||||
WorkspaceConfigurationParams,
|
||||
WorkDoneProgressCancelParams,
|
||||
)
|
||||
from pygls.constants import PARAM_LS
|
||||
from pygls.exceptions import JsonRpcInvalidParams
|
||||
from pygls.protocol.json_rpc import JsonRPCProtocol
|
||||
from pygls.protocol.lsp_meta import LSPMeta
|
||||
from pygls.uris import from_fs_path
|
||||
from pygls.workspace import Workspace
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from collections.abc import Generator
|
||||
from typing import Any, Callable, Optional, Type, TypeVar
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
from cattrs import Converter
|
||||
|
||||
from pygls.lsp.server import LanguageServer
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -109,7 +55,7 @@ def lsp_method(method_name: str) -> Callable[[F], F]:
|
||||
return decorator
|
||||
|
||||
|
||||
class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
class LanguageServerProtocol(JsonRPCProtocol):
|
||||
"""A class that represents language server protocol.
|
||||
|
||||
It contains implementations for generic LSP features.
|
||||
@@ -118,17 +64,19 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
workspace(Workspace): In memory workspace
|
||||
"""
|
||||
|
||||
def __init__(self, server, converter):
|
||||
_server: LanguageServer
|
||||
|
||||
def __init__(self, server: LanguageServer, converter: Converter):
|
||||
super().__init__(server, converter)
|
||||
|
||||
self._workspace: Optional[Workspace] = None
|
||||
self.trace = None
|
||||
self.trace = types.TraceValue.Off
|
||||
|
||||
from pygls.progress import Progress
|
||||
|
||||
self.progress = Progress(self)
|
||||
|
||||
self.server_info = InitializeResultServerInfoType(
|
||||
self.server_info = types.ServerInfo(
|
||||
name=server.name,
|
||||
version=server.version,
|
||||
)
|
||||
@@ -155,40 +103,38 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
return self._workspace
|
||||
|
||||
@lru_cache()
|
||||
def get_message_type(self, method: str) -> Optional[Type]:
|
||||
def get_message_type(self, method: str) -> Type[Any] | None:
|
||||
"""Return LSP type definitions, as provided by `lsprotocol`"""
|
||||
return METHOD_TO_TYPES.get(method, (None,))[0]
|
||||
return types.METHOD_TO_TYPES.get(method, (None,))[0]
|
||||
|
||||
@lru_cache()
|
||||
def get_result_type(self, method: str) -> Optional[Type]:
|
||||
return METHOD_TO_TYPES.get(method, (None, None))[1]
|
||||
def get_result_type(self, method: str) -> Type[Any] | None:
|
||||
return types.METHOD_TO_TYPES.get(method, (None, None))[1]
|
||||
|
||||
def apply_edit(
|
||||
self, edit: WorkspaceEdit, label: Optional[str] = None
|
||||
) -> WorkspaceApplyEditResponse:
|
||||
"""Sends apply edit request to the client."""
|
||||
return self.send_request(
|
||||
WORKSPACE_APPLY_EDIT, ApplyWorkspaceEditParams(edit=edit, label=label)
|
||||
)
|
||||
|
||||
def apply_edit_async(
|
||||
self, edit: WorkspaceEdit, label: Optional[str] = None
|
||||
) -> WorkspaceApplyEditResponse:
|
||||
"""Sends apply edit request to the client. Should be called with `await`"""
|
||||
return self.send_request_async(
|
||||
WORKSPACE_APPLY_EDIT, ApplyWorkspaceEditParams(edit=edit, label=label)
|
||||
)
|
||||
|
||||
@lsp_method(EXIT)
|
||||
def lsp_exit(self, *args) -> None:
|
||||
@lsp_method(types.EXIT)
|
||||
def lsp_exit(self, *args) -> Generator[Any, Any, None]:
|
||||
"""Stops the server process."""
|
||||
if self.transport is not None:
|
||||
self.transport.close()
|
||||
|
||||
sys.exit(0 if self._shutdown else 1)
|
||||
# Ensure that the user handler is called first
|
||||
if (user_handler := self.fm.features.get(types.EXIT)) is not None:
|
||||
yield user_handler, args, None
|
||||
|
||||
@lsp_method(INITIALIZE)
|
||||
def lsp_initialize(self, params: InitializeParams) -> InitializeResult:
|
||||
returncode = 0 if self._shutdown else 1
|
||||
if self.writer is None:
|
||||
sys.exit(returncode)
|
||||
|
||||
res = self.writer.close()
|
||||
if inspect.isawaitable(res):
|
||||
# Only call sys.exit once the close task has completed.
|
||||
fut = asyncio.ensure_future(res)
|
||||
fut.add_done_callback(lambda t: sys.exit(returncode))
|
||||
else:
|
||||
sys.exit(returncode)
|
||||
|
||||
@lsp_method(types.INITIALIZE)
|
||||
def lsp_initialize(
|
||||
self, params: types.InitializeParams
|
||||
) -> Generator[Any, Any, types.InitializeResult]:
|
||||
"""Method that initializes language server.
|
||||
It will compute and return server capabilities based on
|
||||
registered features.
|
||||
@@ -200,19 +146,9 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
text_document_sync_kind = self._server._text_document_sync_kind
|
||||
notebook_document_sync = self._server._notebook_document_sync
|
||||
|
||||
# Initialize server capabilities
|
||||
self.client_capabilities = params.capabilities
|
||||
self.server_capabilities = ServerCapabilitiesBuilder(
|
||||
self.client_capabilities,
|
||||
set({**self.fm.features, **self.fm.builtin_features}.keys()),
|
||||
self.fm.feature_options,
|
||||
list(self.fm.commands.keys()),
|
||||
text_document_sync_kind,
|
||||
notebook_document_sync,
|
||||
).build()
|
||||
logger.debug(
|
||||
"Server capabilities: %s",
|
||||
json.dumps(self.server_capabilities, default=self._serialize_message),
|
||||
position_encoding = ServerCapabilitiesBuilder.choose_position_encoding(
|
||||
self.client_capabilities
|
||||
)
|
||||
|
||||
root_path = params.root_path
|
||||
@@ -220,86 +156,144 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
if root_path is not None and root_uri is None:
|
||||
root_uri = from_fs_path(root_path)
|
||||
|
||||
# Initialize the workspace
|
||||
# Initialize the workspace before yielding to the user's initialize handler
|
||||
workspace_folders = params.workspace_folders or []
|
||||
self._workspace = Workspace(
|
||||
root_uri,
|
||||
text_document_sync_kind,
|
||||
workspace_folders,
|
||||
self.server_capabilities.position_encoding,
|
||||
position_encoding,
|
||||
)
|
||||
|
||||
self.trace = TraceValues.Off
|
||||
if (user_handler := self.fm.features.get(types.INITIALIZE)) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
return InitializeResult(
|
||||
# Now that the user has had the opportunity to setup additional features, calculate
|
||||
# the server's capabilities
|
||||
self.server_capabilities = ServerCapabilitiesBuilder(
|
||||
self.client_capabilities,
|
||||
set({**self.fm.features, **self.fm.builtin_features}.keys()),
|
||||
self.fm.feature_options,
|
||||
list(self.fm.commands.keys()),
|
||||
text_document_sync_kind,
|
||||
notebook_document_sync,
|
||||
position_encoding,
|
||||
).build()
|
||||
logger.debug(
|
||||
"Server capabilities: %s",
|
||||
json.dumps(self.server_capabilities, default=self._serialize_message),
|
||||
)
|
||||
|
||||
return types.InitializeResult(
|
||||
capabilities=self.server_capabilities,
|
||||
server_info=self.server_info,
|
||||
)
|
||||
|
||||
@lsp_method(INITIALIZED)
|
||||
def lsp_initialized(self, *args) -> None:
|
||||
@lsp_method(types.INITIALIZED)
|
||||
def lsp_initialized(self, *args):
|
||||
"""Notification received when client and server are connected."""
|
||||
pass
|
||||
|
||||
@lsp_method(SHUTDOWN)
|
||||
def lsp_shutdown(self, *args) -> None:
|
||||
if (user_handler := self.fm.features.get(types.INITIALIZED)) is not None:
|
||||
yield user_handler, args, None
|
||||
|
||||
@lsp_method(types.SHUTDOWN)
|
||||
def lsp_shutdown(self, *args) -> Generator[Any, Any, None]:
|
||||
"""Request from client which asks server to shutdown."""
|
||||
for future in self._request_futures.values():
|
||||
future.cancel()
|
||||
|
||||
if (user_handler := self.fm.features.get(types.SHUTDOWN)) is not None:
|
||||
yield user_handler, args, None
|
||||
|
||||
# Don't cancel the future for this request!
|
||||
current_id = self.msg_id
|
||||
|
||||
for msg_id, future in self._request_futures.items():
|
||||
if msg_id != current_id and not future.done():
|
||||
future.cancel()
|
||||
|
||||
self._shutdown = True
|
||||
return None
|
||||
|
||||
@lsp_method(TEXT_DOCUMENT_DID_CHANGE)
|
||||
def lsp_text_document__did_change(
|
||||
self, params: DidChangeTextDocumentParams
|
||||
) -> None:
|
||||
@lsp_method(types.TEXT_DOCUMENT_DID_CHANGE)
|
||||
def lsp_text_document__did_change(self, params: types.DidChangeTextDocumentParams):
|
||||
"""Updates document's content.
|
||||
(Incremental(from server capabilities); not configurable for now)
|
||||
"""
|
||||
for change in params.content_changes:
|
||||
self.workspace.update_text_document(params.text_document, change)
|
||||
|
||||
@lsp_method(TEXT_DOCUMENT_DID_CLOSE)
|
||||
def lsp_text_document__did_close(self, params: DidCloseTextDocumentParams) -> None:
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.TEXT_DOCUMENT_DID_CHANGE)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.TEXT_DOCUMENT_DID_CLOSE)
|
||||
def lsp_text_document__did_close(self, params: types.DidCloseTextDocumentParams):
|
||||
"""Removes document from workspace."""
|
||||
self.workspace.remove_text_document(params.text_document.uri)
|
||||
|
||||
@lsp_method(TEXT_DOCUMENT_DID_OPEN)
|
||||
def lsp_text_document__did_open(self, params: DidOpenTextDocumentParams) -> None:
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.TEXT_DOCUMENT_DID_CLOSE)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.TEXT_DOCUMENT_DID_OPEN)
|
||||
def lsp_text_document__did_open(self, params: types.DidOpenTextDocumentParams):
|
||||
"""Puts document to the workspace."""
|
||||
self.workspace.put_text_document(params.text_document)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_OPEN)
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.TEXT_DOCUMENT_DID_OPEN)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.NOTEBOOK_DOCUMENT_DID_OPEN)
|
||||
def lsp_notebook_document__did_open(
|
||||
self, params: DidOpenNotebookDocumentParams
|
||||
) -> None:
|
||||
self, params: types.DidOpenNotebookDocumentParams
|
||||
):
|
||||
"""Put a notebook document into the workspace"""
|
||||
self.workspace.put_notebook_document(params)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_CHANGE)
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.NOTEBOOK_DOCUMENT_DID_OPEN)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.NOTEBOOK_DOCUMENT_DID_CHANGE)
|
||||
def lsp_notebook_document__did_change(
|
||||
self, params: DidChangeNotebookDocumentParams
|
||||
) -> None:
|
||||
self, params: types.DidChangeNotebookDocumentParams
|
||||
):
|
||||
"""Update a notebook's contents"""
|
||||
self.workspace.update_notebook_document(params)
|
||||
|
||||
@lsp_method(NOTEBOOK_DOCUMENT_DID_CLOSE)
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.NOTEBOOK_DOCUMENT_DID_CHANGE)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.NOTEBOOK_DOCUMENT_DID_CLOSE)
|
||||
def lsp_notebook_document__did_close(
|
||||
self, params: DidCloseNotebookDocumentParams
|
||||
) -> None:
|
||||
self, params: types.DidCloseNotebookDocumentParams
|
||||
):
|
||||
"""Remove a notebook document from the workspace."""
|
||||
self.workspace.remove_notebook_document(params)
|
||||
|
||||
@lsp_method(SET_TRACE)
|
||||
def lsp_set_trace(self, params: SetTraceParams) -> None:
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.NOTEBOOK_DOCUMENT_DID_CLOSE)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.SET_TRACE)
|
||||
def lsp_set_trace(self, params: types.SetTraceParams) -> Generator[Any, Any, None]:
|
||||
"""Changes server trace value."""
|
||||
self.trace = params.value
|
||||
|
||||
@lsp_method(WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS)
|
||||
if (user_handler := self.fm.features.get(types.SET_TRACE)) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(types.WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS)
|
||||
def lsp_workspace__did_change_workspace_folders(
|
||||
self, params: DidChangeWorkspaceFoldersParams
|
||||
) -> None:
|
||||
self, params: types.DidChangeWorkspaceFoldersParams
|
||||
):
|
||||
"""Adds/Removes folders from the workspace."""
|
||||
logger.info("Workspace folders changed: %s", params)
|
||||
|
||||
@@ -312,18 +306,35 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
if f_remove:
|
||||
self.workspace.remove_folder(f_remove.uri)
|
||||
|
||||
@lsp_method(WORKSPACE_EXECUTE_COMMAND)
|
||||
def lsp_workspace__execute_command(
|
||||
self, params: ExecuteCommandParams, msg_id: str
|
||||
) -> None:
|
||||
"""Executes commands with passed arguments and returns a value."""
|
||||
cmd_handler = self.fm.commands[params.command]
|
||||
self._execute_request(msg_id, cmd_handler, params.arguments)
|
||||
if (
|
||||
user_handler := self.fm.features.get(
|
||||
types.WORKSPACE_DID_CHANGE_WORKSPACE_FOLDERS
|
||||
)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
@lsp_method(WINDOW_WORK_DONE_PROGRESS_CANCEL)
|
||||
def lsp_work_done_progress_cancel(
|
||||
self, params: WorkDoneProgressCancelParams
|
||||
) -> None:
|
||||
@lsp_method(types.WORKSPACE_EXECUTE_COMMAND)
|
||||
def lsp_workspace__execute_command(
|
||||
self, params: types.ExecuteCommandParams
|
||||
) -> Generator[Any, Any, Any]:
|
||||
"""Executes commands with passed arguments and returns a value."""
|
||||
|
||||
if (handler := self.fm.commands.get(params.command, None)) is None:
|
||||
raise JsonRpcInvalidParams.of(
|
||||
ValueError(f"Command name {params.command!r} is not defined")
|
||||
)
|
||||
|
||||
try:
|
||||
args, kwargs = _prepare_command_arguments(handler, params, self._converter)
|
||||
except Exception as exc:
|
||||
raise JsonRpcInvalidParams.of(exc)
|
||||
|
||||
# Call the user's command handler.
|
||||
result = yield handler, args, kwargs
|
||||
return result
|
||||
|
||||
@lsp_method(types.WINDOW_WORK_DONE_PROGRESS_CANCEL)
|
||||
def lsp_work_done_progress_cancel(self, params: types.WorkDoneProgressCancelParams):
|
||||
"""Received a progress cancellation from client."""
|
||||
future = self.progress.tokens.get(params.token)
|
||||
if future is None:
|
||||
@@ -333,237 +344,85 @@ class LanguageServerProtocol(JsonRPCProtocol, metaclass=LSPMeta):
|
||||
else:
|
||||
future.cancel()
|
||||
|
||||
def get_configuration(
|
||||
self,
|
||||
params: WorkspaceConfigurationParams,
|
||||
callback: Optional[ConfigCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Sends configuration request to the client.
|
||||
if (
|
||||
user_handler := self.fm.features.get(types.WINDOW_WORK_DONE_PROGRESS_CANCEL)
|
||||
) is not None:
|
||||
yield user_handler, (params,), None
|
||||
|
||||
Args:
|
||||
params(WorkspaceConfigurationParams): WorkspaceConfigurationParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(WORKSPACE_CONFIGURATION, params, callback)
|
||||
|
||||
def get_configuration_async(
|
||||
self, params: WorkspaceConfigurationParams
|
||||
) -> asyncio.Future:
|
||||
"""Calls `get_configuration` method but designed to use with coroutines
|
||||
def _prepare_command_arguments(
|
||||
handler: Callable[..., Any],
|
||||
params: types.ExecuteCommandParams,
|
||||
converter: Converter,
|
||||
) -> tuple[tuple[Any, ...], dict[str, Any]]:
|
||||
"""Prepare the arguments to pass to the command handler."""
|
||||
|
||||
Args:
|
||||
params(WorkspaceConfigurationParams): WorkspaceConfigurationParams from lsp specs
|
||||
Returns:
|
||||
asyncio.Future that can be awaited
|
||||
"""
|
||||
return asyncio.wrap_future(self.get_configuration(params))
|
||||
if params.arguments is None:
|
||||
return tuple(), {}
|
||||
|
||||
def log_trace(self, message: str, verbose: Optional[str] = None) -> None:
|
||||
"""Sends trace notification to the client."""
|
||||
if self.trace == TraceValues.Off:
|
||||
return
|
||||
# Import this here to not introduce an import cycle at the module level
|
||||
from pygls.lsp.server import LanguageServer
|
||||
|
||||
params = LogTraceParams(message=message)
|
||||
if verbose and self.trace == TraceValues.Verbose:
|
||||
params.verbose = verbose
|
||||
param_vals = iter(params.arguments)
|
||||
param_defs, annotations = _get_handler_params_annotations(handler)
|
||||
|
||||
self.notify(LOG_TRACE, params)
|
||||
args: list[Any] = []
|
||||
kwargs: dict[str, Any] = {}
|
||||
|
||||
def _publish_diagnostics_deprecator(
|
||||
self,
|
||||
params_or_uri: Union[str, PublishDiagnosticsParams],
|
||||
diagnostics: Optional[List[Diagnostic]],
|
||||
version: Optional[int],
|
||||
**kwargs,
|
||||
) -> PublishDiagnosticsParams:
|
||||
if isinstance(params_or_uri, str):
|
||||
message = "DEPRECATION: "
|
||||
"`publish_diagnostics("
|
||||
"self, doc_uri: str, diagnostics: List[Diagnostic], version: Optional[int] = None)`"
|
||||
"will be replaced with `publish_diagnostics(self, params: PublishDiagnosticsParams)`"
|
||||
logging.warning(message)
|
||||
# param_defs is an OrderedDict so *in theory* at least we don't have to
|
||||
# worry about argument order.
|
||||
found_ls = False
|
||||
for idx, (name, param) in enumerate(param_defs.items()):
|
||||
ptype = annotations.get(name, None)
|
||||
|
||||
# We don't need to provide the injected server instance here.
|
||||
# The @server.command decorator will have already handled it.
|
||||
if idx == 0:
|
||||
if name == PARAM_LS:
|
||||
found_ls = True
|
||||
continue
|
||||
|
||||
if (ptype is not None) and issubclass(ptype, LanguageServer):
|
||||
found_ls = True
|
||||
continue
|
||||
|
||||
if param.kind == inspect.Parameter.VAR_POSITIONAL: # i.e. *args
|
||||
# consume the remaining values
|
||||
args.extend(param_vals)
|
||||
|
||||
params = self._construct_publish_diagnostic_type(
|
||||
params_or_uri, diagnostics, version, **kwargs
|
||||
)
|
||||
else:
|
||||
params = params_or_uri
|
||||
return params
|
||||
try:
|
||||
value = converter.structure(next(param_vals), ptype)
|
||||
except StopIteration as exc:
|
||||
raise TypeError(
|
||||
f"Expected {len(param_defs) - found_ls} arguments, "
|
||||
f"got {len(params.arguments)}"
|
||||
) from exc
|
||||
|
||||
def _construct_publish_diagnostic_type(
|
||||
self,
|
||||
uri: str,
|
||||
diagnostics: Optional[List[Diagnostic]],
|
||||
version: Optional[int],
|
||||
**kwargs,
|
||||
) -> PublishDiagnosticsParams:
|
||||
if diagnostics is None:
|
||||
diagnostics = []
|
||||
args.append(value)
|
||||
|
||||
args = {
|
||||
**{"uri": uri, "diagnostics": diagnostics, "version": version},
|
||||
**kwargs,
|
||||
}
|
||||
|
||||
params = PublishDiagnosticsParams(**args) # type:ignore
|
||||
return params
|
||||
|
||||
def publish_diagnostics(
|
||||
self,
|
||||
params_or_uri: Union[str, PublishDiagnosticsParams],
|
||||
diagnostics: Optional[List[Diagnostic]] = None,
|
||||
version: Optional[int] = None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Sends diagnostic notification to the client.
|
||||
|
||||
.. deprecated:: 1.0.1
|
||||
|
||||
Passing ``(uri, diagnostics, version)`` as arguments is deprecated.
|
||||
Pass an instance of :class:`~lsprotocol.types.PublishDiagnosticParams`
|
||||
instead.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params_or_uri
|
||||
The :class:`~lsprotocol.types.PublishDiagnosticParams` to send to the client.
|
||||
|
||||
diagnostics
|
||||
*Deprecated*. The diagnostics to publish
|
||||
|
||||
version
|
||||
*Deprecated*: The version number
|
||||
"""
|
||||
params = self._publish_diagnostics_deprecator(
|
||||
params_or_uri, diagnostics, version, **kwargs
|
||||
)
|
||||
self.notify(TEXT_DOCUMENT_PUBLISH_DIAGNOSTICS, params)
|
||||
|
||||
def register_capability(
|
||||
self, params: RegistrationParams, callback: Optional[Callable[[], None]] = None
|
||||
) -> Future:
|
||||
"""Register a new capability on the client.
|
||||
|
||||
Args:
|
||||
params(RegistrationParams): RegistrationParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(CLIENT_REGISTER_CAPABILITY, params, callback)
|
||||
|
||||
def register_capability_async(self, params: RegistrationParams) -> asyncio.Future:
|
||||
"""Register a new capability on the client.
|
||||
|
||||
Args:
|
||||
params(RegistrationParams): RegistrationParams from lsp specs
|
||||
|
||||
Returns:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.register_capability(params, None))
|
||||
|
||||
def semantic_tokens_refresh(
|
||||
self, callback: Optional[Callable[[], None]] = None
|
||||
) -> Future:
|
||||
"""Requesting a refresh of all semantic tokens.
|
||||
|
||||
Args:
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(WORKSPACE_SEMANTIC_TOKENS_REFRESH, callback=callback)
|
||||
|
||||
def semantic_tokens_refresh_async(self) -> asyncio.Future:
|
||||
"""Requesting a refresh of all semantic tokens.
|
||||
|
||||
Returns:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.semantic_tokens_refresh(None))
|
||||
|
||||
def show_document(
|
||||
self,
|
||||
params: ShowDocumentParams,
|
||||
callback: Optional[ShowDocumentCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Display a particular document in the user interface.
|
||||
|
||||
Args:
|
||||
params(ShowDocumentParams): ShowDocumentParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(WINDOW_SHOW_DOCUMENT, params, callback)
|
||||
|
||||
def show_document_async(self, params: ShowDocumentParams) -> asyncio.Future:
|
||||
"""Display a particular document in the user interface.
|
||||
|
||||
Args:
|
||||
params(ShowDocumentParams): ShowDocumentParams from lsp specs
|
||||
|
||||
Returns:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.show_document(params, None))
|
||||
|
||||
def show_message(self, message, msg_type=MessageType.Info):
|
||||
"""Sends message to the client to display message."""
|
||||
self.notify(
|
||||
WINDOW_SHOW_MESSAGE, ShowMessageParams(type=msg_type, message=message)
|
||||
# did we consume all the values?
|
||||
if len(list(param_vals)) > 0:
|
||||
raise TypeError(
|
||||
f"Expected {len(param_defs) - found_ls} arguments, "
|
||||
f"got {len(params.arguments)}"
|
||||
)
|
||||
|
||||
def show_message_log(self, message, msg_type=MessageType.Log):
|
||||
"""Sends message to the client's output channel."""
|
||||
self.notify(
|
||||
WINDOW_LOG_MESSAGE, LogMessageParams(type=msg_type, message=message)
|
||||
)
|
||||
return tuple(args), kwargs
|
||||
|
||||
def unregister_capability(
|
||||
self,
|
||||
params: UnregistrationParams,
|
||||
callback: Optional[Callable[[], None]] = None,
|
||||
) -> Future:
|
||||
"""Unregister a new capability on the client.
|
||||
|
||||
Args:
|
||||
params(UnregistrationParams): UnregistrationParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
Returns:
|
||||
concurrent.futures.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return self.send_request(CLIENT_UNREGISTER_CAPABILITY, params, callback)
|
||||
def _get_handler_params_annotations(handler: Callable[..., Any]):
|
||||
"""Return the parameters and corresponding type annotations for the given handler
|
||||
function."""
|
||||
|
||||
def unregister_capability_async(
|
||||
self, params: UnregistrationParams
|
||||
) -> asyncio.Future:
|
||||
"""Unregister a new capability on the client.
|
||||
# If the user's handler requests the language server instance, the real function
|
||||
# is wrapped inside whatever `functools.partial()` returns.
|
||||
if hasattr(handler, "func"):
|
||||
annotations = typing.get_type_hints(handler.func)
|
||||
params = inspect.signature(handler.func).parameters
|
||||
|
||||
Args:
|
||||
params(UnregistrationParams): UnregistrationParams from lsp specs
|
||||
callback(callable): Callabe which will be called after
|
||||
response from the client is received
|
||||
Returns:
|
||||
asyncio.Future object that will be resolved once a
|
||||
response has been received
|
||||
"""
|
||||
return asyncio.wrap_future(self.unregister_capability(params, None))
|
||||
else:
|
||||
annotations = typing.get_type_hints(handler)
|
||||
params = inspect.signature(handler).parameters
|
||||
|
||||
return params, annotations
|
||||
|
||||
@@ -1,51 +0,0 @@
|
||||
import functools
|
||||
import logging
|
||||
from pygls.constants import ATTR_FEATURE_TYPE
|
||||
from pygls.feature_manager import assign_help_attrs
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def call_user_feature(base_func, method_name):
|
||||
"""Wraps generic LSP features and calls user registered feature
|
||||
immediately after it.
|
||||
"""
|
||||
|
||||
@functools.wraps(base_func)
|
||||
def decorator(self, *args, **kwargs):
|
||||
ret_val = base_func(self, *args, **kwargs)
|
||||
|
||||
try:
|
||||
user_func = self.fm.features[method_name]
|
||||
self._execute_notification(user_func, *args, **kwargs)
|
||||
except KeyError:
|
||||
pass
|
||||
except Exception:
|
||||
logger.exception(
|
||||
'Failed to handle user defined notification "%s": %s', method_name, args
|
||||
)
|
||||
|
||||
return ret_val
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
class LSPMeta(type):
|
||||
"""Wraps LSP built-in features (`lsp_` naming convention).
|
||||
|
||||
Built-in features cannot be overridden but user defined features with
|
||||
the same LSP name will be called after them.
|
||||
"""
|
||||
|
||||
def __new__(mcs, cls_name, cls_bases, cls):
|
||||
for attr_name, attr_val in cls.items():
|
||||
if callable(attr_val) and hasattr(attr_val, "method_name"):
|
||||
method_name = attr_val.method_name
|
||||
wrapped = call_user_feature(attr_val, method_name)
|
||||
assign_help_attrs(wrapped, method_name, ATTR_FEATURE_TYPE)
|
||||
cls[attr_name] = wrapped
|
||||
|
||||
logger.debug('Added decorator for lsp method: "%s"', attr_name)
|
||||
|
||||
return super().__new__(mcs, cls_name, cls_bases, cls)
|
||||
+156
-489
@@ -14,213 +14,66 @@
|
||||
# See the License for the specific language governing permissions and #
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import sys
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
import typing
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from threading import Event
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
List,
|
||||
Optional,
|
||||
TextIO,
|
||||
Type,
|
||||
TypeVar,
|
||||
Union,
|
||||
)
|
||||
|
||||
import cattrs
|
||||
from pygls import IS_PYODIDE
|
||||
from pygls.lsp import ConfigCallbackType, ShowDocumentCallbackType
|
||||
from pygls.exceptions import (
|
||||
FeatureNotificationError,
|
||||
JsonRpcInternalError,
|
||||
PyglsError,
|
||||
JsonRpcException,
|
||||
FeatureRequestError,
|
||||
)
|
||||
from lsprotocol.types import (
|
||||
ClientCapabilities,
|
||||
Diagnostic,
|
||||
MessageType,
|
||||
NotebookDocumentSyncOptions,
|
||||
RegistrationParams,
|
||||
ServerCapabilities,
|
||||
ShowDocumentParams,
|
||||
TextDocumentSyncKind,
|
||||
UnregistrationParams,
|
||||
WorkspaceApplyEditResponse,
|
||||
WorkspaceEdit,
|
||||
WorkspaceConfigurationParams,
|
||||
)
|
||||
from pygls.progress import Progress
|
||||
from pygls.protocol import JsonRPCProtocol, LanguageServerProtocol, default_converter
|
||||
from pygls.workspace import Workspace
|
||||
|
||||
if not IS_PYODIDE:
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from pygls import IS_WASM
|
||||
from pygls.exceptions import JsonRpcException, PyglsError
|
||||
from pygls.io_ import StdinAsyncReader, StdoutWriter, run, run_async, run_websocket
|
||||
from pygls.protocol import JsonRPCProtocol
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from typing import Any, BinaryIO, Callable, Optional, Type, TypeVar, Union
|
||||
|
||||
from websockets.asyncio.server import Server as WSServer
|
||||
from websockets.asyncio.server import ServerConnection
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
ServerErrors = Union[type[PyglsError], type[JsonRpcException]]
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
F = TypeVar("F", bound=Callable)
|
||||
|
||||
ServerErrors = Union[
|
||||
PyglsError,
|
||||
JsonRpcException,
|
||||
Type[JsonRpcInternalError],
|
||||
Type[FeatureNotificationError],
|
||||
Type[FeatureRequestError],
|
||||
]
|
||||
|
||||
|
||||
async def aio_readline(loop, executor, stop_event, rfile, proxy):
|
||||
"""Reads data from stdin in separate thread (asynchronously)."""
|
||||
|
||||
CONTENT_LENGTH_PATTERN = re.compile(rb"^Content-Length: (\d+)\r\n$")
|
||||
|
||||
# Initialize message buffer
|
||||
message = []
|
||||
content_length = 0
|
||||
|
||||
while not stop_event.is_set() and not rfile.closed:
|
||||
# Read a header line
|
||||
header = await loop.run_in_executor(executor, rfile.readline)
|
||||
if not header:
|
||||
break
|
||||
message.append(header)
|
||||
|
||||
# Extract content length if possible
|
||||
if not content_length:
|
||||
match = CONTENT_LENGTH_PATTERN.fullmatch(header)
|
||||
if match:
|
||||
content_length = int(match.group(1))
|
||||
logger.debug("Content length: %s", content_length)
|
||||
|
||||
# Check if all headers have been read (as indicated by an empty line \r\n)
|
||||
if content_length and not header.strip():
|
||||
# Read body
|
||||
body = await loop.run_in_executor(executor, rfile.read, content_length)
|
||||
if not body:
|
||||
break
|
||||
message.append(body)
|
||||
|
||||
# Pass message to language server protocol
|
||||
proxy(b"".join(message))
|
||||
|
||||
# Reset the buffer
|
||||
message = []
|
||||
content_length = 0
|
||||
|
||||
|
||||
class StdOutTransportAdapter:
|
||||
"""Protocol adapter which overrides write method.
|
||||
|
||||
Write method sends data to stdout.
|
||||
"""
|
||||
|
||||
def __init__(self, rfile, wfile):
|
||||
self.rfile = rfile
|
||||
self.wfile = wfile
|
||||
|
||||
def close(self):
|
||||
self.rfile.close()
|
||||
self.wfile.close()
|
||||
|
||||
def write(self, data):
|
||||
self.wfile.write(data)
|
||||
self.wfile.flush()
|
||||
|
||||
|
||||
class PyodideTransportAdapter:
|
||||
"""Protocol adapter which overrides write method.
|
||||
|
||||
Write method sends data to stdout.
|
||||
"""
|
||||
|
||||
def __init__(self, wfile):
|
||||
self.wfile = wfile
|
||||
|
||||
def close(self):
|
||||
self.wfile.close()
|
||||
|
||||
def write(self, data):
|
||||
self.wfile.write(data)
|
||||
self.wfile.flush()
|
||||
|
||||
|
||||
class WebSocketTransportAdapter:
|
||||
"""Protocol adapter which calls write method.
|
||||
|
||||
Write method sends data via the WebSocket interface.
|
||||
"""
|
||||
|
||||
def __init__(self, ws, loop):
|
||||
self._ws = ws
|
||||
self._loop = loop
|
||||
|
||||
def close(self) -> None:
|
||||
"""Stop the WebSocket server."""
|
||||
self._ws.close()
|
||||
|
||||
def write(self, data: Any) -> None:
|
||||
"""Create a task to write specified data into a WebSocket."""
|
||||
asyncio.ensure_future(self._ws.send(data))
|
||||
|
||||
|
||||
class Server:
|
||||
class JsonRPCServer:
|
||||
"""Base server class
|
||||
|
||||
Parameters
|
||||
----------
|
||||
protocol_cls
|
||||
Protocol implementation that must be derive from :class:`~pygls.protocol.JsonRPCProtocol`
|
||||
Protocol implementation that should derive from
|
||||
:class:`~pygls.protocol.JsonRPCProtocol`
|
||||
|
||||
converter_factory
|
||||
Factory function to use when constructing a cattrs converter.
|
||||
|
||||
loop
|
||||
The asyncio event loop
|
||||
|
||||
max_workers
|
||||
Maximum number of workers for `ThreadPool` and `ThreadPoolExecutor`
|
||||
Maximum number of workers for `ThreadPoolExecutor`
|
||||
|
||||
"""
|
||||
|
||||
protocol: JsonRPCProtocol
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
protocol_cls: Type[JsonRPCProtocol],
|
||||
converter_factory: Callable[[], cattrs.Converter],
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
max_workers: int = 2,
|
||||
sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
|
||||
max_workers: int | None = None,
|
||||
):
|
||||
if not issubclass(protocol_cls, asyncio.Protocol):
|
||||
raise TypeError("Protocol class should be subclass of asyncio.Protocol")
|
||||
|
||||
self._max_workers = max_workers
|
||||
self._server = None
|
||||
self._stop_event: Optional[Event] = None
|
||||
self._thread_pool: Optional[ThreadPool] = None
|
||||
self._thread_pool_executor: Optional[ThreadPoolExecutor] = None
|
||||
self._server: asyncio.Server | WSServer | None = None
|
||||
self._stop_event: Event | None = None
|
||||
self._thread_pool: ThreadPoolExecutor | None = None
|
||||
|
||||
if sync_kind is not None:
|
||||
self.text_document_sync_kind = sync_kind
|
||||
|
||||
if loop is None:
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
self._owns_loop = True
|
||||
else:
|
||||
self._owns_loop = False
|
||||
|
||||
self.loop = loop
|
||||
|
||||
# TODO: Will move this to `LanguageServer` soon
|
||||
self.lsp = protocol_cls(self, converter_factory()) # type: ignore
|
||||
self.protocol = protocol_cls(self, converter_factory())
|
||||
|
||||
def shutdown(self):
|
||||
"""Shutdown server."""
|
||||
@@ -230,38 +83,55 @@ class Server:
|
||||
self._stop_event.set()
|
||||
|
||||
if self._thread_pool:
|
||||
self._thread_pool.terminate()
|
||||
self._thread_pool.join()
|
||||
|
||||
if self._thread_pool_executor:
|
||||
self._thread_pool_executor.shutdown()
|
||||
self._thread_pool.shutdown()
|
||||
|
||||
if self._server:
|
||||
self._server.close()
|
||||
self.loop.run_until_complete(self._server.wait_closed())
|
||||
|
||||
if self._owns_loop and not self.loop.is_closed():
|
||||
logger.info("Closing the event loop.")
|
||||
self.loop.close()
|
||||
def _report_server_error(
|
||||
self,
|
||||
error: Exception,
|
||||
source: ServerErrors,
|
||||
):
|
||||
# Prevent recursive error reporting
|
||||
try:
|
||||
self.report_server_error(error, source)
|
||||
except Exception:
|
||||
logger.warning("Failed to report error")
|
||||
|
||||
def start_io(self, stdin: Optional[TextIO] = None, stdout: Optional[TextIO] = None):
|
||||
"""Starts IO server."""
|
||||
logger.info("Starting IO server")
|
||||
def report_server_error(self, error: Exception, source: ServerErrors):
|
||||
"""Default error reporter."""
|
||||
logger.error("%s", error)
|
||||
|
||||
def start_io(
|
||||
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
||||
):
|
||||
"""Starts an IO server."""
|
||||
|
||||
if IS_WASM:
|
||||
self._start_io_sync(stdin, stdout)
|
||||
else:
|
||||
self._start_io_async(stdin, stdout)
|
||||
|
||||
def _start_io_async(
|
||||
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
||||
):
|
||||
"""Starts an asynchronous IO server."""
|
||||
logger.info("Starting async IO server")
|
||||
|
||||
self._stop_event = Event()
|
||||
transport = StdOutTransportAdapter(
|
||||
stdin or sys.stdin.buffer, stdout or sys.stdout.buffer
|
||||
)
|
||||
self.lsp.connection_made(transport) # type: ignore[arg-type]
|
||||
reader = StdinAsyncReader(stdin or sys.stdin.buffer, self.thread_pool)
|
||||
writer = StdoutWriter(stdout or sys.stdout.buffer)
|
||||
self.protocol.set_writer(writer)
|
||||
|
||||
try:
|
||||
self.loop.run_until_complete(
|
||||
aio_readline(
|
||||
self.loop,
|
||||
self.thread_pool_executor,
|
||||
self._stop_event,
|
||||
stdin or sys.stdin.buffer,
|
||||
self.lsp.data_received,
|
||||
asyncio.run(
|
||||
run_async(
|
||||
stop_event=self._stop_event,
|
||||
reader=reader,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
except BrokenPipeError:
|
||||
@@ -271,169 +141,104 @@ class Server:
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
def start_pyodide(self):
|
||||
logger.info("Starting Pyodide server")
|
||||
def _start_io_sync(
|
||||
self, stdin: Optional[BinaryIO] = None, stdout: Optional[BinaryIO] = None
|
||||
):
|
||||
"""Starts an synchronous IO server."""
|
||||
logger.info("Starting sync IO server")
|
||||
|
||||
# Note: We don't actually start anything running as the main event
|
||||
# loop will be handled by the web platform.
|
||||
transport = PyodideTransportAdapter(sys.stdout)
|
||||
self.lsp.connection_made(transport) # type: ignore[arg-type]
|
||||
self.lsp._send_only_body = True # Don't send headers within the payload
|
||||
self._stop_event = Event()
|
||||
writer = StdoutWriter(stdout or sys.stdout.buffer)
|
||||
self.protocol.set_writer(writer)
|
||||
|
||||
try:
|
||||
asyncio.run(
|
||||
run(
|
||||
stop_event=self._stop_event,
|
||||
reader=stdin or sys.stdin.buffer,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
)
|
||||
except BrokenPipeError:
|
||||
logger.error("Connection to the client is lost! Shutting down the server.")
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
self.shutdown()
|
||||
|
||||
def start_tcp(self, host: str, port: int) -> None:
|
||||
"""Starts TCP server."""
|
||||
logger.info("Starting TCP server on %s:%s", host, port)
|
||||
|
||||
self._stop_event = Event()
|
||||
self._server = self.loop.run_until_complete( # type: ignore[assignment]
|
||||
self.loop.create_server(self.lsp, host, port)
|
||||
)
|
||||
try:
|
||||
self.loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
self._stop_event = stop_event = Event()
|
||||
|
||||
async def lsp_connection(
|
||||
reader: asyncio.StreamReader, writer: asyncio.StreamWriter
|
||||
):
|
||||
logger.debug("Connected to client")
|
||||
self.protocol.set_writer(writer) # type: ignore
|
||||
await run_async(
|
||||
stop_event=stop_event,
|
||||
reader=reader,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
logger.debug("Main loop finished")
|
||||
self.shutdown()
|
||||
|
||||
async def tcp_server(h: str, p: int):
|
||||
self._server = await asyncio.start_server(lsp_connection, h, p)
|
||||
|
||||
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
|
||||
logger.info(f"Serving on {addrs}")
|
||||
|
||||
async with self._server:
|
||||
await self._server.serve_forever()
|
||||
|
||||
try:
|
||||
asyncio.run(tcp_server(host, port))
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Server was cancelled")
|
||||
|
||||
def start_ws(self, host: str, port: int) -> None:
|
||||
"""Starts WebSocket server."""
|
||||
try:
|
||||
from websockets.server import serve
|
||||
from websockets.asyncio.server import serve
|
||||
except ImportError:
|
||||
logger.error("Run `pip install pygls[ws]` to install `websockets`.")
|
||||
logger.error(
|
||||
"Run `pip install pygls[ws]` to install dependencies required for websockets."
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
logger.info("Starting WebSocket server on {}:{}".format(host, port))
|
||||
self._stop_event = stop_event = Event()
|
||||
|
||||
self._stop_event = Event()
|
||||
self.lsp._send_only_body = True # Don't send headers within the payload
|
||||
|
||||
async def connection_made(websocket, _):
|
||||
"""Handle new connection wrapped in the WebSocket."""
|
||||
self.lsp.transport = WebSocketTransportAdapter(websocket, self.loop)
|
||||
async for message in websocket:
|
||||
self.lsp._procedure_handler(
|
||||
json.loads(message, object_hook=self.lsp._deserialize_message)
|
||||
)
|
||||
|
||||
start_server = serve(connection_made, host, port, loop=self.loop)
|
||||
self._server = start_server.ws_server # type: ignore[assignment]
|
||||
self.loop.run_until_complete(start_server)
|
||||
|
||||
try:
|
||||
self.loop.run_forever()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
finally:
|
||||
self._stop_event.set()
|
||||
async def lsp_connection(websocket: ServerConnection):
|
||||
await run_websocket(
|
||||
stop_event=stop_event,
|
||||
websocket=websocket,
|
||||
protocol=self.protocol,
|
||||
logger=logger,
|
||||
error_handler=self.report_server_error,
|
||||
)
|
||||
self.shutdown()
|
||||
|
||||
if not IS_PYODIDE:
|
||||
async def ws_server(h: str, p: int):
|
||||
self._server = await serve(lsp_connection, host, port)
|
||||
|
||||
@property
|
||||
def thread_pool(self) -> ThreadPool:
|
||||
"""Returns thread pool instance (lazy initialization)."""
|
||||
if not self._thread_pool:
|
||||
self._thread_pool = ThreadPool(processes=self._max_workers)
|
||||
addrs = ", ".join(str(sock.getsockname()) for sock in self._server.sockets)
|
||||
logger.info(f"Serving on {addrs}")
|
||||
|
||||
return self._thread_pool
|
||||
async with self._server:
|
||||
await self._server.serve_forever()
|
||||
|
||||
@property
|
||||
def thread_pool_executor(self) -> ThreadPoolExecutor:
|
||||
"""Returns thread pool instance (lazy initialization)."""
|
||||
if not self._thread_pool_executor:
|
||||
self._thread_pool_executor = ThreadPoolExecutor(
|
||||
max_workers=self._max_workers
|
||||
)
|
||||
|
||||
return self._thread_pool_executor
|
||||
|
||||
|
||||
class LanguageServer(Server):
|
||||
"""The default LanguageServer
|
||||
|
||||
This class can be extended and it can be passed as a first argument to
|
||||
registered commands/features.
|
||||
|
||||
.. |ServerInfo| replace:: :class:`~lsprotocol.types.InitializeResultServerInfoType`
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name
|
||||
Name of the server, used to populate |ServerInfo| which is sent to
|
||||
the client during initialization
|
||||
|
||||
version
|
||||
Version of the server, used to populate |ServerInfo| which is sent to
|
||||
the client during initialization
|
||||
|
||||
protocol_cls
|
||||
The :class:`~pygls.protocol.LanguageServerProtocol` class definition, or any
|
||||
subclass of it.
|
||||
|
||||
max_workers
|
||||
Maximum number of workers for ``ThreadPool`` and ``ThreadPoolExecutor``
|
||||
|
||||
text_document_sync_kind
|
||||
Text document synchronization method
|
||||
|
||||
None
|
||||
No synchronization
|
||||
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Full`
|
||||
Send entire document text with each update
|
||||
|
||||
:attr:`~lsprotocol.types.TextDocumentSyncKind.Incremental`
|
||||
Send only the region of text that changed with each update
|
||||
|
||||
notebook_document_sync
|
||||
Advertise :lsp:`NotebookDocument` support to the client.
|
||||
"""
|
||||
|
||||
lsp: LanguageServerProtocol
|
||||
|
||||
default_error_message = (
|
||||
"Unexpected error in LSP server, see server's logs for details"
|
||||
)
|
||||
"""
|
||||
The default error message sent to the user's editor when this server encounters an uncaught
|
||||
exception.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
version: str,
|
||||
loop=None,
|
||||
protocol_cls: Type[LanguageServerProtocol] = LanguageServerProtocol,
|
||||
converter_factory=default_converter,
|
||||
text_document_sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
|
||||
notebook_document_sync: Optional[NotebookDocumentSyncOptions] = None,
|
||||
max_workers: int = 2,
|
||||
):
|
||||
if not issubclass(protocol_cls, LanguageServerProtocol):
|
||||
raise TypeError(
|
||||
"Protocol class should be subclass of LanguageServerProtocol"
|
||||
)
|
||||
|
||||
self.name = name
|
||||
self.version = version
|
||||
self._text_document_sync_kind = text_document_sync_kind
|
||||
self._notebook_document_sync = notebook_document_sync
|
||||
self.process_id: Optional[Union[int, None]] = None
|
||||
super().__init__(protocol_cls, converter_factory, loop, max_workers)
|
||||
|
||||
def apply_edit(
|
||||
self, edit: WorkspaceEdit, label: Optional[str] = None
|
||||
) -> WorkspaceApplyEditResponse:
|
||||
"""Sends apply edit request to the client."""
|
||||
return self.lsp.apply_edit(edit, label)
|
||||
|
||||
def apply_edit_async(
|
||||
self, edit: WorkspaceEdit, label: Optional[str] = None
|
||||
) -> WorkspaceApplyEditResponse:
|
||||
"""Sends apply edit request to the client. Should be called with `await`"""
|
||||
return self.lsp.apply_edit_async(edit, label)
|
||||
try:
|
||||
asyncio.run(ws_server(host, port))
|
||||
except asyncio.CancelledError:
|
||||
logger.debug("Server was cancelled")
|
||||
|
||||
def command(self, command_name: str) -> Callable[[F], F]:
|
||||
"""Decorator used to register custom commands.
|
||||
@@ -446,17 +251,16 @@ class LanguageServer(Server):
|
||||
def my_cmd(ls, a, b, c):
|
||||
pass
|
||||
"""
|
||||
return self.lsp.fm.command(command_name)
|
||||
return self.protocol.fm.command(command_name)
|
||||
|
||||
@property
|
||||
def client_capabilities(self) -> ClientCapabilities:
|
||||
"""The client's capabilities."""
|
||||
return self.lsp.client_capabilities
|
||||
def thread(self) -> Callable[[F], F]:
|
||||
"""Decorator that mark function to execute it in a thread."""
|
||||
return self.protocol.fm.thread()
|
||||
|
||||
def feature(
|
||||
self,
|
||||
feature_name: str,
|
||||
options: Optional[Any] = None,
|
||||
options: Any | None = None,
|
||||
) -> Callable[[F], F]:
|
||||
"""Decorator used to register LSP features.
|
||||
|
||||
@@ -468,149 +272,12 @@ class LanguageServer(Server):
|
||||
def completions(ls, params: CompletionParams):
|
||||
return CompletionList(is_incomplete=False, items=[CompletionItem("Completion 1")])
|
||||
"""
|
||||
return self.lsp.fm.feature(feature_name, options)
|
||||
|
||||
def get_configuration(
|
||||
self,
|
||||
params: WorkspaceConfigurationParams,
|
||||
callback: Optional[ConfigCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Gets the configuration settings from the client."""
|
||||
return self.lsp.get_configuration(params, callback)
|
||||
|
||||
def get_configuration_async(
|
||||
self, params: WorkspaceConfigurationParams
|
||||
) -> asyncio.Future:
|
||||
"""Gets the configuration settings from the client. Should be called with `await`"""
|
||||
return self.lsp.get_configuration_async(params)
|
||||
|
||||
def log_trace(self, message: str, verbose: Optional[str] = None) -> None:
|
||||
"""Sends trace notification to the client."""
|
||||
self.lsp.log_trace(message, verbose)
|
||||
return self.protocol.fm.feature(feature_name, options)
|
||||
|
||||
@property
|
||||
def progress(self) -> Progress:
|
||||
"""Gets the object to manage client's progress bar."""
|
||||
return self.lsp.progress
|
||||
def thread_pool(self) -> ThreadPoolExecutor:
|
||||
"""Returns thread pool instance (lazy initialization)."""
|
||||
if not self._thread_pool:
|
||||
self._thread_pool = ThreadPoolExecutor(max_workers=self._max_workers)
|
||||
|
||||
def publish_diagnostics(
|
||||
self,
|
||||
uri: str,
|
||||
diagnostics: Optional[List[Diagnostic]] = None,
|
||||
version: Optional[int] = None,
|
||||
**kwargs
|
||||
):
|
||||
"""
|
||||
Sends diagnostic notification to the client.
|
||||
"""
|
||||
params = self.lsp._construct_publish_diagnostic_type(
|
||||
uri, diagnostics, version, **kwargs
|
||||
)
|
||||
self.lsp.publish_diagnostics(params, **kwargs)
|
||||
|
||||
def register_capability(
|
||||
self, params: RegistrationParams, callback: Optional[Callable[[], None]] = None
|
||||
) -> Future:
|
||||
"""Register a new capability on the client."""
|
||||
return self.lsp.register_capability(params, callback)
|
||||
|
||||
def register_capability_async(self, params: RegistrationParams) -> asyncio.Future:
|
||||
"""Register a new capability on the client. Should be called with `await`"""
|
||||
return self.lsp.register_capability_async(params)
|
||||
|
||||
def semantic_tokens_refresh(
|
||||
self, callback: Optional[Callable[[], None]] = None
|
||||
) -> Future:
|
||||
"""Request a refresh of all semantic tokens."""
|
||||
return self.lsp.semantic_tokens_refresh(callback)
|
||||
|
||||
def semantic_tokens_refresh_async(self) -> asyncio.Future:
|
||||
"""Request a refresh of all semantic tokens. Should be called with `await`"""
|
||||
return self.lsp.semantic_tokens_refresh_async()
|
||||
|
||||
def send_notification(self, method: str, params: object = None) -> None:
|
||||
"""Sends notification to the client."""
|
||||
self.lsp.notify(method, params)
|
||||
|
||||
@property
|
||||
def server_capabilities(self) -> ServerCapabilities:
|
||||
"""Return server capabilities."""
|
||||
return self.lsp.server_capabilities
|
||||
|
||||
def show_document(
|
||||
self,
|
||||
params: ShowDocumentParams,
|
||||
callback: Optional[ShowDocumentCallbackType] = None,
|
||||
) -> Future:
|
||||
"""Display a particular document in the user interface."""
|
||||
return self.lsp.show_document(params, callback)
|
||||
|
||||
def show_document_async(self, params: ShowDocumentParams) -> asyncio.Future:
|
||||
"""Display a particular document in the user interface. Should be called with `await`"""
|
||||
return self.lsp.show_document_async(params)
|
||||
|
||||
def show_message(self, message, msg_type=MessageType.Info) -> None:
|
||||
"""Sends message to the client to display message."""
|
||||
self.lsp.show_message(message, msg_type)
|
||||
|
||||
def show_message_log(self, message, msg_type=MessageType.Log) -> None:
|
||||
"""Sends message to the client's output channel."""
|
||||
self.lsp.show_message_log(message, msg_type)
|
||||
|
||||
def _report_server_error(
|
||||
self,
|
||||
error: Exception,
|
||||
source: ServerErrors,
|
||||
):
|
||||
# Prevent recursive error reporting
|
||||
try:
|
||||
self.report_server_error(error, source)
|
||||
except Exception:
|
||||
logger.warning("Failed to report error to client")
|
||||
|
||||
def report_server_error(self, error: Exception, source: ServerErrors):
|
||||
"""
|
||||
Sends error to the client for displaying.
|
||||
|
||||
By default this fucntion does not handle LSP request errors. This is because LSP requests
|
||||
require direct responses and so already have a mechanism for including unexpected errors
|
||||
in the response body.
|
||||
|
||||
All other errors are "out of band" in the sense that the client isn't explicitly waiting
|
||||
for them. For example diagnostics are returned as notifications, not responses to requests,
|
||||
and so can seemingly be sent at random. Also for example consider JSON RPC serialization
|
||||
and deserialization, if a payload cannot be parsed then the whole request/response cycle
|
||||
cannot be completed and so one of these "out of band" error messages is sent.
|
||||
|
||||
These "out of band" error messages are not a requirement of the LSP spec. Pygls simply
|
||||
offers this behaviour as a recommended default. It is perfectly reasonble to override this
|
||||
default.
|
||||
"""
|
||||
|
||||
if source == FeatureRequestError:
|
||||
return
|
||||
|
||||
self.show_message(self.default_error_message, msg_type=MessageType.Error)
|
||||
|
||||
def thread(self) -> Callable[[F], F]:
|
||||
"""Decorator that mark function to execute it in a thread."""
|
||||
return self.lsp.thread()
|
||||
|
||||
def unregister_capability(
|
||||
self,
|
||||
params: UnregistrationParams,
|
||||
callback: Optional[Callable[[], None]] = None,
|
||||
) -> Future:
|
||||
"""Unregister a new capability on the client."""
|
||||
return self.lsp.unregister_capability(params, callback)
|
||||
|
||||
def unregister_capability_async(
|
||||
self, params: UnregistrationParams
|
||||
) -> asyncio.Future:
|
||||
"""Unregister a new capability on the client. Should be called with `await`"""
|
||||
return self.lsp.unregister_capability_async(params)
|
||||
|
||||
@property
|
||||
def workspace(self) -> Workspace:
|
||||
"""Returns in-memory workspace."""
|
||||
return self.lsp.workspace
|
||||
return self._thread_pool
|
||||
|
||||
@@ -21,6 +21,8 @@ A collection of URI utilities with logic built on the VSCode URI library.
|
||||
|
||||
https://github.com/Microsoft/vscode-uri/blob/e59cab84f5df6265aed18ae5f43552d3eef13bb9/lib/index.ts
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import re
|
||||
@@ -75,20 +77,22 @@ def from_fs_path(path: str):
|
||||
return None
|
||||
|
||||
|
||||
def to_fs_path(uri: str):
|
||||
def to_fs_path(uri: str) -> str | None:
|
||||
"""
|
||||
Returns the filesystem path of the given URI.
|
||||
|
||||
Will handle UNC paths and normalize windows drive letters to lower-case.
|
||||
Also uses the platform specific path separator. Will *not* validate the
|
||||
path for invalid characters and semantics.
|
||||
Will *not* look at the scheme of this URI.
|
||||
"""
|
||||
try:
|
||||
# scheme://netloc/path;parameters?query#fragment
|
||||
scheme, netloc, path, _, _, _ = urlparse(uri)
|
||||
|
||||
if netloc and path and scheme == "file":
|
||||
if scheme != "file":
|
||||
return None
|
||||
|
||||
if netloc and path:
|
||||
# unc path: file://shares/c$/far/boo
|
||||
value = f"//{netloc}{path}"
|
||||
|
||||
|
||||
@@ -1,97 +1,11 @@
|
||||
from typing import List
|
||||
import warnings
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from .workspace import Workspace
|
||||
from .text_document import TextDocument
|
||||
from .position_codec import PositionCodec
|
||||
|
||||
# For backwards compatibility
|
||||
Document = TextDocument
|
||||
|
||||
|
||||
def utf16_unit_offset(chars: str):
|
||||
warnings.warn(
|
||||
"'utf16_unit_offset' has been deprecated, instead use "
|
||||
"'PositionCodec.utf16_unit_offset' via 'workspace.position_codec' "
|
||||
"or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.utf16_unit_offset(chars)
|
||||
|
||||
|
||||
def utf16_num_units(chars: str):
|
||||
warnings.warn(
|
||||
"'utf16_num_units' has been deprecated, instead use "
|
||||
"'PositionCodec.client_num_units' via 'workspace.position_codec' "
|
||||
"or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.client_num_units(chars)
|
||||
|
||||
|
||||
def position_from_utf16(lines: List[str], position: types.Position):
|
||||
warnings.warn(
|
||||
"'position_from_utf16' has been deprecated, instead use "
|
||||
"'PositionCodec.position_from_client_units' via "
|
||||
"'workspace.position_codec' or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.position_from_client_units(lines, position)
|
||||
|
||||
|
||||
def position_to_utf16(lines: List[str], position: types.Position):
|
||||
warnings.warn(
|
||||
"'position_to_utf16' has been deprecated, instead use "
|
||||
"'PositionCodec.position_to_client_units' via "
|
||||
"'workspace.position_codec' or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.position_to_client_units(lines, position)
|
||||
|
||||
|
||||
def range_from_utf16(lines: List[str], range: types.Range):
|
||||
warnings.warn(
|
||||
"'range_from_utf16' has been deprecated, instead use "
|
||||
"'PositionCodec.range_from_client_units' via "
|
||||
"'workspace.position_codec' or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.range_from_client_units(lines, range)
|
||||
|
||||
|
||||
def range_to_utf16(lines: List[str], range: types.Range):
|
||||
warnings.warn(
|
||||
"'range_to_utf16' has been deprecated, instead use "
|
||||
"'PositionCodec.range_to_client_units' via 'workspace.position_codec' "
|
||||
"or 'text_document.position_codec'",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
_codec = PositionCodec()
|
||||
return _codec.range_to_client_units(lines, range)
|
||||
|
||||
from .position_codec import PositionCodec, ServerTextPosition, ServerTextRange
|
||||
|
||||
__all__ = (
|
||||
"Workspace",
|
||||
"TextDocument",
|
||||
"PositionCodec",
|
||||
"Document",
|
||||
"utf16_unit_offset",
|
||||
"utf16_num_units",
|
||||
"position_from_utf16",
|
||||
"position_to_utf16",
|
||||
"range_from_utf16",
|
||||
"range_to_utf16",
|
||||
"ServerTextPosition",
|
||||
"ServerTextRange",
|
||||
)
|
||||
|
||||
@@ -17,14 +17,113 @@
|
||||
# limitations under the License. #
|
||||
############################################################################
|
||||
import logging
|
||||
from typing import List, Optional, Union
|
||||
from dataclasses import dataclass
|
||||
from typing import Optional, Union, Sequence, Any
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass(order=True)
|
||||
class ServerTextPosition:
|
||||
line: int
|
||||
character: int
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.line}:{self.character}"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ServerTextRange:
|
||||
start: ServerTextPosition
|
||||
end: ServerTextPosition
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.start}-{self.end}"
|
||||
|
||||
def __contains__(self, position: Any) -> bool:
|
||||
if not isinstance(position, ServerTextPosition):
|
||||
raise TypeError("ServerTextRanges can only contain ServerTextPositions.")
|
||||
return self.start <= position <= self.end
|
||||
|
||||
def includes(self, inner: "ServerTextRange") -> bool:
|
||||
"""
|
||||
Returns whether `inner` is entirely contained within self, i.e. all
|
||||
positions in `inner` are also in `self`.
|
||||
"""
|
||||
return self.start <= inner.start and inner.end <= self.end
|
||||
|
||||
def overlaps(self, other: "ServerTextRange") -> bool:
|
||||
"""
|
||||
Returns whether `self` and `other` overlap, i.e. any positions exist that
|
||||
are included in both self and other.
|
||||
"""
|
||||
return self.start <= other.end and other.start <= self.end
|
||||
|
||||
|
||||
class UnitCounter:
|
||||
def code_units_for_char(self, char: str) -> int:
|
||||
"""
|
||||
Get the number of code units used to encode the given single character.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def num_units(self, chars: str) -> int:
|
||||
"""
|
||||
Get the number of code units used to encode the given string.
|
||||
"""
|
||||
return sum(self.code_units_for_char(c) for c in chars)
|
||||
|
||||
def column_from_utf32(self, line: str, column: int) -> int:
|
||||
"""
|
||||
Convert the codepoint index `column` into code units.
|
||||
"""
|
||||
return sum(self.code_units_for_char(c) for c in line[:column])
|
||||
|
||||
|
||||
class Utf32(UnitCounter):
|
||||
def code_units_for_char(self, char: str) -> int:
|
||||
return 1
|
||||
|
||||
def num_units(self, chars: str) -> int:
|
||||
# We can avoid the loop needed for other encodings here
|
||||
return len(chars)
|
||||
|
||||
def column_from_utf32(self, line: str, column: int) -> int:
|
||||
return column
|
||||
|
||||
|
||||
def is_beyond_basic_multilingual_plane(char: str) -> bool:
|
||||
return ord(char) > 0xFFFF
|
||||
|
||||
|
||||
class Utf16(UnitCounter):
|
||||
def code_units_for_char(self, char: str) -> int:
|
||||
if is_beyond_basic_multilingual_plane(char):
|
||||
return 2
|
||||
return 1
|
||||
|
||||
|
||||
class Utf8(UnitCounter):
|
||||
def code_units_for_char(self, char: str) -> int:
|
||||
codepoint = ord(char)
|
||||
if codepoint < 0x80:
|
||||
return 1
|
||||
if codepoint < 0x800:
|
||||
return 2
|
||||
if codepoint < 0x10000:
|
||||
return 3
|
||||
return 4
|
||||
|
||||
|
||||
impls: dict["str | types.PositionEncodingKind | None", UnitCounter] = {
|
||||
types.PositionEncodingKind.Utf8: Utf8(),
|
||||
types.PositionEncodingKind.Utf16: Utf16(),
|
||||
types.PositionEncodingKind.Utf32: Utf32(),
|
||||
}
|
||||
|
||||
|
||||
class PositionCodec:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -33,39 +132,17 @@ class PositionCodec:
|
||||
] = types.PositionEncodingKind.Utf16,
|
||||
):
|
||||
self.encoding = encoding
|
||||
self.impl = impls.get(encoding, Utf16())
|
||||
|
||||
@classmethod
|
||||
def is_char_beyond_multilingual_plane(cls, char: str) -> bool:
|
||||
return ord(char) > 0xFFFF
|
||||
def __repr__(self):
|
||||
return f"<{self.__class__.__name__}, encoding {self.encoding}>"
|
||||
|
||||
def utf16_unit_offset(self, chars: str):
|
||||
"""
|
||||
Calculate the number of characters which need two utf-16 code units.
|
||||
|
||||
Arguments:
|
||||
chars (str): The string to count occurrences of utf-16 code units for.
|
||||
"""
|
||||
return sum(self.is_char_beyond_multilingual_plane(ch) for ch in chars)
|
||||
|
||||
def client_num_units(self, chars: str):
|
||||
"""
|
||||
Calculate the length of `str` in client-supported UTF-[32|16|8] code units.
|
||||
|
||||
Arguments:
|
||||
chars (str): The string to return the length in UTF-[32|16|8] code units for.
|
||||
"""
|
||||
utf32_units = len(chars)
|
||||
if self.encoding == types.PositionEncodingKind.Utf32:
|
||||
return utf32_units
|
||||
|
||||
if self.encoding == types.PositionEncodingKind.Utf8:
|
||||
return utf32_units + (self.utf16_unit_offset(chars) * 2)
|
||||
|
||||
return utf32_units + self.utf16_unit_offset(chars)
|
||||
def client_num_units(self, string: str):
|
||||
return self.impl.num_units(string)
|
||||
|
||||
def position_from_client_units(
|
||||
self, lines: List[str], position: types.Position
|
||||
) -> types.Position:
|
||||
self, lines: Sequence[str], position: types.Position
|
||||
) -> ServerTextPosition:
|
||||
"""
|
||||
Convert the position.character from UTF-[32|16|8] code units to UTF-32.
|
||||
|
||||
@@ -84,7 +161,7 @@ class PositionCodec:
|
||||
see: https://github.com/microsoft/language-server-protocol/issues/376
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
lines (sequence):
|
||||
The content of the document which the position refers to.
|
||||
position (Position):
|
||||
The line and character offset in UTF-[32|16|8] code units.
|
||||
@@ -93,59 +170,42 @@ class PositionCodec:
|
||||
The position with `character` being converted to UTF-32 code units.
|
||||
"""
|
||||
if len(lines) == 0:
|
||||
return types.Position(0, 0)
|
||||
return ServerTextPosition(0, 0)
|
||||
if position.line >= len(lines):
|
||||
return types.Position(len(lines) - 1, self.client_num_units(lines[-1]))
|
||||
return ServerTextPosition(len(lines) - 1, self.impl.num_units(lines[-1]))
|
||||
|
||||
_line = lines[position.line]
|
||||
_line = _line.replace("\r\n", "\n") # TODO: it's a bit of a hack
|
||||
_client_len = self.client_num_units(_line)
|
||||
_utf32_len = len(_line)
|
||||
_client_len = self.impl.num_units(_line)
|
||||
|
||||
if _client_len == 0:
|
||||
return types.Position(position.line, 0)
|
||||
return ServerTextPosition(position.line, 0)
|
||||
|
||||
_client_end_of_line = self.client_num_units(_line)
|
||||
if position.character > _client_end_of_line:
|
||||
position.character = _client_end_of_line - 1
|
||||
if position.character > _client_len:
|
||||
position.character = _client_len - 1
|
||||
|
||||
_client_index = 0
|
||||
client_position = 0
|
||||
utf32_index = 0
|
||||
while True:
|
||||
_is_searching_queried_position = _client_index < position.character
|
||||
_is_before_end_of_line = utf32_index < _utf32_len
|
||||
_is_searching_for_position = (
|
||||
_is_searching_queried_position and _is_before_end_of_line
|
||||
)
|
||||
if not _is_searching_for_position:
|
||||
for c in _line:
|
||||
if client_position >= position.character:
|
||||
break
|
||||
|
||||
_current_char = _line[utf32_index]
|
||||
_is_double_width = PositionCodec.is_char_beyond_multilingual_plane(
|
||||
_current_char
|
||||
)
|
||||
if _is_double_width:
|
||||
if self.encoding == types.PositionEncodingKind.Utf32:
|
||||
_client_index += 1
|
||||
if self.encoding == types.PositionEncodingKind.Utf8:
|
||||
_client_index += 4
|
||||
_client_index += 2
|
||||
else:
|
||||
_client_index += 1
|
||||
client_position += self.impl.code_units_for_char(c)
|
||||
utf32_index += 1
|
||||
|
||||
position = types.Position(line=position.line, character=utf32_index)
|
||||
return position
|
||||
if client_position < position.character:
|
||||
utf32_index = len(_line)
|
||||
|
||||
return ServerTextPosition(line=position.line, character=utf32_index)
|
||||
|
||||
def position_to_client_units(
|
||||
self, lines: List[str], position: types.Position
|
||||
self, lines: Sequence[str], position: "ServerTextPosition | types.Position"
|
||||
) -> types.Position:
|
||||
"""
|
||||
Convert the position.character from its internal UTF-32 representation
|
||||
to client-supported UTF-[32|16|8] code units.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
lines (sequence):
|
||||
The content of the document which the position refers to.
|
||||
position (Position):
|
||||
The line and character offset in UTF-32 code units.
|
||||
@@ -154,9 +214,7 @@ class PositionCodec:
|
||||
The position with `character` being converted to UTF-[32|16|8] code units.
|
||||
"""
|
||||
try:
|
||||
character = self.client_num_units(
|
||||
lines[position.line][: position.character]
|
||||
)
|
||||
character = self.impl.num_units(lines[position.line][: position.character])
|
||||
return types.Position(
|
||||
line=position.line,
|
||||
character=character,
|
||||
@@ -165,13 +223,13 @@ class PositionCodec:
|
||||
return types.Position(line=len(lines), character=0)
|
||||
|
||||
def range_from_client_units(
|
||||
self, lines: List[str], range: types.Range
|
||||
) -> types.Range:
|
||||
self, lines: Sequence[str], range: types.Range
|
||||
) -> ServerTextRange:
|
||||
"""
|
||||
Convert range.[start|end].character from UTF-[32|16|8] code units to UTF-32.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
lines (sequence):
|
||||
The content of the document which the range refers to.
|
||||
range (Range):
|
||||
The line and character offset in UTF-[32|16|8] code units.
|
||||
@@ -179,26 +237,25 @@ class PositionCodec:
|
||||
Returns:
|
||||
The range with `character` offsets being converted to UTF-32 code units.
|
||||
"""
|
||||
range_new = types.Range(
|
||||
return ServerTextRange(
|
||||
start=self.position_from_client_units(lines, range.start),
|
||||
end=self.position_from_client_units(lines, range.end),
|
||||
)
|
||||
return range_new
|
||||
|
||||
def range_to_client_units(
|
||||
self, lines: List[str], range: types.Range
|
||||
self, lines: Sequence[str], range: "ServerTextRange | types.Range"
|
||||
) -> types.Range:
|
||||
"""
|
||||
Convert range.[start|end].character from UTF-32 to UTF-[32|16|8] code units.
|
||||
|
||||
Arguments:
|
||||
lines (list):
|
||||
lines (sequence):
|
||||
The content of the document which the range refers to.
|
||||
range (Range):
|
||||
The line and character offset in code units.
|
||||
The line and character offset in code points.
|
||||
|
||||
Returns:
|
||||
The range with `character` offsets being converted to UTF-[32|16|8] code units.
|
||||
The range with `character` offsets converted to UTF-[32|16|8] code units.
|
||||
"""
|
||||
return types.Range(
|
||||
start=self.position_to_client_units(lines, range.start),
|
||||
|
||||
@@ -19,13 +19,14 @@
|
||||
import io
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
from typing import List, Optional, Pattern
|
||||
from typing import Optional, Pattern, Sequence
|
||||
|
||||
from lsprotocol import types
|
||||
|
||||
from pygls.uris import to_fs_path
|
||||
from .position_codec import PositionCodec
|
||||
from pygls.uris import urlparse, to_fs_path
|
||||
from .position_codec import PositionCodec, ServerTextPosition, ServerTextRange
|
||||
|
||||
# TODO: this is not the best e.g. we capture numbers
|
||||
RE_END_WORD = re.compile("^[A-Za-z_0-9]*")
|
||||
@@ -47,9 +48,10 @@ class TextDocument(object):
|
||||
):
|
||||
self.uri = uri
|
||||
self.version = version
|
||||
path = to_fs_path(uri)
|
||||
if path is None:
|
||||
raise Exception("`path` cannot be None")
|
||||
|
||||
if (path := to_fs_path(uri)) is None:
|
||||
_, _, path, *_ = urlparse(uri)
|
||||
|
||||
self.path = path
|
||||
self.language_id = language_id
|
||||
self.filename: Optional[str] = os.path.basename(self.path)
|
||||
@@ -73,7 +75,7 @@ class TextDocument(object):
|
||||
return self._position_codec
|
||||
|
||||
def _apply_incremental_change(
|
||||
self, change: types.TextDocumentContentChangeEvent_Type1
|
||||
self, change: types.TextDocumentContentChangePartial
|
||||
) -> None:
|
||||
"""Apply an ``Incremental`` text change to the document"""
|
||||
lines = self.lines
|
||||
@@ -142,7 +144,7 @@ class TextDocument(object):
|
||||
content update client requests in the pygls Python library.
|
||||
|
||||
"""
|
||||
if isinstance(change, types.TextDocumentContentChangeEvent_Type1):
|
||||
if isinstance(change, types.TextDocumentContentChangePartial):
|
||||
if self._is_sync_kind_incremental:
|
||||
self._apply_incremental_change(change)
|
||||
return
|
||||
@@ -161,26 +163,101 @@ class TextDocument(object):
|
||||
self._apply_full_change(change)
|
||||
|
||||
@property
|
||||
def lines(self) -> List[str]:
|
||||
return self.source.splitlines(True)
|
||||
def lines(self) -> Sequence[str]:
|
||||
return tuple(self.source.splitlines(True))
|
||||
|
||||
def offset_at_server_position(self, server_position: ServerTextPosition) -> int:
|
||||
"""
|
||||
Convert server_position to an index into self.source.
|
||||
|
||||
The index is the number of code points preceding the client_position in self.source.
|
||||
"""
|
||||
row, col = server_position.line, server_position.character
|
||||
return col + sum(len(line) for line in self.lines[:row])
|
||||
|
||||
def offset_at_position(self, client_position: types.Position) -> int:
|
||||
"""Return the character offset pointed at by the given client_position."""
|
||||
"""
|
||||
Convert client_position to an index into self.source.
|
||||
|
||||
The index is the number of code points preceding the client_position in self.source.
|
||||
|
||||
Example in a code action request handler:
|
||||
selected_string = document.source[
|
||||
document.offset_at_position(params.range.start) : document.offset_at_position(params.range.end)
|
||||
]
|
||||
"""
|
||||
lines = self.lines
|
||||
server_position = self._position_codec.position_from_client_units(
|
||||
lines, client_position
|
||||
)
|
||||
row, col = server_position.line, server_position.character
|
||||
return col + sum(
|
||||
self._position_codec.client_num_units(line) for line in lines[:row]
|
||||
)
|
||||
return self.offset_at_server_position(server_position)
|
||||
|
||||
def server_position_at_offset(self, offset: int) -> ServerTextPosition:
|
||||
"""
|
||||
Convert a numeric character offset (index into self.source) into a line-column position.
|
||||
"""
|
||||
remaining_offset = offset
|
||||
for lineno, line in enumerate(self.lines):
|
||||
if remaining_offset < len(line):
|
||||
return ServerTextPosition(lineno, remaining_offset)
|
||||
remaining_offset -= len(line)
|
||||
# The desired position is beyond the end of the last line.
|
||||
return ServerTextPosition(lineno + 1, 0)
|
||||
|
||||
def client_position_at_offset(self, offset: int) -> types.Position:
|
||||
"""
|
||||
Convert a numeric character offset (index into self.source) into a line-column position in client units.
|
||||
"""
|
||||
return self.position_to_client_units(self.server_position_at_offset(offset))
|
||||
|
||||
def range_from_client_units(self, range: types.Range) -> ServerTextRange:
|
||||
"""
|
||||
Convert a range from client units into code points, suitable for indexing into `self.lines`.
|
||||
"""
|
||||
return self.position_codec.range_from_client_units(self.lines, range)
|
||||
|
||||
def position_from_client_units(
|
||||
self, position: types.Position
|
||||
) -> ServerTextPosition:
|
||||
"""
|
||||
Convert a position from client units into code points, suitable for indexing into `self.lines`.
|
||||
"""
|
||||
return self.position_codec.position_from_client_units(self.lines, position)
|
||||
|
||||
def range_to_client_units(self, range: ServerTextRange) -> types.Range:
|
||||
"""
|
||||
Convert a range from code points into client units, suitable for sending to the client.
|
||||
"""
|
||||
return self.position_codec.range_to_client_units(self.lines, range)
|
||||
|
||||
def position_to_client_units(self, position: ServerTextPosition) -> types.Position:
|
||||
"""
|
||||
Convert a position from code points into client units, suitable for sending to the client.
|
||||
"""
|
||||
return self.position_codec.position_to_client_units(self.lines, position)
|
||||
|
||||
def text_in_client_range(self, range: types.Range) -> str:
|
||||
"""
|
||||
Given a range in client units, return the text in this range in this document.
|
||||
"""
|
||||
return self.text_in_server_range(self.range_from_client_units(range))
|
||||
|
||||
def text_in_server_range(self, range: ServerTextRange) -> str:
|
||||
"""
|
||||
Given a range in server units, return the text in this range in this document.
|
||||
"""
|
||||
return self.source[
|
||||
self.offset_at_server_position(
|
||||
range.start
|
||||
) : self.offset_at_server_position(range.end)
|
||||
]
|
||||
|
||||
@property
|
||||
def source(self) -> str:
|
||||
if self._source is None:
|
||||
with io.open(self.path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
return self._source
|
||||
if self._source is None and self.path is not None:
|
||||
return pathlib.Path(self.path).read_text(encoding="utf-8")
|
||||
|
||||
return self._source or ""
|
||||
|
||||
def word_at_position(
|
||||
self,
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import warnings
|
||||
from typing import Dict, List, Optional, Union
|
||||
from typing import Dict, Optional, Sequence, Union
|
||||
from urllib.parse import unquote
|
||||
|
||||
from lsprotocol import types
|
||||
from lsprotocol.types import (
|
||||
@@ -40,7 +40,7 @@ class Workspace(object):
|
||||
self,
|
||||
root_uri: Optional[str],
|
||||
sync_kind: TextDocumentSyncKind = TextDocumentSyncKind.Incremental,
|
||||
workspace_folders: Optional[List[WorkspaceFolder]] = None,
|
||||
workspace_folders: Optional[Sequence[WorkspaceFolder]] = None,
|
||||
position_encoding: Optional[
|
||||
Union[PositionEncodingKind, str]
|
||||
] = PositionEncodingKind.Utf16,
|
||||
@@ -48,10 +48,7 @@ class Workspace(object):
|
||||
self._root_uri = root_uri
|
||||
if self._root_uri is not None:
|
||||
self._root_uri_scheme = uri_scheme(self._root_uri)
|
||||
root_path = to_fs_path(self._root_uri)
|
||||
if root_path is None:
|
||||
raise Exception("Couldn't get `root_path` from `root_uri`")
|
||||
self._root_path = root_path
|
||||
self._root_path = to_fs_path(self._root_uri)
|
||||
else:
|
||||
self._root_path = None
|
||||
self._sync_kind = sync_kind
|
||||
@@ -94,17 +91,7 @@ class Workspace(object):
|
||||
)
|
||||
|
||||
def add_folder(self, folder: WorkspaceFolder):
|
||||
self._folders[folder.uri] = folder
|
||||
|
||||
@property
|
||||
def documents(self):
|
||||
warnings.warn(
|
||||
"'workspace.documents' has been deprecated, use "
|
||||
"'workspace.text_documents' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.text_documents
|
||||
self._folders[unquote(folder.uri)] = folder
|
||||
|
||||
@property
|
||||
def notebook_documents(self):
|
||||
@@ -141,10 +128,10 @@ class Workspace(object):
|
||||
The requested notebook document if found, ``None`` otherwise.
|
||||
"""
|
||||
if notebook_uri is not None:
|
||||
return self._notebook_documents.get(notebook_uri)
|
||||
return self._notebook_documents.get(unquote(notebook_uri))
|
||||
|
||||
if cell_uri is not None:
|
||||
notebook_uri = self._cell_in_notebook.get(cell_uri)
|
||||
notebook_uri = self._cell_in_notebook.get(unquote(cell_uri))
|
||||
if notebook_uri is None:
|
||||
return None
|
||||
|
||||
@@ -159,18 +146,25 @@ class Workspace(object):
|
||||
|
||||
See https://github.com/Microsoft/language-server-protocol/issues/177
|
||||
"""
|
||||
return self._text_documents.get(doc_uri) or self._create_text_document(doc_uri)
|
||||
return self._text_documents.get(unquote(doc_uri)) or self._create_text_document(
|
||||
doc_uri
|
||||
)
|
||||
|
||||
def is_local(self):
|
||||
return (
|
||||
self._root_uri_scheme == "" or self._root_uri_scheme == "file"
|
||||
) and os.path.exists(self._root_path)
|
||||
|
||||
if self._root_uri_scheme not in {"", "file"}:
|
||||
return False
|
||||
|
||||
if (path := self._root_path) is None:
|
||||
return False
|
||||
|
||||
return os.path.exists(path)
|
||||
|
||||
def put_notebook_document(self, params: types.DidOpenNotebookDocumentParams):
|
||||
notebook = params.notebook_document
|
||||
|
||||
# Create a fresh instance to ensure our copy cannot be accidentally modified.
|
||||
self._notebook_documents[notebook.uri] = copy.deepcopy(notebook)
|
||||
self._notebook_documents[unquote(notebook.uri)] = copy.deepcopy(notebook)
|
||||
|
||||
for cell_document in params.cell_text_documents:
|
||||
self.put_text_document(cell_document, notebook_uri=notebook.uri)
|
||||
@@ -193,7 +187,7 @@ class Workspace(object):
|
||||
"""
|
||||
doc_uri = text_document.uri
|
||||
|
||||
self._text_documents[doc_uri] = self._create_text_document(
|
||||
self._text_documents[unquote(doc_uri)] = self._create_text_document(
|
||||
doc_uri,
|
||||
source=text_document.text,
|
||||
version=text_document.version,
|
||||
@@ -201,23 +195,23 @@ class Workspace(object):
|
||||
)
|
||||
|
||||
if notebook_uri:
|
||||
self._cell_in_notebook[doc_uri] = notebook_uri
|
||||
self._cell_in_notebook[unquote(doc_uri)] = unquote(notebook_uri)
|
||||
|
||||
def remove_notebook_document(self, params: types.DidCloseNotebookDocumentParams):
|
||||
notebook_uri = params.notebook_document.uri
|
||||
self._notebook_documents.pop(notebook_uri, None)
|
||||
self._notebook_documents.pop(unquote(notebook_uri), None)
|
||||
|
||||
for cell_document in params.cell_text_documents:
|
||||
self.remove_text_document(cell_document.uri)
|
||||
|
||||
def remove_text_document(self, doc_uri: str):
|
||||
self._text_documents.pop(doc_uri, None)
|
||||
self._cell_in_notebook.pop(doc_uri, None)
|
||||
self._text_documents.pop(unquote(doc_uri), None)
|
||||
self._cell_in_notebook.pop(unquote(doc_uri), None)
|
||||
|
||||
def remove_folder(self, folder_uri: str):
|
||||
self._folders.pop(folder_uri, None)
|
||||
self._folders.pop(unquote(folder_uri), None)
|
||||
try:
|
||||
del self._folders[folder_uri]
|
||||
del self._folders[unquote(folder_uri)]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
@@ -231,7 +225,7 @@ class Workspace(object):
|
||||
|
||||
def update_notebook_document(self, params: types.DidChangeNotebookDocumentParams):
|
||||
uri = params.notebook_document.uri
|
||||
notebook = self._notebook_documents[uri]
|
||||
notebook = self._notebook_documents[unquote(uri)]
|
||||
notebook.version = params.notebook_document.version
|
||||
|
||||
if params.change.metadata:
|
||||
@@ -283,41 +277,5 @@ class Workspace(object):
|
||||
change: types.TextDocumentContentChangeEvent,
|
||||
):
|
||||
doc_uri = text_doc.uri
|
||||
self._text_documents[doc_uri].apply_change(change)
|
||||
self._text_documents[doc_uri].version = text_doc.version
|
||||
|
||||
def get_document(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"'workspace.get_document' has been deprecated, use "
|
||||
"'workspace.get_text_document' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.get_text_document(*args, **kwargs)
|
||||
|
||||
def remove_document(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"'workspace.remove_document' has been deprecated, use "
|
||||
"'workspace.remove_text_document' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.remove_text_document(*args, **kwargs)
|
||||
|
||||
def put_document(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"'workspace.put_document' has been deprecated, use "
|
||||
"'workspace.put_text_document' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.put_text_document(*args, **kwargs)
|
||||
|
||||
def update_document(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"'workspace.update_document' has been deprecated, use "
|
||||
"'workspace.update_text_document' instead",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return self.update_text_document(*args, **kwargs)
|
||||
self._text_documents[unquote(doc_uri)].apply_change(change)
|
||||
self._text_documents[unquote(doc_uri)].version = text_doc.version
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: tclint
|
||||
Version: 0.8.0
|
||||
Version: 0.9.0
|
||||
Summary: A CLI utility for linting and analyzing Tcl code.
|
||||
Author-email: Noah Moroze <me@noahmoroze.com>
|
||||
License: MIT License
|
||||
+15
-15
@@ -1,17 +1,17 @@
|
||||
bin/tclfmt.exe,sha256=5o9LOeyU_bfji3TOEQ2sDsjitSg-ZAQISI2LSX9MrCg,47104
|
||||
bin/tclint.exe,sha256=0rGm-shW-XmBQ_KQkCJvGa9Qe1GCHvErXuL6C1D_kjc,47104
|
||||
bin/tclsp.exe,sha256=K9fidz6r40fs43dAcfHVRE8RkH7W0dtQxX42EpMBp6I,47104
|
||||
tclint-0.8.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
tclint-0.8.0.dist-info/METADATA,sha256=QiY1JEN7FiNRwRYDcqb2OqYucGyVl5ddIZIm1vC3WQ0,4015
|
||||
tclint-0.8.0.dist-info/RECORD,,
|
||||
tclint-0.8.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
tclint-0.8.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
||||
tclint-0.8.0.dist-info/entry_points.txt,sha256=K2vcacREnbnOEZvE7-QfzsmF9vazrD7HgM2WmTl_Wgs,161
|
||||
tclint-0.8.0.dist-info/licenses/LICENSE,sha256=PGii0wulXro34f25070gTG-JRGM-TAyXyKVObnNJU68,1055
|
||||
tclint-0.8.0.dist-info/top_level.txt,sha256=_cnnEELsoakzUgD9HHdivUoNbpKG9tUGznWvbGLmHQM,7
|
||||
tclint-0.9.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
tclint-0.9.0.dist-info/METADATA,sha256=apRNFWLq_33SxthxLCUFZfaGZjz2AGucxFQYS7782u4,4015
|
||||
tclint-0.9.0.dist-info/RECORD,,
|
||||
tclint-0.9.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
tclint-0.9.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
|
||||
tclint-0.9.0.dist-info/entry_points.txt,sha256=K2vcacREnbnOEZvE7-QfzsmF9vazrD7HgM2WmTl_Wgs,161
|
||||
tclint-0.9.0.dist-info/licenses/LICENSE,sha256=PGii0wulXro34f25070gTG-JRGM-TAyXyKVObnNJU68,1055
|
||||
tclint-0.9.0.dist-info/top_level.txt,sha256=_cnnEELsoakzUgD9HHdivUoNbpKG9tUGznWvbGLmHQM,7
|
||||
tclint/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
tclint/__main__.py,sha256=Bp_AE4xH3XNsUq3EHISWdLOFUbdvwnZ6YSf3F1NIqWU,65
|
||||
tclint/_version.py,sha256=Rttl-BDadtcW1QzGnNffCWA_Wc9mUKDMOBPZp--Mnsc,704
|
||||
tclint/_version.py,sha256=kSrltAJmP76cnQArqSQbTZHSYAIDCyFeWzh2SIC99Q4,520
|
||||
tclint/checks.py,sha256=wr29RqWd8zuF9gy5YBWjkZs1WQ91pj9ipkfdsxVuDvg,7863
|
||||
tclint/cli/resolver.py,sha256=kDvSvbQiqk_Dhd7zBkiUrSDkplr02uhdZ6k3CFj6geg,3750
|
||||
tclint/cli/tclfmt.py,sha256=8_d7TGmV8gU6VfRhxjFxmlTDt1zBmKtk3016CvdnpXo,6949
|
||||
@@ -19,16 +19,16 @@ tclint/cli/tclint.py,sha256=CTmchC3iL-dEM0paeRVEKJf79cEnUh7_QAQ7c17UAd0,4548
|
||||
tclint/cli/tclsp.py,sha256=TvBA0K2_6CkRKgHXiTSFIDcNykOhH-qHOox7qQABMaY,18051
|
||||
tclint/cli/utils.py,sha256=qa1tJ3St0uC9f4yAKCC4atglu8R3fZYVxu-KZ0dqTAo,1926
|
||||
tclint/commands/__init__.py,sha256=u0zk3di692M1QSZGvOEM7ZYzKBB7r950ytAS8cxmEo0,113
|
||||
tclint/commands/builtin.py,sha256=XO0z4lX6rbEIpjZIEJ5-nGc0mX8a8VZxpAmOTCNIGSA,41587
|
||||
tclint/commands/checks.py,sha256=7SkderCq_M36XbOJbNw5ZDKt1XXlmT4ErAG4K8eY4-k,14133
|
||||
tclint/commands/builtin.py,sha256=WFgDdZzgTogALwbLAHgqt2fGOY1rX5Ukyzc4yCvvbQ4,41938
|
||||
tclint/commands/checks.py,sha256=TOgJ78ngCgkvOCBhHnbsz4Q67R9hWeQAdaNNNJ8L5KA,16757
|
||||
tclint/commands/plugins.py,sha256=Q-FqS3h-D6m0lWzdMH5WFSEF19Ol2vTgPySb2ydCg5Q,5407
|
||||
tclint/commands/schema.py,sha256=nzgPuyQviWZYn1FZYzKs1kGixvIu0RF2GUoXzd3iaLg,1123
|
||||
tclint/commands/schema.py,sha256=_YEtPQfyV4UEO_CRPSYR6zGyB1wgJdfB32DmVNWCq9o,1150
|
||||
tclint/comments.py,sha256=kCfSdnoSVofvNqs4zxnOrg45shlbVvu-e2hKia6EXhk,3047
|
||||
tclint/config.py,sha256=d4QUS9XwZuBNGv9ggz0QpD7jbAXmWVBrenKzGjexx-E,15285
|
||||
tclint/format.py,sha256=YXX6HRy_AOhcBcJL3Ofo_lAdHtjPAGd0piiVTaGKZzg,21753
|
||||
tclint/format.py,sha256=5OgSEki7w7_w1HQ9rCiksTKDX2Ni41ZXTpA7hQ42k9M,23717
|
||||
tclint/lexer.py,sha256=JnDcYCeT8wCGV1kqjXRVjQDUBw48lWBx_5g_4dt-JLc,6069
|
||||
tclint/parser.py,sha256=sl7bkhKN253VpBALin8Bd5FtNxRfi5cEy7NFYtvzPpo,28822
|
||||
tclint/plugins/expect.py,sha256=5fbrwJK3108w20Ib7pvfJhgVNpSW3FTAovlhbspDsIQ,1912
|
||||
tclint/symbol_table.py,sha256=vSbE5Cbgb-_ppdjsXOR4WBNqPQXxhf0k-pZMOFe5vU8,1557
|
||||
tclint/syntax_tree.py,sha256=kkMtbxUkD1EcLT2bXsV5foDF2W6zAYoxzu-dPZGnBbs,13033
|
||||
tclint/symbol_table.py,sha256=jqZli_len0f1nlSi7GF-u45FfK-GvBgXbluBYXCrzLY,1612
|
||||
tclint/syntax_tree.py,sha256=msFx3ihey1EtCzaYtZS3ge0cYOECITuHBhyKoUQp1js,13020
|
||||
tclint/violations.py,sha256=q9y0j1btlMLhy1Pn6HP0u1347d7SF4t3ZVZpIxH3MVc,1232
|
||||
@@ -1,5 +1,5 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: setuptools (82.0.1)
|
||||
Generator: setuptools (83.0.0)
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
# file generated by setuptools-scm
|
||||
# file generated by vcs-versioning
|
||||
# don't change, don't track in version control
|
||||
from __future__ import annotations
|
||||
|
||||
__all__ = [
|
||||
"__version__",
|
||||
@@ -10,25 +11,14 @@ __all__ = [
|
||||
"commit_id",
|
||||
]
|
||||
|
||||
TYPE_CHECKING = False
|
||||
if TYPE_CHECKING:
|
||||
from typing import Tuple
|
||||
from typing import Union
|
||||
|
||||
VERSION_TUPLE = Tuple[Union[int, str], ...]
|
||||
COMMIT_ID = Union[str, None]
|
||||
else:
|
||||
VERSION_TUPLE = object
|
||||
COMMIT_ID = object
|
||||
|
||||
version: str
|
||||
__version__: str
|
||||
__version_tuple__: VERSION_TUPLE
|
||||
version_tuple: VERSION_TUPLE
|
||||
commit_id: COMMIT_ID
|
||||
__commit_id__: COMMIT_ID
|
||||
__version_tuple__: tuple[int | str, ...]
|
||||
version_tuple: tuple[int | str, ...]
|
||||
commit_id: str | None
|
||||
__commit_id__: str | None
|
||||
|
||||
__version__ = version = '0.8.0'
|
||||
__version_tuple__ = version_tuple = (0, 8, 0)
|
||||
__version__ = version = '0.9.0'
|
||||
__version_tuple__ = version_tuple = (0, 9, 0)
|
||||
|
||||
__commit_id__ = commit_id = None
|
||||
|
||||
@@ -573,7 +573,16 @@ def _package_ifneeded(args, parser):
|
||||
|
||||
|
||||
def _proc(args, parser):
|
||||
if len(args) != 3:
|
||||
required_args = ["name", "args", "body"]
|
||||
if len(args) < len(required_args):
|
||||
missing_args = ", ".join(required_args[len(args) :])
|
||||
raise CommandArgError(
|
||||
"missing required"
|
||||
f" argument{'s' if len(args) < len(required_args) - 1 else ''} for proc:"
|
||||
f" {missing_args}"
|
||||
)
|
||||
|
||||
if len(args) > len(required_args):
|
||||
raise CommandArgError(f"wrong # of args to proc: got {len(args)}, expected 3")
|
||||
|
||||
# Parse args as list, then iterate over each item to parse arg specifier lists and
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from collections.abc import Callable, Iterable
|
||||
from difflib import get_close_matches
|
||||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from tclint.syntax_tree import ArgExpansion, BareWord, BracedWord, Node, QuotedWord
|
||||
@@ -18,6 +19,33 @@ class CommandArgError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def get_suggestion(value: str | None, candidates: Iterable[str]) -> Optional[str]:
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
unique_candidates = sorted({candidate for candidate in candidates if candidate})
|
||||
if value in unique_candidates:
|
||||
unique_candidates.remove(value)
|
||||
|
||||
if not unique_candidates:
|
||||
return None
|
||||
|
||||
cutoff = 0.85 if len(value) <= 3 else 0.75
|
||||
matches = get_close_matches(value, unique_candidates, n=1, cutoff=cutoff)
|
||||
if not matches:
|
||||
return None
|
||||
|
||||
return matches[0]
|
||||
|
||||
|
||||
def _did_you_mean_suffix(value: str | None, candidates: Iterable[str]) -> str:
|
||||
suggestion = get_suggestion(value, candidates)
|
||||
if suggestion is None:
|
||||
return ""
|
||||
|
||||
return f"; did you mean {suggestion}?"
|
||||
|
||||
|
||||
def arg_count(args: list[Node], parser: Parser) -> tuple[int, bool]:
|
||||
"""Returns the number of arguments in args, taking {*} into account.
|
||||
|
||||
@@ -83,6 +111,9 @@ def check_count(command, min=None, max=None):
|
||||
|
||||
|
||||
def eval(args: list[Node], parser: Parser, command: str) -> list[Node]:
|
||||
if len(args) == 1:
|
||||
return [parser.parse_script(args[0])]
|
||||
|
||||
if len(args) > 1 and any(isinstance(arg, (QuotedWord, BracedWord)) for arg in args):
|
||||
# Slightly odd restriction, but our syntax tree doesn't have a great way
|
||||
# to handle this case. We require each command argument to correspond to
|
||||
@@ -174,10 +205,19 @@ def check_arg_spec(
|
||||
mapping = map_positionals(positionals, arg_spec["positionals"], command)
|
||||
args = list(args)
|
||||
for arg_i, map_to_spec in zip(positional_args, mapping):
|
||||
arg = args[arg_i]
|
||||
|
||||
if _positional_has_type("script", arg_spec, map_to_spec):
|
||||
args[arg_i] = parser.parse_script(args[arg_i])
|
||||
args[arg_i] = parser.parse_script(arg)
|
||||
elif _positional_has_type("expression", arg_spec, map_to_spec):
|
||||
args[arg_i] = parser.parse_expression(args[arg_i])
|
||||
args[arg_i] = parser.parse_expression(arg)
|
||||
elif len(map_to_spec) == 1:
|
||||
positional_spec = arg_spec["positionals"][map_to_spec[0]]
|
||||
_validate_value(
|
||||
arg,
|
||||
positional_spec["value"],
|
||||
f"{command} {positional_spec['name']}",
|
||||
)
|
||||
|
||||
return args
|
||||
|
||||
@@ -201,12 +241,18 @@ def dispatch_subcommands(
|
||||
if "" in spec:
|
||||
return check_command(command, args, parser, spec[""])
|
||||
|
||||
valid_subcommands = [name for name in spec.keys() if name != ""]
|
||||
|
||||
if subcommand is not None:
|
||||
msg = f"invalid subcommand for {command}: got {subcommand}"
|
||||
suggestion = _did_you_mean_suffix(subcommand, valid_subcommands)
|
||||
else:
|
||||
msg = f"no subcommand provided for {command}"
|
||||
suggestion = ""
|
||||
|
||||
raise CommandArgError(f"{msg}, expected one of {', '.join(spec.keys())}")
|
||||
raise CommandArgError(
|
||||
f"{msg}, expected one of {', '.join(valid_subcommands)}{suggestion}"
|
||||
)
|
||||
|
||||
|
||||
def map_switches(
|
||||
@@ -250,17 +296,23 @@ def map_switches(
|
||||
continue
|
||||
|
||||
if contents in switches:
|
||||
if contents in mapped and not switches[contents]["repeated"]:
|
||||
switch_spec = switches[contents]
|
||||
|
||||
if contents in mapped and not switch_spec["repeated"]:
|
||||
raise CommandArgError(
|
||||
f"duplicate argument for {command_name}: {contents}"
|
||||
)
|
||||
if switches[contents]["value"]:
|
||||
if switch_spec["value"]:
|
||||
arg_i += 1
|
||||
if arg_i > len(args):
|
||||
expected = _switch_value_description(switch_spec)
|
||||
raise CommandArgError(
|
||||
f"invalid arguments for {command_name}: expected value after"
|
||||
f" {contents}"
|
||||
f"invalid arguments for {command_name}: expected"
|
||||
f" {expected} after {contents}"
|
||||
)
|
||||
_validate_value(
|
||||
args[arg_i - 1], switch_spec["value"], f"{command_name} {contents}"
|
||||
)
|
||||
mapped.add(contents)
|
||||
continue
|
||||
|
||||
@@ -281,11 +333,52 @@ def map_switches(
|
||||
f" {', '.join(prefix_matches)}"
|
||||
)
|
||||
|
||||
raise CommandArgError(f"unrecognized argument for {command_name}: {contents}")
|
||||
raise CommandArgError(
|
||||
f"unrecognized argument for {command_name}: {contents}"
|
||||
f"{_did_you_mean_suffix(contents, switches.keys())}"
|
||||
)
|
||||
|
||||
return mapped, positional_args
|
||||
|
||||
|
||||
def _switch_value_description(switch_spec: dict) -> str:
|
||||
metavar = switch_spec.get("metavar")
|
||||
if metavar is not None:
|
||||
return metavar
|
||||
|
||||
value_spec = switch_spec.get("value")
|
||||
if value_spec is None:
|
||||
return "value"
|
||||
|
||||
value_type = value_spec.get("type")
|
||||
if value_type == "int":
|
||||
return "int value"
|
||||
|
||||
return "value"
|
||||
|
||||
|
||||
def _validate_value(arg: Node, value_spec: dict | None, context: str) -> None:
|
||||
if value_spec is None:
|
||||
return
|
||||
|
||||
contents = arg.contents
|
||||
if contents is None:
|
||||
return
|
||||
|
||||
value_type = value_spec.get("type")
|
||||
if value_type in {"any", "variadic", "script", "expression"}:
|
||||
return
|
||||
|
||||
if value_type == "int":
|
||||
try:
|
||||
int(contents, 0)
|
||||
return
|
||||
except ValueError as error:
|
||||
raise CommandArgError(
|
||||
f"invalid value for {context}: got {contents}, expected {value_type}"
|
||||
) from error
|
||||
|
||||
|
||||
def map_positionals(
|
||||
args: list[Node], spec: list[dict], command_name: str
|
||||
) -> list[list[int]]:
|
||||
|
||||
@@ -2,6 +2,15 @@ from collections.abc import Callable
|
||||
|
||||
from voluptuous import Optional, Or, Schema, Self
|
||||
|
||||
_switch_value = Or({"type": "any"}, {"type": "int"}, None)
|
||||
_positional_value = Or(
|
||||
{"type": "any"},
|
||||
{"type": "int"},
|
||||
{"type": "variadic"},
|
||||
{"type": "script"},
|
||||
{"type": "expression"},
|
||||
)
|
||||
|
||||
# Need to define this as a Schema with required=True to ensure that this requirement
|
||||
# persists through the Or in the main schema definition.
|
||||
_command_args = Schema(
|
||||
@@ -10,19 +19,14 @@ _command_args = Schema(
|
||||
{
|
||||
"name": str,
|
||||
"required": bool,
|
||||
"value": Or(
|
||||
{"type": "any"},
|
||||
{"type": "variadic"},
|
||||
{"type": "script"},
|
||||
{"type": "expression"},
|
||||
),
|
||||
"value": _positional_value,
|
||||
}
|
||||
],
|
||||
Optional("switches", default={}): {
|
||||
Optional(str): {
|
||||
"required": bool,
|
||||
"repeated": bool,
|
||||
"value": Or({"type": "any"}, None),
|
||||
"value": _switch_value,
|
||||
Optional("metavar"): str,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,13 +69,15 @@ class Formatter:
|
||||
assert len(debug_char) == 1
|
||||
if space is None:
|
||||
space = self.opts.indent
|
||||
elif isinstance(space, int):
|
||||
space = space * " "
|
||||
if self.opts.debug_whitespace:
|
||||
# Enable this to return a string of debug_char.
|
||||
return len(space) * debug_char
|
||||
return space
|
||||
|
||||
def get_spaces_in_braces(self, space: tuple[int, int]):
|
||||
spaces_in_braces = self.space("A", " ") if self.opts.spaces_in_braces else ""
|
||||
spaces_in_braces = self.space("A", 1) if self.opts.spaces_in_braces else ""
|
||||
if not self.opts.balanced_spaces_in_braces:
|
||||
# No balancing.
|
||||
return spaces_in_braces
|
||||
@@ -88,7 +90,7 @@ class Formatter:
|
||||
if space[0] != -1 and space[1] == -1:
|
||||
# we've got empty braces. Keep "{}" and "{ }" as is, but normalize
|
||||
# more than one space to a single space.
|
||||
return min(space[0], 1) * self.space("B", " ")
|
||||
return min(space[0], 1) * self.space("B", 1)
|
||||
|
||||
# Normalize more than one space to a single space.
|
||||
before = min(space[0], 1)
|
||||
@@ -101,7 +103,7 @@ class Formatter:
|
||||
# Check that we have a balanced expression.
|
||||
assert before == after
|
||||
# Keep either "{1}" or "{ 1 }".
|
||||
return before * self.space("C", " ")
|
||||
return before * self.space("C", 1)
|
||||
|
||||
def _brace(self, lines: list[str], space: tuple[int, int]) -> list[str]:
|
||||
"""Format content between braces.
|
||||
@@ -307,6 +309,10 @@ class Formatter:
|
||||
|
||||
def format_script(self, script: Script, should_indent=True) -> list[str]:
|
||||
lines = self.format_script_contents(script)
|
||||
if not script.braced:
|
||||
# Script came from a non-braced word argument (e.g. plugin called
|
||||
# parse_script on a BareWord). Don't wrap in braces.
|
||||
return lines
|
||||
if script.pos[0] == script.end_pos[0]:
|
||||
space_before = -1
|
||||
space_after = -1
|
||||
@@ -327,7 +333,7 @@ class Formatter:
|
||||
and isinstance(script.children[0], Comment)
|
||||
and script.pos[0] == script.children[0].pos[0]
|
||||
):
|
||||
open_brace += self.space("D", " ") + lines[0]
|
||||
open_brace += self.space("D", 1) + lines[0]
|
||||
lines = lines[1:]
|
||||
|
||||
if should_indent:
|
||||
@@ -353,14 +359,26 @@ class Formatter:
|
||||
child_lines = self.format(child)
|
||||
|
||||
if last_line == child.pos[0]:
|
||||
formatted[-1] += self.space("F", " ")
|
||||
if self.opts.emacs and child_lines[0][-1] == "\\":
|
||||
base_indent = (len(formatted[-1])) * self.space("G", " ")
|
||||
else:
|
||||
base_indent = ""
|
||||
formatted[-1] += self.space("F", 1)
|
||||
base_indent = ""
|
||||
if self.opts.emacs:
|
||||
if child_lines[0][-1] == "\\":
|
||||
base_indent = (len(formatted[-1])) * self.space("G", 1)
|
||||
elif (isinstance(child, BracedExpression)) and child_lines[
|
||||
-1
|
||||
] == "}":
|
||||
child_lines[1:-1] = self._indent(
|
||||
child_lines[1:-1], self.space("V", len(formatted[-1]))
|
||||
)
|
||||
elif (isinstance(child, BracedExpression)) and child_lines[-1][
|
||||
-1
|
||||
] == "}":
|
||||
child_lines[1:] = self._indent(
|
||||
child_lines[1:], self.space("W", len(formatted[-1]))
|
||||
)
|
||||
formatted[-1] += child_lines[0]
|
||||
else:
|
||||
formatted[-1] += self.space("H", " ") + "\\"
|
||||
formatted[-1] += self.space("H", 1) + "\\"
|
||||
formatted.append(self.space("I") + child_lines[0])
|
||||
hanging_indent = True
|
||||
|
||||
@@ -383,13 +401,19 @@ class Formatter:
|
||||
formatted = []
|
||||
contents = self.format_script_contents(command_sub)
|
||||
if len(command_sub.children) > 1 and len(contents) > 1:
|
||||
formatted.append("[")
|
||||
formatted.extend(self._indent(contents, self.space("K")))
|
||||
formatted.append("]")
|
||||
if self.opts.emacs and command_sub.pos[0] == command_sub.children[0].pos[0]:
|
||||
formatted = contents
|
||||
formatted[0] = "[" + formatted[0]
|
||||
formatted[-1] = formatted[-1] + "]"
|
||||
formatted[1:] = self._indent(formatted[1:], self.space("U", 1))
|
||||
else:
|
||||
formatted.append("[")
|
||||
formatted.extend(self._indent(contents, self.space("K")))
|
||||
formatted.append("]")
|
||||
else:
|
||||
formatted.append("[" + contents[0])
|
||||
if self.opts.emacs:
|
||||
indent = self.space("L", " ")
|
||||
indent = self.space("L", 1)
|
||||
else:
|
||||
indent = ""
|
||||
formatted.extend(self._indent(contents[1:], indent))
|
||||
@@ -460,7 +484,7 @@ class Formatter:
|
||||
for child in list_node.children:
|
||||
if last_line is not None:
|
||||
if last_line == child.pos[0]:
|
||||
contents[-1] += self.space("M", " ")
|
||||
contents[-1] += self.space("M", 1)
|
||||
else:
|
||||
newlines = child.pos[0] - last_line
|
||||
newlines = min(newlines, 3)
|
||||
@@ -516,6 +540,29 @@ class Formatter:
|
||||
space_after = expr.end_pos[1] - expr.children[-1].end_pos[1] - 1
|
||||
return self._brace(formatted, (space_before, space_after))
|
||||
|
||||
if self.opts.emacs:
|
||||
pre = []
|
||||
post = []
|
||||
indent_first = 0
|
||||
indent_last = len(formatted)
|
||||
if expr.pos[0] == expr.children[0].pos[0]:
|
||||
formatted[0] = "{" + formatted[0]
|
||||
indent_first = 1
|
||||
else:
|
||||
pre = ["{"]
|
||||
if expr.end_pos[0] == expr.children[-1].end_pos[0]:
|
||||
formatted[-1] = formatted[-1] + "}"
|
||||
else:
|
||||
post = ["}"]
|
||||
formatted = (
|
||||
pre
|
||||
+ formatted[0:indent_first]
|
||||
+ self._indent(formatted[indent_first:indent_last], self.space("X", 1))
|
||||
+ formatted[indent_last:]
|
||||
+ post
|
||||
)
|
||||
return formatted
|
||||
|
||||
return ["{"] + self._indent(formatted, self.space("P")) + ["}"]
|
||||
|
||||
def format_paren_expression(self, expr) -> list[str]:
|
||||
@@ -556,7 +603,7 @@ class Formatter:
|
||||
if last.end_pos[0] != next.pos[0]:
|
||||
formatted.extend(lines)
|
||||
else:
|
||||
formatted[-1] += self.space("R", " ")
|
||||
formatted[-1] += self.space("R", 1)
|
||||
formatted[-1] += lines[0]
|
||||
formatted.extend(lines[1:])
|
||||
last = next
|
||||
@@ -585,7 +632,7 @@ class Formatter:
|
||||
formatted.extend(lines)
|
||||
else:
|
||||
if i > 0:
|
||||
formatted[-1] += self.space("S", " ")
|
||||
formatted[-1] += self.space("S", 1)
|
||||
formatted[-1] += lines[0]
|
||||
formatted.extend(lines[1:])
|
||||
last = child
|
||||
|
||||
@@ -13,6 +13,9 @@ class SymbolTable:
|
||||
def add_proc_definition(self, command: Command) -> None:
|
||||
"""Add definition of procedure"""
|
||||
# command holds the "proc" keyword, so the proc name is 1st argument
|
||||
if len(command.args) == 0:
|
||||
return
|
||||
|
||||
proc_name_node = command.args[0]
|
||||
proc_name = proc_name_node.contents
|
||||
if not proc_name:
|
||||
|
||||
@@ -253,7 +253,7 @@ class Node:
|
||||
class Script(Node):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# hack for spaces-in-braces check
|
||||
# Used by formatter.
|
||||
self.braced = False
|
||||
|
||||
def accept(self, visitor, recurse=False):
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
typing_extensions-4.15.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
typing_extensions-4.15.0.dist-info/METADATA,sha256=wTg3j-jxiTSsmd4GBTXFPsbBOu7WXpTDJkHafuMZKnI,3259
|
||||
typing_extensions-4.15.0.dist-info/RECORD,,
|
||||
typing_extensions-4.15.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
typing_extensions-4.15.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
typing_extensions-4.15.0.dist-info/licenses/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936
|
||||
typing_extensions.py,sha256=Qz0R0XDTok0usGXrwb_oSM6n49fOaFZ6tSvqLUwvftg,160429
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user