Files
sublime-text-3/Packages/SublimeLinter/lint/util.py

1417 lines
41 KiB
Python

# coding=utf8
#
# util.py
# Part of SublimeLinter3, a code checking framework for Sublime Text 3
#
# Written by Ryan Hileman and Aparajita Fishman
#
# Project: https://github.com/SublimeLinter/SublimeLinter3
# License: MIT
#
"""This module provides general utility methods."""
from functools import lru_cache
from glob import glob
import json
from numbers import Number
import os
import re
import shutil
from string import Template
import sublime
import subprocess
import sys
import tempfile
from xml.etree import ElementTree
#
# Public constants
#
STREAM_STDOUT = 1
STREAM_STDERR = 2
STREAM_BOTH = STREAM_STDOUT + STREAM_STDERR
PYTHON_CMD_RE = re.compile(r'(?P<script>[^@]+)?@python(?P<version>[\d\.]+)?')
VERSION_RE = re.compile(r'(?P<major>\d+)(?:\.(?P<minor>\d+))?')
INLINE_SETTINGS_RE = re.compile(r'(?i).*?\[sublimelinter[ ]+(?P<settings>[^\]]+)\]')
INLINE_SETTING_RE = re.compile(r'(?P<key>[@\w][\w\-]*)\s*:\s*(?P<value>[^\s]+)')
MENU_INDENT_RE = re.compile(r'^(\s+)\$menus', re.MULTILINE)
MARK_COLOR_RE = (
r'(\s*<string>sublimelinter\.{}</string>\s*\r?\n'
r'\s*<key>settings</key>\s*\r?\n'
r'\s*<dict>\s*\r?\n'
r'\s*<key>foreground</key>\s*\r?\n'
r'\s*<string>)#.+?(</string>\s*\r?\n)'
)
ANSI_COLOR_RE = re.compile(r'\033\[[0-9;]*m')
# settings utils
def merge_user_settings(settings):
"""Return the default linter settings merged with the user's settings."""
default = settings.get('default', {})
user = settings.get('user', {})
if user:
linters = default.pop('linters', {})
user_linters = user.get('linters', {})
for name, data in user_linters.items():
if name in linters:
linters[name].update(data)
else:
linters[name] = data
default['linters'] = linters
user.pop('linters', None)
default.update(user)
return default
def inline_settings(comment_re, code, prefix=None, alt_prefix=None):
r"""
Return a dict of inline settings within the first two lines of code.
This method looks for settings in the form [SublimeLinter <name>:<value>]
on the first or second line of code if the lines match comment_re.
comment_re should be a compiled regex object whose pattern is unanchored (no ^)
and matches everything through the comment prefix, including leading whitespace.
For example, to specify JavaScript comments, you would use the pattern:
r'\s*/[/*]'
If prefix or alt_prefix is a non-empty string, setting names must begin with
the given prefix or alt_prefix to be considered as a setting.
A dict of matching name/value pairs is returned.
"""
if prefix:
prefix = prefix.lower() + '-'
if alt_prefix:
alt_prefix = alt_prefix.lower() + '-'
settings = {}
pos = -1
for i in range(0, 2):
# Does this line start with a comment marker?
match = comment_re.match(code, pos + 1)
if match:
# If it's a comment, does it have inline settings?
match = INLINE_SETTINGS_RE.match(code, pos + len(match.group()))
if match:
# We have inline settings, stop looking
break
# Find the next line
pos = code.find('\n', )
if pos == -1:
# If no more lines, stop looking
break
if match:
for key, value in INLINE_SETTING_RE.findall(match.group('settings')):
if prefix and key[0] != '@':
if key.startswith(prefix):
key = key[len(prefix):]
elif alt_prefix and key.startswith(alt_prefix):
key = key[len(alt_prefix):]
else:
continue
settings[key] = value
return settings
def get_view_rc_settings(view, limit=None):
"""Return the rc settings, starting at the parent directory of the given view."""
filename = view.file_name()
if filename:
return get_rc_settings(os.path.dirname(filename))
else:
return None
def get_rc_settings(start_dir, limit=None):
"""
Search for a file named .sublimelinterrc starting in start_dir.
From start_dir it ascends towards the root directory for a maximum
of limit directories (including start_dir). If the file is found,
it is read as JSON and the resulting object is returned. If the file
is not found, None is returned.
"""
if not start_dir:
return
path = find_file(start_dir, '.sublimelinterrc', limit=limit)
if path:
try:
with open(path, encoding='utf8') as f:
rc_settings = json.loads(f.read())
return rc_settings
except (OSError, ValueError) as ex:
from . import persist
persist.printf('ERROR: could not load \'{}\': {}'.format(path, str(ex)))
else:
return None
def generate_color_scheme(from_reload=True):
"""Asynchronously call generate_color_scheme_async."""
# If this was called from a reload of prefs, turn off the prefs observer,
# otherwise we'll end up back here when ST updates the prefs with the new color.
if from_reload:
from . import persist
def prefs_reloaded():
persist.settings.observe_prefs()
persist.settings.observe_prefs(observer=prefs_reloaded)
# ST crashes unless this is run async
sublime.set_timeout_async(generate_color_scheme_async, 0)
def generate_color_scheme_async():
"""
Generate a modified copy of the current color scheme that contains SublimeLinter color entries.
from_reload is True if this is called from the change callback for user settings.
The current color scheme is checked for SublimeLinter color entries. If any are missing,
the scheme is copied, the entries are added, and the color scheme is rewritten to Packages/User.
"""
prefs = sublime.load_settings('Preferences.sublime-settings')
scheme = prefs.get('color_scheme')
if scheme is None:
return
scheme_text = sublime.load_resource(scheme)
# Ensure that all SublimeLinter colors are in the scheme
scopes = {
'mark.warning': False,
'mark.error': False,
'gutter-mark': False
}
for scope in scopes:
if re.search(MARK_COLOR_RE.format(re.escape(scope)), scheme_text):
scopes[scope] = True
if False not in scopes.values():
return
# Append style dicts with our styles to the style array
plist = ElementTree.XML(scheme_text)
styles = plist.find('./dict/array')
from . import persist
for style in COLOR_SCHEME_STYLES:
color = persist.settings.get('{}_color'.format(style), DEFAULT_MARK_COLORS[style]).lstrip('#')
styles.append(ElementTree.XML(COLOR_SCHEME_STYLES[style].format(color)))
# Write the amended color scheme to Packages/User
original_name = os.path.splitext(os.path.basename(scheme))[0]
name = original_name + ' (SL)'
scheme_path = os.path.join(sublime.packages_path(), 'User', name + '.tmTheme')
with open(scheme_path, 'w', encoding='utf8') as f:
f.write(COLOR_SCHEME_PREAMBLE)
f.write(ElementTree.tostring(plist, encoding='unicode'))
# Set the amended color scheme to the current color scheme
path = os.path.join('User', os.path.basename(scheme_path))
prefs.set('color_scheme', packages_relative_path(path))
sublime.save_settings('Preferences.sublime-settings')
def change_mark_colors(error_color, warning_color):
"""Change SublimeLinter error/warning colors in user color schemes."""
error_color = error_color.lstrip('#')
warning_color = warning_color.lstrip('#')
path = os.path.join(sublime.packages_path(), 'User', '*.tmTheme')
themes = glob(path)
for theme in themes:
with open(theme, encoding='utf8') as f:
text = f.read()
if re.search(MARK_COLOR_RE.format(r'mark\.error'), text):
text = re.sub(MARK_COLOR_RE.format(r'mark\.error'), r'\1#{}\2'.format(error_color), text)
text = re.sub(MARK_COLOR_RE.format(r'mark\.warning'), r'\1#{}\2'.format(warning_color), text)
with open(theme, encoding='utf8', mode='w') as f:
f.write(text)
def install_syntaxes():
"""Asynchronously call install_syntaxes_async."""
sublime.set_timeout_async(install_syntaxes_async, 0)
def install_syntaxes_async():
"""
Install fixed syntax packages.
Unfortunately the scope definitions in some syntax definitions
(HTML at the moment) incorrectly define embedded scopes, which leads
to spurious lint errors.
This method copies all of the syntax packages in fixed_syntaxes to Packages
so that they override the built in syntax package.
"""
from . import persist
plugin_dir = os.path.dirname(os.path.dirname(__file__))
syntaxes_dir = os.path.join(plugin_dir, 'fixed-syntaxes')
for syntax in os.listdir(syntaxes_dir):
# See if our version of the syntax already exists in Packages
src_dir = os.path.join(syntaxes_dir, syntax)
version_file = os.path.join(src_dir, 'sublimelinter.version')
if not os.path.isdir(src_dir) or not os.path.isfile(version_file):
continue
with open(version_file, encoding='utf8') as f:
my_version = int(f.read().strip())
dest_dir = os.path.join(sublime.packages_path(), syntax)
version_file = os.path.join(dest_dir, 'sublimelinter.version')
if os.path.isdir(dest_dir):
if os.path.isfile(version_file):
with open(version_file, encoding='utf8') as f:
try:
other_version = int(f.read().strip())
except ValueError:
other_version = 0
persist.debug('found existing {} syntax, version {}'.format(syntax, other_version))
copy = my_version > other_version
else:
copy = sublime.ok_cancel_dialog(
'An existing {} syntax definition exists, '.format(syntax) +
'and SublimeLinter wants to overwrite it with its own version. ' +
'Is that okay?')
else:
copy = True
if copy:
copy_syntax(syntax, src_dir, my_version, dest_dir)
update_syntax_map()
def copy_syntax(syntax, src_dir, version, dest_dir):
"""Copy a customized syntax and related files to Packages."""
from . import persist
try:
cached = os.path.join(sublime.cache_path(), syntax)
if os.path.isdir(cached):
shutil.rmtree(cached)
if not os.path.exists(dest_dir):
os.mkdir(dest_dir)
for filename in os.listdir(src_dir):
shutil.copy2(os.path.join(src_dir, filename), dest_dir)
persist.printf('copied {} syntax version {}'.format(syntax, version))
except OSError as ex:
persist.printf(
'ERROR: could not copy {} syntax package: {}'
.format(syntax, str(ex))
)
def update_syntax_map():
"""Update the user syntax_map setting with any missing entries from the defaults."""
from . import persist
syntax_map = {}
syntax_map.update(persist.settings.get('syntax_map', {}))
default_syntax_map = persist.settings.plugin_settings.get('default', {}).get('syntax_map', {})
modified = False
for key, value in default_syntax_map.items():
if key not in syntax_map:
syntax_map[key] = value
modified = True
persist.debug('added syntax mapping: \'{}\' => \'{}\''.format(key, value))
if modified:
persist.settings.set('syntax_map', syntax_map)
persist.settings.save()
# menu utils
def indent_lines(text, indent):
"""Return all of the lines in text indented by prefixing with indent."""
return re.sub(r'^', indent, text, flags=re.MULTILINE)[len(indent):]
def generate_menus(**kwargs):
"""Asynchronously call generate_menus_async."""
sublime.set_timeout_async(generate_menus_async, 0)
def generate_menus_async():
"""
Generate context and Tools SublimeLinter menus.
This is done dynamically so that we can have a submenu with all
of the available gutter themes.
"""
commands = []
for chooser in CHOOSERS:
commands.append({
'caption': chooser,
'menus': build_submenu(chooser),
'toggleItems': ''
})
menus = []
indent = MENU_INDENT_RE.search(CHOOSER_MENU).group(1)
for cmd in commands:
# Indent the commands to where they want to be in the template.
# The first line doesn't need to be indented, remove the extra indent.
cmd['menus'] = indent_lines(cmd['menus'], indent)
if cmd['caption'] in TOGGLE_ITEMS:
cmd['toggleItems'] = TOGGLE_ITEMS[cmd['caption']]
cmd['toggleItems'] = indent_lines(cmd['toggleItems'], indent)
menus.append(Template(CHOOSER_MENU).safe_substitute(cmd))
menus = ',\n'.join(menus)
text = generate_menu('Context', menus)
generate_menu('Main', text)
def generate_menu(name, menu_text):
"""Generate and return a sublime-menu from a template."""
from . import persist
plugin_dir = os.path.join(sublime.packages_path(), persist.PLUGIN_DIRECTORY)
path = os.path.join(plugin_dir, '{}.sublime-menu.template'.format(name))
with open(path, encoding='utf8') as f:
template = f.read()
# Get the indent for the menus within the template,
# indent the chooser menus except for the first line.
indent = MENU_INDENT_RE.search(template).group(1)
menu_text = indent_lines(menu_text, indent)
text = Template(template).safe_substitute({'menus': menu_text})
path = os.path.join(plugin_dir, '{}.sublime-menu'.format(name))
with open(path, mode='w', encoding='utf8') as f:
f.write(text)
return text
def build_submenu(caption):
"""Generate and return a submenu with commands to select a lint mode, mark style, or gutter theme."""
setting = caption.lower()
if setting == 'lint mode':
from . import persist
names = [mode[0].capitalize() for mode in persist.LINT_MODES]
elif setting == 'mark style':
from . import highlight
names = highlight.mark_style_names()
commands = []
for name in names:
commands.append(CHOOSER_COMMAND.format(setting.replace(' ', '_'), name))
return ',\n'.join(commands)
# file/directory/environment utils
def climb(start_dir, limit=None):
"""
Generate directories, starting from start_dir.
If limit is None or <= 0, stop at the root directory.
Otherwise return a maximum of limit directories.
"""
right = True
while right and (limit is None or limit > 0):
yield start_dir
start_dir, right = os.path.split(start_dir)
if limit is not None:
limit -= 1
def find_file(start_dir, name, parent=False, limit=None, aux_dirs=[]):
"""
Find the given file by searching up the file hierarchy from start_dir.
If the file is found and parent is False, returns the path to the file.
If parent is True the path to the file's parent directory is returned.
If limit is None or <= 0, the search will continue up to the root directory.
Otherwise a maximum of limit directories will be checked.
If aux_dirs is not empty and the file hierarchy search failed,
those directories are also checked.
"""
for d in climb(start_dir, limit=limit):
target = os.path.join(d, name)
if os.path.exists(target):
if parent:
return d
return target
for d in aux_dirs:
d = os.path.expanduser(d)
target = os.path.join(d, name)
if os.path.exists(target):
if parent:
return d
return target
def run_shell_cmd(cmd):
"""Run a shell command and return stdout."""
proc = popen(cmd, env=os.environ)
from . import persist
try:
timeout = persist.settings.get('shell_timeout', 10)
out, err = proc.communicate(timeout=timeout)
except subprocess.TimeoutExpired:
proc.kill()
out = b''
persist.printf('shell timed out after {} seconds, executing {}'.format(timeout, cmd))
return out
def extract_path(cmd, delim=':'):
"""Return the user's PATH as a colon-delimited list."""
from . import persist
persist.debug('user shell:', cmd[0])
out = run_shell_cmd(cmd).decode()
path = out.split('__SUBL_PATH__', 2)
if len(path) > 1:
path = path[1]
return ':'.join(path.strip().split(delim))
else:
persist.printf('Could not parse shell PATH output:\n' + (out if out else '<empty>'))
sublime.error_message(
'SublimeLinter could not determine your shell PATH. '
'It is unlikely that any linters will work. '
'\n\n'
'Please see the troubleshooting guide for info on how to debug PATH problems.')
return ''
def get_shell_path(env):
"""
Return the user's shell PATH using shell --login.
This method is only used on Posix systems.
"""
if 'SHELL' in env:
shell_path = env['SHELL']
shell = os.path.basename(shell_path)
# We have to delimit the PATH output with markers because
# text might be output during shell startup.
if shell in ('bash', 'zsh'):
return extract_path(
(shell_path, '-l', '-c', 'echo "__SUBL_PATH__${PATH}__SUBL_PATH__"')
)
elif shell == 'fish':
return extract_path(
(shell_path, '-l', '-c', 'echo "__SUBL_PATH__"; for p in $PATH; echo $p; end; echo "__SUBL_PATH__"'),
'\n'
)
else:
from . import persist
persist.printf('Using an unsupported shell:', shell)
# guess PATH if we haven't returned yet
split = env['PATH'].split(':')
p = env['PATH']
for path in (
'/usr/bin', '/usr/local/bin',
'/usr/local/php/bin', '/usr/local/php5/bin'
):
if not path in split:
p += (':' + path)
return p
@lru_cache(maxsize=None)
def get_environment_variable(name):
"""Return the value of the given environment variable, or None if not found."""
if os.name == 'posix':
value = None
if 'SHELL' in os.environ:
shell_path = os.environ['SHELL']
# We have to delimit the output with markers because
# text might be output during shell startup.
out = run_shell_cmd((shell_path, '-l', '-c', 'echo "__SUBL_VAR__${{{}}}__SUBL_VAR__"'.format(name))).strip()
if out:
value = out.decode().split('__SUBL_VAR__', 2)[1].strip() or None
else:
value = os.environ.get(name, None)
from . import persist
persist.debug('ENV[\'{}\'] = \'{}\''.format(name, value))
return value
def get_path_components(path):
"""Split a file path into its components and return the list of components."""
components = []
while path:
head, tail = os.path.split(path)
if tail:
components.insert(0, tail)
if head:
if head == os.path.sep or head == os.path.altsep:
components.insert(0, head)
break
path = head
else:
break
return components
def packages_relative_path(path, prefix_packages=True):
"""
Return a Packages-relative version of path with '/' as the path separator.
Sublime Text wants Packages-relative paths used in settings and in the plugin API
to use '/' as the path separator on all platforms. This method converts platform
path separators to '/'. If insert_packages = True, 'Packages' is prefixed to the
converted path.
"""
components = get_path_components(path)
if prefix_packages and components and components[0] != 'Packages':
components.insert(0, 'Packages')
return '/'.join(components)
@lru_cache(maxsize=None)
def create_environment():
"""
Return a dict with os.environ augmented with a better PATH.
On Posix systems, the user's shell PATH is added to PATH.
Platforms paths are then added to PATH by getting the
"paths" user settings for the current platform. If "paths"
has a "*" item, it is added to PATH on all platforms.
"""
from . import persist
env = {}
env.update(os.environ)
if os.name == 'posix':
env['PATH'] = get_shell_path(os.environ)
paths = persist.settings.get('paths', {})
if sublime.platform() in paths:
paths = convert_type(paths[sublime.platform()], [])
else:
paths = []
if paths:
env['PATH'] = os.pathsep.join(paths) + os.pathsep + env['PATH']
from . import persist
if persist.debug_mode():
if os.name == 'posix':
if 'SHELL' in env:
shell = 'using ' + env['SHELL']
else:
shell = 'using standard paths'
else:
shell = 'from system'
if env['PATH']:
persist.printf('computed PATH {}:\n{}\n'.format(shell, env['PATH'].replace(os.pathsep, '\n')))
# Many linters use stdin, and we convert text to utf-8
# before sending to stdin, so we have to make sure stdin
# in the target executable is looking for utf-8.
env['PYTHONIOENCODING'] = 'utf8'
return env
def can_exec(path):
"""Return whether the given path is a file and is executable."""
return os.path.isfile(path) and os.access(path, os.X_OK)
@lru_cache(maxsize=None)
def which(cmd, module=None):
"""
Return the full path to the given command, or None if not found.
If cmd is in the form [script]@python[version], find_python is
called to locate the appropriate version of python. The result
is a tuple of the full python path and the full path to the script
(or None if there is no script).
"""
match = PYTHON_CMD_RE.match(cmd)
if match:
args = match.groupdict()
args['module'] = module
return find_python(**args)[0:2]
else:
return find_executable(cmd)
def extract_major_minor_version(version):
"""Extract and return major and minor versions from a string version."""
match = VERSION_RE.match(version)
if match:
return {key: int(value) if value is not None else None for key, value in match.groupdict().items()}
else:
return {'major': None, 'minor': None}
@lru_cache(maxsize=None)
def get_python_version(path):
"""Return a dict with the major/minor version of the python at path."""
try:
output = communicate((path, '-V'), '', output_stream=STREAM_STDERR)
# 'python -V' returns 'Python <version>', extract the version number
return extract_major_minor_version(output.split(' ')[1])
except Exception as ex:
from . import persist
persist.printf(
'ERROR: an error occurred retrieving the version for {}: {}'
.format(path, str(ex)))
return {'major': None, 'minor': None}
@lru_cache(maxsize=None)
def find_python(version=None, script=None, module=None):
"""
Return the path to and version of python and an optional related script.
If not None, version should be a string/numeric version of python to locate, e.g.
'3' or '3.3'. Only major/minor versions are examined. This method then does
its best to locate a version of python that satisfies the requested version.
If module is not None, Sublime Text's python version is tested against the
requested version.
If version is None, the path to the default system python is used, unless
module is not None, in which case '<builtin>' is returned.
If not None, script should be the name of a python script that is typically
installed with easy_install or pip, e.g. 'pep8' or 'pyflakes'.
A tuple of the python path, script path, major version, minor version is returned.
"""
from . import persist
persist.debug(
'find_python(version={!r}, script={!r}, module={!r})'
.format(version, script, module)
)
path = None
script_path = None
requested_version = {'major': None, 'minor': None}
if module is None:
available_version = {'major': None, 'minor': None}
else:
available_version = {
'major': sys.version_info.major,
'minor': sys.version_info.minor
}
if version is None:
# If no specific version is requested and we have a module,
# assume the linter will run using ST's python.
if module is not None:
result = ('<builtin>', script, available_version['major'], available_version['minor'])
persist.debug('find_python: <=', repr(result))
return result
# No version was specified, get the default python
path = find_executable('python')
persist.debug('find_python: default python =', path)
else:
version = str(version)
requested_version = extract_major_minor_version(version)
persist.debug('find_python: requested version =', repr(requested_version))
# If there is no module, we will use a system python.
# If there is a module, a specific version was requested,
# and the builtin version does not fulfill the request,
# use the system python.
if module is None:
need_system_python = True
else:
persist.debug('find_python: available version =', repr(available_version))
need_system_python = not version_fulfills_request(available_version, requested_version)
path = '<builtin>'
if need_system_python:
if sublime.platform() in ('osx', 'linux'):
path = find_posix_python(version)
else:
path = find_windows_python(version)
persist.debug('find_python: system python =', path)
if path and path != '<builtin>':
available_version = get_python_version(path)
persist.debug('find_python: available version =', repr(available_version))
if version_fulfills_request(available_version, requested_version):
if script:
script_path = find_python_script(path, script)
persist.debug('find_python: {!r} path = {}'.format(script, script_path))
if script_path is None:
path = None
else:
path = script_path = None
result = (path, script_path, available_version['major'], available_version['minor'])
persist.debug('find_python: <=', repr(result))
return result
def version_fulfills_request(available_version, requested_version):
"""
Return whether available_version fulfills requested_version.
Both are dicts with 'major' and 'minor' items.
"""
# No requested major version is fulfilled by anything
if requested_version['major'] is None:
return True
# If major version is requested, that at least must match
if requested_version['major'] != available_version['major']:
return False
# Major version matches, if no requested minor version it's a match
if requested_version['minor'] is None:
return True
# If a minor version is requested, the available minor version must be >=
return (
available_version['minor'] is not None and
available_version['minor'] >= requested_version['minor']
)
@lru_cache(maxsize=None)
def find_posix_python(version):
"""Find the nearest version of python and return its path."""
from . import persist
if version:
# Try the exact requested version first
path = find_executable('python' + version)
persist.debug('find_posix_python: python{} => {}'.format(version, path))
# If that fails, try the major version
if not path:
path = find_executable('python' + version[0])
persist.debug('find_posix_python: python{} => {}'.format(version[0], path))
# If the major version failed, see if the default is available
if not path:
path = find_executable('python')
persist.debug('find_posix_python: python =>', path)
else:
path = find_executable('python')
persist.debug('find_posix_python: python =>', path)
return path
@lru_cache(maxsize=None)
def find_windows_python(version):
"""Find the nearest version of python and return its path."""
if version:
# On Windows, there may be no separately named python/python3 binaries,
# so it seems the only reliable way to check for a given version is to
# check the root drive for 'Python*' directories, and try to match the
# version based on the directory names. The 'Python*' directories end
# with the <major><minor> version number, so for matching with the version
# passed in, strip any decimal points.
stripped_version = version.replace('.', '')
prefix = os.path.abspath('\\Python')
prefix_len = len(prefix)
dirs = glob(prefix + '*')
from . import persist
# Try the exact version first, then the major version
for version in (stripped_version, stripped_version[0]):
for python_dir in dirs:
path = os.path.join(python_dir, 'python.exe')
python_version = python_dir[prefix_len:]
persist.debug('find_windows_python: matching =>', path)
# Try the exact version first, then the major version
if python_version.startswith(version) and can_exec(path):
persist.debug('find_windows_python: <=', path)
return path
# No version or couldn't find a version match, try the default python
path = find_executable('python')
persist.debug('find_windows_python: <=', path)
return path
@lru_cache(maxsize=None)
def find_python_script(python_path, script):
"""Return the path to the given script, or None if not found."""
if sublime.platform() in ('osx', 'linux'):
return which(script)
else:
# On Windows, scripts are .py files in <python directory>/Scripts
script_path = os.path.join(os.path.dirname(python_path), 'Scripts', script + '-script.py')
if os.path.exists(script_path):
return script_path
else:
return None
@lru_cache(maxsize=None)
def get_python_paths():
"""
Return sys.path for the system version of python 3.
If python 3 cannot be found on the system, [] is returned.
"""
from . import persist
python_path = which('@python3')[0]
if python_path:
code = r'import sys;print("\n".join(sys.path).strip())'
out = communicate(python_path, code)
paths = out.splitlines()
if persist.debug_mode():
persist.printf('sys.path for {}:\n{}\n'.format(python_path, '\n'.join(paths)))
else:
persist.debug('no python 3 available to augment sys.path')
paths = []
return paths
@lru_cache(maxsize=None)
def find_executable(executable):
"""
Return the path to the given executable, or None if not found.
create_environment is used to augment PATH before searching
for the executable.
"""
env = create_environment()
for base in env.get('PATH', '').split(os.pathsep):
path = os.path.join(os.path.expanduser(base), executable)
# On Windows, if path does not have an extension, try .exe, .cmd, .bat
if sublime.platform() == 'windows' and not os.path.splitext(path)[1]:
for extension in ('.exe', '.cmd', '.bat'):
path_ext = path + extension
if can_exec(path_ext):
return path_ext
elif can_exec(path):
return path
return None
def touch(path):
"""Perform the equivalent of touch on Posix systems."""
with open(path, 'a'):
os.utime(path, None)
def open_directory(path):
"""Open the directory at the given path in a new window."""
cmd = (get_subl_executable_path(), path)
subprocess.Popen(cmd, cwd=path)
def get_subl_executable_path():
"""Return the path to the subl command line binary."""
executable_path = sublime.executable_path()
if sublime.platform() == 'osx':
suffix = '.app/'
app_path = executable_path[:executable_path.rfind(suffix) + len(suffix)]
executable_path = app_path + 'Contents/SharedSupport/bin/subl'
return executable_path
# popen utils
def combine_output(out, sep=''):
"""Return stdout and/or stderr combined into a string, stripped of ANSI colors."""
output = sep.join((
(out[0].decode('utf8') or '') if out[0] else '',
(out[1].decode('utf8') or '') if out[1] else '',
))
return ANSI_COLOR_RE.sub('', output)
def communicate(cmd, code='', output_stream=STREAM_STDOUT, env=None):
"""
Return the result of sending code via stdin to an executable.
The result is a string which comes from stdout, stderr or the
combining of the two, depending on the value of output_stream.
If env is not None, it is merged with the result of create_environment.
"""
out = popen(cmd, output_stream=output_stream, extra_env=env)
if out is not None:
code = code.encode('utf8')
out = out.communicate(code)
return combine_output(out)
else:
return ''
def tmpfile(cmd, code, suffix='', output_stream=STREAM_STDOUT, env=None):
"""
Return the result of running an executable against a temporary file containing code.
It is assumed that the executable launched by cmd can take one more argument
which is a filename to process.
The result is a string combination of stdout and stderr.
If env is not None, it is merged with the result of create_environment.
"""
f = None
try:
with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
if isinstance(code, str):
code = code.encode('utf-8')
f.write(code)
f.flush()
cmd = list(cmd)
if '@' in cmd:
cmd[cmd.index('@')] = f.name
else:
cmd.append(f.name)
out = popen(cmd, output_stream=output_stream, extra_env=env)
if out:
out = out.communicate()
return combine_output(out)
else:
return ''
finally:
if f:
os.remove(f.name)
def tmpdir(cmd, files, filename, code, output_stream=STREAM_STDOUT, env=None):
"""
Run an executable against a temporary file containing code.
It is assumed that the executable launched by cmd can take one more argument
which is a filename to process.
Returns a string combination of stdout and stderr.
If env is not None, it is merged with the result of create_environment.
"""
filename = os.path.basename(filename)
d = tempfile.mkdtemp()
out = None
try:
for f in files:
try:
os.makedirs(os.path.join(d, os.path.dirname(f)))
except OSError:
pass
target = os.path.join(d, f)
if os.path.basename(target) == filename:
# source file hasn't been saved since change, so update it from our live buffer
f = open(target, 'wb')
if isinstance(code, str):
code = code.encode('utf8')
f.write(code)
f.close()
else:
shutil.copyfile(f, target)
os.chdir(d)
out = popen(cmd, output_stream=output_stream, extra_env=env)
if out:
out = out.communicate()
out = combine_output(out, sep='\n')
# filter results from build to just this filename
# no guarantee all syntaxes are as nice about this as Go
# may need to improve later or just defer to communicate()
out = '\n'.join([
line for line in out.split('\n') if filename in line.split(':', 1)[0]
])
else:
out = ''
finally:
shutil.rmtree(d, True)
return out or ''
def popen(cmd, output_stream=STREAM_BOTH, env=None, extra_env=None):
"""Open a pipe to an external process and return a Popen object."""
info = None
if os.name == 'nt':
info = subprocess.STARTUPINFO()
info.dwFlags |= subprocess.STARTF_USESHOWWINDOW
info.wShowWindow = subprocess.SW_HIDE
if output_stream == STREAM_BOTH:
stdout = stderr = subprocess.PIPE
elif output_stream == STREAM_STDOUT:
stdout = subprocess.PIPE
stderr = subprocess.DEVNULL
else: # STREAM_STDERR
stdout = subprocess.DEVNULL
stderr = subprocess.PIPE
if env is None:
env = create_environment()
if extra_env is not None:
env.update(extra_env)
try:
return subprocess.Popen(
cmd, stdin=subprocess.PIPE,
stdout=stdout, stderr=stderr,
startupinfo=info, env=env)
except Exception as err:
from . import persist
persist.printf('ERROR: could not launch', repr(cmd))
persist.printf('reason:', str(err))
persist.printf('PATH:', env.get('PATH', ''))
# view utils
def apply_to_all_views(callback):
"""Apply callback to all views in all windows."""
for window in sublime.windows():
for view in window.views():
callback(view)
# misc utils
def clear_caches():
"""Clear the caches of all methods in this module that use an lru_cache."""
create_environment.cache_clear()
which.cache_clear()
find_python.cache_clear()
get_python_paths.cache_clear()
find_executable.cache_clear()
def convert_type(value, type_value, sep=None, default=None):
"""
Convert value to the type of type_value.
If the value cannot be converted to the desired type, default is returned.
If sep is not None, strings are split by sep (plus surrounding whitespace)
to make lists/tuples, and tuples/lists are joined by sep to make strings.
"""
if type_value is None or isinstance(value, type(type_value)):
return value
if isinstance(value, str):
if isinstance(type_value, (tuple, list)):
if sep is None:
return [value]
else:
if value:
return re.split(r'\s*{}\s*'.format(sep), value)
else:
return []
elif isinstance(type_value, Number):
return float(value)
else:
return default
if isinstance(value, Number):
if isinstance(type_value, str):
return str(value)
elif isinstance(type_value, (tuple, list)):
return [value]
else:
return default
if isinstance(value, (tuple, list)):
if isinstance(type_value, str):
return sep.join(value)
else:
return list(value)
return default
def get_user_fullname():
"""Return the user's full name (or at least first name)."""
if sublime.platform() in ('osx', 'linux'):
import pwd
return pwd.getpwuid(os.getuid()).pw_gecos
else:
return os.environ.get('USERNAME', 'Me')
def center_region_in_view(region, view):
"""
Center the given region in view.
There is a bug in ST3 that prevents a selection change
from being drawn when a quick panel is open unless the
viewport moves. So we get the current viewport position,
move it down 1.0, center the region, see if the viewport
moved, and if not, move it up 1.0 and center again.
"""
x1, y1 = view.viewport_position()
view.set_viewport_position((x1, y1 + 1.0))
view.show_at_center(region)
x2, y2 = view.viewport_position()
if y2 == y1:
view.set_viewport_position((x1, y1 - 1.0))
view.show_at_center(region)
# color-related constants
DEFAULT_MARK_COLORS = {'warning': 'EDBA00', 'error': 'DA2000', 'gutter': 'FFFFFF'}
COLOR_SCHEME_PREAMBLE = '''<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
'''
COLOR_SCHEME_STYLES = {
'warning': '''
<dict>
<key>name</key>
<string>SublimeLinter Warning</string>
<key>scope</key>
<string>sublimelinter.mark.warning</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#{}</string>
</dict>
</dict>
''',
'error': '''
<dict>
<key>name</key>
<string>SublimeLinter Error</string>
<key>scope</key>
<string>sublimelinter.mark.error</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#{}</string>
</dict>
</dict>
''',
'gutter': '''
<dict>
<key>name</key>
<string>SublimeLinter Gutter Mark</string>
<key>scope</key>
<string>sublimelinter.gutter-mark</string>
<key>settings</key>
<dict>
<key>foreground</key>
<string>#FFFFFF</string>
</dict>
</dict>
'''
}
# menu command constants
CHOOSERS = (
'Lint Mode',
'Mark Style'
)
CHOOSER_MENU = '''{
"caption": "$caption",
"children":
[
$menus,
$toggleItems
]
}'''
CHOOSER_COMMAND = '''{{
"command": "sublimelinter_choose_{}", "args": {{"value": "{}"}}
}}'''
TOGGLE_ITEMS = {
'Mark Style': '''
{
"caption": "-"
},
{
"caption": "No Column Highlights Line",
"command": "sublimelinter_toggle_setting", "args":
{
"setting": "no_column_highlights_line",
"checked": true
}
}'''
}