import re from functools import wraps from IPython.core.magic import register_line_magic from IPython import get_ipython # @UnusedImport (used by register_line_magic) from diffcalc.gdasupport.scannable.hkl import Hkl """ For wrapping functions: In [1]: import diffcmd.ipython In [2]: diffcmd.ipython.GLOBAL_NAMESPACE_DICT = globals() In [3]: from IPython.core.magic import register_line_magic In [4]: from diffcmd.ipython import parse_line In [5]: @register_line_magic ...: @parse_line ...: def check_parser(*args): ...: return args ...: In [6]: check_parser Out[6]: In [7]: del check_parser In [8]: check_parser Out[8]: () In [9]: check_parser 1 Out[9]: (1,) In [10]: check_parser 1 2 Out[10]: (1, 2) In [11]: check_parser 1 2 [3] Out[11]: (1, 2, [3]) In [12]: b='bbb' In [13]: check_parser 1 2 [3] b Out[13]: (1, 2, [3], 'bbb') And to create something dynamically from a function: In [28]: def f(a, b, c): ....: ....: return a, b, c ....: In [29]: register_line_magic(parse_line(f)) Out[29]: In [30]: del f In [31]: f 'a' -2 [1 3 -4] Out[31]: ('a', -2, [1, 3, -4]) And from a list of functions: In [32]: def one(a): ....: return a ....: In [33]: def two(a, b): ....: return a, b ....: In [34]: functions = one, two In [35]: del one, two In [36]: for f in functions: ....: register_line_magic(parse_line(f)) ....: In [37]: one 1 Out[37]: 1 In [39]: two 1 2 Out[39]: (1, 2) And to check if we are running in iPython: In [47]: 'get_ipython' in globals() Out[47]: True def in_ipython(): try: get_ipython() return True except NameError: return False """ GLOBAL_NAMESPACE_DICT = {} MATH_OPERATORS = set(['-', '+', '/', '*']) # Keep a copy of python's original help as we may remove it later if 'help' in __builtins__: ORIGINAL_PYTHON_HELP = __builtins__['help'] COMMA_USAGE_HELP = \ ''' | When calling a function without brackets, whitespace must be used in | place of commas. For example: | | >>> function a b [1 2 3] 'c' | | is equivalent to: | | >>> function(a, b, [1, 2, 3], 'c') | ''' MATH_OPERATOR_USAGE_HELP = \ ''' | When calling a function without brackets, whitespace is used in place of | commas. Therefore terms which require evaluation must contain no space. | These will fail for example: | | >>> function - 1 | >>> function a() * 2 | But this | >>> function -1 1-1 +1 a()+1 [-1 0+1 b()] c+1 | | is okay and equivalent to: | | >>> function(-1, 0, 1, a() + 1, [-1, 1, b()], c + 1) | ''' comma_finder = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''') space_finder = re.compile(r'''((?:[^ "']|"[^"]*"|'[^']*')+)''') hash_finder = re.compile(r'''((?:[^#"']|"[^"]*"|'[^']*')+)''') open_square_finder = re.compile(r'''((?:[^["']|"[^"]*"|'[^']*')+)''') close_square_finder = re.compile(r'''((?:[^]"']|"[^"]*"|'[^']*')+)''') def tokenify(s): # Don't accept commas outside strings. # Users are frustrated by not knowing when commas _are_ required. # Making it clear when they are not helps them understand the # difference. if ',' in comma_finder.split(s): print COMMA_USAGE_HELP print "(string was: %s)" % s raise SyntaxError('unexpected comma') # ignore comment hash_split = hash_finder.split(s) if '#' in hash_split: s = '' if hash_split[0] == '#' else hash_split[1] # surround square brackets with spaces to simplify token extraction s = ''.join(' [ ' if e == '[' else e for e in open_square_finder.split(s)) s = ''.join(' ] ' if e == ']' else e for e in close_square_finder.split(s)) # tokens are now separated by spaces tokens = space_finder.split(s)[1::2] tokens = [tok for tok in tokens if tok != ''] return tokens def parse(s, d): s = str(s) tokens = tokenify(s) for tok in tokens: if tok in MATH_OPERATORS: print MATH_OPERATOR_USAGE_HELP raise SyntaxError('could not evaluate: "%s"' % tok) s = ', '.join(tokens) s = s.replace('[, ', '[') s = s.replace(',]', ']') s = s.replace(', ]', ']') try: args = eval('[' + s + ']', d) except SyntaxError: raise SyntaxError('could not evaluate: "%s"' % s) return args def parse_line(f, global_namespace_dict=None): '''A decorator that parses a single string argument into a list of arguments and calls the wrapped function with these. ''' if not global_namespace_dict: global_namespace_dict = GLOBAL_NAMESPACE_DICT @wraps(f) def wrapper(line): args = parse(line, global_namespace_dict) return f(*args) return wrapper _DEFAULT_HELP = \ """ For help with diffcalc's orientation phase try: >>> help ub For help with moving in reciprocal lattice space try: >>> help hkl For more detailed help try for example: >>> help newub For help with driving axes or scanning: >>> help pos >>> help scan For help with regular python try for example: >>> help list For more detailed help with diffcalc go to: https://diffcalc.readthedocs.io """ def magic_commands(global_namespace_dict): """Magic commands left in global_namespace_dict by previous import from diffcalc. Also creates a help command. NOTE that calling this will remove the original commands from the global namespace as otherwise these would shadow the ipython magiced versions. Depends on hkl_commands_for_help & ub_commands_for_help list having been left in the global namespace and assumes there is pos and scan command. """ gnd = global_namespace_dict global GLOBAL_NAMESPACE_DICT GLOBAL_NAMESPACE_DICT = gnd ### Magic commands in namespace ### commands = list(gnd['hkl_commands_for_help']) commands += gnd['ub_commands_for_help'] commands.append(gnd['pos']) commands.append(gnd['scan']) command_map = {} for f in commands: # Skip section headers like 'Motion' if not hasattr(f, '__call__'): continue # magic the function and remove from namespace (otherwise it would # shadow the magiced command) register_line_magic(parse_line(f, gnd)) del gnd[f.__name__] command_map[f.__name__] = f ### Create help function ### #Expects python's original help to be named pythons_help and to be #available in the top-level global namespace (where non-diffcalc #objects may have help called from). def help(s): # @ReservedAssignment """Diffcalc help for iPython """ if s == '': print _DEFAULT_HELP elif s == 'hkl': # Use help injected into hkl object print Hkl.dynamic_docstring elif s == 'ub': # Use help injected into ub command print command_map['ub'].__doc__ elif s in command_map: print "%s (diffcalc command):" %s print command_map[s].__doc__ else: exec('pythons_help(%s)' %s, gnd) ### Setup help command ### gnd['pythons_help'] = ORIGINAL_PYTHON_HELP register_line_magic(help) # Remove builtin help # (otherwise it would shadow magiced command if 'help' in __builtins__: del __builtins__['help']