diff --git a/.gitignore b/.gitignore index 5c1d549..22e6a38 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ dist *.pyc +*swp +MANIFEST +.tox/ +*.egg-info/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c5100d4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# Pythonpy 0.4 +Removed documentation for flags --ji, --jo, --so, --si, --i, and -fx. +These flags are not deprecated. Users should feel comfortable continuing to use them, +but from some simple polling I have found that they are infrequently used due to +their complexity and unorthodox double-dash form. diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index 69e500c..0000000 --- a/MANIFEST +++ /dev/null @@ -1,5 +0,0 @@ -# file GENERATED by distutils, do NOT edit -README.txt -setup.py -bin/pythonpy -pythonpy/__init__.py diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..a307177 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +include pythonpy/pycompletion.sh diff --git a/README.rst b/README.rst index ceab535..f63a524 100644 --- a/README.rst +++ b/README.rst @@ -3,85 +3,50 @@ Installation :: - sudo pip install pythonpy && alias py='pythonpy' + sudo pip install pythonpy :: -For a permanent alias (For Bash users): +Usage +----------------------------------------------- -:: - - echo "alias py='pythonpy'" >> ~/.bashrc - -:: +Pythonpy will evaluate any python expression from the command line. -Float arithmetic ----------------- +Float Arithmetic +~~~~~~~~~~~~~~~~ :: - $ py '3 * 1.5' + $ py '3 * 1.5' 4.5 :: -Exponentiation --------------- - -:: - - $ py '7**3' - 343 - -:: - -Number sequence ---------------- - -:: - - $ py 'range(3)' - 0 - 1 - 2 - -:: - -List comprehensions -------------------- - -:: - - $ py '[x**2 for x in range(1,5)]' - 1 - 4 - 9 - 16 - -:: - -Math library usage ------------------- +Import any module automatically +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: $ py 'math.exp(1)' 2.71828182846 -:: - -Random library usage --------------------- - -:: - $ py 'random.random()' 0.103173957713 + + $ py 'datetime.datetime.now?' + Help on built-in function now: + + now(...) + [tz] -> new datetime with tz's local day and time. + :: -Multiply each line of input by 7. ---------------------------------- +py -x 'foo(x)' will apply foo to each line of input +--------------------------------------------------- + +Multiply each line of input by 7 +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: @@ -92,137 +57,114 @@ Multiply each line of input by 7. :: -Append ".txt" to each line of input ------------------------------------ +Grab the second column of a csv +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - $ py 'range(3)' | py -x 'x + ".txt"' - 0.txt - 1.txt - 2.txt + $ echo $'a1,b1,c1\na2,b2,c2' | py -x 'x.split(",")[1]' + b1 + b2 :: -Sometimes you want to treat the input as a python list ------------------------------------------------------- - -Reverse a list -~~~~~~~~~~~~~~ +Append ".txt" to every file in the directory +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - $ py 'range(4)' | py -l 'sorted(l, reverse=True)' - 3 - 2 - 1 - 0 + $ ls | py -x '"mv `%s` `%s.txt`" % (x,x)' | sh + # sharp quotes are swapped out for single quotes + # single quotes handle spaces in filenames :: -Sum a list of numbers ---------------------- +Remove every file returned by the find command +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - $ py 'range(4)' | py -l 'sum(int(x) for x in l)' - 6 + $ find . -type f | py -x '"rm %s" % x' | sh :: -Count the lines of input ------------------------- +Get only 2 digit numbers +~~~~~~~~~~~~~~~~~~~~~ :: - $ py 'range(17)' | py -l 'len(l)' - 17 + $ py 'range(14)' | py -x 'x if len(x) == 2 else None' + 10 + 11 + 12 + 13 :: -Other times you just want to filter out lines from the input ------------------------------------------------------------- +py -l will set l = list(sys.stdin) +------------------------------------------- -Get only even numbers -~~~~~~~~~~~~~~~~~~~~~ +Lists are printed row by row +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - $ py 'range(8)' | py -x 'x if int(x)%2 == 0 else None' + $ py 'range(3)' 0 + 1 2 - 4 - 6 - -:: - -The shorthand -fx (filter on x) is also available -------------------------------------------------- -Get only odd numbers -~~~~~~~~~~~~~~~~~~~~ + $ py '[range(3)]' + [0, 1, 2] :: - $ py 'range(8)' | py -fx 'int(x)%2 == 1' - 1 - 3 - 5 - 7 +Reverse the input +~~~~~~~~~~~~~~~~~ :: -Get words starting with "and" -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + $ py 'range(3)' | py -l 'l[::-1]' + 2 + 1 + 0 :: - $ cat /usr/share/dict/words | py -fx 're.match(r"and", x)' | head -5 - and - andante - andante's - andantes - andiron +Sum the input +~~~~~~~~~~~~~ :: -Get verbs starting with ba -~~~~~~~~~~~~~~~~~~~~~~~~~~ + $ py 'range(3)' | py -l 'sum(int(x) for x in l)' + 3 :: - $ cat /usr/share/dict/words | py -fx 're.match(r"ba.*ing$", x)' | head -5 - baaing - babbling - babying - babysitting - backbiting +Sort a csv by the second column +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: -Get long palindromes -~~~~~~~~~~~~~~~~~~~~ + $ echo $'a,2\nb,1' | py -l 'sorted(l, key=lambda x: x.split(",")[1])' + b,1 + a,2 :: - $ cat /usr/share/dict/words | py -fx 'x==x[::-1] and len(x) >= 5' | head -5 - civic - deified - kayak - level - ma'am +Count words beginning with each letter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: -Ignore AttributeErrors if they pop up with (--i) ------------------------------------------------- - -Get the local network ip -~~~~~~~~~~~~~~~~~~~~~~~~ + $ cat /usr/share/dict/words | py -x 'x[0].lower()' | py -l 'collections.Counter(l).most_common(5)' + ('s', 11327) + ('c', 9521) + ('p', 7659) + ('b', 6068) + ('m', 5922) :: - $ ifconfig | py -x --i 're.search(r"192\.168[\d\.]+", x).group()' - 192.168.1.41 - -:: +For more examples, check out the `wiki `__ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/README.txt b/README.txt deleted file mode 100644 index 27aef0c..0000000 --- a/README.txt +++ /dev/null @@ -1,104 +0,0 @@ -# install -sudo pip install pythonpy; alias py='pythonpy' - -# float arithmetic -$ py '3 * 1.5' -4.5 - -# exponentiation -$ py '7**3' -343 - -# number sequence -$ py 'range(3)' -0 -1 -2 - -# list comprehensions -$ py '[x**2 for x in range(1,5)]' -1 -4 -9 -16 - -# math library usage -$ py 'math.exp(1)' -2.71828182846 - -# random library usage -$ py 'random.random()' -0.103173957713 - -# multiply each line of input by 7. -$ py 'range(3)' | py -x 'int(x)*7' -0 -7 -14 - -# append ".txt" to each line of input -$ py 'range(3)' | py -x 'x + ".txt"' -0.txt -1.txt -2.txt - -# Sometimes you want to treat the input as a python list. -# reverse a list -$ py 'range(4)' | py -l 'sorted(l, reverse=True)' -3 -2 -1 -0 - -# sum a list of numbers -$ py 'range(4)' | py -l 'sum(int(x) for x in l)' -6 - -# count the lines of input -$ py 'range(17)' | py -l 'len(l)' -17 - -# Other times you just want to filter out lines from the input. -# get only even numbers -$ py 'range(8)' | py -x 'x if int(x)%2 == 0 else None' -0 -2 -4 -6 - -# The shorthand -fx (filter on x) is also available. -# get only odd numbers -$ py 'range(8)' | py -fx 'int(x)%2 == 1' -1 -3 -5 -7 - -# get words starting with "and" -$ cat /usr/share/dict/words | py -fx 're.match(r"and", x)' | head -5 -and -andante -andante's -andantes -andiron - -#get verbs starting with ba -$ cat /usr/share/dict/words | py -fx 're.match(r"ba.*ing$", x)' | head -5 -baaing -babbling -babying -babysitting -backbiting - -# get long palindromes -$ cat /usr/share/dict/words | py -fx 'x==x[::-1] and len(x) >= 5' | head -5 -civic -deified -kayak -level -ma'am - -# ignore AttributeErrors if they pop up with (--i). -# get the local network ip -$ ifconfig | py -x --i 're.search(r"192\.168[\d\.]+", x).group()' -192.168.1.41 diff --git a/bin/pythonpy b/bin/pythonpy deleted file mode 100755 index 4f03da5..0000000 --- a/bin/pythonpy +++ /dev/null @@ -1,87 +0,0 @@ -#!/usr/bin/env python -import argparse -import glob -import itertools -import json -import math -import os -import random -import re -import shutil -import sys - -parser = argparse.ArgumentParser() -parser.add_argument('evaluation', nargs='?', default='None') -parser.add_argument('-c', '--cmd') -parser.add_argument('--ji' '--json_in', - dest='json_input', action='store_const', - const=True, default=False) -parser.add_argument('--jo' '--json_out', - dest='json_output', action='store_const', - const=True, default=False) -parser.add_argument('-x' '--line_by_line', - dest='line_by_line', action='store_const', - const=True, default=False, - help='sum the integers (default: find the max)') -parser.add_argument('-sv' '--split_values', - dest='split_values', - default=False, - help='sum the integers (default: find the max)') -parser.add_argument('-l', '--list_of_stdin', - dest='list_of_stdin', action='store_const', - const=True, default=False) -parser.add_argument('-fx', '--filter', - dest='filter_result', action='store_const', - const=True, default=False) -parser.add_argument('--i', '--ignore_exceptions', - dest='ignore_exceptions', action='store_const', - const=True, default=False) - -args = parser.parse_args() - -if args.json_input: - stdin = (json.loads(x.rstrip()) for x in sys.stdin) -else: - stdin = (x.rstrip() for x in sys.stdin) - -if args.evaluation: - args.evaluation = args.evaluation.replace("`", "'") -if args.cmd: - args.cmd = args.cmd.replace("`", "'") - -if args.cmd: - exec(args.cmd) - -if args.line_by_line: - if args.ignore_exceptions: - def safe_eval(text, x): - try: - return eval(text) - except: - return None - result = (safe_eval(args.evaluation, x) for x in stdin) - else: - result = (eval(args.evaluation) for x in stdin) -elif args.list_of_stdin: - l = list(stdin) - result = eval(args.evaluation) -elif args.filter_result: - result = (x for x in stdin if eval(args.evaluation)) -elif args.split_values: - result = (eval(args.evaluation) for sv in - (re.split(args.split_values, x) for x in stdin)) -else: - result = eval(args.evaluation) - -if hasattr(result, '__iter__'): - for x in result: - if x is not None: - if args.json_output: - print json.dumps(x) - else: - print x -elif result is not None: - if args.json_output: - print json.dumps(result) - else: - print result diff --git a/pythonpy/__init__.py b/pythonpy/__init__.py new file mode 100755 index 0000000..a987347 --- /dev/null +++ b/pythonpy/__init__.py @@ -0,0 +1 @@ +__version__ = '0.4.2' diff --git a/pythonpy/__main__.py b/pythonpy/__main__.py new file mode 100755 index 0000000..e9a5519 --- /dev/null +++ b/pythonpy/__main__.py @@ -0,0 +1,207 @@ +#!/usr/bin/env python2 +from __future__ import (unicode_literals, absolute_import, + print_function, division) +import sys + +if sys.version_info.major == 2: + reload(sys) + sys.setdefaultencoding('utf-8') + +from signal import signal, SIGPIPE, SIG_DFL +signal(SIGPIPE,SIG_DFL) + +import argparse +import json +import re +from collections import Iterable + +try: + from . import __version__ +except (ImportError, ValueError, SystemError): + __version__ = '???' # NOQA +__version_info__ = '''Pythonpy %s +Python %s''' % (__version__, sys.version.split(' ')[0]) + + +def import_matches(query, prefix=''): + matches = set(re.findall(r"(%s[a-zA-Z_][a-zA-Z0-9_]*)\.?" % prefix, query)) + for module_name in matches: + try: + module = __import__(module_name) + globals()[module_name] = module + import_matches(query, prefix='%s.' % module_name) + except ImportError as e: + pass + + +def lazy_imports(*args): + query = ' '.join([x for x in args if x]) + import_matches(query) + + +def current_list(input): + return re.split(r'[^a-zA-Z0-9_\.]', input) + + +def inspect_source(obj): + import inspect + import pydoc + try: + pydoc.pager(''.join(inspect.getsourcelines(obj)[0])) + return None + except: + return help(obj) + +parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + add_help=False) + +group = parser.add_argument_group("Options") + +parser.add_argument('expression', nargs='?', default='None', help="e.g. py '2 ** 32'") +group.add_argument('-x', dest='lines_of_stdin', action='store_const', + const=True, default=False, + help='treat each row of stdin as x') +group.add_argument('-fx', dest='filter_result', action='store_const', + const=True, default=False, + help=argparse.SUPPRESS) +group.add_argument('-l', dest='list_of_stdin', action='store_const', + const=True, default=False, + help='treat list of stdin as l') +group.add_argument('--ji', '--json_input', + dest='json_input', action='store_const', + const=True, default=False, + help=argparse.SUPPRESS) +group.add_argument('--jo', '--json_output', + dest='json_output', action='store_const', + const=True, default=False, + help=argparse.SUPPRESS) +group.add_argument('--si', '--split_input', dest='input_delimiter', + help=argparse.SUPPRESS) +group.add_argument('--so', '--split_output', dest='output_delimiter', + help=argparse.SUPPRESS) +group.add_argument('-c', dest='pre_cmd', help='run code before expression') +group.add_argument('-C', dest='post_cmd', help='run code after expression') +group.add_argument('--i', '--ignore_exceptions', + dest='ignore_exceptions', action='store_const', + const=True, default=False, + help=argparse.SUPPRESS) +group.add_argument('-V', '--version', action='version', version=__version_info__, help='version info') +group.add_argument('-h', '--help', action='help', help="show this help message and exit") + +try: + args = parser.parse_args() + if sum([args.list_of_stdin, args.lines_of_stdin, args.filter_result]) > 1: + sys.stderr.write('Pythonpy accepts at most one of [-x, -l] flags\n') + sys.exit() + + if args.json_input: + def loads(str_): + try: + return json.loads(str_.rstrip()) + except Exception as ex: + if args.ignore_exceptions: + pass + else: + raise ex + stdin = (loads(x) for x in sys.stdin) + elif args.input_delimiter: + stdin = (re.split(args.input_delimiter, x.rstrip()) for x in sys.stdin) + else: + stdin = (x.rstrip() for x in sys.stdin) + + if args.expression: + args.expression = args.expression.replace("`", "'") + if args.expression.startswith('?') or args.expression.endswith('?'): + final_atom = current_list(args.expression.rstrip('?'))[-1] + first_atom = current_list(args.expression.lstrip('?'))[0] + if args.expression.startswith('??'): + import inspect + args.expression = "inspect_source(%s)" % first_atom + elif args.expression.endswith('??'): + import inspect + args.expression = "inspect_source(%s)" % final_atom + elif args.expression.startswith('?'): + args.expression = 'help(%s)' % first_atom + else: + args.expression = 'help(%s)' % final_atom + if args.lines_of_stdin: + from itertools import islice + stdin = islice(stdin,1) + if args.pre_cmd: + args.pre_cmd = args.pre_cmd.replace("`", "'") + if args.post_cmd: + args.post_cmd = args.post_cmd.replace("`", "'") + + lazy_imports(args.expression, args.pre_cmd, args.post_cmd) + + if args.pre_cmd: + exec(args.pre_cmd) + + def safe_eval(text, x): + try: + return eval(text) + except: + return None + + if args.lines_of_stdin: + if args.ignore_exceptions: + result = (safe_eval(args.expression, x) for x in stdin) + else: + result = (eval(args.expression) for x in stdin) + elif args.filter_result: + if args.ignore_exceptions: + result = (x for x in stdin if safe_eval(args.expression, x)) + else: + result = (x for x in stdin if eval(args.expression)) + elif args.list_of_stdin: + l = list(stdin) + result = eval(args.expression) + else: + result = eval(args.expression) + + def format(output): + if output is None: + return None + elif args.json_output: + return json.dumps(output) + elif args.output_delimiter: + return args.output_delimiter.join(output) + else: + return output + + + if isinstance(result, Iterable) and hasattr(result, '__iter__') and not isinstance(result, str): + for x in result: + formatted = format(x) + if formatted is not None: + try: + print(formatted) + except UnicodeEncodeError: + print(formatted.encode('utf-8')) + else: + formatted = format(result) + if formatted is not None: + try: + print(formatted) + except UnicodeEncodeError: + print(formatted.encode('utf-8')) + + if args.post_cmd: + exec(args.post_cmd) +except Exception as ex: + import traceback + pyheader = 'pythonpy/__main__.py' + exprheader = 'File ""' + foundexpr = False + lines = traceback.format_exception(*sys.exc_info()) + for line in lines: + if pyheader in line: + continue + sys.stderr.write(line) + if not foundexpr and line.lstrip().startswith(exprheader) and not isinstance(ex, SyntaxError): + sys.stderr.write(' {}\n'.format(args.expression)) + foundexpr = True + +def main(): + pass diff --git a/pythonpy/debug_pycompletion.sh b/pythonpy/debug_pycompletion.sh new file mode 100644 index 0000000..2c41c15 --- /dev/null +++ b/pythonpy/debug_pycompletion.sh @@ -0,0 +1,6 @@ +_py() +{ + COMPREPLY=($(pycompleter "${COMP_WORDS[@]:1}" )) +} + +complete -F _py -o nospace py diff --git a/pythonpy/pycompleter.py b/pythonpy/pycompleter.py new file mode 100755 index 0000000..76e3125 --- /dev/null +++ b/pythonpy/pycompleter.py @@ -0,0 +1,333 @@ +#!/usr/bin/env python2 +from __future__ import (unicode_literals, absolute_import, + print_function, division) +import sys +import re +from collections import defaultdict + +import rlcompleter + + +def current_raw(input): + if len(input[-1]) > 0 and input[-1][0] in '"\'': + return input[-1][1:] + return input[-1] + + +def current_list(input): + return re.split(r'[^a-zA-Z0-9_\.]', current_raw(input)) + + +def current_prefix(input): + return current_list(input)[-1] + + +def prior(input): + return input[:-1] + + +def lazy_imports(*args): + query = ' '.join([x for x in args if x]) + regex = re.compile("([a-zA-Z_][a-zA-Z0-9_]*)\.?") + matches = regex.findall(query) + for module_name in matches: + try: + module = __import__(module_name) + globals()[module_name] = module + except ImportError as e: + pass + + +def complete_all(prefix, completion_args): + lazy_imports(prefix, completion_args['c_arg']) + if completion_args: + if completion_args['x_arg']: + x = str() + if completion_args['l_arg']: + l = list() + if completion_args['c_arg']: + exec(completion_args['c_arg'].strip('"\'').replace("`", "'")) + context = locals() + context.update(globals()) + completer = rlcompleter.Completer(context) + idx = 0 + options_set = set() + while completer.complete(prefix, idx): + options_set.add(completer.complete(prefix, idx)) + idx += 1 + + module_completion, module_list = get_completerlib() + try: + options = module_completion("import " + prefix) or [] + except: #module_completion may throw exception (e.g. on 'import sqlalchemy_utils.') + options = [] + if options: + options = [x.rstrip(' ') for x in options if x.startswith(prefix)] + + return options + list(options_set) + + +def parse_string(input): + if current_raw(input).startswith('--'): + return ['--si', '--so', '--ji', '--jo', '--i'] + elif current_raw(input).startswith('-'): + return ['-h', '-x', '-fx', '-l', '-c', '-C'] + elif len(prior(input)) > 0 and prior(input)[-1] == '-c': + if 'import'.startswith(current_raw(input)): + options = ["'import"] + elif current_raw(input).startswith('import ') or current_raw(input).startswith('from '): + module_completion, module_list = get_completerlib() + options = module_completion(current_raw(input)) or [] + if options: + options = [x.rstrip(' ') for x in options if x.startswith(current_prefix(input))] + else: + options = complete_all(current_prefix(input), defaultdict(lambda: None)) + if current_prefix(input).endswith('.'): + options = [x for x in options if '._' not in x] + return options + elif current_raw(input) == '': + options = ['sys', 'json', 're', 'csv', 'datetime', 'hashlib', 'itertools', 'math', 'os', 'random', 'shutil'] + if '-x' in input[:-1] or '-fx' in input[:-1]: + options += 'x' + if '-l' in input[:-1]: + options += 'l' + return options + else: + completion_args = defaultdict(lambda: None) + if '-x' in prior(input) or '-fx' in prior(input): + completion_args['x_arg'] = True + if '-l' in prior(input): + completion_args['l_arg'] = True + if '-c' in prior(input): + c_index = prior(input).index('-c') + if (c_index + 1) < len(prior(input)): + completion_args['c_arg'] = prior(input)[c_index + 1] + options = complete_all(current_prefix(input), completion_args) + if current_prefix(input).endswith('.'): + options = [x for x in options if '._' not in x] + return options + + +def get_completerlib(): + """Implementations for various useful completers. + + These are all loaded by default by IPython. + """ + #----------------------------------------------------------------------------- + # Copyright (C) 2010-2011 The IPython Development Team. + # + # Distributed under the terms of the BSD License. + # + # The full license is in the file COPYING.txt, distributed with this software. + #----------------------------------------------------------------------------- + + #----------------------------------------------------------------------------- + # Imports + #----------------------------------------------------------------------------- + #from __future__ import print_function + + import inspect + import os + #import re + #import sys + + try: + # Python >= 3.3 + from importlib.machinery import all_suffixes + _suffixes = all_suffixes() + except ImportError: + from imp import get_suffixes + _suffixes = [ s[0] for s in get_suffixes() ] + + # Third-party imports + from time import time + from zipimport import zipimporter + + TIMEOUT_STORAGE = 2 + + TIMEOUT_GIVEUP = 20 + + # Regular expression for the python import statement + import_re = re.compile(r'(?P[a-zA-Z_][a-zA-Z0-9_]*?)' + r'(?P[/\\]__init__)?' + r'(?P%s)$' % + r'|'.join(re.escape(s) for s in _suffixes)) + + # RE for the ipython %run command (python + ipython scripts) + magic_run_re = re.compile(r'.*(\.ipy|\.ipynb|\.py[w]?)$') + + def module_list(path): + """ + Return the list containing the names of the modules available in the given + folder. + """ + # sys.path has the cwd as an empty string, but isdir/listdir need it as '.' + if path == '': + path = '.' + + # A few local constants to be used in loops below + pjoin = os.path.join + + if os.path.isdir(path): + # Build a list of all files in the directory and all files + # in its subdirectories. For performance reasons, do not + # recurse more than one level into subdirectories. + files = [] + for root, dirs, nondirs in os.walk(path): + subdir = root[len(path)+1:] + if subdir: + files.extend(pjoin(subdir, f) for f in nondirs) + dirs[:] = [] # Do not recurse into additional subdirectories. + else: + files.extend(nondirs) + + else: + try: + files = list(zipimporter(path)._files.keys()) + except: + files = [] + + # Build a list of modules which match the import_re regex. + modules = [] + for f in files: + m = import_re.match(f) + if m: + modules.append(m.group('name')) + return list(set(modules)) + + + def get_root_modules(): + """ + Returns a list containing the names of all the modules available in the + folders of the pythonpath. + + ip.db['rootmodules_cache'] maps sys.path entries to list of modules. + """ + #ip = get_ipython() + #rootmodules_cache = ip.db.get('rootmodules_cache', {}) + rootmodules_cache = {} + rootmodules = list(sys.builtin_module_names) + start_time = time() + #store = False + for path in sys.path: + try: + modules = rootmodules_cache[path] + except KeyError: + modules = module_list(path) + try: + modules.remove('__init__') + except ValueError: + pass + if path not in ('', '.'): # cwd modules should not be cached + rootmodules_cache[path] = modules + if time() - start_time > TIMEOUT_STORAGE and not store: + #store = True + #print("\nCaching the list of root modules, please wait!") + #print("(This will only be done once - type '%rehashx' to " + #"reset cache!)\n") + sys.stdout.flush() + if time() - start_time > TIMEOUT_GIVEUP: + print("This is taking too long, we give up.\n") + return [] + rootmodules.extend(modules) + #if store: + #ip.db['rootmodules_cache'] = rootmodules_cache + rootmodules = list(set(rootmodules)) + return rootmodules + + + def is_importable(module, attr, only_modules): + if only_modules: + return inspect.ismodule(getattr(module, attr)) + else: + return not(attr[:2] == '__' and attr[-2:] == '__') + + + def try_import(mod, only_modules=False): + try: + m = __import__(mod) + except: + return [] + mods = mod.split('.') + for module in mods[1:]: + m = getattr(m, module) + + m_is_init = hasattr(m, '__file__') and '__init__' in m.__file__ + + completions = [] + if (not hasattr(m, '__file__')) or (not only_modules) or m_is_init: + completions.extend( [attr for attr in dir(m) if + is_importable(m, attr, only_modules)]) + + completions.extend(getattr(m, '__all__', [])) + if m_is_init: + completions.extend(module_list(os.path.dirname(m.__file__))) + completions = set(completions) + if '__init__' in completions: + completions.remove('__init__') + return list(completions) + + + def module_completion(line): + """ + Returns a list containing the completion possibilities for an import line. + + The line looks like this : + 'import xml.d' + 'from xml.dom import' + """ + + words = line.split(' ') + nwords = len(words) + + # from whatever -> 'import ' + if nwords == 3 and words[0] == 'from': + return ['import '] + + # 'from xy' or 'import xy' + if nwords < 3 and (words[0] in ['import','from']) : + if nwords == 1: + return get_root_modules() + mod = words[1].split('.') + if len(mod) < 2: + return get_root_modules() + completion_list = try_import('.'.join(mod[:-1]), True) + return ['.'.join(mod[:-1] + [el]) for el in completion_list] + + # 'from xyz import abc' + if nwords >= 3 and words[0] == 'from': + mod = words[1] + return try_import(mod) + + return module_completion, module_list + + +def remove_trailing_paren(str_): + if str_.endswith('('): + return str_[:-1] + return str_ + + +def main(): + input = sys.argv[1:] + if len(input) == 0: + return + elif '<' in input or '>' in input: + print('_longopt') + return + else: + options = list(set(map(remove_trailing_paren, parse_string(input)))) + + if len(options) == 0: + return + + if len(current_list(input)) > 1 and max(map(len, options)) + 1 >= len(current_raw(input)): + options.append(current_prefix(input)) + + if len(options) <= 1: + options = options + [x + "'" for x in options] + print(' '.join(options)) + + +if __name__ == '__main__': + main() diff --git a/pythonpy/pycompletion.sh b/pythonpy/pycompletion.sh new file mode 100644 index 0000000..e218f7c --- /dev/null +++ b/pythonpy/pycompletion.sh @@ -0,0 +1,70 @@ +_py() +{ + COMPREPLY=($(pycompleter "${COMP_WORDS[@]}" 2>/dev/null )) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py2() +{ + COMPREPLY=($(pycompleter2 "${COMP_WORDS[@]}" 2>/dev/null )) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py2.6() +{ + COMPREPLY=($(pycompleter2.6 "${COMP_WORDS[@]}" 2>/dev/null )) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py2.7() +{ + COMPREPLY=($(pycompleter2.7 "${COMP_WORDS[@]}" 2>/dev/null )) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py3() +{ + COMPREPLY=($(pycompleter3 "${COMP_WORDS[@]}" 2>/dev/null )) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py3.3() +{ + COMPREPLY=($(pycompleter3.3 "${COMP_WORDS[@]}" 2>/dev/null )) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} +_py3.4() +{ + COMPREPLY=($(pycompleter3.4 "${COMP_WORDS[@]}" 2>/dev/null )) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + + +complete -F _py -o nospace py +complete -F _py2 -o nospace py2 +complete -F _py2.6 -o nospace py2.6 +complete -F _py2.7 -o nospace py2.7 +complete -F _py3 -o nospace py3 +complete -F _py3.3 -o nospace py3.3 +complete -F _py3.4 -o nospace py3.4 diff --git a/setup.py b/setup.py old mode 100644 new mode 100755 index 82f664a..94c8144 --- a/setup.py +++ b/setup.py @@ -1,11 +1,54 @@ -from distutils.core import setup +#!/usr/bin/env python +from setuptools import setup import os +import sys +import tempfile + +for path in os.environ['PATH'].split(':'): + target = os.path.join(os.path.dirname(path), 'etc', 'bash_completion.d') + if os.path.isdir(target): + break +else: + # Fall back to the default used by many Linux distros + target = '/etc/bash_completion.d' + +try: + with tempfile.TemporaryFile(dir=target) as t: + pass +except OSError as e: + print( +'''****************************************************************************** +Pythonpy was not able to install bash completion because it does not have write +access to /etc/bash_completion.d. +If you would still like to install bash completion, either: +1) Reinstall with `sudo pip install pythonpy` +2) Configure tab completion manually: + source /path/to/virtualenv/bash_completion.d/pycompletion.sh + +Installation proceeding without root access... +******************************************************************************''') + target='bash_completion.d' + +data_files = [(target, ['pythonpy/pycompletion.sh']),] + +py_entry = 'py%s = pythonpy.__main__:main' +pycompleter_entry = 'pycompleter%s = pythonpy.pycompleter:main' +endings = ('', sys.version[:1], sys.version[:3]) +entry_points_scripts = [] +for e in endings: + entry_points_scripts.append(py_entry % e) + entry_points_scripts.append(pycompleter_entry % e) setup( name='pythonpy', - version='0.1.5', - description='Command line utility for Python', - scripts=[os.path.join('bin', 'pythonpy')], - license='Creative Commons Attribution-Noncommercial-Share Alike license', - long_description=open('README.rst').read(), + version='0.4.2', + description='python -c, with tab completion and shorthand', + data_files=data_files, + license='MIT', + url='https://github.com/Russell91/pythonpy', + long_description='https://github.com/Russell91/pythonpy', + packages = ['pythonpy'], + entry_points = { + 'console_scripts': entry_points_scripts + }, ) diff --git a/test/test_pythonpy.py b/test/test_pythonpy.py index a9aa9d1..441eddd 100644 --- a/test/test_pythonpy.py +++ b/test/test_pythonpy.py @@ -3,11 +3,48 @@ class TestPythonPy(unittest.TestCase): def test_empty(self): - self.assertEqual(check_output(['pythonpy']),'') - + self.assertEqual(check_output(['py']), b'') + def test_numbers(self): - self.assertEqual(check_output(['pythonpy', '3 * 4.5']),'13.5\n') + self.assertEqual(check_output(['py', '3 * 4.5']), b'13.5\n') + + def test_range(self): + self.assertEqual(check_output(['py', 'range(3)']), b'0\n1\n2\n') + + def test_split_input(self): + self.assertEqual(check_output(["""echo a,b | py -x 'x[1]' --si ,"""], shell=True), b'b\n') + + def test_split_output(self): + self.assertEqual(check_output(["""echo abc | py -x x --si '' --so ','"""], shell=True), b'a,b,c\n') + + def test_ignore_errors(self): + self.assertEqual(check_output("""echo a | py -x --i 'None.None'""", shell=True), b'') + self.assertEqual(check_output("""echo a | py -fx --i 'None.None'""", shell=True), b'') + + def test_statements(self): + self.assertEqual(check_output("""py -c 'a=5' -C 'print(a)'""", shell=True), b'5\n') + self.assertEqual(check_output("""echo 3 | py -c 'a=5' -x x -C 'print(a)'""", shell=True), b'3\n5\n') + def test_imports(self): + module_commands = ["math.ceil(2.5)", + "calendar.weekday(1955, 11, 5)", + "csv.list_dialects()", + "datetime.timedelta(hours=-5)", + "hashlib.sha224(\"Nobody inspects the spammish repetition\").hexdigest()", + "glob.glob('*')", + "itertools.product(['a','b'], [1,2])", + "json.dumps([1,2,3,{'4': 5, '6': 7}], separators=(',',':'))", + "os.name", + "random.randint(0, 1000)", + "re.compile('[a-z]').findall('abcd')", + "shutil.get_archive_formats()", + "tempfile.gettempdir()", + "uuid.uuid1()", + "math", + "[math]", + ] + for command in module_commands: + check_output("py %r" % command, shell=True) if __name__ == '__main__': unittest.main()