# The MIT License
#
# Copyright (c) 2009-2011 Andreas Stuehrk
#
# 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.
from __future__ import with_statement
from bpython import line as lineparts
import imp
import os
import sys
import warnings
if sys.version_info[0] == 3 and sys.version_info[1] >= 3:
import importlib.machinery
SUFFIXES = importlib.machinery.all_suffixes()
else:
SUFFIXES = [suffix for suffix, mode, type in imp.get_suffixes()]
try:
from warnings import catch_warnings
except ImportError:
import contextlib
@contextlib.contextmanager
def catch_warnings():
"""Stripped-down version of `warnings.catch_warnings()`
(available in Py >= 2.6)."""
filters = warnings.filters
warnings.filters = list(filters)
try:
yield
finally:
warnings.filters = filters
from bpython._py3compat import py3
# The cached list of all known modules
modules = set()
fully_loaded = False
def module_matches(cw, prefix=''):
"""Modules names to replace cw with"""
full = '%s.%s' % (prefix, cw) if prefix else cw
matches = [name for name in modules
if (name.startswith(full) and
name.find('.', len(full)) == -1)]
if prefix:
return [match[len(prefix)+1:] for match in matches]
else:
return matches
def attr_matches(cw, prefix='', only_modules=False):
"""Attributes to replace name with"""
full = '%s.%s' % (prefix, cw) if prefix else cw
module_name, _, name_after_dot = full.rpartition('.')
if module_name not in sys.modules:
return []
module = sys.modules[module_name]
if only_modules:
matches = [name for name in dir(module)
if name.startswith(name_after_dot) and
'%s.%s' % (module_name, name) in sys.modules]
else:
matches = [name for name in dir(module) if name.startswith(name_after_dot)]
module_part, _, _ = cw.rpartition('.')
if module_part:
return ['%s.%s' % (module_part, m) for m in matches]
return matches
def module_attr_matches(name):
"""Only attributes which are modules to replace name with"""
return attr_matches(name, prefix='', only_modules=True)
def complete(cursor_offset, line):
"""Construct a full list of possibly completions for imports."""
tokens = line.split()
if 'from' not in tokens and 'import' not in tokens:
return None
result = lineparts.current_word(cursor_offset, line)
if result is None:
return None
if lineparts.current_from_import_from(cursor_offset, line) is not None:
if lineparts.current_from_import_import(cursor_offset, line) is not None:
# `from a import ` completion
return (module_matches(lineparts.current_from_import_import(cursor_offset, line)[2],
lineparts.current_from_import_from(cursor_offset, line)[2]) +
attr_matches(lineparts.current_from_import_import(cursor_offset, line)[2],
lineparts.current_from_import_from(cursor_offset, line)[2]))
else:
# `from ` completion
return (module_attr_matches(lineparts.current_from_import_from(cursor_offset, line)[2]) +
module_matches(lineparts.current_from_import_from(cursor_offset, line)[2]))
elif lineparts.current_import(cursor_offset, line):
# `import ` completion
return (module_matches(lineparts.current_import(cursor_offset, line)[2]) +
module_attr_matches(lineparts.current_import(cursor_offset, line)[2]))
else:
return None
def find_modules(path):
"""Find all modules (and packages) for a given directory."""
if not os.path.isdir(path):
# Perhaps a zip file
return
try:
filenames = os.listdir(path)
except EnvironmentError:
filenames = []
for name in filenames:
if not any(name.endswith(suffix) for suffix in SUFFIXES):
# Possibly a package
if '.' in name:
continue
elif os.path.isdir(os.path.join(path, name)):
# Unfortunately, CPython just crashes if there is a directory
# which ends with a python extension, so work around.
continue
for suffix in SUFFIXES:
if name.endswith(suffix):
name = name[:-len(suffix)]
break
if py3 and name == "badsyntax_pep3120":
# Workaround for issue #166
continue
try:
with catch_warnings():
warnings.simplefilter("ignore", ImportWarning)
fo, pathname, _ = imp.find_module(name, [path])
except (ImportError, IOError, SyntaxError):
continue
except UnicodeEncodeError:
# Happens with Python 3 when there is a filename in some
# invalid encoding
continue
else:
if fo is not None:
fo.close()
else:
# Yay, package
for subname in find_modules(pathname):
if subname != '__init__':
yield '%s.%s' % (name, subname)
yield name
def find_all_modules(path=None):
"""Return a list with all modules in `path`, which should be a list of
directory names. If path is not given, sys.path will be used."""
if path is None:
modules.update(sys.builtin_module_names)
path = sys.path
for p in path:
if not p:
p = os.curdir
for module in find_modules(p):
if not py3 and not isinstance(module, unicode):
try:
module = module.decode(sys.getfilesystemencoding())
except UnicodeDecodeError:
# Not importable anyway, ignore it
continue
modules.add(module)
yield
def find_coroutine():
global fully_loaded
if fully_loaded:
return None
try:
find_iterator.next()
except StopIteration:
fully_loaded = True
return True
def reload():
"""Refresh the list of known modules."""
modules.clear()
for _ in find_all_modules():
pass
find_iterator = find_all_modules()