Skip to content

Commit 0bfc611

Browse files
themoenenbrian316
andauthored
History memory leak fix (#143)
* refactor readline history logic * bugfix: fxn syntax Signed-off-by: Brian Duenas <brian.duenas@ibm.com> --------- Signed-off-by: Brian Duenas <brian.duenas@ibm.com> Co-authored-by: Brian Duenas <brian.duenas@ibm.com>
1 parent 33ddb97 commit 0bfc611

7 files changed

Lines changed: 113 additions & 81 deletions

File tree

openad/app/main.py

Lines changed: 7 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from openad.gui.ws_server import ws_server # Web socket server for gui - experimental
2222
from openad.helpers.output import output_table
2323
from openad.helpers.plugins import display_plugin_overview
24+
from openad.helpers.history import init_history, update_history_file, add_history_entry
2425

2526
# Core
2627
import openad.core.help as openad_help
@@ -508,27 +509,11 @@ def do_help(self, inp, display_info=True, starts_with_only=False, disable_catego
508509

509510
def preloop(self):
510511
"""CMD class called function: Preloop is called by cmd to get an update the history file each History File"""
511-
if readline and os.path.exists(self.histfile):
512-
# note history files can get corrupted so using try to compensate
513-
try:
514-
readline.read_history_file(self.histfile)
515-
except Exception: # pylint: disable=broad-exception-caught # do not need to know exception
516-
# Create history file in case it doesn't exist yet.
517-
# - - -
518-
# To trigger:
519-
# >> create new workspace foobar
520-
# >> ctrl+c
521-
# (Reboot)
522-
readline.write_history_file(self.histfile)
512+
init_history(self)
523513

524514
def postloop(self):
525515
"""CMD class called function: Post loop is called by cmd to get an update the history file"""
526-
readline.set_history_length(self.histfile_size)
527-
readline.write_history_file(self.histfile)
528-
529-
def add_history(self, inp):
530-
"""CMD class called function: adds history file"""
531-
readline.add_history(inp)
516+
update_history_file(self)
532517

533518
def complete(self, text, state):
534519
"""CMD class called function:
@@ -748,10 +733,6 @@ def emptyline(self):
748733
def default(self, line):
749734
"""Default method call on hitting of the return Key, it tries to parse and execute the statements."""
750735

751-
# Prevent the history from growing too large
752-
if readline.get_current_history_length() > self.histfile_size:
753-
readline.remove_history_item(0)
754-
755736
inp = line # assigning line to input value
756737

757738
x = None
@@ -1031,7 +1012,6 @@ def api_remote(
10311012
"""
10321013

10331014
global MAGIC_PROMPT
1034-
# GLOBAL_SETTINGS["display"] = "notebook"
10351015

10361016
initialise()
10371017

@@ -1066,14 +1046,7 @@ def api_remote(
10661046
set_context(magic_prompt, x)
10671047

10681048
magic_prompt.api_variables = api_var_list
1069-
# We now manage history. The history sometimes gets corrupted through no fault of ours.
1070-
# If so, we just reset it.
1071-
try:
1072-
readline.read_history_file(magic_prompt.histfile)
1073-
except Exception: # pylint: disable=broad-exception-caught # could be a number of errors
1074-
readline.add_history("")
1075-
readline.write_history_file(magic_prompt.histfile)
1076-
readline.read_history_file(magic_prompt.histfile)
1049+
10771050
for i in arguments:
10781051
inp = inp + a_space + i
10791052
a_space = " "
@@ -1105,9 +1078,8 @@ def api_remote(
11051078
# Note, may be possible add code completion here #revisit
11061079
else:
11071080
magic_prompt.preloop()
1108-
magic_prompt.add_history(inp)
1081+
add_history_entry(magic_prompt, inp)
11091082
magic_prompt.postloop()
1110-
readline.write_history_file(magic_prompt.histfile)
11111083

11121084
result = magic_prompt.default(inp)
11131085

@@ -1171,15 +1143,15 @@ def cmd_line():
11711143
and command_line.settings["context"] == words[2 + word_increment].upper()
11721144
):
11731145
command_line.preloop()
1174-
command_line.add_history(str(" ".join(words[3 + word_increment :])).strip())
1146+
add_history_entry(command_line, str(" ".join(words[3 + word_increment :])).strip())
11751147
command_line.postloop()
11761148
result = command_line.default(str(" ".join(words[3 + word_increment :])).strip())
11771149
else:
11781150
# If there is an argument and it is not help, attempt to run the command
11791151
# Note, may be possible add code completion here #revisit
11801152

11811153
command_line.preloop()
1182-
command_line.add_history(inp[+increment:].strip())
1154+
add_history_entry(command_line, inp[+increment:].strip())
11831155
command_line.postloop()
11841156
result = command_line.default(inp[+increment:].strip())
11851157
command_line.do_exit("dummy do not remove")

openad/core/lang_runs.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
# Global variables
99
from openad.app.global_var_lib import GLOBAL_SETTINGS
10+
from openad.helpers.history import update_history_file
1011

1112
# Helpers
1213
from openad.helpers.output import output_text, output_error, output_success, output_table
@@ -23,7 +24,7 @@ def _create_workspace_dir_if_nonexistent(cmd_pointer, dir_name):
2324
def save_run(cmd_pointer, parser):
2425
"""Saves a Run"""
2526
_create_workspace_dir_if_nonexistent(cmd_pointer, "_runs")
26-
readline.write_history_file(cmd_pointer.histfile)
27+
update_history_file(cmd_pointer)
2728

2829
# f =_meta_workspaces+'/'+ cmd_pointer.settings['workspace'].upper()+'/.cmd_history'
2930
runlist = []
@@ -111,9 +112,6 @@ def display_run(cmd_pointer, parser):
111112
# Create _runs directory if it does not exist yet.
112113
_create_workspace_dir_if_nonexistent(cmd_pointer, "_runs")
113114

114-
# import readline
115-
# readline.write_history_file(cmd_pointer.histfile) # @Phil, I put this back but it was commented out
116-
117115
# Read the run file.
118116
commands = []
119117
run_file_path = (

openad/core/lang_workspaces.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import os
44
from time import sleep
55

6-
import readline
7-
86
# Core
97
from openad.core.lang_sessions_and_registry import write_registry, update_main_registry_env_var
108

@@ -16,32 +14,31 @@
1614
from openad.helpers.output_msgs import msg
1715
from openad.helpers.general import other_sessions_exist, user_input
1816
from openad.helpers.spinner import spinner
17+
from openad.helpers.history import clear_memory_history, init_history
1918

2019

2120
# Sets the current workspace from the fgiven workspaces available
2221
def set_workspace(cmd_pointer, parser):
2322
"""Sets the current Workspace"""
24-
readline.write_history_file(cmd_pointer.histfile)
23+
2524
current_workspace_name = cmd_pointer.settings["workspace"].upper()
2625
new_workspace_name = parser["Workspace_Name"].upper()
2726
if new_workspace_name not in cmd_pointer.settings["workspaces"]:
2827
return output_error(msg("invalid_workpace", new_workspace_name))
2928

3029
elif new_workspace_name == current_workspace_name:
3130
return output_warning(msg("warn_workspace_already_active", new_workspace_name))
31+
32+
# New workspace
3233
else:
3334
cmd_pointer.settings["workspace"] = new_workspace_name
3435
write_registry(cmd_pointer.settings, cmd_pointer)
3536
cmd_pointer.histfile = os.path.expanduser(cmd_pointer.workspace_path(new_workspace_name) + "/.cmd_history")
36-
readline.clear_history()
3737

38-
try: # Open history file if not corrupt
39-
if readline and os.path.exists(cmd_pointer.histfile):
40-
readline.read_history_file(cmd_pointer.histfile)
41-
except Exception:
42-
readline.write_history_file(cmd_pointer.histfile)
38+
# Switch history
39+
clear_memory_history(cmd_pointer)
40+
init_history(cmd_pointer)
4341

44-
readline.write_history_file(cmd_pointer.histfile)
4542
return output_success(msg("success_workspace_set", new_workspace_name))
4643

4744

@@ -131,11 +128,9 @@ def remove_workspace(cmd_pointer, parser):
131128

132129
def create_workspace(cmd_pointer, parser):
133130
"""Creates a Workspace"""
134-
# Make sure existing workspace history file is saved.
135-
readline.write_history_file(cmd_pointer.histfile)
131+
136132
cmd_pointer.refresh_vector = True
137133
cmd_pointer.refresh_train = True
138-
139134
cmd_pointer.settings["env_vars"]["refresh_help_ai"] = True
140135
update_main_registry_env_var(cmd_pointer, "refresh_help_ai", True)
141136

@@ -167,7 +162,7 @@ def create_workspace(cmd_pointer, parser):
167162
cmd_pointer.settings["descriptions"][workspace_name] = description
168163
write_registry(cmd_pointer.settings, cmd_pointer, True) # Create registry
169164
write_registry(cmd_pointer.settings, cmd_pointer) # Create session registry
170-
except Exception as err:
165+
except Exception as err: # pylint: disable=broad-exception-caught
171166
return output_error(msg("err_workspace_description", err))
172167

173168
# Create workspace.
@@ -198,16 +193,17 @@ def create_workspace(cmd_pointer, parser):
198193
if not os.path.exists(dir_path):
199194
os.mkdir(dir_path)
200195
else:
201-
# This currently happens when you remove a workspace and then try to recreate it.
202-
# @Phil - we probably should move or archive the workspace folder when removing the workspace.
196+
# When you remove a workspace and then recreate it
203197
os.chdir(dir_path)
204198
error_creating_dir = msg("warn_workspace_folder_already_exists", workspace_name)
199+
205200
# Main and session registry writes
206201
write_registry(cmd_pointer.settings, cmd_pointer, True)
207202
write_registry(cmd_pointer.settings, cmd_pointer)
208203

209-
readline.clear_history()
210-
readline.write_history_file(cmd_pointer.histfile)
204+
# Switch history
205+
clear_memory_history(cmd_pointer)
206+
211207
# raise ValueError('This is a test error.\n') @later this causes the app to break permamenently.
212208
except Exception as err:
213209
error_other = msg("err_workspace_create", err)

openad/helpers/general.py

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ def refresh_prompt(settings):
2929
def is_notebook_mode():
3030
"""Return True if we are running inside a Jupyter Notebook or Jupyter Lab."""
3131
try:
32-
get_ipython() # pylint: disable=undefined-variable
32+
get_ipython() # pylint: disable=undefined-variable # noqa: F821
3333
return True
3434
except Exception: # pylint: disable=broad-exception-caught
3535
return False
@@ -191,7 +191,7 @@ def load_module_from_path(module_name, file_path):
191191
sys.modules[module_name] = module
192192
spec.loader.exec_module(module)
193193
return module
194-
except Exception as err:
194+
except Exception:
195195
# Silent fail - only enable this for debugging
196196
# output_error(f"load_module_from_path('{module_name}', {file_path})\n<soft>{err}</soft>")
197197
return None
@@ -201,7 +201,7 @@ def load_module_from_path(module_name, file_path):
201201
def print_separator(style=None, width=None, return_val=False):
202202
from openad.app.global_var_lib import GLOBAL_SETTINGS
203203

204-
if GLOBAL_SETTINGS["display"] == "terminal" or GLOBAL_SETTINGS["display"] == None:
204+
if GLOBAL_SETTINGS["display"] == "terminal" or GLOBAL_SETTINGS["display"] is None:
205205
cli_width = get_print_width(full=True)
206206
width = cli_width if not width or cli_width < width else width
207207
if style:
@@ -439,19 +439,3 @@ async def _loop(text="", i=0, line_length=0):
439439
# _loop(text=text) # Sync
440440
asyncio.run(_loop(text=text))
441441
sys.stdout.write("\033[?25h") # Show cursor
442-
443-
444-
# NOT USED
445-
# Clear the current line.
446-
# Couldn't get this to work, because I can't clear the buffer.
447-
# As a result, (eg.) when you try ctrl-c twice with n answer,
448-
# the second time it will have the first time's response in the buffer,
449-
# causing it to mess up the layout.
450-
def clear_current_line():
451-
buffer = readline.get_line_buffer()
452-
line_length = len(buffer)
453-
readline.clear_history()
454-
eraser = "\b \b" * line_length
455-
sys.stdout.write(eraser)
456-
# readline.insert_text(' ')
457-
# print(len(buffer), buffer)

openad/helpers/history.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import os
2+
import readline
3+
from openad.helpers.output import output_text, output_error, output_success
4+
5+
DEBUG_HIST = False
6+
7+
8+
def init_history(cmd_pointer):
9+
"""
10+
Load history content from the .cmd_history file:
11+
- On startup
12+
- When switching workspaces
13+
"""
14+
readline.set_history_length(cmd_pointer.histfile_size)
15+
if readline and os.path.exists(cmd_pointer.histfile):
16+
try:
17+
is_startup = readline.get_current_history_length() == 0
18+
if is_startup:
19+
readline.read_history_file(cmd_pointer.histfile)
20+
if DEBUG_HIST:
21+
output_success(f"load_history_file: {cmd_pointer.histfile}", return_val=False)
22+
except Exception as err: # pylint: disable=broad-exception-caught
23+
if DEBUG_HIST:
24+
output_error(["load_history_file", err], return_val=False)
25+
elif DEBUG_HIST:
26+
output_error(
27+
[
28+
f"load_history_file - .cmd_history not found in {cmd_pointer.settings['workspace']}",
29+
cmd_pointer.histfile,
30+
],
31+
return_val=False,
32+
)
33+
34+
35+
def clear_memory_history(cmd_pointer):
36+
"""
37+
Clear the in-memory history without removing the history file.
38+
Used when switching workspaces.
39+
40+
Workaround needed because readline doesn't let you clear in-memory
41+
history without also deleting the history file.
42+
"""
43+
try:
44+
readline.write_history_file(cmd_pointer.histfile + "--temp")
45+
readline.clear_history()
46+
os.remove(cmd_pointer.histfile + "--temp")
47+
except Exception as err: # pylint: disable=broad-exception-caught
48+
if DEBUG_HIST:
49+
output_error(["refresh_history", err], return_val=False)
50+
51+
52+
def add_history_entry(cmd_pointer, inp):
53+
"""
54+
Add the current command to the in-memory history.
55+
This is called when a command is executed.
56+
"""
57+
try:
58+
# Ignore super long commands
59+
if len(str(str(inp).strip())) < int(4096):
60+
readline.add_history(str(str(inp).strip()))
61+
62+
# Cap the in-memory history
63+
# Without this, it will keep on growing until you switch workspaces or restart kernel
64+
if readline.get_current_history_length() > cmd_pointer.histfile_size:
65+
readline.remove_history_item(0)
66+
67+
if DEBUG_HIST:
68+
output_text(f"add_history_entry #{cmd_pointer.histfile_size}: {inp}", return_val=False)
69+
except Exception as err: # pylint: disable=broad-exception-caught
70+
if DEBUG_HIST:
71+
output_error(["add_history_entry", err], return_val=False)
72+
73+
74+
def update_history_file(cmd_pointer):
75+
"""
76+
Write in-memory history to disk.
77+
"""
78+
try:
79+
readline.write_history_file(cmd_pointer.histfile)
80+
if DEBUG_HIST:
81+
output_text(f"update_history_file: {cmd_pointer.histfile}", return_val=False)
82+
except Exception as err: # pylint: disable=broad-exception-caught
83+
if DEBUG_HIST:
84+
output_error(["update_history_file", err], return_val=False)

openad/helpers/spinner.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,10 @@
2626

2727

2828
class Spinner(Halo):
29-
3029
def __init__(self):
31-
3230
# Fancy spinner, but requires more CPU, blocking the main thread
3331
# To do: see if separating thread for spinner resolves this
34-
wave_spinner = {
32+
wave_spinner = { # noqa: F841
3533
"interval": 700,
3634
"frames": [
3735
"▉▋▍▎▏▏",

openad/user_toolkits/RXN/fn_reactions/fn_predict_retro.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ def predict_retro(inputs: dict, cmd_pointer):
261261
"",
262262
return_val=False,
263263
)
264-
if num_results < 4 or GLOBAL_SETTINGS["display"] == "api"::
264+
if num_results < 4 or GLOBAL_SETTINGS["display"] == "api":
265265
results[str(index)] = {"confidence": tree["confidence"], "reactions": []}
266266

267267
output_text(
@@ -273,7 +273,7 @@ def predict_retro(inputs: dict, cmd_pointer):
273273
)
274274

275275
for reaction in collect_reactions_from_retrosynthesis(tree):
276-
if num_results < 4 or GLOBAL_SETTINGS["display"] == "api"::
276+
if num_results < 4 or GLOBAL_SETTINGS["display"] == "api":
277277
results[str(index)]["reactions"].append(reactions_text[i])
278278
output_text("<green> Reaction: </green>" + reactions_text[i], return_val=False)
279279
i = i + 1

0 commit comments

Comments
 (0)