python3 << EndPython3 import collections import os import sys import vim def strtobool(text): if text.lower() in ["y", "yes", "t", "true", "on", "1"]: return True if text.lower() in ["n", "no", "f", "false", "off", "0"]: return False raise ValueError(f"{text} is not convertible to boolean") class Flag(collections.namedtuple("FlagBase", "name, cast")): @property def var_name(self): return self.name.replace("-", "_") @property def vim_rc_name(self): name = self.var_name if name == "line_length": name = name.replace("_", "") return "g:black_" + name FLAGS = [ Flag(name="line_length", cast=int), Flag(name="fast", cast=strtobool), Flag(name="skip_string_normalization", cast=strtobool), Flag(name="quiet", cast=strtobool), Flag(name="skip_magic_trailing_comma", cast=strtobool), Flag(name="preview", cast=strtobool), ] def _get_python_binary(exec_prefix, pyver): try: default = vim.eval("g:pymode_python").strip() except vim.error: default = "" if default and os.path.exists(default): return default if sys.platform[:3] == "win": return exec_prefix / "python.exe" bin_path = exec_prefix / "bin" exec_path = (bin_path / f"python{pyver[0]}.{pyver[1]}").resolve() if exec_path.exists(): return exec_path # It is possible that some environments may only have python3 exec_path = (bin_path / "python3").resolve() if exec_path.exists(): return exec_path raise ValueError("python executable not found") def _get_pip(venv_path): if sys.platform[:3] == "win": return venv_path / "Scripts" / "pip.exe" return venv_path / "bin" / "pip" def _get_virtualenv_site_packages(venv_path, pyver): if sys.platform[:3] == "win": return venv_path / "Lib" / "site-packages" if ( venv_path.exists() and not (venv_path / "lib" / f"python{pyver[0]}.{pyver[1]}").exists() ): # The virtualenv already exists but it doesn't seem to have the expected # Python version, so we disregard the requested `pyver` and # discover the real Python interpreter from this virtualenv import subprocess result = subprocess.run( [_get_python_binary(venv_path, pyver), "--version"], stdout=subprocess.PIPE, text=True, ) venv_version = result.stdout.split(" ")[1].strip().split(".") return ( venv_path / "lib" / f"python{venv_version[0]}.{venv_version[1]}" / "site-packages" ) else: return venv_path / "lib" / f"python{pyver[0]}.{pyver[1]}" / "site-packages" def _initialize_black_env(upgrade=False): if vim.eval("g:black_use_virtualenv ? 'true' : 'false'") == "false": if upgrade: print("Upgrade disabled due to g:black_use_virtualenv being disabled.") print( "Either use your system package manager (or pip) to upgrade black" " separately," ) print("or modify your vimrc to have 'let g:black_use_virtualenv = 1'.") return False else: # Nothing needed to be done. return True pyver = sys.version_info[:3] if pyver < (3, 10): print("Sorry, Black requires Python 3.10+ to run.") return False from pathlib import Path import subprocess import venv virtualenv_path = Path(vim.eval("g:black_virtualenv")).expanduser() virtualenv_site_packages = str( _get_virtualenv_site_packages(virtualenv_path, pyver) ) first_install = False if not virtualenv_path.is_dir(): print("Please wait, one time setup for Black.") _executable = sys.executable _base_executable = getattr(sys, "_base_executable", _executable) try: executable = str(_get_python_binary(Path(sys.exec_prefix), pyver)) sys.executable = executable sys._base_executable = executable print(f"Creating a virtualenv in {virtualenv_path}...") print( "(this path can be customized in .vimrc by setting g:black_virtualenv)" ) venv.create(virtualenv_path, with_pip=True) except Exception: print( "Encountered exception while creating virtualenv (see traceback below)." ) print(f"Removing {virtualenv_path}...") import shutil shutil.rmtree(virtualenv_path) raise finally: sys.executable = _executable sys._base_executable = _base_executable first_install = True if first_install: print("Installing Black with pip...") if upgrade: print("Upgrading Black with pip...") if first_install or upgrade: subprocess.run( [str(_get_pip(virtualenv_path)), "install", "-U", "black"], stdout=subprocess.PIPE, ) print("DONE! You are all set, thanks for waiting ✨ 🍰 ✨") if first_install: print( "Pro-tip: to upgrade Black in the future, use the :BlackUpgrade command and" " restart Vim.\n" ) if virtualenv_site_packages not in sys.path: sys.path.insert(0, virtualenv_site_packages) return True if _initialize_black_env(): try: import black except ImportError: print(f"Could not import black from any of: {', '.join(sys.path)}.") import time def get_target_version(tv): if isinstance(tv, black.TargetVersion): return tv ret = None try: ret = black.TargetVersion[tv.upper()] except KeyError: print( f"WARNING: Target version {tv!r} not recognized by Black, using default" " target" ) return ret def Black(**kwargs): """ kwargs allows you to override ``target_versions`` argument of ``black.FileMode``. ``target_version`` needs to be cleaned because ``black.FileMode`` expects the ``target_versions`` argument to be a set of TargetVersion enums. Allow kwargs["target_version"] to be a string to allow to type it more quickly. Using also target_version instead of target_versions to remain consistent to Black's documentation of the structure of pyproject.toml. """ start = time.time() configs = get_configs() black_kwargs = {} if "target_version" in kwargs: target_version = kwargs["target_version"] if not isinstance(target_version, (list, set)): target_version = [target_version] target_version = set( filter(lambda x: x, map(lambda tv: get_target_version(tv), target_version)) ) black_kwargs["target_versions"] = target_version mode = black.FileMode( line_length=configs["line_length"], string_normalization=not configs["skip_string_normalization"], is_pyi=vim.current.buffer.name.endswith(".pyi"), magic_trailing_comma=not configs["skip_magic_trailing_comma"], preview=configs["preview"], **black_kwargs, ) quiet = configs["quiet"] buffer_str = "\n".join(vim.current.buffer) + "\n" try: new_buffer_str = black.format_file_contents( buffer_str, fast=configs["fast"], mode=mode, ) except black.NothingChanged: if not quiet: print( "Black: already well formatted, good job. (took" f" {time.time() - start:.4f}s)" ) except Exception as exc: print(f"Black: {exc}") else: current_buffer = vim.current.window.buffer cursors = [] for i, tabpage in enumerate(vim.tabpages): if tabpage.valid: for j, window in enumerate(tabpage.windows): if window.valid and window.buffer == current_buffer: cursors.append((i, j, window.cursor)) vim.current.buffer[:] = new_buffer_str.split("\n")[:-1] for i, j, cursor in cursors: window = vim.tabpages[i].windows[j] try: window.cursor = cursor except vim.error: window.cursor = (len(window.buffer), 0) if not quiet: print(f"Black: reformatted in {time.time() - start:.4f}s.") def get_configs(): filename = vim.eval("@%") path_pyproject_toml = black.find_pyproject_toml((filename,)) if path_pyproject_toml: toml_config = black.parse_pyproject_toml(path_pyproject_toml) else: toml_config = {} return { flag.var_name: toml_config.get(flag.name, flag.cast(vim.eval(flag.vim_rc_name))) for flag in FLAGS } def BlackUpgrade(): _initialize_black_env(upgrade=True) def BlackVersion(): print(f"Black, version {black.__version__} on Python {sys.version}.") EndPython3 function black#Black(...) let kwargs = {} for arg in a:000 let arg_list = split(arg, '=') let kwargs[arg_list[0]] = arg_list[1] endfor python3 << EOF import vim kwargs = vim.eval("kwargs") EOF :py3 Black(**kwargs) endfunction function black#BlackUpgrade() :py3 BlackUpgrade() endfunction function black#BlackVersion() :py3 BlackVersion() endfunction