Skip to content

Commit 88ba422

Browse files
committed
Support multi-argument Pythons (like env python)
The changes in this commit add support for things like `#!/usr/bin/env python3` and make errors easier to find. Here are the details: 1. **Support multi-argument Python interpreters** Many people specify a Python interpreter with a line like this: `/usr/bin/env python3`. However, SublimePythonIDE allows only to specify the path to a Python interpreter. This would be less portable: Python 3 might be at `/usr/bin/python3` on one system and `/usr/local/bin/python3` on another. This commit makes it possible. How: This commit changes the python interpreter path (the thing named `python` in `proxy_for()` and `self.python` in `Proxy`) to be a list of strings instead of a single string. This allows for multi-argument Pythons to be specified by any of the Python path providers, including `get_setting("python_interpreter")` (aka, the `python_interpreter` setting can now be a list). This change is backwards-compatible: Python providers that emit a string will have it converted into the correct format (`"/usr/bin/python" -> ["/usr/bin/python"]`). A new function, `normalize_path`, makes normalizing easier. 2. **Auto-detect Python interpreter from shebang line** Perhaps the most common place to add such a Python interpreter specification is in the shebang (`#!`) line. This commit will choose the Python interpreter based on this. /pull/61 tried to do this earlier, but without support for multi-argument Pythons, it was forced to resolve the path to the actual Python. As such, it only worked for absolute paths to Python and `env` and for `env` it had to simulate `env`'s behavior (it "simulated" this by assuming Python is always in the same directory as `env`). This change is backwards-compatible: If the first line of the current file does not start with `#!`, the other Python path providers are tried. 3. **More informative error messages** This commit makes the "Could not detect python" message more informative by indicating which paths were tried. Here's an example: <img width="784" src="https://cloud.githubusercontent.com/assets/1570168/11181148/d40ce048-8c14-11e5-9990-f010174455f5.png">
1 parent 7564033 commit 88ba422

1 file changed

Lines changed: 49 additions & 17 deletions

File tree

sublime_python.py

Lines changed: 49 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -128,10 +128,10 @@ def restart(self):
128128
elif SERVER_DEBUGGING:
129129
# debug mode two
130130
self.port = self.get_free_port()
131-
proc_args = [self.python, SERVER_SCRIPT,
131+
proc_args = self.python + [SERVER_SCRIPT,
132132
str(self.port), " --debug"]
133133
self.proc = subprocess.Popen(
134-
proc_args, cwd=os.path.dirname(self.python),
134+
proc_args, cwd=os.path.dirname(self.python[0]),
135135
stderr=subprocess.PIPE, creationflags=CREATION_FLAGS)
136136
self.queue = Queue()
137137
self.stderr_reader = AsynchronousFileReader(
@@ -144,9 +144,9 @@ def restart(self):
144144
else:
145145
# standard run of the server in end-user mode
146146
self.port = self.get_free_port()
147-
proc_args = [self.python, SERVER_SCRIPT, str(self.port)]
147+
proc_args = self.python + [SERVER_SCRIPT, str(self.port)]
148148
self.proc = subprocess.Popen(
149-
proc_args, cwd=os.path.dirname(self.python),
149+
proc_args, cwd=os.path.dirname(self.python[0]),
150150
creationflags=CREATION_FLAGS)
151151
print("started server on port %i with %s" %
152152
(self.port, self.python))
@@ -300,35 +300,67 @@ def project_venv_python(view):
300300
return python
301301

302302

303+
def shebang_line_python(view):
304+
shebang_line = view.substr(view.line(0))
305+
if shebang_line.startswith('#!'):
306+
return shebang_line[2:].split(None, 1)
307+
308+
309+
def normalize_path(args, make_abs=False):
310+
if not args:
311+
return None
312+
elif type(args) is str:
313+
args = [args]
314+
elif type(args) is not list:
315+
args = list(args)
316+
317+
# args is guaranteed to be a non-empty list at this point
318+
if make_abs:
319+
args[0] = os.path.abspath(os.path.realpath(os.path.expanduser(args[0])))
320+
return args
321+
322+
303323
def proxy_for(view):
304324
'''retrieve an existing proxy for an external Python process.
305-
will automatically create a new proxy if non exists for the
325+
will automatically create a new proxy if none exists for the
306326
requested interpreter'''
307327
proxy = None
308328
with PROXY_LOCK:
309-
python = get_setting("python_interpreter", view, "")
310-
if python == "":
311-
python = project_venv_python(view) or system_python()
312-
else:
313-
python = os.path.abspath(
314-
os.path.realpath(os.path.expanduser(python)))
329+
pythons = (
330+
normalize_path(get_setting("python_interpreter", view, ""), True),
331+
normalize_path(project_venv_python(view)),
332+
normalize_path(shebang_line_python(view)),
333+
normalize_path(system_python()),
334+
)
335+
336+
python = pythons[0] or pythons[1] or pythons[2] or pythons[3]
315337

316-
if not os.path.exists(python):
338+
if not python or not os.path.exists(python[0]):
317339
raise OSError("""
318340
--------------------------------------------------------------------------------------------------------------
319341
Could not detect python, please set the python_interpreter (see README) using an absolute path or make sure a
320-
system python is installed and is reachable on the PATH.
321-
--------------------------------------------------------------------------------------------------------------""")
342+
system python is installed and is reachable on the PATH. Here is the order in which paths are tried:
343+
344+
- By "python_interpreter" Sublime settings: %r
345+
- By auto-detecting venv: %r
346+
- By #! (shebang) line in this file: %r
347+
- By auto-detecting system Python: %r
348+
349+
We use the first non-None value and ensure that the path exists before proceeding.
350+
--------------------------------------------------------------------------------------------------------------"""
351+
% pythons)
322352

323-
if python in PROXIES:
324-
proxy = PROXIES[python]
353+
# Since lists cannot be used as keys, a temporary tuple version of this is created.
354+
python_as_key = tuple(python)
355+
if python_as_key in PROXIES:
356+
proxy = PROXIES[python_as_key]
325357
else:
326358
try:
327359
proxy = Proxy(python)
328360
except OSError:
329361
pass
330362
else:
331-
PROXIES[python] = proxy
363+
PROXIES[python_as_key] = proxy
332364
return proxy
333365

334366

0 commit comments

Comments
 (0)