mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-01-24 06:20:05 +08:00
151 lines
4.8 KiB
Python
Vendored
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))
|