# 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 versionsinto tool config files automatically instead of having to updateeverything everywhere manually. It also provides a centralized locationfor some tool defaults across all my projects."""from__future__importannotationsfrompathlibimportPathfromtypingimportTYPE_CHECKINGfromefro.terminalimportClrifTYPE_CHECKING:frompathlibimportPath
[docs]definstall_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.ifsrc.suffix=='.toml'anddst.suffix=='.json':importtomllibimportjsonwithsrc.open(encoding='utf-8')asinfile: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:withsrc.open(encoding='utf-8')asinfile:cfg=infile.read()# Some substitutions, etc.cfg=_filter_tool_config(projroot,cfg)# Add an auto-generated notice.comment=Noneifdst.namein['.dir-locals.el']:comment=';;'elifdst.namein['.mypy.ini','.pylintrc','.style.yapf','.clang-format','.editorconfig','.rgignore',]:comment='#'ifcommentisnotNone:cfg=(f'{comment} THIS FILE WAS AUTOGENERATED; DO NOT EDIT.\n'f'{comment} Source: {src}.\n\n'+cfg)withdst.open('w',encoding='utf-8')asoutfile:outfile.write(cfg)
def_filter_tool_config(projroot:Path,cfg:str)->str:# pylint: disable=too-many-localsimporttextwrapfromefrotools.projectimportgetprojectconfigfromefrotools.pyverimportPYVER# Emacs dir-locals defaults. Note that these contain other# replacements so need to be at the top.name='__EFRO_EMACS_STANDARD_CPP_LSP_SETUP__'ifnameincfg: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__'ifnameincfg: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__'ifnameincfg:cfg=cfg.replace(name,PYVER)# Project Python version as a binary name; 'python3.12', etc.name='__EFRO_PY_BIN__'ifnameincfg:cfg=cfg.replace(name,str(Path(projroot,'.venv','bin','python')))# Colon-separated list of project Python paths.name='__EFRO_PYTHON_PATHS__'ifnameincfg:pypaths=getprojectconfig(projroot).get('python_paths')ifpypathsisNone:raiseRuntimeError('python_paths not set in project config')assertnotany(' 'inpforpinpypaths)cfg=cfg.replace(name,':'.join(f'{projroot}/{p}'forpinpypaths))# Quoted relative space-separated list of project Python paths.name='__EFRO_PYTHON_PATHS_Q_REL_STR__'ifnameincfg:pypaths=getprojectconfig(projroot).get('python_paths')ifpypathsisNone:raiseRuntimeError('python_paths not set in project config')assertnotany(' 'inpforpinpypaths)cfg=cfg.replace(name,' '.join(f'"{p}"'forpinpypaths))# 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__'ifnameincfg:fromefrotools.codeimportblack_base_argsbargs=black_base_args(projroot)assertbargs[2]=='black'cfg=cfg.replace(name,'('+' '.join(f'"{b}"'forbinbargs[3:])+')')# Gen a pylint init hook that sets up our Python paths.pylint_init_tag='__EFRO_PYLINT_INIT__'ifpylint_init_tagincfg:pypaths=getprojectconfig(projroot).get('python_paths')ifpypathsisNone:raiseRuntimeError('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.fori,pathinenumerate(pypaths):cstr+=f" sys.path.insert({i}, '{projroot}/{path}');"cfg=cfg.replace(pylint_init_tag,cstr)returncfg