def _clean_user_namespace(self, venv): """ clean ipython username to remove old project's code TODO: document and refactor """ msg = "cleaning user namespace" smash_log.info(msg) self.report(msg) names = [] pm = get_smash().project_manager pdir = pm._current_project and pm.project_map[pm._current_project] namespace = get_smash().shell.user_ns for name, obj in namespace.items(): try: fname = inspect.getfile(obj) except TypeError: # happens for anything that's not a module, class # method, function, traceback, frame or code object fname = None if fname: test = fname.startswith(venv) if pdir is not None: test = test or fname.startswith(pdir) if test and \ not name.startswith('_') and \ not name.startswith(SMASH_DIR): names.append(name) for name in names: del self.smash.shell.user_ns[name] if names: self.report("wiped from namespace: {0}".format(names))
def _showtraceback(self, etype, evalue, string_tb_lines): """ before we display the traceback, give smash error handlers one more chance to do something smart """ string_tb = '\n'.join(string_tb_lines) smash_log.info('dispatching last-ditch-effort error handling: {0}'.format( [etype, evalue, string_tb])) sooper = super(SmashTerminalInteractiveShell, self) if self.smash is not None: for handler in self.smash.error_handlers: handled = handler( self._smash_last_input, etype, evalue) if handled: return if etype == NameError and '<ipython-input' in string_tb: #len(self._smash_last_input.split('\n')) == 1 and \ #string is hardcoded in smashlib/ipy3x/core/compilerop.py # inside function code_name(code, number=0) try: msg = 'smash: "{0}": input error' msg = msg.format( self._smash_last_input.strip()) except UnicodeEncodeError: msg=str(evalue) print msg else: return sooper._showtraceback(etype, evalue, string_tb_lines)
def _unload_alias_group(self, group_name): smash_log.info('unloading alias group: {0}'.format(group_name)) aliases, macros = self._get_alias_group(group_name) for alias in aliases: name, cmd = alias try: get_smash().shell.alias_manager.undefine_alias(name) except ValueError: continue
def system(self, cmd, quiet=False, **kargs): # print 'wrapping system call',cmd result = super(SmashTerminalInteractiveShell, self).system( cmd, **kargs) # put exit code into os.environ? error = self.user_ns['_exit_code'] smash_log.info("exit code: {0}".format(error)) # if error: # get_smash().publish(C_COMMAND_FAIL, cmd, error) if not quiet and result: print result
def _load_alias_group(self, group_name): smash_log.info('loading alias group: {0}'.format(group_name)) aliases, macros = self._get_alias_group(group_name) for alias in aliases: name, cmd = alias get_smash().shell.alias_manager.define_alias(name, cmd) smash_log.info(' alias: {0}'.format(name)) self.report("Loaded {0} aliases".format(len(aliases))) for m in macros: name, macro = m self._load_macro(macro, name=name)
def _activate(self, path): absfpath = abspath(expanduser(path)) self.publish(C_PRE_ACTIVATE, name=absfpath) if True: vbin = to_vbin(absfpath) vlib = to_vlib(absfpath) # compute e.g. <venv>/lib/python2.6. # we call bullshit if they have a more than one dir; # it might be a chroot but i dont think it's a venv python_dir = glob.glob(opj(vlib, 'python*/')) if len(python_dir) == 0: raise RuntimeError('no python dir in {0}'.format(vlib)) if len(python_dir) > 1: err = "multiple python dirs matching in {0}".format(vlib) raise RuntimeError(err) python_dir = python_dir[0] # this bit might enable switching between two venv's # that are be "no-global-site" vs "use-global-site" # .. tabling it for now # site_file = opj(python_dir, 'site.py') # assert ope(site_file) # tmp = dict(__file__=site_file) # execfile(site_file, tmp) # tmp['main']() # some environment variable manipulation that would # normally be done by 'source bin/activate', but is # not handled by activate_this.py #path = get_path().split(':') os.environ['VIRTUAL_ENV'] = absfpath sandbox = dict(__file__=opj(vbin, 'activate_this.py')) execfile(opj(vbin, 'activate_this.py'), sandbox) self.reset_path = sandbox['prev_sys_path'] # libraries like 'datetime' can very occasionally fail on import # if this isnt done, and i'm not sure why activate_this.py doesnt # accomplish it. it might have something to do with venv's using # mixed pythons (different versions) or mixed --no-site-packages # tabling it for now # dynload = opj(python_dir, 'lib-dynload') # sys.path.append(dynload) # NB: this rehash will update bins but iirc kills aliases! msg = '$PATH was adjusted to {0}'.format(os.environ['PATH']) smash_log.debug(msg) self.report('Adjusting $PATH') msg = 'rehashing aliases' smash_log.info(msg) self.shell.magic('rehashx') self.publish(C_POST_ACTIVATE, absfpath)
def fabric_cmd_completer(self, event): completion_log.info("event [{0}]".format(event.__dict__)) if event.symbol.startswith("-"): return [] if ope("fabfile.py"): _fabfile = "fabfile.py" elif ope("Fabfile.py"): _fabfile = "Fabfile.py" else: smash_log.info("no fabfile was found") return [] with open(_fabfile, "r") as fhandle: src = fhandle.read() node = ast.parse(src) return list([x.name for x in ast.walk(node) if isinstance(x, ast.FunctionDef)])
def deactivate(self): try: venv = get_venv() except KeyError: self.warning("no venv to deactivate") return False else: if not venv: return False self.report("venv_deactivate: " + venv) self.publish(C_PRE_DEACTIVATE, venv) if not ope(venv): self.warning('refusing to deactivate (relocated?) venv') return # raise RuntimeError(err) del os.environ['VIRTUAL_ENV'] path = get_path() path = path.split(':') # clean $PATH according to bash.. # TODO: also rehash? vbin = to_vbin(venv) if vbin in path: msg = 'removing old venv bin from PATH: ' + \ summarize_fpath(str(vbin)) self.report(msg) path.remove(vbin) os.environ['PATH'] = ':'.join(path) # clean sys.path according to python.. # stupid, but this seems to work msg = 'clean sys.path' # ,sys.path_changes) self.report(msg) smash_log.info(msg) reset_path = getattr(self, 'reset_path', None) if reset_path is not None: msg = str(set(sys.path) ^ set(reset_path)) msg = 'sys.path difference: {0}'.format(msg) self.report(msg) smash_log.debug(msg) sys.path = reset_path self.reset_path = sys.path else: self.reset_path = sys.path self._clean_user_namespace(venv) self.publish(C_POST_DEACTIVATE, venv) return True
def source_file_namespace(fname): """ returns a dictionary of { fxn_name : FunctionMagic() } for bash functions in `fname` """ if not os.path.exists(fname): raise ValueError("{0} does not exist".format(fname)) fname = os.path.abspath(fname) smash_log.info("attempting to source: {0}".format(fname)) fxns = get_functions_from_file(fname) smash_log.info("found functions: {0}".format(fxns)) out = dict() for fxn_name in fxns: cmd = FunctionMagic(fxn_name, source=fname) out[fxn_name] = cmd get_smash().shell.magics_manager.register_function( cmd, magic_name=fxn_name) return out
def doit(_fpath, _suffix, _opener, _rest): if ope(_fpath) and not isdir(_fpath): if _opener is not None: self.report( 'Using _opener "{0}" for "{1}"'.format(_opener, _suffix)) return '{0} {1}'.format(_opener, _fpath + _rest) else: msg = "Legit file input, but no _suffix alias could be found for " + \ _suffix self.report(msg) if is_editable(_fpath): self.report( "File looks like ASCII text, assuming I should edit it") return doit(_fpath, _suffix, 'ed', _rest) else: msg = 'Attempted file input, but path "{0}" does not exist' msg = msg.format(fpath) smash_log.info(msg)
def __init__(self, shell, **kargs): if self.plugin_instance: raise Exception,'singleton' super(SmashPlugin, self).__init__(config=shell.config, shell=shell) # plugins should use self.contribute_* commands, which update the installation # record automatically. later the installation record will help with uninstalling # plugins cleanly and completely self.installation_record = defaultdict(list) self.shell.configurables.append(self) self.init_logger() smash_log.info("initializing {0}".format(self)) self.init_eventful() self.init_bus() self.init_magics() self.init_scheduled_tasks() self.init() self.plugin_instance = self
def run_cell(self, raw_cell, store_history=False, silent=False, shell_futures=True): #assert self.smash #import re #regex = r'`[^`]*`'; #line ='foo`bar`baz`a`'; #tick_groups = [x[1:-1] for x in re.findall(regex, line)]; #if tick_groups: # print tick_groups #avoid race on embedded shell publish = getattr(self.smash,'publish',None) if publish: publish(C_PRE_RUN_CELL, raw_cell) smash_log.info("[{0}]".format(raw_cell.encode('utf-8').strip())) # as long as the expressions are semicolon separated, # this section allows hybrid bash/python expressions bits = split_on_unquoted_semicolons(raw_cell) if len(bits) > 1: smash_log.info("detected chaining with this input") out = [] for x in bits: out.append( self.run_cell( x, store_history=store_history, silent=silent, shell_futures=shell_futures)) return out sooper = super(SmashTerminalInteractiveShell, self) out = sooper.run_cell( raw_cell, store_history=store_history, silent=silent, shell_futures=shell_futures) if self.smash is not None: if self._smash_last_input: # translated-input this = self.user_ns['In'][-1].strip() # untranslated-input last_input = self._smash_last_input self.smash.publish(C_POST_RUN_CELL, this) self.smash.publish(C_POST_RUN_INPUT, last_input) self._smash_last_input = "" return [out]
def init_config_inheritance(self): if self.load_bash_aliases: for alias, cmd in bash.get_aliases(): # this check is a hack, but users will frequently override # cd to pushd which is already done in smash anyway if alias not in 'ed cd'.split(): self.shell.magic("alias {0} {1}".format(alias, cmd)) self.shell.magics_manager.register_function( bash.source, magic_name='source') if self.load_bash_functions: smash_log.info("Loading bash functions") fxns = bash.get_functions() for fxn_name in fxns: fxn_bridge = bash.FunctionMagic(fxn_name, source='__host_shell__') self.shell.magics_manager.register_function( fxn_bridge, magic_name=fxn_name) msg = "registered magic for bash functions: " + str(fxns) smash_log.info(msg)
def showsyntaxerror(self, filename=None): """ when a syntax error is encountered, consider just broadcasting a signal instead of showing a traceback. """ smash_log.info( "last efforts to do something " "meaningful with input before " "syntax error") lastline = self._smash_last_input clean_line = lastline.strip() if not clean_line: # this is not input! # possibly there is actually an error in smash itself raise if is_path(clean_line): # NB: in this case a regex looks like a path or URL, # but it's not necessarily true that the endpoint # actuals exists smash_log.info("detected path input: {0}".format(clean_line)) self.smash.publish(C_FILE_INPUT, clean_line) elif is_path(clean_line.split()[0]): self.system(clean_line) else: smash_log.info('nothing to do but call super()') sooper = super(SmashTerminalInteractiveShell, self) return sooper.showsyntaxerror(filename=filename)
def load_from_etc(self, fname, schema=None): """ if schema is given, validate it. otherwise just load blindly """ smash_log.info('loading and validating {0}'.format(fname)) schema = schema or _find_schema(fname) absf = opj(DIR_SMASH_ETC, fname) try: with open(absf) as fhandle: data = demjson.decode(fhandle.read()) except demjson.JSONDecodeError: err = "file is not json: {0}".format(absf) # boot_log.critical(err) raise ConfigError(err) except IOError: smash_log.info("{0} does not exist..".format(absf)) if getattr(schema, 'default', None) is not None: smash_log.info("..but a default is defined. writing file") default = schema.default if not isinstance(default, basestring): default = demjson.encode(schema.default) with open(absf, 'w') as fhandle: fhandle.write(default) return self.load_from_etc(fname, schema) else: err = ("{0} does not exist, and no default" " is defined").format(absf) # boot_log.critical(err) self.log.critical(err) raise SystemExit(err) try: schema(data) except voluptuous.Invalid, e: raise SystemExit("error validating {0}\n\t{1}".format(absf, e))
def check(self, line_info): """ note: the way that line_info splitting happens means that for a command like "apt-get foo", first/rest will be apt/-get foo. it's better to just use line_info.line """ if line_info.continue_prompt or not line_info.line.strip(): return None shandler = self.prefilter_manager.get_handler_by_name(HANDLER_NAME) line = line_info.line prefixed_assignments = initial_assignments.search(line) if prefixed_assignments: smash_log.info('detected prefixed assignments') only_assignments = prefixed_assignments.group() nonassignment_section = line[len(only_assignments):] maybe_command = nonassignment_section.split() maybe_command = maybe_command and maybe_command[0] if have_alias(maybe_command): smash_log.info('prefixed assignments are followed by command alias') return shandler split_line = line.split() if have_alias(split_line[0]): return shandler
def handle(self, line_info): """ """ cmd = line_info.line.strip() smash_log.info("shellhandler: {0}".format(cmd)) return 'get_ipython().system(%r)' % (cmd, )
def venv_activate(self, parameter_s=''): msg = "venv_activate: " + parameter_s smash_log.info(msg) self.report(msg) self.plugin_obj.activate(parameter_s)
def init(self): smash_log.info("initializing") self._load_prompt_config() self.update_prompt() self.contribute_hook('pre_prompt_hook', self.update_prompt)
def input_finished_hook(self, raw_finished_input): for x in REHASH_IF: if x in raw_finished_input: msg = "detected possible $PATH changes (rehashing)" smash_log.info(msg) self.smash.shell.magic('rehashx')
def complete(to_complete): """ wow! so this is stupid, but what can you do? to understand the command required to get completion information out of bash, start by executing "printf '/et\x09\x09' | bash -i". What this command does is put bash into interactive mode, then simulate typing "/et<tab><tab>" inside bash. The tab-completion information can be scraped out of the text, but several things complicate the final solution: 1) the tab-completion info, apart from being post-processed, must be scraped from stderr, not from stdout. 2) for post-processing, without knowledge of how the prompt will be rendered or if there is some kind of banner that will be printed, it's hard to know where exactly to start capturing tab-completion options. 3) the method used to get the tab completion involves the bash builtins "printf", meaning we have to launch subprocess with "bash -c" 4) completion output could be paginated by bash if there are lots of options. have to account for that and still try to get all the options instead of just the first page 5) sending EOF does not work unless it comes after a newline. this means we have to take extra steps to avoid actually executing the command we want to complete (executing the command is not what the user expects and is possibly unsafe). to avoid executing the line, after getting tab completion you have to simulate control-a (go to start of line) followed by '#'. this comments the line and prevents it from executing. note: you cannot send control-c to cancel the execution of the line because we are dealing with pipes, whereas control-c is processed only by proper tty's. """ # smash_log.info("[{0}]".format(to_complete)) if not to_complete: return [] cmd = '''bash -c "printf 'PS1=\"\";echo MARKER\n{complete}\t\t\x01#\necho MARKER'|bash -i"'''.format( complete=to_complete) p1 = Popen(cmd, shell=True, stdout=PIPE, stdin=PIPE, stderr=PIPE) out, err = p1.communicate() lines = err.split('\n') # smash_log.info(err) first_marker = None last_marker = None for i in range(len(lines)): line = lines[i] if line.strip().endswith('echo MARKER'): if first_marker is None: first_marker = i else: last_marker = i # SPECIAL CASE: pagination if last_marker is None: # when this happens there are too many options, # ie bash is asking something like this: # Display all 103 possibilities? (y or n) # Pagination indicators like '--More--'must be removed lines = [x for x in lines if not line.startswith('--More')] last_marker = len(lines) - 3 first_marker += 1 complete_lines = lines[first_marker + 2:last_marker - 1] # SPECIAL-CASE: no completion options or only one option if not complete_lines: # NOTE: # if there is only one option, readline simply applies it, # which affects the current line in place. apparently this # results in tons of control-characters being dumped onto # the line, and we have to clean those up for the output try: the_line = lines[first_marker + 1:last_marker][0] except IndexError: smash_log.info( 'error completing '+str([ lines, first_marker, last_marker])) return [] the_line = remove_control_characters(the_line) tmp = the_line[the_line.rfind(to_complete) + len(to_complete):] result = to_complete.split()[-1] + tmp if '#' in result: # this seems to only happen for directories. not sure why result = result[:result.find('#')] if result == to_complete.split()[-1]: # SPECIAL-CASE: no completion options at all. return [] return [result] else: # there are multiple completion options completion_choices_by_row = [x.split() for x in complete_lines] completion_choices = reduce( lambda x, y: x + y, completion_choices_by_row) return completion_choices