Compare commits
13
Commits
2026.8.200
...
2026.9.200
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7eb72f417 | ||
|
|
af5acfc946 | ||
|
|
20f76b6a20 | ||
|
|
081b488fe3 | ||
|
|
b37eea43d9 | ||
|
|
89add18273 | ||
|
|
e47c3bda69 | ||
|
|
41d117fe2c | ||
|
|
53ebc5d055 | ||
|
|
a0a0d38fe5 | ||
|
|
52b81b3dcf | ||
|
|
c3d5116885 | ||
|
|
07ccd2d26a |
+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,17 @@
|
||||
## Unreleased
|
||||
|
||||
- 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,11 @@ 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
|
||||
- **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 +25,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 +47,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 +100,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
|
||||
|
||||
|
||||
@@ -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 }
|
||||
@@ -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()
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "nx-post-support",
|
||||
"version": "2025.9.200",
|
||||
"version": "2026.8.201",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "nx-post-support",
|
||||
"version": "2025.9.200",
|
||||
"version": "2026.8.201",
|
||||
"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.100",
|
||||
"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
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
Metadata-Version: 2.4
|
||||
Name: typing_extensions
|
||||
Version: 4.15.0
|
||||
Version: 4.16.0
|
||||
Summary: Backported and Experimental Type Hints for Python 3.9+
|
||||
Keywords: annotations,backport,checker,checking,function,hinting,hints,type,typechecking,typehinting,typehints,typing
|
||||
Author-email: "Guido van Rossum, Jukka Lehtosalo, Łukasz Langa, Michael Lee" <levkivskyi@gmail.com>
|
||||
@@ -19,6 +19,7 @@ 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: Topic :: Software Development
|
||||
License-File: LICENSE
|
||||
Project-URL: Bug Tracker, https://github.com/python/typing_extensions/issues
|
||||
@@ -0,0 +1,7 @@
|
||||
typing_extensions-4.16.0.dist-info/INSTALLER,sha256=5hhM4Q4mYTT9z6QB6PGpUAW81PGNFrYrdXMj4oM_6ak,2
|
||||
typing_extensions-4.16.0.dist-info/METADATA,sha256=sFCEyh1Qh5hlF42f_5-r6rYb37HzYb-96VQh_8j5vkY,3310
|
||||
typing_extensions-4.16.0.dist-info/RECORD,,
|
||||
typing_extensions-4.16.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
||||
typing_extensions-4.16.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
|
||||
typing_extensions-4.16.0.dist-info/licenses/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936
|
||||
typing_extensions.py,sha256=QEDKGh7L7gDROFwSqTCE0cW9RvC3dPB-WufpHE9V5pY,165012
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
Wheel-Version: 1.0
|
||||
Generator: flit 3.9.0
|
||||
Generator: flit 3.12.0
|
||||
Root-Is-Purelib: true
|
||||
Tag: py3-none-any
|
||||
+229
-124
@@ -91,6 +91,7 @@ __all__ = [
|
||||
'overload',
|
||||
'override',
|
||||
'Protocol',
|
||||
'sentinel',
|
||||
'Sentinel',
|
||||
'reveal_type',
|
||||
'runtime',
|
||||
@@ -148,7 +149,6 @@ __all__ = [
|
||||
'ValuesView',
|
||||
'cast',
|
||||
'no_type_check',
|
||||
'no_type_check_decorator',
|
||||
]
|
||||
|
||||
# for backward compatibility
|
||||
@@ -160,18 +160,122 @@ _PEP_696_IMPLEMENTED = sys.version_info >= (3, 13, 0, "beta")
|
||||
# Added with bpo-45166 to 3.10.1+ and some 3.9 versions
|
||||
_FORWARD_REF_HAS_CLASS = "__forward_is_class__" in typing.ForwardRef.__slots__
|
||||
|
||||
|
||||
def _caller(depth=1, default='__main__'):
|
||||
try:
|
||||
return sys._getframemodulename(depth + 1) or default
|
||||
except AttributeError: # For platforms without _getframemodulename()
|
||||
pass
|
||||
try:
|
||||
return sys._getframe(depth + 1).f_globals.get('__name__', default)
|
||||
except (AttributeError, ValueError): # For platforms without _getframe()
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# Placeholder for sentinel methods, because sentinels can not have their own sentinels
|
||||
_sentinel_placeholder = object()
|
||||
|
||||
if hasattr(builtins, "sentinel"): # 3.15+
|
||||
sentinel = builtins.sentinel
|
||||
else:
|
||||
class sentinel:
|
||||
"""Create a unique sentinel object.
|
||||
|
||||
*name* should be the name of the variable to which the return value
|
||||
shall be assigned.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
__name: str = _sentinel_placeholder,
|
||||
__repr: typing.Optional[str] = _sentinel_placeholder,
|
||||
/,
|
||||
*,
|
||||
repr: typing.Optional[str] = None,
|
||||
name: str = _sentinel_placeholder,
|
||||
) -> None:
|
||||
if name is not _sentinel_placeholder:
|
||||
warnings.warn(
|
||||
"Passing 'name' as a keyword argument is deprecated; "
|
||||
"pass it positionally instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
__name = name
|
||||
if __name is _sentinel_placeholder:
|
||||
raise TypeError("First parameter 'name' is required")
|
||||
if __repr is not _sentinel_placeholder:
|
||||
warnings.warn(
|
||||
"Passing 'repr' as a positional argument is deprecated; "
|
||||
"pass it by keyword instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
repr = __repr
|
||||
|
||||
self._name = __name
|
||||
self._repr = repr if repr is not None else __name
|
||||
|
||||
# For pickling as a singleton:
|
||||
self.__module__ = _caller()
|
||||
|
||||
def __init_subclass__(cls):
|
||||
warnings.warn(
|
||||
"Subclassing sentinel is deprecated "
|
||||
"and will be disallowed in Python 3.15",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__init_subclass__()
|
||||
|
||||
def __setattr__(self, attr: str, value: object) -> None:
|
||||
if attr not in {"_name", "_repr", "__module__"}:
|
||||
warnings.warn(
|
||||
f"Setting attribute {attr!r} on sentinel objects is deprecated "
|
||||
"and will be disallowed in Python 3.15.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
super().__setattr__(attr, value)
|
||||
|
||||
@property
|
||||
def __name__(self) -> str:
|
||||
return self._name
|
||||
|
||||
@__name__.setter
|
||||
def __name__(self, value: str) -> None:
|
||||
self._name = value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self._repr
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
# The presence of this method convinces typing._type_check
|
||||
# that Sentinels are types.
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise TypeError(f"{type(self).__name__!r} object is not callable")
|
||||
|
||||
# Breakpoint: https://github.com/python/cpython/pull/21515
|
||||
if sys.version_info >= (3, 10):
|
||||
def __or__(self, other):
|
||||
return typing.Union[self, other]
|
||||
|
||||
def __ror__(self, other):
|
||||
return typing.Union[other, self]
|
||||
|
||||
def __reduce__(self) -> str:
|
||||
"""Reduce this sentinel to a singleton."""
|
||||
return self.__name__ # Module is taken from the __module__ attribute
|
||||
|
||||
Sentinel = sentinel
|
||||
|
||||
_marker = sentinel("sentinel")
|
||||
|
||||
|
||||
# The functions below are modified copies of typing internal helpers.
|
||||
# They are needed by _ProtocolMeta and they provide support for PEP 646.
|
||||
|
||||
|
||||
class _Sentinel:
|
||||
def __repr__(self):
|
||||
return "<sentinel>"
|
||||
|
||||
|
||||
_marker = _Sentinel()
|
||||
|
||||
|
||||
# Breakpoint: https://github.com/python/cpython/pull/27342
|
||||
if sys.version_info >= (3, 10):
|
||||
def _should_collect_from_parameters(t):
|
||||
@@ -524,7 +628,9 @@ else:
|
||||
|
||||
|
||||
class _SpecialGenericAlias(typing._SpecialGenericAlias, _root=True):
|
||||
def __init__(self, origin, nparams, *, inst=True, name=None, defaults=()):
|
||||
def __init__(self, origin, nparams, *, defaults, inst=True, name=None):
|
||||
assert nparams > 0, "`nparams` must be a positive integer"
|
||||
assert defaults, "Must always specify a non-empty sequence for `defaults`"
|
||||
super().__init__(origin, nparams, inst=inst, name=name)
|
||||
self._defaults = defaults
|
||||
|
||||
@@ -542,20 +648,14 @@ else:
|
||||
msg = "Parameters to generic types must be types."
|
||||
params = tuple(typing._type_check(p, msg) for p in params)
|
||||
if (
|
||||
self._defaults
|
||||
and len(params) < self._nparams
|
||||
len(params) < self._nparams
|
||||
and len(params) + len(self._defaults) >= self._nparams
|
||||
):
|
||||
params = (*params, *self._defaults[len(params) - self._nparams:])
|
||||
actual_len = len(params)
|
||||
|
||||
if actual_len != self._nparams:
|
||||
if self._defaults:
|
||||
expected = f"at least {self._nparams - len(self._defaults)}"
|
||||
else:
|
||||
expected = str(self._nparams)
|
||||
if not self._nparams:
|
||||
raise TypeError(f"{self} is not a generic class")
|
||||
expected = f"at least {self._nparams - len(self._defaults)}"
|
||||
raise TypeError(
|
||||
f"Too {'many' if actual_len > self._nparams else 'few'}"
|
||||
f" arguments for {self};"
|
||||
@@ -587,10 +687,13 @@ else:
|
||||
_PROTO_ALLOWLIST = {
|
||||
'collections.abc': [
|
||||
'Callable', 'Awaitable', 'Iterable', 'Iterator', 'AsyncIterable',
|
||||
'Hashable', 'Sized', 'Container', 'Collection', 'Reversible', 'Buffer',
|
||||
'AsyncIterator', 'Hashable', 'Sized', 'Container', 'Collection',
|
||||
'Reversible', 'Buffer',
|
||||
],
|
||||
'contextlib': ['AbstractContextManager', 'AbstractAsyncContextManager'],
|
||||
'io': ['Reader', 'Writer'],
|
||||
'typing_extensions': ['Buffer'],
|
||||
'os': ['PathLike'],
|
||||
}
|
||||
|
||||
|
||||
@@ -612,22 +715,12 @@ def _get_protocol_attrs(cls):
|
||||
return attrs
|
||||
|
||||
|
||||
def _caller(depth=1, default='__main__'):
|
||||
try:
|
||||
return sys._getframemodulename(depth + 1) or default
|
||||
except AttributeError: # For platforms without _getframemodulename()
|
||||
pass
|
||||
try:
|
||||
return sys._getframe(depth + 1).f_globals.get('__name__', default)
|
||||
except (AttributeError, ValueError): # For platforms without _getframe()
|
||||
pass
|
||||
return None
|
||||
|
||||
|
||||
# `__match_args__` attribute was removed from protocol members in 3.13,
|
||||
# we want to backport this change to older Python versions.
|
||||
# Breakpoint: https://github.com/python/cpython/pull/110683
|
||||
if sys.version_info >= (3, 13):
|
||||
# 3.14 additionally added `io.Reader`, `io.Writer` and `os.PathLike` to
|
||||
# the list of allowed protocol allowlist.
|
||||
# https://github.com/python/cpython/issues/127647
|
||||
if sys.version_info >= (3, 14):
|
||||
Protocol = typing.Protocol
|
||||
else:
|
||||
def _allow_reckless_class_checks(depth=2):
|
||||
@@ -1038,10 +1131,10 @@ if _NEEDS_SINGLETONMETA:
|
||||
|
||||
|
||||
# Update this to something like >=3.13.0b1 if and when
|
||||
# PEP 728 is implemented in CPython
|
||||
_PEP_728_IMPLEMENTED = False
|
||||
# PEP 764 is implemented in CPython
|
||||
_PEP_764_IMPLEMENTED = False
|
||||
|
||||
if _PEP_728_IMPLEMENTED:
|
||||
if _PEP_764_IMPLEMENTED:
|
||||
# The standard library TypedDict in Python 3.9.0/1 does not honour the "total"
|
||||
# keyword with old-style TypedDict(). See https://bugs.python.org/issue42059
|
||||
# The standard library TypedDict below Python 3.11 does not store runtime
|
||||
@@ -1051,7 +1144,8 @@ if _PEP_728_IMPLEMENTED:
|
||||
# to enable better runtime introspection.
|
||||
# On 3.13 we deprecate some odd ways of creating TypedDicts.
|
||||
# Also on 3.13, PEP 705 adds the ReadOnly[] qualifier.
|
||||
# PEP 728 (still pending) makes more changes.
|
||||
# PEP 728 (Python 3.15+) adds the `extra_items` and `closed` keywords.
|
||||
# PEP 764 (still pending) allows the `TypedDict` special form to be subscripted.
|
||||
TypedDict = typing.TypedDict
|
||||
_TypedDictMeta = typing._TypedDictMeta
|
||||
is_typeddict = typing.is_typeddict
|
||||
@@ -1155,8 +1249,14 @@ else:
|
||||
|
||||
if sys.version_info <= (3, 14):
|
||||
annotations.update(base_dict.get('__annotations__', {}))
|
||||
required_keys.update(base_dict.get('__required_keys__', ()))
|
||||
optional_keys.update(base_dict.get('__optional_keys__', ()))
|
||||
base_required = base_dict.get('__required_keys__', set())
|
||||
required_keys |= base_required
|
||||
optional_keys -= base_required
|
||||
|
||||
base_optional = base_dict.get('__optional_keys__', set())
|
||||
required_keys -= base_optional
|
||||
optional_keys |= base_optional
|
||||
|
||||
readonly_keys.update(base_dict.get('__readonly_keys__', ()))
|
||||
mutable_keys.update(base_dict.get('__mutable_keys__', ()))
|
||||
|
||||
@@ -1184,13 +1284,19 @@ else:
|
||||
qualifiers = set(_get_typeddict_qualifiers(annotation_type))
|
||||
|
||||
if Required in qualifiers:
|
||||
required_keys.add(annotation_key)
|
||||
is_required = True
|
||||
elif NotRequired in qualifiers:
|
||||
optional_keys.add(annotation_key)
|
||||
elif total:
|
||||
is_required = False
|
||||
else:
|
||||
is_required = total
|
||||
|
||||
if is_required:
|
||||
required_keys.add(annotation_key)
|
||||
optional_keys.discard(annotation_key)
|
||||
else:
|
||||
optional_keys.add(annotation_key)
|
||||
required_keys.discard(annotation_key)
|
||||
|
||||
if ReadOnly in qualifiers:
|
||||
mutable_keys.discard(annotation_key)
|
||||
readonly_keys.add(annotation_key)
|
||||
@@ -1798,7 +1904,7 @@ elif hasattr(typing, 'ParamSpec'):
|
||||
paramspec = typing.ParamSpec(name, bound=bound,
|
||||
covariant=covariant,
|
||||
contravariant=contravariant)
|
||||
paramspec.__infer_variance__ = infer_variance
|
||||
paramspec.__infer_variance__ = bool(infer_variance)
|
||||
|
||||
_set_default(paramspec, default)
|
||||
_set_module(paramspec)
|
||||
@@ -1894,10 +2000,7 @@ else:
|
||||
self.__covariant__ = bool(covariant)
|
||||
self.__contravariant__ = bool(contravariant)
|
||||
self.__infer_variance__ = bool(infer_variance)
|
||||
if bound:
|
||||
self.__bound__ = typing._type_check(bound, 'Bound must be a type.')
|
||||
else:
|
||||
self.__bound__ = None
|
||||
self.__bound__ = bound
|
||||
_DefaultMixin.__init__(self, default)
|
||||
|
||||
# for pickling:
|
||||
@@ -1929,6 +2032,9 @@ else:
|
||||
def __call__(self, *args, **kwargs):
|
||||
pass
|
||||
|
||||
def __init_subclass__(cls) -> None:
|
||||
raise TypeError(f"type '{__name__}.ParamSpec' is not an acceptable base type")
|
||||
|
||||
|
||||
# 3.9
|
||||
if not hasattr(typing, 'Concatenate'):
|
||||
@@ -1956,7 +2062,9 @@ if not hasattr(typing, 'Concatenate'):
|
||||
__class__ = typing._GenericAlias
|
||||
|
||||
def __init__(self, origin, args):
|
||||
super().__init__(args)
|
||||
# Cannot use `super().__init__` here because of the `__class__` assignment
|
||||
# in the class body (https://github.com/python/typing_extensions/issues/661)
|
||||
list.__init__(self, args)
|
||||
self.__origin__ = origin
|
||||
self.__args__ = args
|
||||
|
||||
@@ -2259,10 +2367,10 @@ else:
|
||||
return typing._GenericAlias(self, (item,))
|
||||
|
||||
|
||||
# 3.14+?
|
||||
# 3.15+?
|
||||
if hasattr(typing, 'TypeForm'):
|
||||
TypeForm = typing.TypeForm
|
||||
# <=3.13
|
||||
# <=3.14
|
||||
else:
|
||||
class _TypeFormForm(_ExtensionsSpecialForm, _root=True):
|
||||
# TypeForm(X) is equivalent to X but indicates to the type checker
|
||||
@@ -2515,7 +2623,10 @@ else: # <=3.11
|
||||
def __getitem__(self, args):
|
||||
if self.__typing_is_unpacked_typevartuple__:
|
||||
return args
|
||||
return super().__getitem__(args)
|
||||
# Cannot use `super().__getitem__` here because of the `__class__` assignment
|
||||
# in the class body on Python <=3.11
|
||||
# (https://github.com/python/typing_extensions/issues/661)
|
||||
return typing._GenericAlias.__getitem__(self, args)
|
||||
|
||||
@_UnpackSpecialForm
|
||||
def Unpack(self, parameters):
|
||||
@@ -2537,20 +2648,33 @@ def _unpack_args(*args):
|
||||
return newargs
|
||||
|
||||
|
||||
if _PEP_696_IMPLEMENTED:
|
||||
if sys.version_info >= (3, 15):
|
||||
from typing import TypeVarTuple
|
||||
|
||||
elif hasattr(typing, "TypeVarTuple"): # 3.11+
|
||||
|
||||
# Add default parameter - PEP 696
|
||||
# Add default parameter - PEP 696 and bound/variance parameters
|
||||
class TypeVarTuple(metaclass=_TypeVarLikeMeta):
|
||||
"""Type variable tuple."""
|
||||
|
||||
_backported_typevarlike = typing.TypeVarTuple
|
||||
|
||||
def __new__(cls, name, *, default=NoDefault):
|
||||
tvt = typing.TypeVarTuple(name)
|
||||
_set_default(tvt, default)
|
||||
def __new__(cls, name, *, bound=None,
|
||||
covariant=False, contravariant=False,
|
||||
infer_variance=False, default=NoDefault):
|
||||
|
||||
if _PEP_696_IMPLEMENTED:
|
||||
# can pass default argument
|
||||
tvt = typing.TypeVarTuple(name, default=default)
|
||||
else:
|
||||
tvt = typing.TypeVarTuple(name)
|
||||
_set_default(tvt, default)
|
||||
|
||||
tvt.__bound__ = bound
|
||||
tvt.__covariant__ = bool(covariant)
|
||||
tvt.__contravariant__ = bool(contravariant)
|
||||
tvt.__infer_variance__ = bool(infer_variance)
|
||||
|
||||
_set_module(tvt)
|
||||
|
||||
def _typevartuple_prepare_subst(alias, args):
|
||||
@@ -2655,8 +2779,13 @@ else: # <=3.10
|
||||
def __iter__(self):
|
||||
yield self.__unpacked__
|
||||
|
||||
def __init__(self, name, *, default=NoDefault):
|
||||
def __init__(self, name, *, bound=None, covariant=False, contravariant=False,
|
||||
infer_variance=False, default=NoDefault):
|
||||
self.__name__ = name
|
||||
self.__covariant__ = bool(covariant)
|
||||
self.__contravariant__ = bool(contravariant)
|
||||
self.__infer_variance__ = bool(infer_variance)
|
||||
self.__bound__ = bound
|
||||
_DefaultMixin.__init__(self, default)
|
||||
|
||||
# for pickling:
|
||||
@@ -2667,7 +2796,15 @@ else: # <=3.10
|
||||
self.__unpacked__ = Unpack[self]
|
||||
|
||||
def __repr__(self):
|
||||
return self.__name__
|
||||
if self.__infer_variance__:
|
||||
prefix = ''
|
||||
elif self.__covariant__:
|
||||
prefix = '+'
|
||||
elif self.__contravariant__:
|
||||
prefix = '-'
|
||||
else:
|
||||
prefix = '~'
|
||||
return prefix + self.__name__
|
||||
|
||||
def __hash__(self):
|
||||
return object.__hash__(self)
|
||||
@@ -2873,9 +3010,9 @@ else: # <=3.11
|
||||
return arg
|
||||
|
||||
|
||||
# Python 3.13.3+ contains a fix for the wrapped __new__
|
||||
# Breakpoint: https://github.com/python/cpython/pull/132160
|
||||
if sys.version_info >= (3, 13, 3):
|
||||
# Python 3.13.8+ and 3.14.1+ contain a fix for the wrapped __init_subclass__
|
||||
# Breakpoint: https://github.com/python/cpython/pull/138210
|
||||
if ((3, 13, 8) <= sys.version_info < (3, 14)) or sys.version_info >= (3, 14, 1):
|
||||
deprecated = warnings.deprecated
|
||||
else:
|
||||
_T = typing.TypeVar("_T")
|
||||
@@ -2968,33 +3105,32 @@ else:
|
||||
|
||||
arg.__new__ = staticmethod(__new__)
|
||||
|
||||
original_init_subclass = arg.__init_subclass__
|
||||
# We need slightly different behavior if __init_subclass__
|
||||
# is a bound method (likely if it was implemented in Python)
|
||||
if isinstance(original_init_subclass, MethodType):
|
||||
original_init_subclass = original_init_subclass.__func__
|
||||
if "__init_subclass__" in arg.__dict__:
|
||||
# __init_subclass__ is directly present on the decorated class.
|
||||
# Synthesize a wrapper that calls this method directly.
|
||||
original_init_subclass = arg.__init_subclass__
|
||||
# We need slightly different behavior if __init_subclass__
|
||||
# is a bound method (likely if it was implemented in Python).
|
||||
# Otherwise, it likely means it's a builtin such as
|
||||
# object's implementation of __init_subclass__.
|
||||
if isinstance(original_init_subclass, MethodType):
|
||||
original_init_subclass = original_init_subclass.__func__
|
||||
|
||||
@functools.wraps(original_init_subclass)
|
||||
def __init_subclass__(*args, **kwargs):
|
||||
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
|
||||
return original_init_subclass(*args, **kwargs)
|
||||
|
||||
arg.__init_subclass__ = classmethod(__init_subclass__)
|
||||
# Or otherwise, which likely means it's a builtin such as
|
||||
# object's implementation of __init_subclass__.
|
||||
else:
|
||||
@functools.wraps(original_init_subclass)
|
||||
def __init_subclass__(*args, **kwargs):
|
||||
def __init_subclass__(cls, *args, **kwargs):
|
||||
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
|
||||
return original_init_subclass(*args, **kwargs)
|
||||
return super(arg, cls).__init_subclass__(*args, **kwargs)
|
||||
|
||||
arg.__init_subclass__ = __init_subclass__
|
||||
arg.__init_subclass__ = classmethod(__init_subclass__)
|
||||
|
||||
arg.__deprecated__ = __new__.__deprecated__ = msg
|
||||
__init_subclass__.__deprecated__ = msg
|
||||
return arg
|
||||
elif callable(arg):
|
||||
import asyncio.coroutines
|
||||
import functools
|
||||
import inspect
|
||||
|
||||
@@ -3003,11 +3139,13 @@ else:
|
||||
warnings.warn(msg, category=category, stacklevel=stacklevel + 1)
|
||||
return arg(*args, **kwargs)
|
||||
|
||||
if asyncio.coroutines.iscoroutinefunction(arg):
|
||||
if inspect.iscoroutinefunction(arg):
|
||||
# Breakpoint: https://github.com/python/cpython/pull/99247
|
||||
if sys.version_info >= (3, 12):
|
||||
wrapper = inspect.markcoroutinefunction(wrapper)
|
||||
else:
|
||||
import asyncio.coroutines
|
||||
|
||||
wrapper._is_coroutine = asyncio.coroutines._is_coroutine
|
||||
|
||||
arg.__deprecated__ = wrapper.__deprecated__ = msg
|
||||
@@ -3579,14 +3717,14 @@ else:
|
||||
return typing.Union[other, self]
|
||||
|
||||
|
||||
# Breakpoint: https://github.com/python/cpython/pull/124795
|
||||
if sys.version_info >= (3, 14):
|
||||
# Breakpoint: https://github.com/python/cpython/pull/149172
|
||||
if sys.version_info >= (3, 15):
|
||||
TypeAliasType = typing.TypeAliasType
|
||||
# <=3.13
|
||||
# <=3.14
|
||||
else:
|
||||
# Breakpoint: https://github.com/python/cpython/pull/103764
|
||||
if sys.version_info >= (3, 12):
|
||||
# 3.12-3.13
|
||||
# 3.12-3.14
|
||||
def _is_unionable(obj):
|
||||
"""Corresponds to is_unionable() in unionobject.c in CPython."""
|
||||
return obj is None or isinstance(obj, (
|
||||
@@ -3699,7 +3837,7 @@ else:
|
||||
self.__name__ = name
|
||||
|
||||
def __setattr__(self, name: str, value: object, /) -> None:
|
||||
if hasattr(self, "__name__"):
|
||||
if hasattr(self, "__name__") and name != "__module__":
|
||||
self._raise_attribute_error(name)
|
||||
super().__setattr__(name, value)
|
||||
|
||||
@@ -3710,7 +3848,7 @@ else:
|
||||
# Match the Python 3.12 error messages exactly
|
||||
if name == "__name__":
|
||||
raise AttributeError("readonly attribute")
|
||||
elif name in {"__value__", "__type_params__", "__parameters__", "__module__"}:
|
||||
elif name in {"__value__", "__type_params__", "__parameters__"}:
|
||||
raise AttributeError(
|
||||
f"attribute '{name}' of 'typing.TypeAliasType' objects "
|
||||
"is not writable"
|
||||
@@ -3829,8 +3967,8 @@ else:
|
||||
>>> class P(Protocol):
|
||||
... def a(self) -> str: ...
|
||||
... b: int
|
||||
>>> get_protocol_members(P)
|
||||
frozenset({'a', 'b'})
|
||||
>>> get_protocol_members(P) == frozenset({'a', 'b'})
|
||||
True
|
||||
|
||||
Raise a TypeError for arguments that are not Protocols.
|
||||
"""
|
||||
@@ -4207,44 +4345,6 @@ else:
|
||||
)
|
||||
|
||||
|
||||
class Sentinel:
|
||||
"""Create a unique sentinel object.
|
||||
|
||||
*name* should be the name of the variable to which the return value shall be assigned.
|
||||
|
||||
*repr*, if supplied, will be used for the repr of the sentinel object.
|
||||
If not provided, "<name>" will be used.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
name: str,
|
||||
repr: typing.Optional[str] = None,
|
||||
):
|
||||
self._name = name
|
||||
self._repr = repr if repr is not None else f'<{name}>'
|
||||
|
||||
def __repr__(self):
|
||||
return self._repr
|
||||
|
||||
if sys.version_info < (3, 11):
|
||||
# The presence of this method convinces typing._type_check
|
||||
# that Sentinels are types.
|
||||
def __call__(self, *args, **kwargs):
|
||||
raise TypeError(f"{type(self).__name__!r} object is not callable")
|
||||
|
||||
# Breakpoint: https://github.com/python/cpython/pull/21515
|
||||
if sys.version_info >= (3, 10):
|
||||
def __or__(self, other):
|
||||
return typing.Union[self, other]
|
||||
|
||||
def __ror__(self, other):
|
||||
return typing.Union[other, self]
|
||||
|
||||
def __getstate__(self):
|
||||
raise TypeError(f"Cannot pickle {type(self).__name__!r} object")
|
||||
|
||||
|
||||
if sys.version_info >= (3, 14, 0, "beta"):
|
||||
type_repr = annotationlib.type_repr
|
||||
else:
|
||||
@@ -4302,11 +4402,16 @@ _typing_names = [
|
||||
"ValuesView",
|
||||
"cast",
|
||||
"no_type_check",
|
||||
"no_type_check_decorator",
|
||||
# This is private, but it was defined by typing_extensions for a long time
|
||||
# and some users rely on it.
|
||||
"_AnnotatedAlias",
|
||||
]
|
||||
|
||||
# Breakpoint: https://github.com/python/cpython/pull/133602
|
||||
if sys.version_info < (3, 15, 0):
|
||||
_typing_names.append("no_type_check_decorator")
|
||||
__all__.append("no_type_check_decorator")
|
||||
|
||||
globals().update(
|
||||
{name: getattr(typing, name) for name in _typing_names if hasattr(typing, name)}
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user