1from __future__ import absolute_import, division, print_function 2 3__copyright__ = """ 4Copyright (C) 2009-2017 Andreas Kloeckner 5Copyright (C) 2014-2017 Aaron Meurer 6""" 7 8__license__ = """ 9Permission is hereby granted, free of charge, to any person obtaining a copy 10of this software and associated documentation files (the "Software"), to deal 11in the Software without restriction, including without limitation the rights 12to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13copies of the Software, and to permit persons to whom the Software is 14furnished to do so, subject to the following conditions: 15 16The above copyright notice and this permission notice shall be included in 17all copies or substantial portions of the Software. 18 19THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25THE SOFTWARE. 26""" 27 28import os 29import sys 30 31from pudb.py3compat import ConfigParser 32from pudb.lowlevel import (lookup_module, get_breakpoint_invalid_reason, 33 settings_log) 34 35# minor LGPL violation: stolen from python-xdg 36 37_home = os.environ.get("HOME", None) 38xdg_data_home = os.environ.get("XDG_DATA_HOME", 39 os.path.join(_home, ".local", "share") if _home else None) 40 41 42XDG_CONFIG_HOME = os.environ.get("XDG_CONFIG_HOME", 43 os.path.join(_home, ".config") if _home else None) 44 45if XDG_CONFIG_HOME: 46 XDG_CONFIG_DIRS = [XDG_CONFIG_HOME] 47else: 48 XDG_CONFIG_DIRS = os.environ.get("XDG_CONFIG_DIRS", "/etc/xdg").split(":") 49 50 51def get_save_config_path(*resource): 52 if XDG_CONFIG_HOME is None: 53 return None 54 if not resource: 55 resource = [XDG_CONF_RESOURCE] 56 resource = os.path.join(*resource) 57 assert not resource.startswith("/") 58 path = os.path.join(XDG_CONFIG_HOME, resource) 59 if not os.path.isdir(path): 60 os.makedirs(path, 448) # 0o700 61 return path 62 63# end LGPL violation 64 65 66CONF_SECTION = "pudb" 67XDG_CONF_RESOURCE = "pudb" 68CONF_FILE_NAME = "pudb.cfg" 69 70SAVED_BREAKPOINTS_FILE_NAME = "saved-breakpoints-%d.%d" % sys.version_info[:2] 71BREAKPOINTS_FILE_NAME = "breakpoints-%d.%d" % sys.version_info[:2] 72 73 74_config_ = [None] 75 76 77def load_config(): 78 # Only ever do this once 79 if _config_[0] is not None: 80 return _config_[0] 81 82 from os.path import join, isdir 83 84 cparser = ConfigParser() 85 86 conf_dict = {} 87 try: 88 cparser.read([ 89 join(cdir, XDG_CONF_RESOURCE, CONF_FILE_NAME) 90 for cdir in XDG_CONFIG_DIRS if isdir(cdir)]) 91 92 if cparser.has_section(CONF_SECTION): 93 conf_dict.update(dict(cparser.items(CONF_SECTION))) 94 except Exception: 95 settings_log.exception("Failed to load config") 96 97 conf_dict.setdefault("shell", "internal") 98 conf_dict.setdefault("theme", "classic") 99 conf_dict.setdefault("line_numbers", "False") 100 conf_dict.setdefault("seen_welcome", "a") 101 102 conf_dict.setdefault("sidebar_width", 0.5) 103 conf_dict.setdefault("variables_weight", 1) 104 conf_dict.setdefault("stack_weight", 1) 105 conf_dict.setdefault("breakpoints_weight", 1) 106 107 conf_dict.setdefault("current_stack_frame", "top") 108 109 conf_dict.setdefault("stringifier", "type") 110 111 conf_dict.setdefault("custom_theme", "") 112 conf_dict.setdefault("custom_stringifier", "") 113 conf_dict.setdefault("custom_shell", "") 114 115 conf_dict.setdefault("wrap_variables", "True") 116 conf_dict.setdefault("default_variables_access_level", "public") 117 118 conf_dict.setdefault("display", "auto") 119 120 conf_dict.setdefault("prompt_on_quit", "True") 121 122 conf_dict.setdefault("hide_cmdline_win", "False") 123 124 def normalize_bool_inplace(name): 125 try: 126 if conf_dict[name].lower() in ["0", "false", "off"]: 127 conf_dict[name] = False 128 else: 129 conf_dict[name] = True 130 except Exception: 131 settings_log.exception("Failed to process config") 132 133 normalize_bool_inplace("line_numbers") 134 normalize_bool_inplace("wrap_variables") 135 normalize_bool_inplace("prompt_on_quit") 136 normalize_bool_inplace("hide_cmdline_win") 137 138 _config_[0] = conf_dict 139 return conf_dict 140 141 142def save_config(conf_dict): 143 from os.path import join 144 145 cparser = ConfigParser() 146 cparser.add_section(CONF_SECTION) 147 148 for key in sorted(conf_dict): 149 cparser.set(CONF_SECTION, key, str(conf_dict[key])) 150 151 try: 152 save_path = get_save_config_path() 153 if not save_path: 154 return 155 outf = open(join(save_path, CONF_FILE_NAME), "w") 156 cparser.write(outf) 157 outf.close() 158 except Exception: 159 settings_log.exception("Failed to save config") 160 161 162def edit_config(ui, conf_dict): 163 import urwid 164 165 old_conf_dict = conf_dict.copy() 166 167 def _update_theme(): 168 ui.setup_palette(ui.screen) 169 ui.screen.clear() 170 171 def _update_line_numbers(): 172 for sl in ui.source: 173 sl._invalidate() 174 175 def _update_prompt_on_quit(): 176 pass 177 178 def _update_hide_cmdline_win(): 179 ui.update_cmdline_win() 180 181 def _update_current_stack_frame(): 182 ui.update_stack() 183 184 def _update_stringifier(): 185 import pudb.var_view 186 pudb.var_view.custom_stringifier_dict = {} 187 ui.update_var_view() 188 189 def _update_default_variables_access_level(): 190 ui.update_var_view() 191 192 def _update_wrap_variables(): 193 ui.update_var_view() 194 195 def _update_config(check_box, new_state, option_newvalue): 196 option, newvalue = option_newvalue 197 new_conf_dict = {option: newvalue} 198 if option == "theme": 199 # only activate if the new state of the radio button is 'on' 200 if new_state: 201 if newvalue is None: 202 # Select the custom theme entry dialog 203 lb.set_focus(lb_contents.index(theme_edit_list_item)) 204 return 205 206 conf_dict.update(theme=newvalue) 207 _update_theme() 208 209 elif option == "line_numbers": 210 new_conf_dict["line_numbers"] = not check_box.get_state() 211 conf_dict.update(new_conf_dict) 212 _update_line_numbers() 213 214 elif option == "prompt_on_quit": 215 new_conf_dict["prompt_on_quit"] = not check_box.get_state() 216 conf_dict.update(new_conf_dict) 217 _update_prompt_on_quit() 218 219 elif option == "hide_cmdline_win": 220 new_conf_dict["hide_cmdline_win"] = not check_box.get_state() 221 conf_dict.update(new_conf_dict) 222 _update_hide_cmdline_win() 223 224 elif option == "current_stack_frame": 225 # only activate if the new state of the radio button is 'on' 226 if new_state: 227 conf_dict.update(new_conf_dict) 228 _update_current_stack_frame() 229 230 elif option == "stringifier": 231 # only activate if the new state of the radio button is 'on' 232 if new_state: 233 if newvalue is None: 234 lb.set_focus(lb_contents.index(stringifier_edit_list_item)) 235 return 236 237 conf_dict.update(stringifier=newvalue) 238 _update_stringifier() 239 240 elif option == "default_variables_access_level": 241 # only activate if the new state of the radio button is 'on' 242 if new_state: 243 conf_dict.update(default_variables_access_level=newvalue) 244 _update_default_variables_access_level() 245 246 elif option == "wrap_variables": 247 new_conf_dict["wrap_variables"] = not check_box.get_state() 248 conf_dict.update(new_conf_dict) 249 _update_wrap_variables() 250 251 heading = urwid.Text("This is the preferences screen for PuDB. " 252 "Hit Ctrl-P at any time to get back to it.\n\n" 253 "Configuration settings are saved in " 254 "$HOME/.config/pudb or $XDG_CONFIG_HOME/pudb " 255 "environment variable. If both variables are not set " 256 "configurations settings will not be saved.\n") 257 258 cb_line_numbers = urwid.CheckBox("Show Line Numbers", 259 bool(conf_dict["line_numbers"]), on_state_change=_update_config, 260 user_data=("line_numbers", None)) 261 262 cb_prompt_on_quit = urwid.CheckBox("Prompt before quitting", 263 bool(conf_dict["prompt_on_quit"]), on_state_change=_update_config, 264 user_data=("prompt_on_quit", None)) 265 266 hide_cmdline_win = urwid.CheckBox("Hide command line (Ctrl-X) window " 267 "when not in use", 268 bool(conf_dict["hide_cmdline_win"]), on_state_change=_update_config, 269 user_data=("hide_cmdline_win", None)) 270 271 # {{{ shells 272 273 shell_info = urwid.Text("This is the shell that will be " 274 "used when you hit '!'.\n") 275 shells = ["internal", "classic", "ipython", "bpython", "ptpython", "ptipython"] 276 known_shell = conf_dict["shell"] in shells 277 shell_edit = urwid.Edit(edit_text=conf_dict["custom_shell"]) 278 shell_edit_list_item = urwid.AttrMap(shell_edit, "value") 279 280 shell_rb_group = [] 281 shell_rbs = [ 282 urwid.RadioButton(shell_rb_group, name, 283 conf_dict["shell"] == name) 284 for name in shells]+[ 285 urwid.RadioButton(shell_rb_group, "Custom:", 286 not known_shell, on_state_change=_update_config, 287 user_data=("shell", None)), 288 shell_edit_list_item, 289 urwid.Text("\nTo use a custom shell, see example-shell.py " 290 "in the pudb distribution. Enter the full path to a " 291 "file like it in the box above. '~' will be expanded " 292 "to your home directory. The file should contain a " 293 "function called pudb_shell(_globals, _locals) " 294 "at the module level. See the PuDB documentation for " 295 "more information."), 296 ] 297 298 # }}} 299 300 # {{{ themes 301 302 from pudb.theme import THEMES 303 304 known_theme = conf_dict["theme"] in THEMES 305 306 theme_rb_group = [] 307 theme_edit = urwid.Edit(edit_text=conf_dict["custom_theme"]) 308 theme_edit_list_item = urwid.AttrMap(theme_edit, "value") 309 theme_rbs = [ 310 urwid.RadioButton(theme_rb_group, name, 311 conf_dict["theme"] == name, on_state_change=_update_config, 312 user_data=("theme", name)) 313 for name in THEMES]+[ 314 urwid.RadioButton(theme_rb_group, "Custom:", 315 not known_theme, on_state_change=_update_config, 316 user_data=("theme", None)), 317 theme_edit_list_item, 318 urwid.Text("\nTo use a custom theme, see example-theme.py in the " 319 "pudb distribution. Enter the full path to a file like it in " 320 "the box above. '~' will be expanded to your home directory. " 321 "Note that a custom theme will not be applied until you close " 322 "this dialog."), 323 ] 324 325 # }}} 326 327 # {{{ stack 328 329 stack_rb_group = [] 330 stack_opts = ["top", "bottom"] 331 stack_info = urwid.Text("Show the current stack frame at the\n") 332 stack_rbs = [ 333 urwid.RadioButton(stack_rb_group, name, 334 conf_dict["current_stack_frame"] == name, 335 on_state_change=_update_config, 336 user_data=("current_stack_frame", name)) 337 for name in stack_opts 338 ] 339 340 # }}} 341 342 # {{{ stringifier 343 344 stringifier_opts = ["type", "str", "repr"] 345 known_stringifier = conf_dict["stringifier"] in stringifier_opts 346 stringifier_rb_group = [] 347 stringifier_edit = urwid.Edit(edit_text=conf_dict["custom_stringifier"]) 348 stringifier_info = urwid.Text("This is the default function that will be " 349 "called on variables in the variables list. Note that you can change " 350 "this on a per-variable basis by selecting a variable and hitting Enter " 351 "or by typing t/s/r. Note that str and repr will be slower than type " 352 "and have the potential to crash PuDB.\n") 353 stringifier_edit_list_item = urwid.AttrMap(stringifier_edit, "value") 354 stringifier_rbs = [ 355 urwid.RadioButton(stringifier_rb_group, name, 356 conf_dict["stringifier"] == name, 357 on_state_change=_update_config, 358 user_data=("stringifier", name)) 359 for name in stringifier_opts 360 ]+[ 361 urwid.RadioButton(stringifier_rb_group, "Custom:", 362 not known_stringifier, on_state_change=_update_config, 363 user_data=("stringifier", None)), 364 stringifier_edit_list_item, 365 urwid.Text("\nTo use a custom stringifier, see " 366 "example-stringifier.py in the pudb distribution. Enter the " 367 "full path to a file like it in the box above. " 368 "'~' will be expanded to your home directory. " 369 "The file should contain a function called pudb_stringifier() " 370 "at the module level, which should take a single argument and " 371 "return the desired string form of the object passed to it. " 372 "Note that if you choose a custom stringifier, the variables " 373 "view will not be updated until you close this dialog."), 374 ] 375 376 # }}} 377 378 # {{{ variables access level 379 380 default_variables_access_level_opts = ["public", "private", "all"] 381 default_variables_access_level_rb_group = [] 382 default_variables_access_level_info = urwid.Text( 383 "Set the default attribute visibility " 384 "of variables in the variables list.\n" 385 "\nNote that you can change this option on " 386 "a per-variable basis by selecting the " 387 "variable and pressing '*'.") 388 default_variables_access_level_rbs = [ 389 urwid.RadioButton(default_variables_access_level_rb_group, name, 390 conf_dict["default_variables_access_level"] == name, 391 on_state_change=_update_config, 392 user_data=("default_variables_access_level", name)) 393 for name in default_variables_access_level_opts 394 ] 395 396 # }}} 397 398 # {{{ wrap variables 399 400 cb_wrap_variables = urwid.CheckBox("Wrap variables", 401 bool(conf_dict["wrap_variables"]), on_state_change=_update_config, 402 user_data=("wrap_variables", None)) 403 404 wrap_variables_info = urwid.Text("\nNote that you can change this option on " 405 "a per-variable basis by selecting the " 406 "variable and pressing 'w'.") 407 408 # }}} 409 410 # {{{ display 411 412 display_info = urwid.Text("What driver is used to talk to your terminal. " 413 "'raw' has the most features (colors and highlighting), " 414 "but is only correct for " 415 "XTerm and terminals like it. 'curses' " 416 "has fewer " 417 "features, but it will work with just about any terminal. 'auto' " 418 "will attempt to pick between the two based on availability and " 419 "the $TERM environment variable.\n\n" 420 "Changing this setting requires a restart of PuDB.") 421 422 displays = ["auto", "raw", "curses"] 423 424 display_rb_group = [] 425 display_rbs = [ 426 urwid.RadioButton(display_rb_group, name, 427 conf_dict["display"] == name) 428 for name in displays] 429 430 # }}} 431 432 lb_contents = ( 433 [heading] 434 + [urwid.AttrMap(urwid.Text("General:\n"), "group head")] 435 + [cb_line_numbers] 436 + [cb_prompt_on_quit] 437 + [hide_cmdline_win] 438 439 + [urwid.AttrMap(urwid.Text("\nShell:\n"), "group head")] 440 + [shell_info] 441 + shell_rbs 442 443 + [urwid.AttrMap(urwid.Text("\nTheme:\n"), "group head")] 444 + theme_rbs 445 446 + [urwid.AttrMap(urwid.Text("\nStack Order:\n"), "group head")] 447 + [stack_info] 448 + stack_rbs 449 450 + [urwid.AttrMap(urwid.Text("\nVariable Stringifier:\n"), "group head")] 451 + [stringifier_info] 452 + stringifier_rbs 453 454 + [urwid.AttrMap(urwid.Text("\nVariables Attribute Visibility:\n"), 455 "group head")] 456 + [default_variables_access_level_info] 457 + default_variables_access_level_rbs 458 459 + [urwid.AttrMap(urwid.Text("\nWrap Variables:\n"), "group head")] 460 + [cb_wrap_variables] 461 + [wrap_variables_info] 462 463 + [urwid.AttrMap(urwid.Text("\nDisplay driver:\n"), "group head")] 464 + [display_info] 465 + display_rbs 466 ) 467 468 lb = urwid.ListBox(urwid.SimpleListWalker(lb_contents)) 469 470 if ui.dialog(lb, [ 471 ("OK", True), 472 ("Cancel", False), 473 ], 474 title="Edit Preferences"): 475 # Only update the settings here that instant-apply (above) doesn't take 476 # care of. 477 478 # if we had a custom theme, it wasn't updated live 479 if theme_rb_group[-1].state: 480 newvalue = theme_edit.get_edit_text() 481 conf_dict.update(theme=newvalue, custom_theme=newvalue) 482 _update_theme() 483 484 # Ditto for custom stringifiers 485 if stringifier_rb_group[-1].state: 486 newvalue = stringifier_edit.get_edit_text() 487 conf_dict.update(stringifier=newvalue, custom_stringifier=newvalue) 488 _update_stringifier() 489 490 if shell_rb_group[-1].state: 491 newvalue = shell_edit.get_edit_text() 492 conf_dict.update(shell=newvalue, custom_shell=newvalue) 493 else: 494 for shell, shell_rb in zip(shells, shell_rbs): 495 if shell_rb.get_state(): 496 conf_dict["shell"] = shell 497 498 for display, display_rb in zip(displays, display_rbs): 499 if display_rb.get_state(): 500 conf_dict["display"] = display 501 502 else: # The user chose cancel, revert changes 503 conf_dict.update(old_conf_dict) 504 _update_theme() 505 # _update_line_numbers() is equivalent to _update_theme() 506 _update_current_stack_frame() 507 _update_stringifier() 508 509 510# {{{ breakpoint saving 511 512def parse_breakpoints(lines): 513 # b [ (filename:lineno | function) [, "condition"] ] 514 515 breakpoints = [] 516 for arg in lines: 517 if not arg: 518 continue 519 arg = arg[1:] 520 521 filename = None 522 lineno = None 523 cond = None 524 comma = arg.find(",") 525 526 if comma > 0: 527 # parse stuff after comma: "condition" 528 cond = arg[comma+1:].lstrip() 529 arg = arg[:comma].rstrip() 530 531 colon = arg.rfind(":") 532 funcname = None 533 534 if colon > 0: 535 filename = arg[:colon].strip() 536 537 f = lookup_module(filename) 538 if not f: 539 continue 540 else: 541 filename = f 542 543 arg = arg[colon+1:].lstrip() 544 try: 545 lineno = int(arg) 546 except ValueError: 547 continue 548 else: 549 continue 550 551 if get_breakpoint_invalid_reason(filename, lineno) is None: 552 breakpoints.append((filename, lineno, False, cond, funcname)) 553 554 return breakpoints 555 556 557def get_breakpoints_file_name(): 558 from os.path import join 559 save_path = get_save_config_path() 560 if not save_path: 561 return None 562 else: 563 return join(save_path, SAVED_BREAKPOINTS_FILE_NAME) 564 565 566def load_breakpoints(): 567 """ 568 Loads and check saved breakpoints out from files 569 Returns: list of tuples 570 """ 571 from os.path import join, isdir 572 573 file_names = [] 574 for cdir in XDG_CONFIG_DIRS: 575 if isdir(cdir): 576 for name in [SAVED_BREAKPOINTS_FILE_NAME, BREAKPOINTS_FILE_NAME]: 577 file_names.append(join(cdir, XDG_CONF_RESOURCE, name)) 578 579 lines = [] 580 for fname in file_names: 581 try: 582 rc_file = open(fname, "rt") 583 except IOError: 584 pass 585 else: 586 lines.extend([line.strip() for line in rc_file.readlines()]) 587 rc_file.close() 588 589 return parse_breakpoints(lines) 590 591 592def save_breakpoints(bp_list): 593 """ 594 :arg bp_list: a list of `bdb.Breakpoint` objects 595 """ 596 save_path = get_breakpoints_file_name() 597 if not save_path: 598 return 599 600 histfile = open(get_breakpoints_file_name(), "w") 601 bp_list = set([(bp.file, bp.line, bp.cond) for bp in bp_list]) 602 for bp in bp_list: 603 line = "b %s:%d" % (bp[0], bp[1]) 604 if bp[2]: 605 line += ", %s" % bp[2] 606 line += "\n" 607 histfile.write(line) 608 histfile.close() 609 610# }}} 611 612# vim:foldmethod=marker 613