liveConsole 1.6.2__py3-none-any.whl → 1.7.11__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ from pysole import *
@@ -0,0 +1,217 @@
1
+ Metadata-Version: 2.4
2
+ Name: liveConsole
3
+ Version: 1.7.11
4
+ Summary: An IDLE-like debugger to allow for real-time command injection for debugging and testing python code
5
+ Author-email: Tzur Soffer <tzur.soffer@gmail.com>
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/TzurSoffer/Pysole
8
+ Project-URL: Repository, https://github.com/TzurSoffer/Pysole
9
+ Requires-Python: >=3.7
10
+ Description-Content-Type: text/markdown
11
+ License-File: LICENSE
12
+ Requires-Dist: customtkinter
13
+ Requires-Dist: pygments
14
+ Dynamic: license-file
15
+
16
+ # PYSOLE
17
+
18
+ ## You can finally test your code in real time without using idle!
19
+ ### If you found [this repository](https://github.com/TzurSoffer/Pysole) useful, please give it a ⭐!.
20
+
21
+ ## Showcase (click to watch on Youtube)
22
+ [![Watch the demo](Showcase/thumbnail.png)](https://www.youtube.com/shorts/pjoelNjc3O0)
23
+
24
+
25
+ Table of contents
26
+
27
+ - Features
28
+ - Installation
29
+ - Usage
30
+ - Parameters
31
+ - Keyboard Shortcuts
32
+ - Troubleshooting/Notes
33
+ - Contributing
34
+ - License
35
+
36
+ ## Features
37
+
38
+ Pysole provides a compact but powerful set of features designed to make interactive debugging and live testing fast and pleasant.
39
+
40
+ 1. Live GUI console (syntax highlighting)
41
+ - Real-time syntax highlighting using Pygments.
42
+ - Monokai style by default, configurable through themes.
43
+
44
+ 2. Autocomplete & suggestions
45
+ - Autocomplete for Python keywords, built-ins and variables in scope.
46
+ - Popup suggestions after typing (configurable behavior) and insert-on-confirm.
47
+
48
+ 3. Run remaining script code at startup
49
+ - `runRemainingCode=True` will execute the remainder of the calling script after `probe()` is invoked.
50
+ - `printStartupCode=True` prints the captured code chunks as they execute.
51
+
52
+ 4. Thread-safe execution + output capture
53
+ - User code runs in a background thread to avoid blocking the GUI.
54
+ - `stdout` and `stderr` are redirected into the GUI console output area.
55
+
56
+ 5. Multi-line input, indentation & history
57
+ - Shift+Enter inserts a newline with proper indentation.
58
+ - Command history with clickable entries for easy reuse.
59
+
60
+ 6. Integrated Help Panel and Features tab
61
+ - Right-hand help panel shows `help(obj)` output.
62
+ - A Features view is available from the File menu and shows the built-in features summary.
63
+
64
+ 7. Themes & persistent settings
65
+ - Theme picker in the File menu; selected theme is written to `settings.json`.
66
+ - Settings (THEME, BEHAVIOR, FONT) are loaded at startup from `src/pysole/settings.json`.
67
+
68
+
69
+ ## Installation
70
+
71
+ Quick install
72
+
73
+ ```powershell
74
+ pip install liveConsole
75
+ ```
76
+
77
+ Notes: the package is published under the name `liveConsole` (see `pyproject.toml`).
78
+
79
+ If you prefer to install from source, clone this repo and run:
80
+
81
+ ```powershell
82
+ pip install -e .
83
+ ```
84
+
85
+ Command-line entry points
86
+
87
+ After installation, two console commands are provided:
88
+
89
+ - `pysole` — open the GUI console (same as `liveconsole`)
90
+ - `liveconsole` — open the GUI console
91
+
92
+
93
+ ## Usage
94
+
95
+ Programmatic usage (embed in scripts):
96
+
97
+ - Basic, automatic caller capture:
98
+ ```python
99
+ import pysole
100
+ pysole.probe()
101
+ ```
102
+
103
+ - Run the remaining code in the current file inside the console and print the code as it executes
104
+ ```python
105
+ pysole.probe(runRemainingCode=True, printStartupCode=True)
106
+ ```
107
+
108
+ - Override appearance and prompt
109
+ ```python
110
+ pysole.probe(primaryPrompt='PY> ', font='Consolas', fontSize=14)
111
+ ```
112
+
113
+ ## Parameters
114
+
115
+ This section documents the parameters accepted by `pysole.probe()` and the `InteractiveConsole` constructor in `src/pysole/pysole.py`. Most of these parameters are optional; reasonable defaults are taken from the calling frame and `settings.json`.
116
+
117
+ Note: `probe(...)` forwards its arguments to `InteractiveConsole(...)` so you can pass any of the parameters below to `probe()` directly.
118
+
119
+ runRemainingCode
120
+ - Meaning: When `True`, Pysole will read the remainder of the source file that contains the `probe()` call and run those lines inside the console's namespace.
121
+ - Type: bool
122
+ - Default: `False`
123
+ - Behavior: The implementation inspects `callerFrame.f_code.co_filename` for the filename and `callerFrame.f_lineno` for the line number where `probe()` was called. Lines after that line are captured into `startupCode` and executed on console startup.
124
+ - Caution: This requires the source file to be readable from disk (not packaged/compiled away). Large files will be read into memory.
125
+
126
+ printStartupCode
127
+ - Meaning: Controls how the startup code (captured by `runRemainingCode=True`) is executed: printed chunk-by-chunk to the console and executed interactively, or executed silently.
128
+ - Type: bool
129
+ - Default: `False`
130
+ - Behavior: If `True`, the startup code is split into logical top-level chunks (top-level statements and their indented blocks). Each chunk is printed and executed sequentially so you can see what runs. If `False`, the entire remaining code is executed silently in one go (but output is still captured and shown).
131
+
132
+ primaryPrompt
133
+ - Meaning: Overrides the primary prompt string (for example `>>>`). This updates the in-memory `BEHAVIOR['PRIMARY_PROMPT']` used by the console and the default can also be changed in the settings file directly.
134
+ - Type: string
135
+ - Default: The prompt value defined in `settings.json` under `BEHAVIOR -> PRIMARY_PROMPT`.
136
+ - Notes: Passing this parameter changes the prompt for the current session only.
137
+
138
+ font
139
+ - Meaning: Overrides the font family used in console widgets.
140
+ - Type: string (font family name, e.g. "Consolas", "Courier New")
141
+ - Default: The font specified by `settings.json` -> `THEME` -> `FONT`.
142
+
143
+ fontSize
144
+ - Meaning: Overrides the font size used in console widgets.
145
+ - Type: int (font size in points / pixels depending on the platform and Tk configuration)
146
+ - Default: Value from `settings.json` -> `THEME` -> `FONT_SIZE`.
147
+
148
+ removeWaterMark
149
+ - Meaning: Controls whether a short welcome watermark message (with a GitHub link and request to star the project) is printed at startup.
150
+ - Type: bool
151
+ - Default: `False` (watermark shown)
152
+
153
+ userGlobals
154
+ - Meaning: The `globals()` mapping that the console will use as its global namespace. Variables, functions, and imports in this mapping will be visible to code executed in the console.
155
+ - Type: dict-like (typically the dict returned by `globals()`)
156
+ - Default: If omitted, the console infers the caller's globals using `callerFrame.f_globals` (or from `inspect.currentframe().f_back` if `callerFrame` is also omitted).
157
+ - When to pass: Provide this when you want the console to operate on a specific module or custom namespace.
158
+
159
+ userLocals
160
+ - Meaning: The `locals()` mapping used as the console's local namespace. Local variables available at the call site will be visible here.
161
+ - Type: dict-like (typically the dict returned by `locals()`)
162
+ - Default: Inferred from the caller's frame (`callerFrame.f_locals`) if not provided.
163
+
164
+ callerFrame
165
+ - Meaning: An `inspect` frame object used to infer both `userGlobals` and `userLocals` when they are not supplied. It's also used to determine the source file and line number for the "run remaining code" feature.
166
+ - Type: frame object (as returned by `inspect.currentframe()` and `frame.f_back`)
167
+ - Default: If omitted, `probe()` sets `callerFrame = inspect.currentframe().f_back` to automatically capture the frame of the caller.
168
+ - When to pass: Use an explicit frame when calling `probe()` from helper wrappers or non-standard contexts where automatic frame detection would be wrong.
169
+
170
+ Behavioral notes and edge cases
171
+ - `probe()` replaces `sys.stdout`, `sys.stderr`, and `sys.stdin` with console-aware redirectors while the console is running. These streams are restored when the console's `onClose()` runs (but be mindful when embedding Pysole in larger apps).
172
+ - If `runRemainingCode=True` but the source file cannot be read (packaged app, missing file, permission issues), the attempt to read the file will fail — in that case either run Pysole without `runRemainingCode` or pass an explicit `startupCode` (if you extend the API).
173
+ - When `printStartupCode=True`, chunks are determined by top-level lines (zero indent) and their following indented lines. This makes printed execution easier to follow for functions, classes and loops.
174
+
175
+ ## Keyboard Shortcuts
176
+
177
+ | Key | Action |
178
+ | --- | --- |
179
+ | `Enter` | Execute command (if complete) |
180
+ | `Shift+Enter` | Insert newline with auto-indent |
181
+ | `Tab` | Complete the current word / show suggestions |
182
+ | `Up/Down` | Navigate suggestion list |
183
+ | `Escape` | Hide suggestions |
184
+ | `Ctrl Click` | open help panel on the current method/func/class... |
185
+
186
+
187
+ ## Troubleshooting/Notes
188
+
189
+ Behavioral notes and edge cases
190
+
191
+ - `probe()` temporarily replaces `sys.stdout`, `sys.stderr` and `sys.stdin` with redirectors that send text to the GUI console. These are restored on close (`onClose()`). Embedding Pysole in larger apps should take this into account.
192
+ - `runRemainingCode=True` requires the calling module's source file to be available on disk. Running this in frozen/packaged environments may fail.
193
+ - `printStartupCode=True` prints chunks determined by top-level statements (zero indent) and their indented blocks so function/class/loop definitions are grouped with their bodies.
194
+
195
+ Settings and themes
196
+
197
+ Default UI and behavior settings are loaded from `src/pysole/settings.json` (path built from `src/pysole/utils.py`). Themes are listed in `src/pysole/themes.json`. The in-app Theme Picker writes the selected theme back to `settings.json` to persist across sessions.
198
+
199
+ Troubleshooting
200
+
201
+ - If the GUI doesn't start, make sure `customtkinter` is installed for your Python version.
202
+ - On Linux, ensure your Tk/Tcl support is present (system package) and `DISPLAY` is set when running headful UIs.
203
+ - If `runRemainingCode` appears to run the wrong code, check where `probe()` is called (wrappers can shift the caller frame). Use `callerFrame=` to pass an explicit frame if needed.
204
+
205
+ ## Contributing
206
+
207
+ - Bug reports and PRs welcome. Please open issues on the upstream GitHub repository: https://github.com/TzurSoffer/Pysole
208
+ - Keep test changes small and focused. Include a short description of the error / feature and steps to reproduce.
209
+
210
+ Changelog (high level)
211
+
212
+ - See `pyproject.toml` for the current package version.
213
+
214
+ ## License
215
+
216
+ This project is available under the MIT license — see the `LICENSE` file in the repository root.
217
+
@@ -0,0 +1,19 @@
1
+ liveConsole/__init__.py,sha256=GyV_Y3iiiS12JoEiqE55uZPQ8ZDYfOSKvxh5rcfaD1U,20
2
+ liveconsole-1.7.11.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
3
+ pysole/__init__.py,sha256=0Jq2s5WxFVveZW-pGwThGZLt_mVSdGcsT9lpJNEZ6ds,124
4
+ pysole/__main__.py,sha256=QvVFH8J2yzgQaF9MosQ6ajCW67uiRbYliePsTEUCIAg,82
5
+ pysole/commandHistory.py,sha256=xJtWbJ_vgJo2QGgaZJsApTOi_Hm8Yz0V9_zqQUCItj8,1084
6
+ pysole/helpTab.py,sha256=o0uSY-8sw7FuuBrt0FQwcgK6ljNVx3IgRnW7heZ6GvY,2454
7
+ pysole/liveConsole.py,sha256=lzS3dphAQ1i8pQC4E2FY-FyMMSKi-dAN0xr6XUgrNmo,168
8
+ pysole/mainConsole.py,sha256=TSiqbR4WLUuI0hOzjuI9X_TwIEYa6vGnIbgGhKBHJ3Q,15537
9
+ pysole/pysole.py,sha256=PxZAWFkxVqF2OcgecTe9LZ0xZdEdHiS7UYSCbNPf22c,14136
10
+ pysole/settings.json,sha256=cmOtIhRDWHMwmQMESuykWuzJd_jG6iT2tJ-uhSps_3U,722
11
+ pysole/styledTextbox.py,sha256=qQnkShALNbvbZY7hYDM8Gigke63TfbtQYPUsrm4Qk6Q,2145
12
+ pysole/suggestionManager.py,sha256=CGR1wsRIU4le3Bq_6CPAytM1CzTQAV_jUl5KZbg9LPU,6544
13
+ pysole/themes.json,sha256=2KvEfxm-eDsfVKIdBhWk-Qd93wYQub3YwkxbS6CGKH0,2525
14
+ pysole/utils.py,sha256=cKsSPWeYRxPhkiM8Xrb-aMW6w0SU5Xuu9WCgsu6_xtA,1364
15
+ liveconsole-1.7.11.dist-info/METADATA,sha256=hUYw5e9NhozaoUvJvjLnruc6j9uSnCmhmPZa5_gXfmE,10408
16
+ liveconsole-1.7.11.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ liveconsole-1.7.11.dist-info/entry_points.txt,sha256=qtvuJHcex4QqM97x_UawFWJYnfhQRl0jhqLcWRpnAGo,84
18
+ liveconsole-1.7.11.dist-info/top_level.txt,sha256=YGhC2H7bvcDnMwEtvLXF6qQNIZT2Saayyk1KOJyndN8,19
19
+ liveconsole-1.7.11.dist-info/RECORD,,
@@ -0,0 +1,2 @@
1
+ liveConsole
2
+ pysole
pysole/__init__.py CHANGED
@@ -1 +1,4 @@
1
1
  from .pysole import probe, _standalone, InteractiveConsole
2
+ import sys
3
+
4
+ sys.modules['liveConsole'] = sys.modules[__name__]
pysole/mainConsole.py CHANGED
@@ -3,6 +3,7 @@ import traceback
3
3
  from .suggestionManager import CodeSuggestionManager
4
4
  from .commandHistory import CommandHistory
5
5
  from .styledTextbox import StyledTextWindow
6
+ from .utils import stdPrint
6
7
 
7
8
  import tkinter as tk
8
9
 
@@ -11,16 +12,16 @@ class InteractiveConsoleText(StyledTextWindow):
11
12
  def __init__(self, master, helpTab, theme, font, behavior, userLocals=None, userGlobals=None, **kwargs):
12
13
  super().__init__(master, theme=theme, font=font, **kwargs)
13
14
  self.font=(font["FONT"], font["FONT_SIZE"])
14
-
15
+
15
16
  # Initialize components
16
17
  self.PROMPT = behavior["PRIMARY_PROMPT"]
17
18
  self.PROMPT_LENGTH = len(self.PROMPT)
18
19
  self.suggestionManager = CodeSuggestionManager(self, userLocals=userLocals, userGlobals=userGlobals, theme=theme, font=font)
19
20
  self.helpTab = helpTab
20
-
21
+
21
22
  self.navigatingHistory = False
22
23
  self.history = CommandHistory()
23
-
24
+
24
25
  self.inputVar = tk.StringVar()
25
26
  self.waitingForInput = False
26
27
  self.inputLine = "1.0"
@@ -28,10 +29,10 @@ class InteractiveConsoleText(StyledTextWindow):
28
29
  # Track current command
29
30
  self.currentCommandLine = 1
30
31
  self.isExecuting = False
31
-
32
+
32
33
  # Setup bindings
33
34
  self._setupBindings()
34
-
35
+
35
36
  # Initialize with first prompt
36
37
  self.addPrompt()
37
38
 
@@ -49,29 +50,51 @@ class InteractiveConsoleText(StyledTextWindow):
49
50
  self.bind("<Up>", self.onUp)
50
51
  self.bind("<Down>", self.onDown)
51
52
 
53
+ def getPromptPosition(self):
54
+ """Get the position right after the prompt on current command line."""
55
+ return(f"{self.currentCommandLine}.{self.PROMPT_LENGTH}")
56
+
52
57
  def getCurrentLineNumber(self):
53
58
  """Get the line number where current command starts."""
54
59
  return(int(self.index("end-1c").split(".")[0]))
55
-
60
+
61
+ def resetCurrentLineNumber(self):
62
+ self.currentCommandLine = self.getCurrentLineNumber()
63
+
56
64
  def getCommandStartPosition(self):
57
65
  """Get the starting position of the current command."""
58
66
  return(f"{self.currentCommandLine}.0")
59
67
 
60
- def replaceCurrentCommand(self, newCommand):
61
- """Replace the current command with new text."""
68
+ def writeToPrompt(self, text):
69
+ """Write text to the prompt of the console"""
62
70
  if self.isExecuting:
63
71
  return
64
-
72
+
73
+ if text:
74
+ self.insert("end", text)
75
+ self.mark_set("insert", "end")
76
+ self.see("end")
77
+ self.updateStyling(start=self.getPromptPosition()) #< Ensure styling/lexer applied after programmatic change:
78
+
79
+ def replaceCurrentCommand(self, newCommand):
80
+ """Replace the current command with new text."""
65
81
  start = self.getPromptPosition()
66
82
  end = "end-1c"
67
-
68
83
  self.delete(start, end)
69
- if newCommand:
70
- self.insert(start, newCommand)
71
- self.mark_set("insert", "end")
72
- self.see("end")
73
- # Ensure styling/lexer applied after programmatic change:
74
- self.updateStyling(start=self.getPromptPosition())
84
+ self.writeToPrompt(newCommand)
85
+
86
+ def runCommand(self, command, printCommand=False, clearPrompt=True):
87
+ """Insert code into the console prompt and execute it as if Enter was pressed."""
88
+ if self.isExecuting:
89
+ return(False)
90
+ if printCommand:
91
+ if clearPrompt:
92
+ self.replaceCurrentCommand(command) #< Replace current command with the new code
93
+ else:
94
+ self.writeToPrompt(command)
95
+ self.onEnter(None, insertWhitespace=False) #< Simulate pressing Enter to run the command
96
+ else:
97
+ self.executeCommandThreaded(command, addPrompt=False)
75
98
 
76
99
  def isCursorInEditableArea(self):
77
100
  """Check if cursor is in the editable command area."""
@@ -84,7 +107,7 @@ class InteractiveConsoleText(StyledTextWindow):
84
107
  return((cursorLine >= self.currentCommandLine and
85
108
  (cursorLine > self.currentCommandLine or cursorCol >= self.PROMPT_LENGTH)))
86
109
 
87
- def onEnter(self, event):
110
+ def onEnter(self, event, insertWhitespace=True):
88
111
  """Handle Enter key - execute command."""
89
112
  self.suggestionManager.hideSuggestions()
90
113
 
@@ -94,25 +117,25 @@ class InteractiveConsoleText(StyledTextWindow):
94
117
  self.inputVar.set(line)
95
118
  self.waitingForInput = False
96
119
  return("break")
97
-
120
+
98
121
  if self.isExecuting:
99
122
  return("break")
100
-
123
+
101
124
  command = self.getCurrentCommand()
102
-
125
+ # print(command)
126
+
103
127
  if not command.strip():
104
128
  return("break")
105
-
129
+
106
130
  # Check if statement is incomplete
107
- if self.isIncompleteStatement(command):
131
+ if self.isIncompleteStatement(command) and insertWhitespace:
108
132
  return(self.onShiftEnter(event))
109
-
110
133
  # Execute the command
111
134
  self.history.add(command)
112
135
  self.mark_set("insert", "end")
113
136
  self.insert("end", "\n")
114
137
  self.see("end")
115
-
138
+
116
139
  # Execute in thread
117
140
  self.isExecuting = True
118
141
  threading.Thread(
@@ -120,7 +143,7 @@ class InteractiveConsoleText(StyledTextWindow):
120
143
  args=(command,),
121
144
  daemon=True
122
145
  ).start()
123
-
146
+
124
147
  return("break")
125
148
 
126
149
  def readInput(self):
@@ -324,17 +347,24 @@ class InteractiveConsoleText(StyledTextWindow):
324
347
 
325
348
  return(currentIndent)
326
349
 
327
- def writeOutput(self, text, tag="output"):
350
+ def writeOutput(self, text, tag="output", loc="end", timeout=None):
328
351
  """Write output to the console (thread-safe)."""
352
+ doneEvent = threading.Event()
353
+
329
354
  def _write():
330
- self.insert("end", text + "\n", tag)
331
- self.see("end")
332
-
355
+ try:
356
+ self.insert(loc, text + "\n", tag)
357
+ self.see("end")
358
+ finally:
359
+ doneEvent.set() # Signal completion
360
+
333
361
  self.after(0, _write)
334
362
 
335
- def getPromptPosition(self):
336
- """Get the position right after the prompt on current command line."""
337
- return(f"{self.currentCommandLine}.{self.PROMPT_LENGTH}")
363
+ doneEvent.wait(timeout)
364
+
365
+ def newline(self):
366
+ """Insert a newline at the end."""
367
+ self.writeOutput("")
338
368
 
339
369
  def getCurrentCommand(self):
340
370
  """Extract the current command text (without prompt)."""
@@ -357,16 +387,15 @@ class InteractiveConsoleText(StyledTextWindow):
357
387
  self.mark_set("insert", "end")
358
388
  self.see("end")
359
389
  self.isExecuting = False
360
-
390
+
361
391
  if self.isExecuting:
362
392
  self.after(0, _add)
363
393
  else:
364
394
  _add()
365
-
366
- def executeCommandThreaded(self, command):
395
+
396
+ def executeCommandThreaded(self, command, addPrompt=True):
367
397
  """Execute a command in a separate thread."""
368
398
  try:
369
- # Try eval first for expressions
370
399
  result = eval(command, self.master.userGlobals, self.master.userLocals)
371
400
  if result is not None:
372
401
  self.writeOutput(str(result), "result")
@@ -380,5 +409,7 @@ class InteractiveConsoleText(StyledTextWindow):
380
409
  except Exception:
381
410
  self.writeOutput(traceback.format_exc(), "error")
382
411
 
383
- # Add new prompt after execution
384
- self.addPrompt()
412
+ if addPrompt:
413
+ self.addPrompt()
414
+ else:
415
+ self.isExecuting = False
pysole/pysole.py CHANGED
@@ -1,3 +1,5 @@
1
+ import threading
2
+ import time
1
3
  import customtkinter as ctk
2
4
  import tkinter as tk
3
5
  from tkinter import messagebox
@@ -5,8 +7,9 @@ import inspect
5
7
  import sys
6
8
  import io
7
9
  import json
10
+ import signal
8
11
 
9
- from .utils import settingsPath, themesPath
12
+ from .utils import settingsPath, themesPath, stdPrint, normalizeWhitespace, findUnindentedLine
10
13
  from .helpTab import HelpTab
11
14
  from .mainConsole import InteractiveConsoleText
12
15
 
@@ -35,23 +38,30 @@ class StdinRedirect(io.StringIO):
35
38
 
36
39
  class InteractiveConsole(ctk.CTk):
37
40
  """Main console window application."""
38
-
39
- def __init__(self, userGlobals=None, userLocals=None, callerFrame=None, theme=None, defaultSize=None, primaryPrompt=None):
41
+
42
+ def __init__(self, userGlobals=None, userLocals=None, callerFrame=None,
43
+ defaultSize=None, primaryPrompt=None, font=None, fontSize=None,
44
+ runRemainingCode=False, printStartupCode=False,
45
+ removeWaterMark=False):
40
46
  super().__init__()
41
47
  with open(settingsPath, "r") as f:
42
48
  settings = json.load(f)
43
49
  self.THEME = settings["THEME"]
44
50
  self.FONT = self.THEME["FONT"]
45
51
  self.BEHAVIOR = settings["BEHAVIOR"]
46
-
52
+
47
53
  if primaryPrompt != None:
48
54
  self.BEHAVIOR["PRIMARY_PROMPT"] = primaryPrompt
49
55
  if defaultSize != None:
50
56
  self.BEHAVIOR["DEFAULT_SIZE"] = defaultSize
57
+ if font != None:
58
+ self.FONT["FONT"] = font
59
+ if fontSize != None:
60
+ self.FONT["FONT_SIZE"] = fontSize
51
61
 
52
62
  self.title("Live Interactive Console")
53
63
  self.geometry(self.BEHAVIOR["DEFAULT_SIZE"])
54
-
64
+
55
65
  ctk.set_appearance_mode(self.THEME["APPEARANCE"])
56
66
  ctk.set_default_color_theme("blue")
57
67
 
@@ -63,17 +73,54 @@ class InteractiveConsole(ctk.CTk):
63
73
  userGlobals = callerFrame.f_globals
64
74
  if userLocals is None:
65
75
  userLocals = callerFrame.f_locals
66
-
76
+
77
+ self.callerFrame = callerFrame
67
78
  self.userGlobals = userGlobals
68
79
  self.userLocals = userLocals
69
-
80
+
70
81
  # Create UI
71
82
  self._createMenu()
72
83
  self._createUi()
84
+
85
+ self.protocol("WM_DELETE_WINDOW", self.onClose) #< Wire up window close button (X)
73
86
 
87
+ # Set up signal handler for Ctrl+C from terminal to close gracefully
88
+ def sigint_handler(signum, frame):
89
+ self.onClose()
90
+
91
+ signal.signal(signal.SIGINT, sigint_handler)
92
+
74
93
  # Redirect stdout/stderr
75
94
  self._setupOutputRedirect()
76
95
  self._setupInputRedirect()
96
+
97
+ self.runRemainingCode = runRemainingCode
98
+ self.printStartupCode = printStartupCode
99
+ self.removeWaterMark = removeWaterMark
100
+ self.startupCode = []
101
+ if runRemainingCode:
102
+ self.startupCode = self._getStartupCode()
103
+
104
+ def _getStartupCode(self):
105
+ code_obj = self.callerFrame.f_code
106
+ callStartLineIndex = inspect.getframeinfo(self.callerFrame).positions.lineno #< start of the probe call
107
+ callEndLineIndex = inspect.getframeinfo(self.callerFrame).positions.end_lineno #< end of the probe call
108
+ filename = code_obj.co_filename
109
+
110
+ # Read the rest of the file after the call to probe()
111
+ with open(filename, "r", encoding="utf-8") as f:
112
+ lines = f.readlines()
113
+
114
+ startLine = lines[callStartLineIndex-1]
115
+ for line in lines[callStartLineIndex:callEndLineIndex]:
116
+ startLine += line.strip()
117
+
118
+ startupCode = normalizeWhitespace(lines[callEndLineIndex:]) #< ensure the code is not indented too much (egg if in __name__ == "__main__")
119
+ firstUnindentedLine = findUnindentedLine(startupCode)
120
+ while firstUnindentedLine != 0 and firstUnindentedLine != None: #< handle if probe is inside a loop/if/etc by simply unindenting the call (while is for nested calls)
121
+ startupCode[:firstUnindentedLine] = normalizeWhitespace(startupCode[:firstUnindentedLine])
122
+ firstUnindentedLine = findUnindentedLine(startupCode)
123
+ return(startupCode)
77
124
 
78
125
  def _createMenu(self):
79
126
  """Create a menu bar using CTkOptionMenu."""
@@ -99,7 +146,7 @@ class InteractiveConsole(ctk.CTk):
99
146
  self._editSettings()
100
147
  elif choice == "Load Theme":
101
148
  self._loadTheme()
102
-
149
+
103
150
  def _loadTheme(self):
104
151
  """
105
152
  Open a CTk popup to let the user choose a theme from themes.json.
@@ -175,7 +222,7 @@ class InteractiveConsole(ctk.CTk):
175
222
  def saveSettings():
176
223
  try:
177
224
  newSettings = json.loads(textbox.get("0.0", "end-1c"))
178
- with open("settings.json", "w") as f:
225
+ with open(settingsPath, "w") as f:
179
226
  json.dump(newSettings, f, indent=4)
180
227
  messagebox.showinfo("Success", "Settings saved!")
181
228
  editor.destroy()
@@ -218,28 +265,99 @@ class InteractiveConsole(ctk.CTk):
218
265
  self.console.pack(fill="both", expand=True, padx=5, pady=5)
219
266
  self.console.master = self
220
267
 
221
-
222
268
  def _setupOutputRedirect(self):
223
269
  """Setup stdout/stderr redirection to console."""
224
270
  sys.stdout = StdoutRedirect(self.console.writeOutput)
225
271
  sys.stderr = StdoutRedirect(
226
- lambda text, tag: self.console.writeOutput(text, "error")
272
+ lambda text, tag: self.console.writeOutput(text, "error", "end")
227
273
  )
228
274
 
229
275
  def _setupInputRedirect(self):
230
276
  """Setup stdin redirection to console."""
231
277
  sys.stdin = StdinRedirect(self.console.readInput)
278
+
279
+ def onClose(self):
280
+ sys.stdout = sys.__stdout__
281
+ sys.stderr = sys.__stderr__
282
+ self.destroy()
283
+
284
+ def _printWaterMark(self):
285
+ m = (
286
+ "Welcome to Pysole, if you find me useful, please star me on GitHub:\n"
287
+ "https://github.com/TzurSoffer/Pysole"
288
+ )
289
+ stdPrint(m)
290
+ self.console.newline()
291
+ self.console.writeOutput(m, "instruction")
292
+ time.sleep(0.1)
293
+ self.console.addPrompt()
294
+ if self.runRemainingCode and self.printStartupCode == False:
295
+ self.console.newline()
296
+
297
+ def _splitCodeIntoChunks(self):
298
+ codeChunks = []
299
+ currentChunk = []
300
+
301
+ for line in self.startupCode:
302
+ strippedLine = line.lstrip()
303
+ indentLevel = len(line) - len(strippedLine)
304
+
305
+ if not strippedLine: #< Blank line, keep it in current chunk
306
+ currentChunk.append(line)
307
+ continue
308
+
309
+ if indentLevel != 0:
310
+ currentChunk.append(line)
311
+ else:
312
+ codeChunks.append(currentChunk)
313
+ currentChunk = [line]
314
+
315
+ if currentChunk:
316
+ codeChunks.append(currentChunk)
317
+
318
+ return(["\n".join(chunk).strip() for chunk in codeChunks if any(line.strip() for line in chunk)])
319
+
232
320
 
321
+ def _runStartup(self):
322
+ if self.removeWaterMark == False:
323
+ self._printWaterMark()
324
+
325
+ if self.runRemainingCode == False:
326
+ return
327
+
328
+ if self.printStartupCode == False:
329
+ self.console.newline()
330
+ chunks = self._splitCodeIntoChunks()
331
+ for chunk in chunks:
332
+ while self.console.isExecuting:
333
+ time.sleep(0.01)
334
+ if self.printStartupCode == False:
335
+ self.console.executeCommandThreaded(chunk, addPrompt=False)
336
+ else:
337
+ self.console.runCommand(chunk, printCommand=True, clearPrompt=True)
338
+ if self.printStartupCode == False:
339
+ self.console.addPrompt()
340
+
233
341
  def probe(self, *args, **kwargs):
234
342
  """Start the console main loop."""
343
+ self.after(0, threading.Thread(target=self._runStartup).start)
235
344
  self.mainloop(*args, **kwargs)
236
345
 
237
- def probe(userGlobals=None, userLocals=None, callerFrame=None):
346
+ def probe(userGlobals=None, userLocals=None, callerFrame=None,
347
+ runRemainingCode=False, printStartupCode=False,
348
+ primaryPrompt=None, font=None, fontSize=None, removeWaterMark=False, **kwargs):
238
349
  if callerFrame == None:
239
350
  callerFrame = inspect.currentframe().f_back
240
351
  InteractiveConsole(userGlobals=userGlobals,
241
352
  userLocals=userLocals,
242
- callerFrame=callerFrame).probe()
353
+ callerFrame=callerFrame,
354
+ runRemainingCode=runRemainingCode,
355
+ printStartupCode=printStartupCode,
356
+ primaryPrompt=primaryPrompt,
357
+ font=font,
358
+ fontSize=fontSize,
359
+ removeWaterMark=removeWaterMark,
360
+ **kwargs).probe()
243
361
 
244
362
  def _standalone():
245
363
  import pysole
pysole/settings.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "OUTPUT": "#ffffff",
12
12
  "ERROR": "#ff0000",
13
13
  "RESULT": "#66ccff",
14
+ "INSTRUCTION": "#ffccdd",
14
15
  "SUGGESTION_BOX_BG": "#2d2d2d",
15
16
  "SUGGESTION_BOX_SELECTION_BG": "#0066cc",
16
17
  "FONT": {
pysole/styledTextbox.py CHANGED
@@ -6,7 +6,8 @@ from pygments.styles import get_style_by_name
6
6
  class StyledTextWindow(tk.Text):
7
7
  def __init__(self, master, theme, font, **kwargs):
8
8
  super().__init__(master, **kwargs)
9
-
9
+ self.configure(font=(font["FONT"], font["FONT_SIZE"])) #< default
10
+
10
11
  # Syntax highlighting setup
11
12
  self.lexer = PythonLexer()
12
13
  self.style = get_style_by_name(theme["LEXER_STYLE"])
@@ -16,9 +17,10 @@ class StyledTextWindow(tk.Text):
16
17
  def _setupTags(self, theme, font):
17
18
  """Configure text tags for different output types."""
18
19
  self.tag_configure("prompt", foreground=theme["PROMPT"], font=(font["FONT"], font["FONT_SIZE"], "bold"))
19
- self.tag_configure("output", foreground=theme["OUTPUT"], font=(font["FONT"], font["FONT_SIZE"]))
20
- self.tag_configure("error", foreground=theme["ERROR"], font=(font["FONT"], font["FONT_SIZE"]))
21
- self.tag_configure("result", foreground=theme["RESULT"], font=(font["FONT"], font["FONT_SIZE"]))
20
+ self.tag_configure("output", foreground=theme["OUTPUT"])
21
+ self.tag_configure("error", foreground=theme["ERROR"])
22
+ self.tag_configure("result", foreground=theme["RESULT"])
23
+ self.tag_configure("instruction", foreground=theme["INSTRUCTION"])
22
24
 
23
25
  # Configure syntax highlighting tags
24
26
  for token, style in self.style:
@@ -107,8 +107,11 @@ class CodeSuggestionManager:
107
107
  self.suggestionListbox.selection_set(0)
108
108
 
109
109
  # Position window near cursor
110
- self._positionSuggestionWindow()
111
- self.suggestionWindow.deiconify()
110
+ try: #< some weird errors idk
111
+ self._positionSuggestionWindow()
112
+ self.suggestionWindow.deiconify()
113
+ except:
114
+ pass
112
115
 
113
116
  def _createSuggestionWindow(self):
114
117
  """Create the suggestion popup window."""
pysole/themes.json CHANGED
@@ -11,6 +11,7 @@
11
11
  "OUTPUT": "#ffffff",
12
12
  "ERROR": "#ff0000",
13
13
  "RESULT": "#66ccff",
14
+ "INSTRUCTION": "#ffccdd",
14
15
  "SUGGESTION_BOX_BG": "#2d2d2d",
15
16
  "SUGGESTION_BOX_SELECTION_BG": "#0066cc",
16
17
  "FONT": {
@@ -30,6 +31,7 @@
30
31
  "OUTPUT": "#000000",
31
32
  "ERROR": "#ff0000",
32
33
  "RESULT": "#0066cc",
34
+ "INSTRUCTION": "#ffccdd",
33
35
  "SUGGESTION_BOX_BG": "#f0f0f0",
34
36
  "SUGGESTION_BOX_SELECTION_BG": "#cce6ff",
35
37
  "FONT": {
@@ -49,6 +51,7 @@
49
51
  "OUTPUT": "#586e75",
50
52
  "ERROR": "#dc322f",
51
53
  "RESULT": "#2aa198",
54
+ "INSTRUCTION": "#ffccdd",
52
55
  "SUGGESTION_BOX_BG": "#eee8d5",
53
56
  "SUGGESTION_BOX_SELECTION_BG": "#b58900",
54
57
  "FONT": {
@@ -68,6 +71,7 @@
68
71
  "OUTPUT": "#f8f8f2",
69
72
  "ERROR": "#ff5555",
70
73
  "RESULT": "#8be9fd",
74
+ "INSTRUCTION": "#ffccdd",
71
75
  "SUGGESTION_BOX_BG": "#44475a",
72
76
  "SUGGESTION_BOX_SELECTION_BG": "#6272a4",
73
77
  "FONT": {
pysole/utils.py CHANGED
@@ -1,4 +1,45 @@
1
1
  import os
2
+ import sys
3
+ import re
2
4
 
3
5
  settingsPath = os.path.join(os.path.dirname(__file__), "settings.json")
4
6
  themesPath = os.path.join(os.path.dirname(__file__), "themes.json")
7
+
8
+ def stdPrint(text):
9
+ """Print text to the terminal."""
10
+ try:
11
+ sys.__stdout__.write(f"{text}\n")
12
+ sys.__stdout__.flush()
13
+ except:
14
+ pass
15
+
16
+ def normalizeWhitespace(lines):
17
+ """
18
+ Normalize leading whitespace across a list of code lines.
19
+ Removes common leading indentation and trims excess on over-indented lines.
20
+ """
21
+
22
+ if type(lines) == str:
23
+ lines = lines.split('\n')
24
+
25
+ # Remove empty lines and preserve original line endings
26
+ strippedLines = [line.rstrip('\n') for line in lines if line.strip()]
27
+ if not strippedLines:
28
+ return []
29
+
30
+ # Find minimum indentation across non-empty lines
31
+ indentLevels = [
32
+ len(re.match(r'^[ \t]*', line).group())
33
+ for line in strippedLines
34
+ ]
35
+ minIndent = min(indentLevels)
36
+
37
+
38
+ normalized = [line[minIndent:] if len(line) >= minIndent else line for line in lines] #< Normalize by removing minIndent from each line
39
+ return(normalized)
40
+
41
+ def findUnindentedLine(lines):
42
+ for i, line in enumerate(lines):
43
+ if re.match(r'^\S', line): # Line starts with non-whitespace
44
+ return(i)
45
+ return(None)
@@ -1,138 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: liveConsole
3
- Version: 1.6.2
4
- Summary: An IDLE-like debugger to allow for real-time command injection for debugging and testing python code
5
- Author-email: Tzur Soffer <tzur.soffer@gmail.com>
6
- License: MIT
7
- Requires-Python: >=3.7
8
- Description-Content-Type: text/markdown
9
- License-File: LICENSE
10
- Requires-Dist: customtkinter
11
- Requires-Dist: pygments
12
- Dynamic: license-file
13
-
14
- # PYSOLE
15
-
16
- ## You can finally test your code in real time without using idle!
17
- ### If you found [this repository](https://github.com/TzurSoffer/Pysole) useful, please give it a ⭐!.
18
-
19
- A fully-featured, **live Python console GUI** built with **CustomTkinter** and **Tkinter**, featuring:
20
-
21
- * Python syntax highlighting via **Pygments**
22
-
23
- * Autocomplete for **keywords, built-ins, and local/global variables**
24
-
25
- * Thread-safe execution of Python code
26
-
27
- * Output capturing for `stdout` and `stderr`
28
-
29
- * Multi-line input with auto-indentation
30
-
31
- * History of previous commands
32
-
33
- * **Integrated Help Panel** for quick access to Python object documentation
34
-
35
-
36
- ## Installation
37
-
38
- `pip install liveConsole`
39
-
40
-
41
- ## Features
42
-
43
- ### Standalone Launch
44
-
45
- * Once installed, you can launch the console directly by simply typing ```pysole``` or ```liveconsole``` in the terminal
46
-
47
- * This opens the full GUI without needing to write any code. Perfect for quick debugging and experimenting.
48
-
49
- ### Syntax Highlighting
50
-
51
- * Real-time syntax highlighting using **Pygments** and the **Monokai** style.
52
-
53
- * Highlights Python keywords, built-ins, and expressions in the console.
54
-
55
-
56
- ### Autocomplete
57
-
58
- * Suggests **keywords**, **built-in functions**, and **variables** in scope.
59
-
60
- * Popup list appears after typing at least 2 characters.
61
-
62
- * Only inserts the **missing portion** of a word.
63
-
64
- * Navigate suggestions with **Up/Down arrows**, confirm with **Tab/Return**.
65
-
66
-
67
- ### Multi-Line Input
68
-
69
- * Supports **Shift+Enter** for inserting a new line with proper indentation.
70
-
71
- * Automatically detects incomplete statements and continues the prompt.
72
-
73
-
74
- ### Thread-Safe Execution
75
-
76
- * Executes user code in a separate thread to prevent GUI freezing.
77
-
78
- * Captures both `stdout` and `stderr` output and prints them in the console.
79
-
80
- * Supports both **expressions (`eval`)** and **statements (`exec`)**.
81
-
82
-
83
- ### Clickable History
84
-
85
- * Hover previous commands to see them highlighted.
86
-
87
- * Click to copy them back to the prompt for editing or re-execution.
88
-
89
-
90
- ### Help Panel
91
-
92
- * A resizable right-hand panel that displays Python documentation (help()) for any object.
93
-
94
- * Opens when clicking ctrl+click on a function/method and can be closed with the "X" button.
95
-
96
- * Scrollable and syntax-styled.
97
-
98
- * Perfect for quick reference without leaving the console.
99
-
100
-
101
- ### Easy Integration
102
-
103
- * Automatically grabs **caller frame globals and locals** if not provided.
104
-
105
- * Can be used standalone or embedded in larger CustomTkinter applications.
106
-
107
- ## Usage
108
-
109
- ```
110
- import pysole
111
- pysole.probe()
112
- ```
113
-
114
- * Type Python commands in the `>>>` prompt and see live output.
115
-
116
- ## Keyboard Shortcuts
117
-
118
- | Key | Action |
119
- | --- | --- |
120
- | `Enter` | Execute command (if complete) |
121
- | `Shift+Enter` | Insert newline with auto-indent |
122
- | `Tab` | Complete the current word / show suggestions |
123
- | `Up/Down` | Navigate suggestion list |
124
- | `Escape` | Hide suggestions |
125
- | `Mouse Click` | Select previous command from history |
126
-
127
-
128
- ## Customization
129
-
130
- * **Appearance mode**: Dark mode is default, but can be changed via files menu
131
-
132
- * **Themes**: Pysole has multiple preconfigured themes. You can choose a theme via the Theme Picker, which updates the console colors and appearance. Preconfigured themes are loaded from themes.json and the selected theme is saved in settings.json so it persists across sessions.
133
-
134
-
135
-
136
- ## License
137
-
138
- MIT License – free to use, modify, and distribute.
@@ -1,18 +0,0 @@
1
- liveconsole-1.6.2.dist-info/licenses/LICENSE,sha256=7dZ0zL72aGaFE0C9DxacOpnaSkC5jajhG6iL7lqhWmU,1064
2
- pysole/__init__.py,sha256=SfaSBmFVSmhyf55UedBCqhi2Ss6Tre-BCKtZb3bSr2k,60
3
- pysole/__main__.py,sha256=QvVFH8J2yzgQaF9MosQ6ajCW67uiRbYliePsTEUCIAg,82
4
- pysole/commandHistory.py,sha256=xJtWbJ_vgJo2QGgaZJsApTOi_Hm8Yz0V9_zqQUCItj8,1084
5
- pysole/helpTab.py,sha256=o0uSY-8sw7FuuBrt0FQwcgK6ljNVx3IgRnW7heZ6GvY,2454
6
- pysole/liveConsole.py,sha256=lzS3dphAQ1i8pQC4E2FY-FyMMSKi-dAN0xr6XUgrNmo,168
7
- pysole/mainConsole.py,sha256=CkZGUOxLhHRRb21gHfxRmuGOwf-H2T0feI9KDskXdgg,14397
8
- pysole/pysole.py,sha256=vTmPvS4nzapVudNPtf2JrnwBWB-x_n-XxBsr4jW7MK8,9230
9
- pysole/settings.json,sha256=6wCdMlemV6PblPKeQQsIiCrGWPLDLOMFD3hLMVAXL24,687
10
- pysole/styledTextbox.py,sha256=zpbaN0qX5FduhNyuWGU7y8Ll8J9p9YqczpSuRLo4PX0,2120
11
- pysole/suggestionManager.py,sha256=EUFeCQoZnLS8EjPPNuZpzjY0BR07m2KP5TloyaMDVGY,6457
12
- pysole/themes.json,sha256=2RG3ohn2ZBc2-h9bdooQZD6hW5pD41a5c2jdaKDA4MA,2385
13
- pysole/utils.py,sha256=VN42ukHMJpOvGb7ZV-qhF_zRUt13hzJKV_uRn9t6580,155
14
- liveconsole-1.6.2.dist-info/METADATA,sha256=WSL_DMarxOF1Gvz0BzC1aL8PgyI_1WFP6LuIm2nAatw,4011
15
- liveconsole-1.6.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- liveconsole-1.6.2.dist-info/entry_points.txt,sha256=qtvuJHcex4QqM97x_UawFWJYnfhQRl0jhqLcWRpnAGo,84
17
- liveconsole-1.6.2.dist-info/top_level.txt,sha256=DlpA93ScJbRZcF8kGSc5YoO8Ntu1ib1_MEZMb4xea_Q,7
18
- liveconsole-1.6.2.dist-info/RECORD,,
@@ -1 +0,0 @@
1
- pysole