# 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:
pass
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.13', etc.
name = '__EFRO_PY_VER__'
if name in cfg:
cfg = cfg.replace(name, PYVER)
# Project Python version as a binary name; 'python3.13', 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
# Docs-generation hack; import some stuff that we likely only forward-declared
# in our actual source code so that docs tools can find it.
from typing import (Coroutine, Any, Literal, Callable,
Generator, Awaitable, Sequence, Self)
import asyncio
from concurrent.futures import Future
from pathlib import Path
from enum import Enum