Source code for efrotools.toolconfig

# Released under the MIT License. See LICENSE for details.
#
"""Functionality for wrangling tool config files.

This lets us auto-populate values such as python-paths or versions
into tool config files automatically instead of having to update
everything everywhere manually. It also provides a centralized location
for some tool defaults across all my projects.
"""

from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING

from efro.terminal import Clr

if TYPE_CHECKING:
    from pathlib import Path


[docs] def install_tool_config(projroot: Path, src: Path, dst: Path) -> None: """Install a config.""" print(f'Creating tool config: {Clr.BLD}{dst}{Clr.RST}') # Special case: if we've got a src .toml and a dst .json, convert. # This can be handy to add annotations/etc. in the src which isn't # possible with json. if src.suffix == '.toml' and dst.suffix == '.json': import tomllib import json with src.open(encoding='utf-8') as infile: contents = tomllib.loads(infile.read()) cfg = json.dumps(contents, indent=2, sort_keys=True) # In normal cases we just push the source file straight through. else: with src.open(encoding='utf-8') as infile: cfg = infile.read() # Some substitutions, etc. cfg = _filter_tool_config(projroot, cfg) # Add an auto-generated notice. comment = None if dst.name in ['.dir-locals.el']: comment = ';;' elif dst.name in [ '.mypy.ini', '.pylintrc', '.style.yapf', '.clang-format', '.editorconfig', '.rgignore', ]: comment = '#' if comment is not None: cfg = ( f'{comment} THIS FILE WAS AUTOGENERATED; DO NOT EDIT.\n' f'{comment} Source: {src}.\n\n' + cfg ) with dst.open('w', encoding='utf-8') as outfile: outfile.write(cfg)
def _filter_tool_config(projroot: Path, cfg: str) -> str: # pylint: disable=too-many-locals import textwrap from efrotools.project import getprojectconfig from efrotools.pyver import PYVER # Emacs dir-locals defaults. Note that these contain other # replacements so need to be at the top. name = '__EFRO_EMACS_STANDARD_CPP_LSP_SETUP__' if name in cfg: cfg = cfg.replace( name, ';; Set up clangd as our C++ language server.\n' ' (c++-ts-mode . ((eglot-server-programs' ' . ((c++-ts-mode . ("clangd"' ' "--compile-commands-dir=.cache/compile_commands_db"))))))', ) name = '__EFRO_EMACS_STANDARD_PYTHON_LSP_SETUP__' if name in cfg: cfg = cfg.replace( name, ';; Set up python-lsp-server as our Python language server.\n' ' (python-ts-mode . (\n' ' (eglot-server-programs . (\n' ' (python-ts-mode . ("__EFRO_PY_BIN__" "-m" "pylsp"))))\n' ' (python-shell-interpreter . "__EFRO_PY_BIN__")\n' ' (eglot-workspace-configuration . (\n' ' (:pylsp . (:plugins (\n' ' :pylint (:enabled t)\n' ' :flake8 (:enabled :json-false)\n' ' :pycodestyle (:enabled :json-false)\n' ' :mccabe (:enabled :json-false)\n' ' :autopep8 (:enabled :json-false)\n' ' :pyflakes (:enabled :json-false)\n' ' :rope_autoimport (:enabled :json-false)\n' ' :rope_completion (:enabled :json-false)\n' ' :rope_rename (:enabled :json-false)\n' ' :yapf (:enabled :json-false)\n' ' :black (:enabled t\n' ' :skip_string_normalization t\n' ' :line_length 80\n' ' :cache_config t)\n' ' :jedi (:extra_paths' ' [__EFRO_PYTHON_PATHS_Q_REL_STR__])\n' ' :pylsp_mypy (:enabled t\n' ' :live_mode :json-false\n' ' :report_progress t\n' ' :dmypy :json-false))))))))\n', ) # Stick project-root wherever they want. cfg = cfg.replace('__EFRO_PROJECT_ROOT__', str(projroot)) # Project Python version; '3.12', etc. name = '__EFRO_PY_VER__' if name in cfg: cfg = cfg.replace(name, PYVER) # Project Python version as a binary name; 'python3.12', etc. name = '__EFRO_PY_BIN__' if name in cfg: cfg = cfg.replace(name, str(Path(projroot, '.venv', 'bin', 'python'))) # Colon-separated list of project Python paths. name = '__EFRO_PYTHON_PATHS__' if name in cfg: pypaths = getprojectconfig(projroot).get('python_paths') if pypaths is None: raise RuntimeError('python_paths not set in project config') assert not any(' ' in p for p in pypaths) cfg = cfg.replace(name, ':'.join(f'{projroot}/{p}' for p in pypaths)) # Quoted relative space-separated list of project Python paths. name = '__EFRO_PYTHON_PATHS_Q_REL_STR__' if name in cfg: pypaths = getprojectconfig(projroot).get('python_paths') if pypaths is None: raise RuntimeError('python_paths not set in project config') assert not any(' ' in p for p in pypaths) cfg = cfg.replace(name, ' '.join(f'"{p}"' for p in pypaths)) # Short project name. short_names = { 'ballistica-internal': 'ba-i', 'ballistica': 'ba', 'ballistica-master-server': 'bmas', 'ballistica-master-server-legacy': 'bmasl', 'ballistica-server-node': 'basn', } shortname = short_names.get(projroot.name, projroot.name) cfg = cfg.replace('__EFRO_PROJECT_SHORTNAME__', shortname) mypy_standard_settings = textwrap.dedent( """ # We don't want all of our plain scripts complaining # about __main__ being redefined. scripts_are_modules = True # Try to be as strict as we can about using types everywhere. no_implicit_optional = True warn_unused_ignores = True warn_no_return = True warn_return_any = True warn_redundant_casts = True warn_unreachable = True warn_unused_configs = True disallow_incomplete_defs = True disallow_untyped_defs = True disallow_untyped_decorators = True disallow_untyped_calls = True disallow_any_unimported = True disallow_subclassing_any = True strict_equality = True local_partial_types = True no_implicit_reexport = True enable_error_code = redundant-expr, truthy-bool, \ truthy-function, unused-awaitable, explicit-override """ ).strip() cfg = cfg.replace('__EFRO_MYPY_STANDARD_SETTINGS__', mypy_standard_settings) name = '__PYTHON_BLACK_EXTRA_ARGS__' if name in cfg: from efrotools.code import black_base_args bargs = black_base_args(projroot) assert bargs[2] == 'black' cfg = cfg.replace( name, '(' + ' '.join(f'"{b}"' for b in bargs[3:]) + ')' ) # Gen a pylint init hook that sets up our Python paths. pylint_init_tag = '__EFRO_PYLINT_INIT__' if pylint_init_tag in cfg: pypaths = getprojectconfig(projroot).get('python_paths') if pypaths is None: raise RuntimeError('python_paths not set in project config') cstr = 'init-hook=import sys;' # Stuff our paths in at the beginning in the order they appear # in our projectconfig. for i, path in enumerate(pypaths): cstr += f" sys.path.insert({i}, '{projroot}/{path}');" cfg = cfg.replace(pylint_init_tag, cstr) return cfg