1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 06:20:05 +08:00
SpaceVim/bundle/python-imports.vim/pythonx/python_imports.py

151 lines
4.8 KiB
Python
Vendored

"""
" HACK to make it possible to reload this by sourcing it in vim
py3 import importlib; importlib.reload(python_imports)
finish
"""
import re
import os
from typing import NamedTuple, Iterable, Optional
import vim
DEFAULT_CONFIG_FILE = os.path.expanduser('~/.vim/python-imports.cfg')
WS = r'\s+'
MAYBE_WS = r'\s*'
DOTTEDNAME = r'[a-zA-Z_.][a-zA-Z_0-9.]*'
NAME = r'[a-zA-Z_][a-zA-Z_0-9]*'
NAMEWITHALIAS = NAME + r'(?:' + WS + 'as' + WS + NAME + ')?'
NAMELIST = NAMEWITHALIAS + r'(?:' + MAYBE_WS + r',' + MAYBE_WS + NAMEWITHALIAS + r')*'
# technically this allows unbalanced parens like 'import (aaaa' or 'import a, b)';
# I don't care
NAMES = r'[(]?' + MAYBE_WS + '(?P<names>' + NAMELIST + r')' + MAYBE_WS + r',?' + MAYBE_WS + r'[)]?'
MODNAME = r'(?P<modname>' + DOTTEDNAME + r')'
# technically 'import (a, b, c)' should not be allowed; I don't care
IMPORT_RX = re.compile(r'^import' + WS + NAMES + r'$')
# technically 'from foo import(a, b, c)' should be allowed without whitespace
# before the (, but I don't care
FROM_IMPORT_RX = re.compile(r'^from' + WS + MODNAME + WS + r'import' + WS + NAMES + r'$')
class Error(Exception):
"""Syntax error in the config file."""
class Line(str):
"""Line that remembers its source location."""
filename = None
lineno = None
def with_location(self, filename: Optional[str], lineno: Optional[int]) -> 'Line':
self.filename = filename
self.lineno = lineno
return self
def with_same_location_as(self, line: 'Line') -> 'Line':
self.filename = line.filename
self.lineno = line.lineno
return self
class ImportedName(NamedTuple):
"""Information about the canonical location of an import."""
modname: str # fully qualified module (or package) name (can be blank)
name: str # name of the importable thing
alias: str # alias to give to the importable thing (often same as `name`)
@property
def has_alias(self) -> bool:
return self.alias != self.name
def __str__(self) -> str:
bits = [self.name]
if self.has_alias:
bits.append(f"as {self.alias}")
if self.modname:
bits.append(f"from {self.modname}")
return " ".join(bits)
def parse_names(names: str, modname: str = '') -> Iterable[ImportedName]:
"""Parse a list of imported names.
The grammar is::
names ::= <name> [as <alias>] [, <names>]
"""
for name in names.split(','):
bits = name.split()
# it's either [name] or [name, 'as', alias], and the following works
# for both
yield ImportedName(modname, bits[0], bits[-1])
def parse_line(line: Line) -> Iterable[ImportedName]:
"""Parse an import configuration line.
The grammar is::
line ::= import <names>
| from <modname> import <names>
"""
m = IMPORT_RX.match(line)
if m:
return parse_names(m.group('names'))
m = FROM_IMPORT_RX.match(line)
if m:
modname = m.group('modname')
return parse_names(m.group('names'), modname)
raise Error(f'could not parse line {line.lineno}: {line}')
def strip_comments(lines: Iterable[str], filename: Optional[str] = None) -> Iterable[Line]:
"""Strip whitespace and comments and return nonblank lines."""
for n, line in enumerate(lines, 1):
line = line.partition('#')[0].strip()
if line:
yield Line(line).with_location(filename, n)
def joined_lines(lines: Iterable[Line]) -> Iterable[Line]:
"""Join lines until parentheses match."""
balance = 0
buffer = []
for line in lines:
buffer.append(line)
balance += line.count('(') - line.count(')')
if balance == 0:
yield Line(' '.join(buffer)).with_same_location_as(buffer[0])
del buffer[:]
if buffer:
# this is unbalanced, but let's let the next level report the error
yield Line(' '.join(buffer)).with_same_location_as(buffer[0])
def parse_python_imports_cfg(filename: str = DEFAULT_CONFIG_FILE, verbose: bool = False) -> None:
"""Parse python-imports.cfg if it exists.
Stores the parsed configuration directly in vim's g:pythonImports and g:pythonImportAliases
global variables, which must exist and be defined as dictionaries.
"""
try:
with open(filename) as f:
for line in joined_lines(strip_comments(f, filename)):
for name in parse_line(line):
if verbose >= 2:
print(name)
vim.command("let g:pythonImports['%s'] = '%s'" % (name.name, name.modname))
if name.has_alias:
vim.command("let g:pythonImportAliases['%s'] = '%s'"
% (name.alias, name.name))
continue
except (Error, IOError) as e:
if verbose:
print("Failed to load %s: %s" % (filename, e))