Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- **The Debug Console and Watch work at a `breakpoint()` or red dot.** Type Python into the Debug
Console and it runs in the frame selected in the Call Stack: its locals, closed-over names, its
module's globals (an imported module's own, not the notebook's) and the builtins, as the paused
line itself would see them. A result opens like a Variables row; `print()` shows in the console;
names you bind (`y = 5`, `import json`, a `def`) last until the run resumes. An error reads as a
Jupyter traceback, frame by frame with the line that raised marked. A line runs as long as it
takes, and after a few seconds a notification offers to stop it; a watch keeps a two-second limit.
Code the console calls runs straight past `breakpoint()` and red dots. Not yet: `cls` in a
classmethod's frame, and rebinding a local (`n = 99`) changes the console's copy, not the paused
function.
- **Show in Debug Console and Evaluate with self… on a Variables row.** Right-click any row, however
deep in the tree, and **Show in Debug Console** opens the console with that object in it: an
object with its attributes listed beneath, each by name; a list, set or dict, or an object with
more than 20 attributes, as one row that opens like the Variables row. Its name is left at the
console's prompt, or `_` bound to it for a row whose name means nothing there, such as a list item. **Evaluate with self…** asks for an expression and shows what it gives
with `self` bound to the row's object, in the frame selected in Call Stack.
- **Variables shows each value's type, and opens objects whole.** Every row starts with its type,
and a container's with its size too, as PyCharm shows them: `{Point} Point(3, 4)`,
`{list: 3} […]`, `{list: 150}` for one too big to show. **Copy Value** copies the value alone,
Expand All @@ -29,6 +45,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
continues with it. What Python refuses is refused with Python's reason, such as a property with
no setter. A method's receiver (`self`, or whatever the def calls it), a tuple's items, group
rows and a saved stack's values cannot be changed, and VS Code does not offer Set Value on them.
- **GemDB commands in the Debug Console.** A line starting with `%` is a GemDB command rather than
Python: `%persist p` adds `p` to Persisted Objects (or `%persist expr as key`), shown there as
added until the notebook commits; `%commit` commits the notebook; `%names` lists the names you
have bound in the console at this pause; `%help` lists the commands. Typing `%` suggests them,
and `%persist ` suggests your console names. The first pause in a window says so, once. `gemdb`
is available in the console without an import, so a line copied from Persisted Objects
(`gemdb.root["orders"]`) works in any program, and each persisted entry is there by its key
(`orders`) at every pause, unless the paused code has a name of its own spelled so, and is
labelled `orders is gemdb.root["orders"]` when typed; `persisted.orders` reaches it whatever
`orders` means at the pause. Typing a name offers `persisted.orders` in autocomplete for each
persisted entry it starts, and `persisted.` offers the keys.
- **The Debug Console on a saved stack.** A stack reopened with **Restore a Saved Stack…** now has a
Debug Console, in each frame, on the saved objects: evaluate, call methods, bind names, change
objects, `%names`, `%persist`, `%commit`, Show in Debug Console and Evaluate with self…. The stack
opens in a database session of its own, so a change waits there uncommitted, as a notebook's
does. When the stack opens, the console warns that `%commit` (or ✓ in Persisted Objects) would
write a change into the saved stack, which then no longer holds the values it was saved with. Closing the stack
with changes pending asks whether to commit them; otherwise they are discarded.

### Fixed

- **A classmethod's or staticmethod's frame is named by its class.** The Call Stack read
`Account class.open` for a paused `@classmethod` or `@staticmethod`; it now reads `Account.open`,
as CPython names it.

## [1.6.0] - 2026-10-07

Expand Down Expand Up @@ -198,7 +238,7 @@ The engine is unchanged at GemStone 4.0.0.a4, so a database created by GemDB
Saved Stack…** (a button in Run and Debug, and in the GemDB panel) lists the saved stacks, or
opens the only one, in Run and Debug again; Persisted Objects can open it from its row too — after
a restart, and with the notebook or file gone. Each frame shows the copy of its source saved with
the stack, marked "(saved <time>)" in its tab and Call Stack row. It is a read-only snapshot: the run itself is not resumed.
the stack, marked "(saved <time>)" in its tab and Call Stack row. It is a snapshot: the run itself is not resumed.
- **A Persisted Objects view** under GemDB, and in Run and Debug while a cell is paused, lists what
`gemdb.root` has committed, with a check mark, and under each notebook the objects added from the
debugger that it has not committed yet. Its title bar always has **Commit** and **Abort**, for the
Expand Down
25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -422,12 +422,33 @@ database, select that kernel from the kernel picker at the top right.
a local, a global, an attribute, a list item or a dict entry. The value you type is a Python
expression, and the run continues with it. It can use the notebook's globals, or a `.py` file's,
on any row, and on a Locals row the frame's locals too. **Continue** resumes the cell where it paused; **Stop**
ends it, along with any cells queued after it. Stepping is not supported yet, and neither are red
ends it, along with any cells queued after it. The **Debug Console** and **Watch** work at the
pause: type Python and it runs in the frame selected in the Call Stack, with that frame's locals,
its module's globals and the builtins, and a result opens like a Variables row. Names you bind
there last until the run resumes; an error reads as a notebook traceback; a long-running line can
be stopped from the notification that appears. A change to an object made there is in the
notebook's transaction, as it would be from a cell. Right-click a Variables row for **Show in
Debug Console**, which puts that object in the console (an object with its attributes
listed, a list or dict as one row that opens) and leaves its name at the prompt, or `_` for a
nested row, ready for the next line, or **Evaluate with self…**,
which runs an expression with `self` bound to it (`len(self.history)`) and shows the answer
there. Lines starting with `%` are GemDB commands, not Python: `%persist p` adds `p` to
Persisted Objects (`%persist expr as key` for anything else), `%commit` commits the notebook,
`%names` lists what you have bound in the console, and `%help` lists them all. Type `%` and the
console suggests them. `gemdb` is always available in the console, so a line copied from Persisted
Objects, such as `gemdb.root["orders"]`, works even if the program never imported it. What Persisted Objects holds is available by its
key too (`o` for `gemdb.root["o"]`), unless the paused code has its own name `o`; `persisted.o` reaches it whatever `o` means, and a
bare persisted name is labelled with where it came from. While you type a name, autocomplete
offers `persisted.o` for each persisted entry it starts (press Tab to take a suggestion). Stepping is not supported yet, and neither are red
dots in a cell (they work in `.py` files; see above). Right-click a Variables row and choose **Add to Persisted Objects…** to keep that object: it
goes in `gemdb.root` under the key you give (one is suggested) and is written at the notebook's
next commit. The **Persisted Objects** view, under GemDB and in Run and Debug, lists what `gemdb.root`
holds; its title bar always has **Commit** and **Abort** for the notebook you are working in. **Add Stack to Persisted Objects…** on the Call Stack saves the whole paused stack the same
way; once committed, Persisted Objects reopens it in the debugger, even after a restart.
way; once committed, Persisted Objects reopens it in the debugger, even after a restart. Its
Debug Console works on the saved objects: call a method, bind a name, change an object,
`%persist` one. A change waits uncommitted; the console says when the stack opens that `%commit`
(or ✓) would write it into the saved stack, and closing the stack asks whether to commit or
discard what is pending.
**Debug Python File in GemDB** does the same for a `.py` file (see above). In the GemDB Shell
and in **Run Python File in GemDB**, `breakpoint()` prints where it was and the code carries on.
- Each notebook has its own session and its own transaction, so a commit in one notebook never
Expand Down
30 changes: 29 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,16 @@
"title": "Keep the Database Running After Logout",
"category": "GemDB"
},
{
"command": "gemdb.showInDebugConsole",
"title": "Show in Debug Console",
"category": "GemDB"
},
{
"command": "gemdb.evaluateWithSelf",
"title": "Evaluate with self…",
"category": "GemDB"
},
{
"command": "gemdb.saveVariable",
"title": "Add to Persisted Objects…",
Expand Down Expand Up @@ -594,6 +604,14 @@
"command": "gemdb.configureRemoveIpc",
"when": "isLinux"
},
{
"command": "gemdb.showInDebugConsole",
"when": "false"
},
{
"command": "gemdb.evaluateWithSelf",
"when": "false"
},
{
"command": "gemdb.saveVariable",
"when": "false"
Expand Down Expand Up @@ -637,9 +655,19 @@
],
"debug/variables/context": [
{
"command": "gemdb.saveVariable",
"command": "gemdb.showInDebugConsole",
"when": "debugType == gemdb",
"group": "z_gemdb@1"
},
{
"command": "gemdb.evaluateWithSelf",
"when": "debugType == gemdb",
"group": "z_gemdb@2"
},
{
"command": "gemdb.saveVariable",
"when": "debugType == gemdb",
"group": "z_gemdb@3"
}
],
"view/item/context": [
Expand Down
32 changes: 32 additions & 0 deletions src/__integration__/breakpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1221,6 +1221,38 @@ describe.skipIf(!haveExtent || !canMakeFixture())('breakpoint()', () => {
expect(result.value).toBe('7');
});

it('names a classmethod’s and a staticmethod’s frame by their class, as CPython does', async () => {
const names: string[][] = [];
setHaltHandler(async (request) => {
names.push(
parsePythonStack(await request.query(pythonStackQuery(request.process))).map((f) => f.name),
);
return 'continue';
});

await runPython(
[
'class Account:',
' @classmethod',
' def open(cls):',
' breakpoint()',
' return cls()',
' @staticmethod',
' def check():',
' breakpoint()',
' return 1',
'Account.open()',
'Account.check()',
].join('\n'),
NB,
);

expect(names).toEqual([
['Account.open', '<module>'],
['Account.check', '<module>'],
]);
});

it('turns an interrupt while paused into KeyboardInterrupt and tells the handler', async () => {
let cancelled = false;
setHaltHandler(
Expand Down
Loading
Loading