Skip to content

Commit 2dddafb

Browse files
committed
site-packages/bin console-scripts support
* check python syntax with ast.parse() for non .py files (maybe blacklist needed) * site-packages/bin scripts completions * avoid the KeyboardInterrupt error in Pythonista 3.4: fix from ywangd#499 * updated scripts matching logics, now able to find console-scripts installed with `pip` in `site-packages/bin`
1 parent 31fc9c7 commit 2dddafb

3 files changed

Lines changed: 83 additions & 17 deletions

File tree

__main__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@
66
import sys
77
import argparse
88

9+
# fix from https://github.com/ywangd/stash/pull/499
10+
if hasattr(sys, "_exit"):
11+
sys.exit = sys._exit
12+
913
module_names = (
1014
"stash",
1115
"system.shcommon",

system/shcommon.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
The Control, Escape and Graphics are taken from pyte (https://github.com/selectel/pyte)
44
"""
55

6+
import ast
67
import os
78
import sys
89
import platform
@@ -70,7 +71,7 @@
7071

7172
# directory for stash extensions
7273
_STASH_EXTENSION_PATH = os.path.abspath(
73-
os.path.join(os.getenv("HOME"), "Documents", "stash_extensions"),
74+
os.path.join(os.path.expanduser("~"), "Documents", "stash_extensions"),
7475
)
7576
# directory for stash bin extensions
7677
_STASH_EXTENSION_BIN_PATH = os.path.join(_STASH_EXTENSION_PATH, "bin")
@@ -181,6 +182,22 @@ def writelines(self, lines):
181182
_OS_ENVIRON = os.environ
182183

183184

185+
def is_true_python_file(filename):
186+
try:
187+
with open(filename, "rb", encoding="utf-8") as fp:
188+
ast.parse(fp.read(), filename)
189+
return True
190+
except SyntaxError:
191+
return False
192+
except Exception:
193+
return False
194+
195+
196+
def has_py_extension(filename):
197+
if filename.endswith(".py"):
198+
return True
199+
200+
184201
def is_binary_file(filename, nbytes=1024):
185202
"""
186203
An approximate way to tell whether a file is binary.

system/shruntime.py

Lines changed: 61 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,12 @@
3737

3838
# noinspection PyProtectedMember
3939
from .shcommon import _STASH_ROOT, _STASH_HISTORY_FILE, _SYS_STDOUT, _SYS_STDERR
40-
from .shcommon import is_binary_file, _STASH_EXTENSION_BIN_PATH
40+
from .shcommon import (
41+
is_binary_file,
42+
is_true_python_file,
43+
has_py_extension,
44+
_STASH_EXTENSION_BIN_PATH,
45+
)
4146
from .shparsers import ShPipeSequence
4247
from .shthreads import (
4348
ShBaseThread,
@@ -48,6 +53,11 @@
4853
)
4954
from .shhistory import ShHistory
5055

56+
_HOME2 = os.path.join(os.path.expanduser("~"), "Documents")
57+
_SITE_PACKAGES = os.path.join(_HOME2, "site-packages")
58+
_SITE_PACKAGES_BIN = os.path.join(_SITE_PACKAGES, "bin")
59+
_SITE_PACKAGES_COMPLETER_MAX_FILES_LIMIT = 100
60+
5161
# Default .stashrc file
5262
_DEFAULT_RC = r"""BIN_PATH=~/Documents/bin:{bin_ext}:$BIN_PATH
5363
SELFUPDATE_TARGET=master
@@ -79,14 +89,16 @@ def __init__(self, stash, parser, expander, no_historyfile=False, debug=False):
7989
self.state = ShState(
8090
environ=dict(
8191
os.environ,
82-
HOME2=os.path.join(os.environ["HOME"], "Documents"),
92+
HOME2=_HOME2,
8393
STASH_ROOT=_STASH_ROOT,
8494
STASH_PY_VERSION=platform.python_version(),
8595
BIN_PATH=os.path.join(_STASH_ROOT, "bin"),
8696
# Must have a placeholder because it is needed before _DEFAULT_RC is loaded
8797
PROMPT=r"[\W]$ ",
8898
PYTHONISTA_ROOT=os.path.dirname(sys.executable),
8999
TMPDIR=os.environ.get("TMPDIR", tempfile.gettempdir()),
100+
# site-packages/bin (console scripts added via pip
101+
SITE_PACKAGES_BIN=_SITE_PACKAGES_BIN,
90102
),
91103
sys_stdin=self.stash.io,
92104
sys_stdout=self.stash.io,
@@ -170,30 +182,46 @@ def write_error_message(self, stream, msg, prefix=None, log=True):
170182
def find_script_file(self, filename):
171183
_, current_state = self.get_current_worker_and_state()
172184

173-
dir_match_found = False
174185
# direct match of the filename, e.g. full path, relative path etc.
175186
for fname in (filename, filename + ".py", filename + ".sh"):
176187
if os.path.exists(fname):
177188
if os.path.isdir(fname):
178-
dir_match_found = True
189+
raise ShIsDirectory("%s: is a directory" % filename)
179190
else:
180191
return fname
181192

182193
# Match for commands in current dir and BIN_PATH
183194
# Effectively, current dir is always the first in BIN_PATH
184195
for path in ["."] + current_state.environ_get("BIN_PATH").split(":"):
185196
path = os.path.abspath(os.path.expanduser(path))
186-
if os.path.exists(path):
187-
for f in os.listdir(path):
188-
if f == filename or f == filename + ".py" or f == filename + ".sh":
189-
if os.path.isdir(f):
190-
dir_match_found = True
191-
else:
192-
return os.path.join(path, f)
193-
if dir_match_found:
194-
raise ShIsDirectory("%s: is a directory" % filename)
195-
else:
196-
raise ShFileNotFound("%s: command not found" % filename)
197+
file_match_found = self._find_matching_executable(filename, path)
198+
if file_match_found:
199+
return file_match_found
200+
201+
# match for commands in site-packages/bin
202+
# is nothing was found in BIN_PATH
203+
# assume all files in site-packages/bin is executable
204+
path = current_state.environ_get("SITE_PACKAGES_BIN")
205+
path = os.path.abspath(path)
206+
file_match_found = self._find_matching_executable(filename, path)
207+
if file_match_found:
208+
return file_match_found
209+
210+
raise ShFileNotFound("%s: command not found" % filename)
211+
212+
@staticmethod
213+
def _find_matching_executable(filename, path):
214+
if not os.path.exists(path):
215+
return None
216+
217+
for f in os.listdir(path):
218+
if f in (filename, filename + ".py", filename + ".sh"):
219+
found = os.path.join(path, f)
220+
if os.path.isdir(found):
221+
raise ShIsDirectory("%s: is a directory" % filename)
222+
return found
223+
224+
return None
197225

198226
def get_all_script_names(self):
199227
"""This function used for completer, whitespaces in names are escaped"""
@@ -207,6 +235,22 @@ def get_all_script_names(self):
207235
f.endswith(".py") or f.endswith(".sh")
208236
):
209237
all_names.append(f.replace(" ", "\\ "))
238+
239+
# find executables in site-packages/bin
240+
path = current_state.environ_get("SITE_PACKAGES_BIN")
241+
if os.path.exists(path):
242+
count_unknown = 0
243+
for f in os.listdir(path):
244+
if not os.path.isdir(f):
245+
if f.endswith(".py") or f.endswith(".sh"):
246+
all_names.append(f.replace(" ", "\\ "))
247+
else:
248+
# assume we check only some range of .undefined
249+
if count_unknown < _SITE_PACKAGES_COMPLETER_MAX_FILES_LIMIT:
250+
if is_true_python_file(f):
251+
all_names.append(f.replace(" ", "\\ "))
252+
count_unknown += 1
253+
210254
return all_names
211255

212256
def run(
@@ -503,7 +547,8 @@ def run_pipe_sequence(
503547
else:
504548
simple_command_args = simple_command.args
505549

506-
if script_file.endswith(".py"):
550+
# check file extension or if syntax match
551+
if has_py_extension(script_file) or is_true_python_file(script_file):
507552
self.exec_py_file(
508553
script_file, simple_command_args, ins, outs, errs
509554
)

0 commit comments

Comments
 (0)