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..da7eb23 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# 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. + +# 0.4.9 +Pythonpy no longer automatically tries to install pycompletion.sh into /etc/bash_completion.d +The .sh script is provided in the completion directory and users that want this feature may add + + source `find_pycompletion.sh` + +to their .bashrc. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..c02be80 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright (C) 2016 The Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 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..68ac8fe 100644 --- a/README.rst +++ b/README.rst @@ -3,85 +3,50 @@ Installation :: - sudo pip install pythonpy && alias py='pythonpy' + 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,121 @@ 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 -:: + $ py '[range(3)]' + [0, 1, 2] -The shorthand -fx (filter on x) is also available -------------------------------------------------- +:: -Get only odd numbers -~~~~~~~~~~~~~~~~~~~~ +Reverse the input +~~~~~~~~~~~~~~~~~ :: - $ py 'range(8)' | py -fx 'int(x)%2 == 1' + $ py 'range(3)' | py -l 'l[::-1]' + 2 1 - 3 - 5 - 7 + 0 :: -Get words starting with "and" -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Sum the input +~~~~~~~~~~~~~ :: - $ cat /usr/share/dict/words | py -fx 're.match(r"and", x)' | head -5 - and - andante - andante's - andantes - andiron + $ py 'range(3)' | py -l 'sum(int(x) for x in l)' + 3 :: -Get verbs starting with ba -~~~~~~~~~~~~~~~~~~~~~~~~~~ +Sort a csv by the second column +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - $ cat /usr/share/dict/words | py -fx 're.match(r"ba.*ing$", x)' | head -5 - baaing - babbling - babying - babysitting - backbiting + $ echo $'a,2\nb,1' | py -l 'sorted(l, key=lambda x: x.split(",")[1])' + b,1 + a,2 :: -Get long palindromes -~~~~~~~~~~~~~~~~~~~~ +Count words beginning with each letter +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ :: - $ cat /usr/share/dict/words | py -fx 'x==x[::-1] and len(x) >= 5' | head -5 - civic - deified - kayak - level - ma'am + $ 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) :: -Ignore AttributeErrors if they pop up with (--i) ------------------------------------------------- +For more examples, check out the `wiki `__. -Get the local network ip -~~~~~~~~~~~~~~~~~~~~~~~~ +Pythonpy also supports ipython style tab completion, which you can enable as follows :: - $ ifconfig | py -x --i 're.search(r"192\.168[\d\.]+", x).group()' - 192.168.1.41 + $ if command -v find_pycompletion.sh>/dev/null; then source `find_pycompletion.sh`; fi :: 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..d4f3346 --- /dev/null +++ b/pythonpy/__init__.py @@ -0,0 +1 @@ +__version__ = '0.4.11' diff --git a/pythonpy/__main__.py b/pythonpy/__main__.py new file mode 100755 index 0000000..8602b28 --- /dev/null +++ b/pythonpy/__main__.py @@ -0,0 +1,215 @@ +#!/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 raw_module_name in matches: + if re.match('np(\..*)?$', raw_module_name): + module_name = re.sub('^np', 'numpy', raw_module_name) + elif re.match('pd(\..*)?$', raw_module_name): + module_name = re.sub('^pd', 'pandas', raw_module_name) + else: + module_name = raw_module_name + try: + module = __import__(module_name) + globals()[raw_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(1) + + 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 + + sys.exit(1) + +def main(): + pass diff --git a/pythonpy/completion/__init__.py b/pythonpy/completion/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/pythonpy/completion/debug_pycompletion.sh b/pythonpy/completion/debug_pycompletion.sh new file mode 100644 index 0000000..2c41c15 --- /dev/null +++ b/pythonpy/completion/debug_pycompletion.sh @@ -0,0 +1,6 @@ +_py() +{ + COMPREPLY=($(pycompleter "${COMP_WORDS[@]:1}" )) +} + +complete -F _py -o nospace py diff --git a/pythonpy/completion/pycompletion.sh b/pythonpy/completion/pycompletion.sh new file mode 100644 index 0000000..50a6528 --- /dev/null +++ b/pythonpy/completion/pycompletion.sh @@ -0,0 +1,70 @@ +_py() +{ + COMPREPLY=($(pycompleter "${COMP_WORDS[@]}" 2>/dev/null | sed 's/.*1034h//')) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py2() +{ + COMPREPLY=($(pycompleter2 "${COMP_WORDS[@]}" 2>/dev/null | sed 's/.*1034h//')) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py2.6() +{ + COMPREPLY=($(pycompleter2.6 "${COMP_WORDS[@]}" 2>/dev/null | sed 's/.*1034h//')) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py2.7() +{ + COMPREPLY=($(pycompleter2.7 "${COMP_WORDS[@]}" 2>/dev/null | sed 's/.*1034h//')) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py3() +{ + COMPREPLY=($(pycompleter3 "${COMP_WORDS[@]}" 2>/dev/null | sed 's/.*1034h//')) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} + +_py3.3() +{ + COMPREPLY=($(pycompleter3.3 "${COMP_WORDS[@]}" 2>/dev/null | sed 's/.*1034h//')) + if [[ ${COMPREPLY[0]} == '_longopt' ]]; then + COMPREPLY=() + _longopt 2>/dev/null + fi +} +_py3.4() +{ + COMPREPLY=($(pycompleter3.4 "${COMP_WORDS[@]}" 2>/dev/null | sed 's/.*1034h//')) + 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/pythonpy/find_pycompletion.sh b/pythonpy/find_pycompletion.sh new file mode 100755 index 0000000..c60e3ad --- /dev/null +++ b/pythonpy/find_pycompletion.sh @@ -0,0 +1,5 @@ +py -c ' +import os +import pkg_resources +print(os.path.abspath(pkg_resources.resource_filename("pythonpy.completion", "pycompletion.sh"))) +' diff --git a/pythonpy/pycompleter.py b/pythonpy/pycompleter.py new file mode 100755 index 0000000..2e14d1d --- /dev/null +++ b/pythonpy/pycompleter.py @@ -0,0 +1,339 @@ +#!/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 raw_module_name in matches: + if re.match('np(\..*)?$', raw_module_name): + module_name = re.sub('^np', 'numpy', raw_module_name) + elif re.match('pd(\..*)?$', raw_module_name): + module_name = re.sub('^pd', 'pandas', raw_module_name) + else: + module_name = raw_module_name + try: + module = __import__(module_name) + globals()[raw_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/setup.py b/setup.py old mode 100644 new mode 100755 index 82f664a..67d269b --- a/setup.py +++ b/setup.py @@ -1,11 +1,27 @@ -from distutils.core import setup -import os +#!/usr/bin/env python +from setuptools import setup +import sys + +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.11', + 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', 'pythonpy.completion'], + package_data={'pythonpy': ['completion/pycompletion.sh']}, + scripts=['pythonpy/find_pycompletion.sh'], + 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()