-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathPyRef.py
More file actions
1743 lines (1552 loc) · 101 KB
/
Copy pathPyRef.py
File metadata and controls
1743 lines (1552 loc) · 101 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import tkinter as tk # GUI toolkit for creating the user interface
import builtins # Provides access to Python's built-in functions, exceptions, and attributes
import importlib # Allows dynamic importing of modules, useful for inspecting installed packages
import subprocess # Enables running external commands, like 'pip freeze'
import inspect # Provides tools for examining live objects, like getting function signatures and docstrings
import requests # Used for making HTTP requests, specifically to fetch data from PyPI
import json # For encoding and decoding JSON data, used for caching
import os # Provides functions for interacting with the operating system, like file paths and directories
import sys # Provides access to system-specific parameters and functions, like sys.path
import site # Provides access to site-specific configuration, like site-packages directories
import time # For time-related functions, used in cache expiry calculations
from tkinter import font, scrolledtext, messagebox, Menu # Specific Tkinter widgets and modules
import re # Regular expression operations, used for parsing docstrings and highlighting
import webbrowser # Allows opening web browsers, used for PyPI links
# --- Global Configuration and Directories ---
# Define directory paths for caching and user notes.
# os.path.expanduser("~") gets the user's home directory, ensuring cross-platform compatibility.
CACHE_DIR = os.path.join(os.path.expanduser("~"), ".pyref_cache")
NOTES_DIR = os.path.join(os.path.expanduser("~"), ".pyref_notes")
# Define specific file paths within the cache directory for different data types.
STANDARD_CACHE_FILE = os.path.join(CACHE_DIR, "standard_commands.json")
INSTALLED_CACHE_FILE = os.path.join(CACHE_DIR, "installed_modules.json")
PYPI_INDEX_CACHE_FILE = os.path.join(CACHE_DIR, "pypi_index.json")
PYPI_DETAIL_CACHE_DIR = os.path.join(CACHE_DIR, "pypi_details")
# --- Cache Expiry Settings ---
# Define how long different types of cached data remain valid (in seconds).
# This prevents displaying stale information and ensures periodic updates.
CACHE_EXPIRY_SECONDS = {
"standard": 3600 * 24 * 30, # Standard commands cache (30 days) - rarely changes
"installed": 3600 * 24 * 7, # Installed modules cache (7 days) - updates when pip changes
"pypi_index": 3600 * 24, # PyPI index cache (1 day) - frequently updated
"pypi_detail": 3600 * 24 * 7 # Individual PyPI package details (7 days)
}
# --- Cache Management Functions ---
def ensure_cache_dir():
"""Ensures that the necessary cache and notes directories exist.
If they don't exist, they are created. This prevents FileNotFoundError.
"""
os.makedirs(CACHE_DIR, exist_ok=True) # Creates the main cache directory if it doesn't exist
os.makedirs(NOTES_DIR, exist_ok=True) # Creates the notes directory if it doesn't exist
os.makedirs(PYPI_DETAIL_CACHE_DIR, exist_ok=True) # Creates the specific directory for PyPI package details
def load_cache(cache_file: str, cache_type: str):
"""Loads data from a specified cache file if it's not expired.
Args:
cache_file (str): The full path to the cache file.
cache_type (str): The type of cache (e.g., "standard", "installed")
to determine its expiry time from CACHE_EXPIRY_SECONDS.
Returns:
dict or list or None: The loaded data if valid and not expired, otherwise None.
"""
ensure_cache_dir() # Make sure directories are ready before trying to load/save
if os.path.exists(cache_file):
file_mod_time = os.path.getmtime(cache_file) # Get the last modification time of the cache file
# Check if the cache file is still valid (not expired)
if (time.time() - file_mod_time) < CACHE_EXPIRY_SECONDS.get(cache_type, 0):
try:
with open(cache_file, 'r', encoding='utf-8') as f:
return json.load(f) # Load and return the JSON data
except json.JSONDecodeError:
# Handle corrupted JSON files
print(f"Error decoding JSON from {cache_file}. Cache will be refreshed.")
os.remove(cache_file) # Delete the corrupted cache to force a refresh
return None
except Exception as e:
# Catch any other unexpected errors during file loading
print(f"Unexpected error loading cache from {cache_file}: {e}")
return None
return None # Return None if cache file doesn't exist or is expired
def save_cache(data, cache_file: str):
"""Saves data to a specified cache file in JSON format.
Args:
data: The data (e.g., list, dictionary) to be saved.
cache_file (str): The full path to the cache file.
"""
ensure_cache_dir() # Ensure directories exist before saving
try:
with open(cache_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=4) # Save data as pretty-printed JSON
except IOError as e:
# Handle potential errors during file writing
print(f"Error saving cache to {cache_file}: {e}")
# --- PyRef GUI Class ---
class PythonHelperGUI:
"""The main application class for PyRef, handling the GUI and logic."""
def __init__(self, master: tk.Tk):
"""Initializes the PyRef GUI application.
Args:
master (tk.Tk): The root Tkinter window.
"""
self.master = master # Store the root Tkinter window
master.title("PyRef") # Set the window title
master.geometry("1000x700") # Set the initial window size
self._setup_sys_path() # Configure system path for module imports
# --- Curated Syntax Overrides for C-implemented Built-ins ---
# This dictionary provides explicit syntax, parameters, and simple examples for
# built-in functions that `inspect.signature()` cannot properly introspect
# (e.g., because they are implemented in C). This ensures clear documentation
# for these common functions.
self._builtin_syntax_override = {
"abs": {
"syntax": "abs(number)",
"parameters": [
{"name": "number", "description": "A numeric value (integer, float, or complex)."}
],
"example": "x = abs(-7.25)\nprint(x) # Output: 7.25"
},
"aiter": {
"syntax": "aiter(async_iterable)",
"parameters": [
{"name": "async_iterable", "description": "An asynchronous iterable object."}
],
"example": "async def my_async_gen():\n yield 1\n yield 2\n\nasync def main():\n it = aiter(my_async_gen())\n print(await anext(it)) # Output: 1\n\nimport asyncio\nasyncio.run(main())"
},
"all": {
"syntax": "all(iterable)",
"parameters": [
{"name": "iterable", "description": "An iterable (e.g., list, tuple, string) containing items to check."}
],
"example": "all([True, True, False]) # Output: False\nall([1, 2, 3]) # Output: True (all are truthy)"
},
"anext": {
"syntax": "anext(async_iterator[, default])",
"parameters": [
{"name": "async_iterator", "description": "An asynchronous iterator object."},
{"name": "default", "description": "Optional. The value to return if the iterator is exhausted."}
],
"example": "async def my_async_gen():\n yield 1\n yield 2\n\nasync def main():\n it = aiter(my_async_gen())\n print(await anext(it)) # Output: 1\n print(await anext(it)) # Output: 2\n print(await anext(it, 'End')) # Output: End\n\nimport asyncio\nasyncio.run(main())"
},
"any": {
"syntax": "any(iterable)",
"parameters": [
{"name": "iterable", "description": "An iterable containing items to check."}
],
"example": "any([False, False, True]) # Output: True\nany([]) # Output: False"
},
"ascii": {
"syntax": "ascii(object)",
"parameters": [
{"name": "object", "description": "An object to represent as an ASCII string."}
],
"example": "ascii('€') # Output: '\\u20ac'\nascii('hello') # Output: 'hello'"
},
"bin": {
"syntax": "bin(number)",
"parameters": [
{"name": "number", "description": "An integer."}
],
"example": "bin(10) # Output: '0b1010'"
},
"bool": {
"syntax": "bool([x])",
"parameters": [
{"name": "x", "description": "An optional value to convert to boolean. If omitted, returns False."}
],
"example": "bool(0) # Output: False\nbool('hello') # Output: True"
},
"breakpoint": {
"syntax": "breakpoint(*args, **kwargs)",
"parameters": [
{"name": "*args, **kwargs", "description": "Arguments passed to the debugger callable."}
],
"example": "def my_func():\n a = 10\n breakpoint() # Execution will pause here\n b = 20\n print(a + b)\n\n# To use:\n# Run your script, when it hits breakpoint(), you'll enter the debugger (e.g., pdb).\n# Type 'c' to continue execution."
},
"bytes": {
"syntax": "bytes([source[, encoding[, errors]]])",
"parameters": [
{"name": "source", "description": "Optional. An int, iterable of ints, str, or buffer object."}
],
"example": "bytes(5) # Output: b'\\x00\\x00\\x00\\x00\\x00'\nb = 'hello'.encode('utf-8')\nprint(b) # Output: b'hello'"
},
"bytearray": {
"syntax": "bytearray([source[, encoding[, errors]]])",
"parameters": [
{"name": "source", "description": "Optional. An int, iterable of ints, str, or buffer object."}
],
"example": "arr = bytearray(b'hello')\narr[0] = ord('J')\nprint(arr) # Output: bytearray(b'Jello')"
},
"callable": {
"syntax": "callable(object)",
"parameters": [
{"name": "object", "description": "The object to check."}
],
"example": "def func(): pass\nprint(callable(func)) # Output: True\nprint(callable(10)) # Output: False"
},
"chr": {
"syntax": "chr(i)",
"parameters": [
{"name": "i", "description": "An integer representing a Unicode code point."}
],
"example": "chr(97) # Output: 'a'"
},
"compile": {
"syntax": "compile(source, filename, mode, flags=0, dont_inherit=False, optimize=-1)",
"parameters": [
{"name": "source", "description": "The source code as a string, bytes, or AST object."},
{"name": "filename", "description": "The filename (used for error messages)."},
{"name": "mode", "description": "Specifies the kind of code: 'eval', 'exec', or 'single'."}
],
"example": "code_obj = compile('a = 10\\nprint(a)', '<string>', 'exec')\nexec(code_obj) # Output: 10"
},
"copyright": {
"syntax": "copyright",
"parameters": [],
"example": "print(copyright) # Displays Python's copyright notice"
},
"credits": {
"syntax": "credits",
"parameters": [],
"example": "print(credits) # Displays Python's credits"
},
"dict": {
"syntax": "dict(**kwargs) or dict(mapping, **kwargs) or dict(iterable, **kwargs)",
"parameters": [
{"name": "kwargs", "description": "Keyword arguments where keys are strings and values are dictionary values."},
{"name": "mapping", "description": "A dictionary or other mapping object."},
{"name": "iterable", "description": "An iterable of key-value pairs (e.g., a list of tuples)."}
],
"example": "d1 = dict(a=1, b=2) # Output: {'a': 1, 'b': 2}\nd2 = dict([('c', 3), ('d', 4)]) # Output: {'c': 3, 'd': 4}"
},
"dir": {
"syntax": "dir([object])",
"parameters": [
{"name": "object", "description": "Optional. An object. If omitted, returns names in the current scope."}
],
"example": "dir() # List names in current scope\ndir([]) # List methods of a list"
},
"divmod": {
"syntax": "divmod(a, b)",
"parameters": [
{"name": "a", "description": "Dividend."},
{"name": "b", "description": "Divisor."}
],
"example": "divmod(7, 3) # Output: (2, 1) (quotient, remainder)"
},
"enumerate": {
"syntax": "enumerate(iterable, start=0)",
"parameters": [
{"name": "iterable", "description": "A sequence, an iterator, or some other object that supports iteration."},
{"name": "start", "description": "Optional. The index value for the first item (default is 0)."}
],
"example": "for i, item in enumerate(['a', 'b', 'c']):\n print(f'{i}: {item}')\n# Output:\n# 0: a\n# 1: b\n# 2: c"
},
"eval": {
"syntax": "eval(expression[, globals[, locals]])",
"parameters": [
{"name": "expression", "description": "A string containing a Python expression."},
{"name": "globals", "description": "Optional. A dictionary of global names."},
{"name": "locals", "description": "Optional. A dictionary of local names."}
],
"example": "x = 10\nprint(eval('x + 5')) # Output: 15\nprint(eval('sum([1, 2, 3])')) # Output: 6"
},
"exec": {
"syntax": "exec(object[, globals[, locals]])",
"parameters": [
{"name": "object", "description": "A string containing Python statements, or a code object."},
{"name": "globals", "description": "Optional. A dictionary of global names."},
{"name": "locals", "description": "Optional. A dictionary of local names."}
],
"example": "code = 'for i in range(3): print(i)'\nexec(code)\n# Output:\n# 0\n# 1\n# 2"
},
"exit": {
"syntax": "exit([code=None])",
"parameters": [
{"name": "code", "description": "Optional. An exit status code (default None)."}
],
"example": "import sys\n# exit()\n# exit('Exiting program')\n# Note: Calling exit() directly in some environments (like IDLE) might just raise SystemExit"
},
"filter": {
"syntax": "filter(function, iterable)",
"parameters": [
{"name": "function", "description": "A function to test if an element of an iterable passes a condition."},
{"name": "iterable", "description": "An iterable that is to be filtered."}
],
"example": "numbers = [1, 2, 3, 4, 5]\neven_numbers = list(filter(lambda x: x % 2 == 0, numbers))\nprint(even_numbers) # Output: [2, 4]"
},
"float": {
"syntax": "float([x])",
"parameters": [
{"name": "x", "description": "Optional. A number or string representing a number."}
],
"example": "float('3.14') # Output: 3.14\nfloat(5) # Output: 5.0"
},
"format": {
"syntax": "format(value[, format_spec])",
"parameters": [
{"name": "value", "description": "The value to be formatted."},
{"name": "format_spec", "description": "Optional. A format specifier string (e.g., '.2f', '>10s')."}
],
"example": "format(3.14159, '.2f') # Output: '3.14'\nformat(123, '0>5') # Output: '00123'"
},
"frozenset": {
"syntax": "frozenset([iterable])",
"parameters": [
{"name": "iterable", "description": "Optional. An iterable from which to initialize the frozenset."}
],
"example": "fs = frozenset([1, 2, 3])\nprint(fs) # Output: frozenset({1, 2, 3})"
},
"getattr": {
"syntax": "getattr(object, name[, default])",
"parameters": [
{"name": "object", "description": "The object to get the attribute from."},
{"name": "name", "description": "A string representing the attribute's name."},
{"name": "default", "description": "Optional. The value to return if the named attribute does not exist."}
],
"example": "class MyClass:\n value = 10\nobj = MyClass()\nprint(getattr(obj, 'value')) # Output: 10\nprint(getattr(obj, 'other', 'default')) # Output: default"
},
"globals": {
"syntax": "globals()",
"parameters": [],
"example": "print(globals()) # Returns a dictionary of the current global symbol table"
},
"hasattr": {
"syntax": "hasattr(object, name)",
"parameters": [
{"name": "object", "description": "The object to check."},
{"name": "name", "description": "A string representing the attribute's name."}
],
"example": "class MyClass:\n value = 10\nobj = MyClass()\nprint(hasattr(obj, 'value')) # Output: True\nprint(hasattr(obj, 'other')) # Output: False"
},
"hash": {
"syntax": "hash(object)",
"parameters": [
{"name": "object", "description": "The object to hash."}
],
"example": "hash('hello') # Returns an integer hash value"
},
"help": {
"syntax": "help([object])",
"parameters": [
{"name": "object", "description": "Optional. The object for which to display help."}
],
"example": "help(list) # Displays help for the list type\nhelp('modules') # Lists all available modules"
},
"hex": {
"syntax": "hex(number)",
"parameters": [
{"name": "number", "description": "An integer."}
],
"example": "hex(255) # Output: '0xff'"
},
"id": {
"syntax": "id(object)",
"parameters": [
{"name": "object", "description": "Any object."}
],
"example": "x = 10\nid(x) # Returns the identity of x (an integer)"
},
"input": {
"syntax": "input([prompt])",
"parameters": [
{"name": "prompt", "description": "Optional. A string that is printed to the console before reading input."}
],
"example": "name = input('Enter your name: ')\nprint(f'Hello, {name}')"
},
"int": {
"syntax": "int([x=0]) or int(x, base=10)",
"parameters": [
{"name": "x", "description": "Optional. A number or string to convert to an integer."},
{"name": "base", "description": "Optional. The base of the number if `x` is a string (default 10)."}
],
"example": "int(3.14) # Output: 3\nint('FF', 16) # Output: 255"
},
"isinstance": {
"syntax": "isinstance(object, classinfo)",
"parameters": [
{"name": "object", "description": "The object to check."},
{"name": "classinfo", "description": "A class, type, or tuple of classes and types."}
],
"example": "isinstance(10, int) # Output: True\nisinstance('hello', (str, list)) # Output: True"
},
"issubclass": {
"syntax": "issubclass(class, classinfo)",
"parameters": [
{"name": "class", "description": "The class to check."},
{"name": "classinfo", "description": "A class, type, or tuple of classes and types."}
],
"example": "class A: pass\nclass B(A): pass\nissubclass(B, A) # Output: True"
},
"iter": {
"syntax": "iter(object[, sentinel])",
"parameters": [
{"name": "object", "description": "An object that supports iteration (e.g., list, tuple) or a callable."},
{"name": "sentinel", "description": "Optional. If provided, `object` must be a callable; iteration stops when `object()` returns `sentinel`."}
],
"example": "my_list = [1, 2, 3]\nmy_iter = iter(my_list)\nprint(next(my_iter)) # Output: 1"
},
"len": {
"syntax": "len(s)",
"parameters": [
{"name": "s", "description": "An object that has a length (e.g., sequence, collection, string)."}
],
"example": "len('hello') # Output: 5\nlen([1, 2, 3]) # Output: 3"
},
"license": {
"syntax": "license",
"parameters": [],
"example": "print(license) # Displays Python's license information"
},
"list": {
"syntax": "list([iterable])",
"parameters": [
{"name": "iterable", "description": "Optional. An iterable from which to create the list."}
],
"example": "my_list = list('abc')\nprint(my_list) # Output: ['a', 'b', 'c']"
},
"locals": {
"syntax": "locals()",
"parameters": [],
"example": "def my_func():\n x = 10\n y = 20\n print(locals()) # Returns a dictionary of the current local symbol table\nmy_func()"
},
"map": {
"syntax": "map(function, iterable, ...)",
"parameters": [
{"name": "function", "description": "A function to apply to each item of the iterable(s)."},
{"name": "iterable", "description": "One or more iterables."}
],
"example": "numbers = [1, 2, 3]\nsquared = list(map(lambda x: x*x, numbers))\nprint(squared) # Output: [1, 4, 9]"
},
"max": {
"syntax": "max(iterable, *[, key, default]) or max(arg1, arg2, *args[, key])",
"parameters": [
{"name": "iterable", "description": "An iterable of values."},
{"name": "arg1, arg2, *args", "description": "Two or more positional arguments."},
{"name": "key", "description": "Optional. A function to customize the comparison (like `sorted`)."},
{"name": "default", "description": "Optional. The value to return if the iterable is empty (only when one iterable is provided)."}
],
"example": "max([1, 5, 2]) # Output: 5\nmax(10, 20, 5) # Output: 20"
},
"min": {
"syntax": "min(iterable, *[, key, default]) or min(arg1, arg2, *args[, key])",
"parameters": [
{"name": "iterable", "description": "An iterable of values."},
{"name": "arg1, arg2, *args", "description": "Two or more positional arguments."},
{"name": "key", "description": "Optional. A function to customize the comparison (like `sorted`)."},
{"name": "default", "description": "Optional. The value to return if the iterable is empty (only when one iterable is provided)."}
],
"example": "min([1, 5, 2]) # Output: 1\nmin(10, 20, 5) # Output: 5"
},
"next": {
"syntax": "next(iterator[, default])",
"parameters": [
{"name": "iterator", "description": "An iterator object."},
{"name": "default", "description": "Optional. The value to return if the iterator is exhausted."}
],
"example": "it = iter([1, 2])\nprint(next(it)) # Output: 1\nprint(next(it)) # Output: 2\nprint(next(it, 'End')) # Output: End"
},
"object": {
"syntax": "object()",
"parameters": [],
"example": "obj = object()\nprint(type(obj)) # Output: <class 'object'>"
},
"oct": {
"syntax": "oct(number)",
"parameters": [
{"name": "number", "description": "An integer."}
],
"example": "oct(8) # Output: '0o10'"
},
"open": {
"syntax": "open(file, mode='r', encoding=None, ...)",
"parameters": [
{"name": "file", "description": "Path to the file or file descriptor."},
{"name": "mode", "description": "Optional. Mode string ('r', 'w', 'a', 'b', 't', '+', etc.)."},
{"name": "encoding", "description": "Optional. Encoding for text mode (e.g., 'utf-8')."}
],
"example": "with open('my_file.txt', 'w') as f:\n f.write('Hello, world!')"
},
"ord": {
"syntax": "ord(c)",
"parameters": [
{"name": "c", "description": "A single Unicode character."}
],
"example": "ord('A') # Output: 65"
},
"pow": {
"syntax": "pow(base, exp[, mod])",
"parameters": [
{"name": "base", "description": "The base number."},
{"name": "exp", "description": "The exponent."},
{"name": "mod", "description": "Optional. The modulus (if provided, returns (base**exp) % mod)."}
],
"example": "pow(2, 3) # Output: 8\npow(2, 3, 3) # Output: 2 (8 % 3)"
},
"print": {
"syntax": "print(*objects, sep=' ', end='\\n', file=sys.stdout, flush=False)",
"parameters": [
{"name": "objects", "description": "One or more objects to print."},
{"name": "sep", "description": "Optional. String inserted between values, default a space."},
{"name": "end", "description": "Optional. String appended after the last value, default a newline."},
{"name": "file", "description": "Optional. A file-like object (stream) to write to, default sys.stdout."},
{"name": "flush", "description": "Optional. If True, the stream is forcibly flushed."}
],
"example": "print('Hello', 'World', sep='-') # Output: Hello-World\nprint('Done.', end='')"
},
"property": {
"syntax": "property(fget=None, fset=None, fdel=None, doc=None)",
"parameters": [
{"name": "fget", "description": "Optional. Function to get an attribute value."},
{"name": "fset", "description": "Optional. Function to set an attribute value."},
{"name": "fdel", "description": "Optional. Function to delete an attribute value."},
{"name": "doc", "description": "Optional. Docstring for the property."}
],
"example": "class C:\n def __init__(self, x):\n self._x = x\n def getx(self):\n return self._x\n def setx(self, value):\n self._x = value\n x = property(getx, setx)"
},
"quit": {
"syntax": "quit([code=None])",
"parameters": [
{"name": "code", "description": "Optional. An exit status code (default None)."}
],
"example": "import sys\n# quit()\n# quit('Exiting program')\n# Note: Calling quit() directly in some environments (like IDLE) might just raise SystemExit"
},
"range": {
"syntax": "range(stop) or range(start, stop[, step])",
"parameters": [
{"name": "start", "description": "Optional. The starting number of the sequence (inclusive, default 0)."},
{"name": "stop", "description": "The ending number of the sequence (exclusive)."},
{"name": "step", "description": "Optional. The increment between numbers (default 1)."}
],
"example": "list(range(5)) # Output: [0, 1, 2, 3, 4]\nlist(range(1, 10, 2)) # Output: [1, 3, 5, 7, 9]"
},
"repr": {
"syntax": "repr(object)",
"parameters": [
{"name": "object", "description": "Any object."}
],
"example": "repr('hello') # Output: \"'hello'\"\nrepr([1, 2]) # Output: '[1, 2]'"
},
"reversed": {
"syntax": "reversed(seq)",
"parameters": [
{"name": "seq", "description": "A sequence object (list, tuple, string) that supports `__len__()` or `__getitem__()`."}
],
"example": "list(reversed([1, 2, 3])) # Output: [3, 2, 1]"
},
"round": {
"syntax": "round(number[, ndigits])",
"parameters": [
{"name": "number", "description": "The number to round."},
{"name": "ndigits", "description": "Optional. The number of decimal places to round to. If omitted, rounds to the nearest integer."}
],
"example": "round(3.14159, 2) # Output: 3.14\nround(2.5) # Output: 2 (rounds to nearest even)"
},
"set": {
"syntax": "set([iterable])",
"parameters": [
{"name": "iterable", "description": "Optional. An iterable from which to initialize the set."}
],
"example": "my_set = set([1, 2, 2, 3])\nprint(my_set) # Output: {1, 2, 3}"
},
"setattr": {
"syntax": "setattr(object, name, value)",
"parameters": [
{"name": "object", "description": "The object to set the attribute on."},
{"name": "name", "description": "A string representing the attribute's name."},
{"name": "value", "description": "The value to set the attribute to."}
],
"example": "class MyClass:\n pass\nobj = MyClass()\nsetattr(obj, 'attribute_name', 'some_value')\nprint(obj.attribute_name) # Output: some_value"
},
"slice": {
"syntax": "slice(stop) or slice(start, stop[, step])",
"parameters": [
{"name": "start", "description": "Optional. The starting index (inclusive, default 0)."},
{"name": "stop", "description": "The ending index (exclusive)."},
{"name": "step", "description": "Optional. The step or increment (default 1)."}
],
"example": "my_list = [1, 2, 3, 4, 5]\ns = slice(1, 4)\nprint(my_list[s]) # Output: [2, 3, 4]"
},
"sorted": {
"syntax": "sorted(iterable, *, key=None, reverse=False)",
"parameters": [
{"name": "iterable", "description": "An iterable to be sorted."},
{"name": "key", "description": "Optional. A function to be called on each list element prior to making comparisons."},
{"name": "reverse", "description": "Optional. If True, sort in descending order."}
],
"example": "sorted([3, 1, 4]) # Output: [1, 3, 4]\nsorted(['apple', 'Banana'], key=str.lower) # Output: ['Banana', 'apple']"
},
"staticmethod": {
"syntax": "@staticmethod",
"parameters": [],
"example": "class MyClass:\n @staticmethod\n def my_static_method():\n return 'This is a static method'"
},
"str": {
"syntax": "str(object='') or str(object, encoding, errors)",
"parameters": [
{"name": "object", "description": "Optional. An object to convert to a string."},
{"name": "encoding", "description": "Optional. The encoding of the object if it's bytes."},
{"name": "errors", "description": "Optional. How to handle encoding errors."}
],
"example": "str(123) # Output: '123'\nstr(b'bytes', 'utf-8') # Output: 'bytes'"
},
"sum": {
"syntax": "sum(iterable, start=0)",
"parameters": [
{"name": "iterable", "description": "An iterable of numbers."},
{"name": "start", "description": "Optional. An initial value to which the items are added (default 0)."}
],
"example": "sum([1, 2, 3]) # Output: 6\nsum([1, 2, 3], 10) # Output: 16"
},
"super": {
"syntax": "super([type[, object_or_type]])",
"parameters": [
{"name": "type", "description": "The type of the class that calls `super()`."},
{"name": "object_or_type", "description": "An instance of `type` or a subtype of `type`."}
],
"example": "class Parent:\n def greet(self): return 'Hello from Parent'\nclass Child(Parent):\n def greet(self):\n return super().greet() + ' and Child'\nprint(Child().greet()) # Output: Hello from Parent and Child"
},
"tuple": {
"syntax": "tuple([iterable])",
"parameters": [
{"name": "iterable", "description": "Optional. An iterable from which to create the tuple."}
],
"example": "my_tuple = tuple([1, 2, 3])\nprint(my_tuple) # Output: (1, 2, 3)"
},
"type": {
"syntax": "type(object) or type(name, bases, dict)",
"parameters": [
{"name": "object", "description": "The object to get the type of."},
{"name": "name", "description": "String representing the class name."},
{"name": "bases", "description": "Tuple of base classes."},
{"name": "dict", "description": "Dictionary containing the class's namespace."}
],
"example": "type(1) # Output: <class 'int'>\nclass MyClass: pass\nMyClassType = type('MyNewClass', (object,), {'x': 1})\nobj = MyClassType()\nprint(obj.x) # Output: 1"
},
"vars": {
"syntax": "vars([object])",
"parameters": [
{"name": "object", "description": "Optional. An object. If omitted, returns the `__dict__` of the current module."}
],
"example": "class MyClass:\n def __init__(self):\n self.x = 1\n self.y = 2\nobj = MyClass()\nprint(vars(obj)) # Output: {'x': 1, 'y': 2}"
},
"zip": {
"syntax": "zip(*iterables)",
"parameters": [
{"name": "iterables", "description": "One or more iterables."}
],
"example": "list(zip([1, 2], ['a', 'b'])) # Output: [(1, 'a'), (2, 'b')]"
}
}
# End of curated syntax overrides
# --- Menu Bar Setup ---
self.menubar = Menu(master) # Create a menu bar
master.config(menu=self.menubar) # Assign the menu bar to the root window
self.help_menu = Menu(self.menubar, tearoff=0) # Create a 'Help' menu
self.menubar.add_cascade(label="Help", menu=self.help_menu) # Add 'Help' to the menu bar
self.help_menu.add_command(label="About PyRef", command=self.show_about_dialog) # Add 'About' command
# --- Top Control Frame (Search, Navigation, Font Size) ---
top_controls_frame = tk.Frame(master) # Frame to hold top-level controls
top_controls_frame.pack(pady=5, fill=tk.X) # Pack it at the top with padding
self.search_entry = tk.Entry(top_controls_frame) # Input field for search queries
self.search_entry.pack(side=tk.LEFT, padx=5, expand=True, fill=tk.X) # Pack to the left, expands horizontally
self.search_button = tk.Button(top_controls_frame, text="Search", command=self.search) # Search button
self.search_button.pack(side=tk.LEFT, padx=5)
self.clear_search_button = tk.Button(top_controls_frame, text="Clear Search", command=self.clear_search) # Clear search button
self.clear_search_button.pack(side=tk.LEFT, padx=5)
self.back_button = tk.Button(top_controls_frame, text="Back", command=self.go_back) # Back button for history
self.back_button.pack(side=tk.LEFT, padx=5)
self.back_button.config(state=tk.DISABLED) # Disable initially as there's no history yet
self.forward_button = tk.Button(top_controls_frame, text="Forward", command=self.go_forward) # Forward button for history
self.forward_button.pack(side=tk.LEFT, padx=5)
self.forward_button.config(state=tk.DISABLED) # Disable initially
self.current_font_size = 12 # Default font size
self.min_font_size = 8 # Minimum allowed font size
self.max_font_size = 24 # Maximum allowed font size
self.font_decrease_button = tk.Button(top_controls_frame, text="A-", command=self.decrease_font_size) # Decrease font button
self.font_decrease_button.pack(side=tk.RIGHT, padx=2)
self.font_increase_button = tk.Button(top_controls_frame, text="A+", command=self.increase_font_size) # Increase font button
self.font_increase_button.pack(side=tk.RIGHT, padx=2)
# --- Main Paned Window (Left/Right Panels) ---
# A PanedWindow allows the user to resize the left and right panels.
self.main_paned_window = tk.PanedWindow(master, orient=tk.HORIZONTAL, sashrelief=tk.RAISED)
self.main_paned_window.pack(fill=tk.BOTH, expand=True)
# --- Left Frame (Category Buttons and Listbox) ---
left_frame = tk.Frame(self.main_paned_window)
self.main_paned_window.add(left_frame, width=250) # Add left frame to the paned window with initial width
# Category selection buttons
self.standard_button = tk.Button(left_frame, text="STANDARD", command=self.show_standard)
self.standard_button.pack(fill=tk.X, pady=2)
self.installed_button = tk.Button(left_frame, text="INSTALLED", command=self.show_installed)
self.installed_button.pack(fill=tk.X, pady=2)
self.not_installed_button = tk.Button(left_frame, text="NOT INSTALLED (PyPi)", command=self.show_pypi)
self.not_installed_button.pack(fill=tk.X, pady=2)
self.menu_listbox_font = font.Font(family="TkDefaultFont", size=self.current_font_size)
listbox_frame = tk.Frame(left_frame) # Frame to hold the listbox and its scrollbar
listbox_frame.pack(fill=tk.BOTH, expand=True)
self.menu_listbox = tk.Listbox(listbox_frame, width=30, font=self.menu_listbox_font) # Listbox to display commands/modules
self.menu_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
# Bind the listbox selection event to our handler
self.menu_listbox.bind('<<ListboxSelect>>', self._handle_listbox_select)
scrollbar = tk.Scrollbar(listbox_frame, orient="vertical", command=self.menu_listbox.yview) # Scrollbar for the listbox
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
self.menu_listbox.config(yscrollcommand=scrollbar.set) # Connect scrollbar to listbox
# --- Right Frame (Info Display and Notes) ---
right_frame = tk.Frame(self.main_paned_window)
self.main_paned_window.add(right_frame) # Add right frame to the paned window
info_text_controls_frame = tk.Frame(right_frame)
info_text_controls_frame.pack(fill=tk.BOTH, expand=True)
self.info_text_font = font.Font(family="TkDefaultFont", size=self.current_font_size)
# ScrolledText widget for displaying documentation and user notes.
# `wrap=tk.WORD` ensures text wraps at word boundaries.
self.info_text = scrolledtext.ScrolledText(info_text_controls_frame, wrap=tk.WORD, font=self.info_text_font)
self.info_text.pack(fill=tk.BOTH, expand=True)
# Bind FocusOut event to save notes automatically when the text widget loses focus.
self.info_text.bind("<FocusOut>", self.save_user_notes)
self.save_notes_button = tk.Button(right_frame, text="Save Notes", command=self.save_user_notes) # Manual save notes button
self.save_notes_button.pack(pady=5)
# --- Application State Variables ---
self.current_selected_item = None # Stores the name of the currently displayed item
self.current_category = "STANDARD" # Stores the currently active category (STANDARD, INSTALLED, PYPI, SEARCH)
self.history = [] # List to store navigation history (tuples of (category, item_name_with_prefix))
self.history_index = -1 # Current position in the history list
# --- Status Bar ---
# Displays messages to the user about current operations or status.
self.status_bar = tk.Label(master, text="Welcome to PyRef! Initializing...", bd=1, relief=tk.SUNKEN, anchor=tk.W)
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X)
self._configure_tags() # Setup text tags for syntax highlighting and clickable URLs
# --- Initial Data Loading and Caching ---
self.status_bar.config(text="Initializing caches...")
self.master.update_idletasks() # Force GUI update to show status message
# Load standard commands cache or build it if not available/expired.
self.standard_commands_cache = load_cache(STANDARD_CACHE_FILE, "standard")
if self.standard_commands_cache is None:
self.status_bar.config(text="Building standard commands cache (first run, this is fast)...")
self.master.update_idletasks()
self.standard_commands_cache = sorted(dir(builtins)) # Get all built-in names
save_cache(self.standard_commands_cache, STANDARD_CACHE_FILE)
self.status_bar.config(text="Standard commands cache built.")
self.standard_commands = self.standard_commands_cache # Assign to active variable
# Load installed modules cache and PyPI index cache.
self.installed_modules_cache = load_cache(INSTALLED_CACHE_FILE, "installed") or {}
# Corrected variable name from PYPI_INDEX_FILE to PYPI_INDEX_CACHE_FILE
self.pypi_index_cache = load_cache(PYPI_INDEX_CACHE_FILE, "pypi_index") or []
# Update installed modules (checks for changes since last run)
self.update_installed_modules()
# Fetch PyPI packages if the cache is empty (first run or expired)
if not self.pypi_index_cache:
self.fetch_pypi_packages()
self.show_standard() # Display standard commands by default on startup
self.status_bar.config(text="PyRef is ready!") # Final status message
def _setup_sys_path(self):
"""Adds standard Python site-packages directories to sys.path.
This ensures that dynamically imported modules (e.g., in INSTALLED category)
can be found by the interpreter.
"""
# Add global site-packages directories
for sp_dir in site.getsitepackages():
if sp_dir not in sys.path:
sys.path.append(sp_dir)
# Add user-specific site-packages directory
user_site_packages = site.getusersitepackages()
if user_site_packages not in sys.path:
sys.path.append(user_site_packages)
def _configure_tags(self):
"""Configures text tags for syntax highlighting and clickable URLs in the info_text widget.
Tags allow applying specific formatting (e.g., color, underline) to parts of the text.
"""
self.info_text.tag_config("keyword", foreground="blue")
self.info_text.tag_config("string", foreground="green")
self.info_text.tag_config("comment", foreground="gray")
self.info_text.tag_config("function", foreground="purple")
self.info_text.tag_config("class", foreground="darkred")
self.info_text.tag_config("builtin", foreground="darkorange")
self.info_text.tag_config("number", foreground="darkcyan")
self.info_text.tag_config("url", foreground="blue", underline=True)
# Bind a click event to the "url" tag to open the URL in a web browser
self.info_text.tag_bind("url", "<Button-1>", self._open_url)
def _apply_syntax_highlighting(self, text_widget: scrolledtext.ScrolledText, content_start_line: str, content_end_line: str):
"""Applies basic Python syntax highlighting to a given text range in a ScrolledText widget.
Args:
text_widget (scrolledtext.ScrolledText): The Tkinter ScrolledText widget to highlight.
content_start_line (str): The starting text index (e.g., "1.0").
content_end_line (str): The ending text index (e.g., "end-1c").
"""
# Remove all existing tags from the specified range to clear previous highlighting
for tag in ["keyword", "string", "comment", "function", "class", "builtin", "number", "url"]:
text_widget.tag_remove(tag, content_start_line, content_end_line)
# Define regular expressions for different syntax elements
keywords = r'\b(False|None|True|and|as|assert|async|await|break|class|continue|def|del|elif|else|except|finally|for|from|global|if|import|in|is|lambda|nonlocal|not|or|pass|raise|return|try|while|with|yield)\b'
builtins_re = r'\b(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)\b'
strings = r'(\"\"\"[\s\S]*?\"\"\"|\'\'\'[\s\S]*?\'\'\'|\".*?\"|\'.*?\')' # Handles single/double quoted and triple-quoted strings
comments = r'\#.*$' # Matches comments from '#' to end of line
numbers = r'\b\d+(\.\d+)?([eE][+-]?\d+)?\b' # Matches integers, floats, scientific notation
functions_def = r'\bdef\s+([a-zA-Z_]\w*)\s*\(' # Captures function names after 'def'
classes_def = r'\bclass\s+([a-zA-Z_]\w*)\s*(\(|\:)' # Captures class names after 'class'
urls_re = r'https?://[^\s<>"]+|www\.[^\s<>"]+' # Matches common URL patterns
# Get the text content from the specified range for processing
text_content = text_widget.get(content_start_line, content_end_line)
lines = text_content.splitlines() # Split content into individual lines
# Determine the starting line number in the widget for correct index calculation
start_line_num = int(float(content_start_line))
# Iterate through each line and apply highlighting based on regex matches
for i, line in enumerate(lines):
current_line_index = f"{start_line_num + i}.0" # Current line's starting index in the widget
# Apply tags for each regex pattern
for match in re.finditer(keywords, line):
start = f"{current_line_index}+{match.start()}c"
end = f"{current_line_index}+{match.end()}c"
text_widget.tag_add("keyword", start, end)
for match in re.finditer(builtins_re, line):
start = f"{current_line_index}+{match.start()}c"
end = f"{current_line_index}+{match.end()}c"
text_widget.tag_add("builtin", start, end)
for match in re.finditer(strings, line):
start = f"{current_line_index}+{match.start()}c"
end = f"{current_line_index}+{match.end()}c"
text_widget.tag_add("string", start, end)
for match in re.finditer(comments, line):
start = f"{current_line_index}+{match.start()}c"
end = f"{current_line_index}+{match.end()}c"
text_widget.tag_add("comment", start, end)
for match in re.finditer(numbers, line):
start = f"{current_line_index}+{match.start()}c"
end = f"{current_line_index}+{match.end()}c"
text_widget.tag_add("number", start, end)
# Highlighting for function definitions (only the name)
for match in re.finditer(functions_def, line):
name_start = match.start(1)
name_end = match.end(1)
start = f"{current_line_index}+{name_start}c"
end = f"{current_line_index}+{name_end}c"
text_widget.tag_add("function", start, end)
# Highlighting for class definitions (only the name)
for match in re.finditer(classes_def, line):
name_start = match.start(1)
name_end = match.end(1)
start = f"{current_line_index}+{name_start}c"
end = f"{current_line_index}+{name_end}c"
text_widget.tag_add("class", start, end)
# Highlighting for URLs
for match in re.finditer(urls_re, line):
start = f"{current_line_index}+{match.start()}c"
end = f"{current_line_index}+{match.end()}c"
text_widget.tag_add("url", start, end)
def _open_url(self, event: tk.Event):
"""Event handler for clicking on a URL-tagged text in the info_text widget.
Args:
event (tk.Event): The Tkinter event object containing click coordinates.
"""
# Get the text index at the clicked coordinates
index = self.info_text.index(f"@{event.x},{event.y}")
# Check if the clicked index has the "url" tag applied
if "url" in self.info_text.tag_names(index):
# Get the full text of the line where the click occurred
line_start_index = self.info_text.index(f"{index} linestart")
line_end_index = self.info_text.index(f"{index} lineend")
line_text = self.info_text.get(line_start_index, line_end_index)
# Find all URLs on that line
urls_on_line = re.findall(r'https?://[^\s<>"]+|www\.[^\s<>"]+', line_text)
# Determine the column of the click within the line
clicked_column = int(index.split('.')[1])
# Iterate through found URLs to see which one was clicked
for url in urls_on_line:
start_match = line_text.find(url)
end_match = start_match + len(url)
# Check if the click was within the bounds of this URL
if start_match <= clicked_column < end_match:
try:
webbrowser.open_new_tab(url) # Open the URL in the default web browser
except Exception as e:
messagebox.showerror("Error Opening URL", f"Could not open URL: {url}\nError: {e}")
return # Exit after opening the first matching URL
def _handle_listbox_select(self, event: tk.Event):
"""Handles selection events in the main listbox.
When a user clicks on an item in the listbox, this function
identifies the selected item and triggers its information display.
Args:
event (tk.Event): The Tkinter event object.
"""
selected_indices = self.menu_listbox.curselection() # Get indices of selected items
if not selected_indices:
return # Do nothing if no item is selected
item_name_with_prefix = self.menu_listbox.get(selected_indices[0]) # Get the text of the selected item
# Determine the category based on the prefix of the item name
item_category = self.current_category # Default to current category
if item_name_with_prefix.startswith("STANDARD: "):
item_category = "STANDARD"
elif item_name_with_prefix.startswith("INSTALLED: "):
item_category = "INSTALLED"
elif item_name_with_prefix.startswith("NOT INSTALLED (PyPi): "):
item_category = "PYPI"
# Manage navigation history. Add to history only if it's a new selection.
if not self.history or self.history[self.history_index] != (item_category, item_name_with_prefix):
# If we navigated back and then selected a new item, clear forward history
if self.history_index < len(self.history) - 1:
self.history = self.history[:self.history_index + 1]
self.history.append((item_category, item_name_with_prefix)) # Add new item to history
self.history_index = len(self.history) - 1 # Update history index
self._update_history_buttons() # Enable/disable Back/Forward buttons based on history state
self.display_info(item_name_with_prefix, item_category) # Display information for the selected item
def _update_history_buttons(self):
"""Updates the state (enabled/disabled) of the Back and Forward buttons."""
if self.history_index > 0:
self.back_button.config(state=tk.NORMAL) # Enable Back if not at the beginning of history
else:
self.back_button.config(state=tk.DISABLED) # Disable Back
if self.history_index < len(self.history) - 1:
self.forward_button.config(state=tk.NORMAL) # Enable Forward if not at the end of history
else:
self.forward_button.config(state=tk.DISABLED) # Disable Forward
def go_back(self):
"""Navigates back in the history of viewed items."""
if self.history_index > 0:
self.history_index -= 1 # Move back one step in history
category, item_name_with_prefix = self.history[self.history_index] # Get the item from history
try:
# Clear current selection and try to select the item in the listbox
self.menu_listbox.selection_clear(0, tk.END)
current_listbox_items = list(self.menu_listbox.get(0, tk.END))
if item_name_with_prefix in current_listbox_items:
idx = current_listbox_items.index(item_name_with_prefix)
self.menu_listbox.selection_set(idx) # Select the item
self.menu_listbox.see(idx) # Scroll to make it visible
# Display info for the item, explicitly not adding to history to avoid loops
self.display_info(item_name_with_prefix, category, add_to_history=False)
except Exception as e:
self.status_bar.config(text=f"Error going back: Could not re-display '{item_name_with_prefix}'.")
print(f"Error re-displaying history item: {e}")
self.history_index += 1 # If error, revert history index
self._update_history_buttons() # Update button states