Repository navigation
Commit 713cb2d
authored
PEP 723 parser + silent detection telemetry (#1555)
## Why we're doing this
[PEP
723](https://packaging.python.org/en/latest/specifications/inline-script-metadata/)
("inline script metadata") lets a single-file Python script declare its
own runtime requirements in a TOML block at the top of the file:
```python
# /// script
# requires-python = ">=3.11"
# dependencies = [
# "requests<3",
# "rich",
# ]
# ///
import requests
```
Runners like `uv run --script`, `pipx run`, and `hatch run` already read
this block to set up an environment automatically. Today,
`ms-python.vscode-python-envs` does nothing with it.
Before we commit to any of that work, we want to answer a basic
question: **how many users of this extension actually have PEP 723
scripts in their workspaces, and how many edit them vs. just open
them?**
The follow-on stages involve UX choices that are still being worked
through in design review — environment backend (venv vs uv vs
cache-discovery), persistence model (sibling `.venv` vs
content-addressed cache), the trigger surface for env creation, and so
on. Sizing the population first lets us prioritize honestly instead of
building speculatively.
This PR is the **measurement-only first slice**: a parser plus a silent
telemetry observer. There is no UI, no setting, no project registration,
and no environment creation. The extension behaves exactly as it does
today; the only change a user could observe is two new entries in their
telemetry stream.
## What changed and how it works
### `src/common/inlineScriptMetadata.ts` — the parser
Pure-function code that every later stage will build on. Exports:
| Symbol | Purpose |
|---|---|
| `readInlineScriptMetadata(text)` | Parse from in-memory script source.
Returns parsed `requires-python`, `dependencies`, opaque `[tool]` table,
and the block's character offsets — or `undefined` for no block,
malformed block, multiple blocks (per spec MUST error), or TOML errors.
|
| `readInlineScriptMetadataFromFile(uri)` | Same, but reads only the
first 8 KiB of a file on disk via `fs.open` + `fileHandle.read`. Skips
non-`file:` URIs. Swallows I/O errors. |
| `matchesPythonVersion(specifier, version)` | Tests a Python version
against a PEP 440 specifier: `==`, `!=`, `>=`, `<=`, `>`, `<`, `~=`,
`===`; comma-separated AND; `==3.12.*` wildcards. |
Handles the edges that come up in real files: leading UTF-8 BOM
(Windows), CRLF and lone-CR line endings (normalized before regex),
shebang lines, encoding declarations, the spec's content-extraction rule
(`# foo` → `foo`, bare `#` → blank line; rejects `#foo`, `##foo`,
`#\tfoo`), multiple `script` blocks (returns `undefined` + `traceWarn`),
and unknown top-level block types (silently ignored, per spec). Test
coverage in
[src/test/common/inlineScriptMetadata.unit.test.ts](src/test/common/inlineScriptMetadata.unit.test.ts)
(~30 cases).
### `src/features/inlineScriptLazyDetector.ts` — the silent observer
Subscribes to `onDidOpenTextDocument`, `onDidSaveTextDocument`, and
`onDidChangeTextDocument`. For each `.py` file inside an open workspace
folder, on open or save, it reads the first 8 KiB and runs the parser.
When a valid block is detected, it emits anonymized telemetry — and
that's it. No projects registered, no UI shown.
Behaviour worth flagging:
- **Per-session dedup by URI.** `PEP723.DETECTED` fires at most once per
URI per session; repeat opens/saves of the same file are silent.
`PEP723.EDITED` only fires for URIs already counted as detected, and
only on the first `contentChanges`-bearing event.
- **In-flight coalescing.** Rapid open+save on the same URI shares one
read promise via an `inFlight` map.
- **Activation catch-up.** On activation, the detector replays every
already-open `.py` document through the same handler. The extension's
`onLanguage:python` activation event fires *after* VS Code has opened
any restored editors, so the open events for those files are gone by the
time we subscribe. The replay is deferred via `setImmediate` to avoid
racing VS Code's own document registration; per-URI dedup keeps it
idempotent if a live event happens to arrive too.
- **Disposal-safe.** A `disposed` flag guards async continuations so a
read that completes after `dispose()` does not emit telemetry on a
torn-down host.
- **No setting gate.** The operation is cheap (8 KiB head read, regex,
TOML parse — sub-millisecond) and side-effect free. The
`python-envs.useInlineScriptMetadata` setting reserved in
[plan.md](plan.md) becomes relevant in Stage 2+ when we start writing to
disk.
### `src/common/telemetry/constants.ts` — the two events
```
PEP723.DETECTED properties: { trigger: 'open' | 'save', hasRequiresPython: boolean }
measures: { dependencyCount: number }
PEP723.EDITED measures: { duration: number /* ms between detection and first edit */ }
```
Together, the events answer two questions: how many users have PEP 723
files at all (`DETECTED` count), and how many of those users actually
edit them rather than just opening them once (`EDITED` / `DETECTED`
ratio). Full GDPR annotations land alongside the enum members in the
same file.
**What is not in the events:** no URIs, no file paths, no file content,
no workspace identifiers, no project names. The events are pure counters
plus a small set of shape metadata.
### Supporting changes
- `src/common/workspace.apis.ts` — wrapper exports for
`onDidOpenTextDocument`, `onDidSaveTextDocument`,
`onDidChangeTextDocument`, and `getOpenTextDocuments`, matching the
existing wrapper pattern for testability.
- `src/extension.ts` — constructs and activates the observer alongside
the existing project creators; pushes it onto `context.subscriptions`
for disposal.
- `src/test/features/inlineScriptLazyDetector.unit.test.ts` — covers
open/save dispatch, activation replay, per-URI dedup, edit-event gating,
in-flight coalescing, and disposal safety.1 parent dc598c8 commit 713cb2d
7 files changed
Lines changed: 1688 additions & 0 deletions
File tree
- src
- common
- telemetry
- features
- test
- common
- features
0 commit comments