From 5d7c72c3659c25db5810aa91abeee2579f8e4f47 Mon Sep 17 00:00:00 2001 From: Eric Froemling Date: Mon, 1 Jun 2020 15:24:36 -0700 Subject: [PATCH] C++ layer cleanup --- .efrocachemap | 24 +++--- .idea/dictionaries/ericf.xml | 18 +++++ assets/src/ba_data/python/ba/_appdelegate.py | 4 +- assets/src/ba_data/python/ba/_gameactivity.py | 4 +- .../src/ba_data/python/bastd/appdelegate.py | 2 +- config/config.json | 3 +- config/toolconfigsrc/mypy.ini | 3 + docs/ba_module.md | 6 +- tests/test_efro/test_dataclasses.py | 78 ++++++++++++++----- tools/bacommon/servermanager.py | 10 ++- tools/batools/build.py | 16 +++- tools/efro/dataclasses.py | 73 +++++++++++------ tools/efrotools/code.py | 55 ++++++++++--- 13 files changed, 218 insertions(+), 78 deletions(-) diff --git a/.efrocachemap b/.efrocachemap index 13656eaf..ce583a6d 100644 --- a/.efrocachemap +++ b/.efrocachemap @@ -4132,16 +4132,16 @@ "assets/build/windows/x64/python.exe": "https://files.ballistica.net/cache/ba1/25/a7/dc87c1be41605eb6fefd0145144c", "assets/build/windows/x64/python37.dll": "https://files.ballistica.net/cache/ba1/b9/e4/d912f56e42e9991bcbb4c804cfcb", "assets/build/windows/x64/pythonw.exe": "https://files.ballistica.net/cache/ba1/6c/bb/b6f52c306aa4e88061510e96cefe", - "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/cd/cc/837483543f1d6b184f64b5e6a950", - "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/38/e6/d372324c7c08b5b300490fa5594e", - "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/0e/11/3dda0974b64f51be4961628f572c", - "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/cc/76/3f6356dd599091f5955ce349593b", - "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/e1/23/3fe78cdef456a99140837ede7e49", - "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1c/bd/4c73637ee172630ee00145032ce7", - "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7a/a7/342bea2a6ec2f94d5de7bb52a59f", - "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/aa/24/5426cb7e6ca01b9e5d67ad3217b0", - "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/cb/73/469fe3eb016f1e9c7f5c7811b182", - "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/7c/fb/7e0880c1ab90b0484cdedc41c8ae", - "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/f3/f2/097e861cf6ca981b18191e4c43b5", - "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/6b/7c/8407f4a7326b8b19f3e187d3ffcb" + "build/prefab/linux-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/1e/d6/ebdfb7bda48f2dec85c5df52ced9", + "build/prefab/linux-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4a/0e/d344541dc2ae2a6eaf1024c99f7c", + "build/prefab/linux/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/68/c3/fe2482149437800c83fed338140e", + "build/prefab/linux/release/ballisticacore": "https://files.ballistica.net/cache/ba1/74/00/78f77d5ab9cc74a2f624f3a7253b", + "build/prefab/mac-server/debug/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/ea/d1/017aaa42a7ace25a7b1f782632e5", + "build/prefab/mac-server/release/dist/ballisticacore_headless": "https://files.ballistica.net/cache/ba1/4f/b3/b149964180b661161e5f86109d99", + "build/prefab/mac/debug/ballisticacore": "https://files.ballistica.net/cache/ba1/7c/97/177b59dcf24b839ca2d932200bb8", + "build/prefab/mac/release/ballisticacore": "https://files.ballistica.net/cache/ba1/d8/55/9edbc1eea06cf354fe37b50d68d2", + "build/prefab/windows-server/debug/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/fc/ec/eb46555d63e5e3fd55497d1c1079", + "build/prefab/windows-server/release/dist/ballisticacore_headless.exe": "https://files.ballistica.net/cache/ba1/71/73/b8d329287a0a46cc8ed2ef08a7d9", + "build/prefab/windows/debug/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/62/79/66fc05a4388c4b5a9e9a7fd68a46", + "build/prefab/windows/release/BallisticaCore.exe": "https://files.ballistica.net/cache/ba1/c3/5d/eb6c20729c0572e717c907482205" } \ No newline at end of file diff --git a/.idea/dictionaries/ericf.xml b/.idea/dictionaries/ericf.xml index a91841b1..cc614da7 100644 --- a/.idea/dictionaries/ericf.xml +++ b/.idea/dictionaries/ericf.xml @@ -262,6 +262,7 @@ centeuro centiseconds cfconfig + cfenv cfgdir cfgkey cfgkeys @@ -280,6 +281,7 @@ charname charstr chatmessage + cheadersline checkarg checkboxwidget checkenv @@ -323,6 +325,7 @@ codefilenames codefiles codehash + codelines codeop collapsable collidemodel @@ -374,6 +377,7 @@ cpplintcode cpplintcodefull cpplintfull + cpplintmodule cpuinfo cpus cpython @@ -398,6 +402,7 @@ cutscenes cval cwdg + cxxabi cyaml cygwinccompiler darwiin @@ -750,6 +755,7 @@ genmapjson genstartercache genutils + getaccountid getactivity getclass getcollide @@ -833,6 +839,7 @@ hatmotion hattach hdpi + headercheckline headerregistry heapqmodule hehe @@ -960,6 +967,7 @@ keywd keywds khronos + kickable kickin kickstart killcount @@ -987,6 +995,7 @@ lazybuilddir lbits lbld + lbval lcfg lcolor lcrypto @@ -999,8 +1008,10 @@ levelmodule levelname lfull + lfval libcrypto libegl + libgen libinst liblzma libmain @@ -1034,6 +1045,7 @@ lintscriptsfast listobj listvalidconfigs + lival llzma lmerged lmod @@ -1070,6 +1082,7 @@ lstart lstr lstrs + lsval ltex lzma lzmamodule @@ -1208,6 +1221,7 @@ ndkpath neededsettings ness + netlink nettesting netutils nevermind @@ -1576,6 +1590,7 @@ rsdr rsms rstr + rtnetlink rtxt runmypy runonly @@ -1667,6 +1682,7 @@ sharedctypes sharedobj sharedobjs + shellapi shiftdelay shiftposition shobs @@ -1792,6 +1808,8 @@ subrepos subsel subval + subvalue + subvaluetype successfull suiciding sunau diff --git a/assets/src/ba_data/python/ba/_appdelegate.py b/assets/src/ba_data/python/ba/_appdelegate.py index b3d7c768..a1bcb9de 100644 --- a/assets/src/ba_data/python/ba/_appdelegate.py +++ b/assets/src/ba_data/python/ba/_appdelegate.py @@ -34,7 +34,7 @@ class AppDelegate: Category: App Classes """ - def create_default_game_config_ui( + def create_default_game_settings_ui( self, gameclass: Type[ba.GameActivity], sessionclass: Type[ba.Session], config: Optional[Dict[str, Any]], completion_call: Callable[[Optional[Dict[str, Any]]], @@ -47,4 +47,4 @@ class AppDelegate: del gameclass, sessionclass, config, completion_call # unused from ba import _error _error.print_error( - "create_default_game_config_ui needs to be overridden") + "create_default_game_settings_ui needs to be overridden") diff --git a/assets/src/ba_data/python/ba/_gameactivity.py b/assets/src/ba_data/python/ba/_gameactivity.py index 099ca094..6ed91a80 100644 --- a/assets/src/ba_data/python/ba/_gameactivity.py +++ b/assets/src/ba_data/python/ba/_gameactivity.py @@ -103,8 +103,8 @@ class GameActivity(Activity[PlayerType, TeamType]): """ delegate = _ba.app.delegate assert delegate is not None - delegate.create_default_game_config_ui(cls, sessionclass, settings, - completion_call) + delegate.create_default_game_settings_ui(cls, sessionclass, settings, + completion_call) @classmethod def get_score_info(cls) -> ba.ScoreInfo: diff --git a/assets/src/ba_data/python/bastd/appdelegate.py b/assets/src/ba_data/python/bastd/appdelegate.py index 91e24ac0..11b145d4 100644 --- a/assets/src/ba_data/python/bastd/appdelegate.py +++ b/assets/src/ba_data/python/bastd/appdelegate.py @@ -32,7 +32,7 @@ if TYPE_CHECKING: class AppDelegate(ba.AppDelegate): """Defines handlers for high level app functionality.""" - def create_default_game_config_ui( + def create_default_game_settings_ui( self, gameclass: Type[ba.GameActivity], sessionclass: Type[ba.Session], config: Optional[Dict[str, Any]], completion_call: Callable[[Optional[Dict[str, Any]]], diff --git a/config/config.json b/config/config.json index d3c4e230..793059ca 100644 --- a/config/config.json +++ b/config/config.json @@ -12,7 +12,8 @@ "pytz", "yaml", "requests", - "typing_extensions" + "typing_extensions", + "cpplint" ], "python_paths": [ "assets/src/ba_data/python", diff --git a/config/toolconfigsrc/mypy.ini b/config/toolconfigsrc/mypy.ini index 249b3f67..021dc310 100644 --- a/config/toolconfigsrc/mypy.ini +++ b/config/toolconfigsrc/mypy.ini @@ -21,6 +21,9 @@ no_implicit_reexport = False [mypy-pylint.*] ignore_missing_imports = True +[mypy-cpplint.*] +ignore_missing_imports = True + [mypy-xml.*] ignore_missing_imports = True diff --git a/docs/ba_module.md b/docs/ba_module.md index 9768dcf4..79375f9e 100644 --- a/docs/ba_module.md +++ b/docs/ba_module.md @@ -1,5 +1,5 @@ -

last updated on 2020-05-31 for Ballistica version 1.5.0 build 20036

+

last updated on 2020-06-01 for Ballistica version 1.5.0 build 20037

This page documents the Python classes and functions in the 'ba' module, which are the ones most relevant to modding in Ballistica. If you come across something you feel should be included here or could be better explained, please let me know. Happy modding!


@@ -1079,8 +1079,8 @@ manually.

Methods:

-

create_default_game_config_ui()

-

create_default_game_config_ui(self, gameclass: Type[ba.GameActivity], sessionclass: Type[ba.Session], config: Optional[Dict[str, Any]], completion_call: Callable[[Optional[Dict[str, Any]]], None]) -> None

+

create_default_game_settings_ui()

+

create_default_game_settings_ui(self, gameclass: Type[ba.GameActivity], sessionclass: Type[ba.Session], config: Optional[Dict[str, Any]], completion_call: Callable[[Optional[Dict[str, Any]]], None]) -> None

Launch a UI to configure the given game config.

diff --git a/tests/test_efro/test_dataclasses.py b/tests/test_efro/test_dataclasses.py index a2bf51f5..f00c4aa9 100644 --- a/tests/test_efro/test_dataclasses.py +++ b/tests/test_efro/test_dataclasses.py @@ -22,7 +22,7 @@ from __future__ import annotations -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING import pytest @@ -30,11 +30,12 @@ import pytest from efro.dataclasses import dataclass_assign, dataclass_validate if TYPE_CHECKING: - from typing import Optional + from typing import Optional, List def test_assign() -> None: """Testing various assignments.""" + # pylint: disable=too-many-statements @dataclass class _TestClass: @@ -46,6 +47,10 @@ def test_assign() -> None: osval: Optional[str] = None obval: Optional[bool] = None ofval: Optional[float] = None + lsval: List[str] = field(default_factory=list) + lival: List[int] = field(default_factory=list) + lbval: List[bool] = field(default_factory=list) + lfval: List[float] = field(default_factory=list) tclass = _TestClass() @@ -66,24 +71,39 @@ def test_assign() -> None: dataclass_assign(tclass, {'nonexistent': 'foo'}) # Correct types. - dataclass_assign(tclass, { - 'ival': 1, - 'sval': 'foo', - 'bval': True, - 'fval': 2.0, - }) - dataclass_assign(tclass, { - 'oival': None, - 'osval': None, - 'obval': None, - 'ofval': None, - }) - dataclass_assign(tclass, { - 'oival': 1, - 'osval': 'foo', - 'obval': True, - 'ofval': 2.0, - }) + dataclass_assign( + tclass, { + 'ival': 1, + 'sval': 'foo', + 'bval': True, + 'fval': 2.0, + 'lsval': ['foo'], + 'lival': [10], + 'lbval': [False], + 'lfval': [1.0] + }) + dataclass_assign( + tclass, { + 'oival': None, + 'osval': None, + 'obval': None, + 'ofval': None, + 'lsval': [], + 'lival': [], + 'lbval': [], + 'lfval': [] + }) + dataclass_assign( + tclass, { + 'oival': 1, + 'osval': 'foo', + 'obval': True, + 'ofval': 2.0, + 'lsval': ['foo', 'bar', 'eep'], + 'lival': [10, 11, 12], + 'lbval': [False, True], + 'lfval': [1.0, 2.0, 3.0] + }) # Type mismatches. with pytest.raises(TypeError): @@ -107,6 +127,21 @@ def test_assign() -> None: with pytest.raises(TypeError): dataclass_assign(tclass, {'ofval': 'blah'}) + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lsval': 'blah'}) + + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lsval': [1]}) + + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lbval': [None]}) + + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lival': ['foo']}) + + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lfval': [True]}) + # More subtle ones (we currently require EXACT type matches) with pytest.raises(TypeError): dataclass_assign(tclass, {'ival': True}) @@ -120,6 +155,9 @@ def test_assign() -> None: with pytest.raises(TypeError): dataclass_assign(tclass, {'ofval': 1}) + with pytest.raises(TypeError): + dataclass_assign(tclass, {'lfval': [1]}) + def test_validate() -> None: """Testing validation.""" diff --git a/tools/bacommon/servermanager.py b/tools/bacommon/servermanager.py index c00d2db8..afa1d87d 100644 --- a/tools/bacommon/servermanager.py +++ b/tools/bacommon/servermanager.py @@ -22,7 +22,7 @@ from __future__ import annotations from enum import Enum -from dataclasses import dataclass +from dataclasses import dataclass, field from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -45,6 +45,14 @@ class ServerConfig: # be enabled unless you are hosting on a LAN with no internet connection. authenticate_clients: bool = True + # IDs of server admins. Server admins are not kickable through the default + # kick vote system and they are able to kick players without a vote. To get + # your account id, enter 'getaccountid' in settings->advanced->enter-code. + admins: List[str] = field(default_factory=list) + + # Whether the default kick-voting system is enabled. + enable_default_kick_voting: bool = True + # UDP port to host on. Change this to work around firewalls or run multiple # servers on one machine. # 43210 is the default and the only port that will show up in the LAN diff --git a/tools/batools/build.py b/tools/batools/build.py index db1c366e..c4fa9561 100644 --- a/tools/batools/build.py +++ b/tools/batools/build.py @@ -33,7 +33,7 @@ from typing import TYPE_CHECKING from efro.terminal import Clr if TYPE_CHECKING: - from typing import List, Sequence, Optional + from typing import List, Sequence, Optional, Any # Python pip packages we require for this project. @@ -612,7 +612,11 @@ def _get_server_config_template_yaml(projroot: str) -> str: assert vname.endswith(':') vname = vname[:-1] assert veq == '=' - vval = eval(vval_raw) # pylint: disable=eval-used + vval: Any + if vval_raw == 'field(default_factory=list)': + vval = [] + else: + vval = eval(vval_raw) # pylint: disable=eval-used # Filter/override a few things. if vname == 'playlist_code': @@ -621,7 +625,13 @@ def _get_server_config_template_yaml(projroot: str) -> str: elif vname == 'stats_url': vval = ('https://mystatssite.com/' 'showstats?player=${ACCOUNT}') - lines_out.append('#' + yaml.dump({vname: vval}).strip()) + elif vname == 'admins': + vval = ['pb-yOuRAccOuNtIdHErE', 'pb-aNdMayBeAnotherHeRE'] + lines_out += [ + '#' + l for l in yaml.dump({ + vname: vval + }).strip().splitlines() + ] else: # Convert comments referring to python bools to yaml bools. line = line.replace('True', 'true').replace('False', 'false') diff --git a/tools/efro/dataclasses.py b/tools/efro/dataclasses.py index c313fb2f..38edc2d2 100644 --- a/tools/efro/dataclasses.py +++ b/tools/efro/dataclasses.py @@ -30,9 +30,9 @@ if TYPE_CHECKING: # For fields with these string types, we require a passed value's type # to exactly match one of the tuple values to consider the assignment valid. _SIMPLE_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = { - 'bool': (bool, ), - 'str': (str, ), 'int': (int, ), + 'str': (str, ), + 'bool': (bool, ), 'float': (float, ), 'Optional[int]': (int, type(None)), 'Optional[str]': (str, type(None)), @@ -40,6 +40,13 @@ _SIMPLE_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = { 'Optional[float]': (float, type(None)), } +_LIST_ASSIGN_TYPES: Dict[str, Tuple[Type, ...]] = { + 'List[int]': (int, ), + 'List[str]': (str, ), + 'List[bool]': (bool, ), + 'List[float]': (float, ), +} + def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None: """Safely assign values from a dict to a dataclass instance. @@ -60,6 +67,22 @@ def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None: the increased safety checks may be worth the speed tradeoff in some cases. """ + _dataclass_validate(instance, values) + for key, value in values.items(): + setattr(instance, key, value) + + +def dataclass_validate(instance: Any) -> None: + """Ensure values in a dataclass are correct types. + + Note that this will always fail if a dataclass contains field types + not supported by this module. + """ + _dataclass_validate(instance, dataclasses.asdict(instance)) + + +def _dataclass_validate(instance: Any, values: Dict[str, Any]) -> None: + # pylint: disable=too-many-branches if not dataclasses.is_dataclass(instance): raise TypeError(f'Passed instance {instance} is not a dataclass.') if not isinstance(values, dict): @@ -82,10 +105,10 @@ def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None: f' been created without "from __future__ import annotations";' f' those dataclasses are unsupported here.') - reqtypes = _SIMPLE_ASSIGN_TYPES.get(fieldtype) - if reqtypes is not None: - # pylint: disable=unidiomatic-typecheck - if not any(type(value) is t for t in reqtypes): + if fieldtype in _SIMPLE_ASSIGN_TYPES: + reqtypes = _SIMPLE_ASSIGN_TYPES[fieldtype] + valuetype = type(value) + if not any(valuetype is t for t in reqtypes): if len(reqtypes) == 1: expected = reqtypes[0].__name__ else: @@ -93,23 +116,25 @@ def dataclass_assign(instance: Any, values: Dict[str, Any]) -> None: expected = f'Union[{names}]' raise TypeError(f'Invalid value type for "{key}";' f' expected "{expected}", got' - f' "{type(value).__name__}".') + f' "{valuetype.__name__}".') + + elif fieldtype in _LIST_ASSIGN_TYPES: + reqtypes = _LIST_ASSIGN_TYPES[fieldtype] + if not isinstance(value, list): + raise TypeError( + f'Invalid value for "{key}";' + f' expected a list, got a "{type(value).__name__}"') + for subvalue in value: + subvaluetype = type(subvalue) + if not any(subvaluetype is t for t in reqtypes): + if len(reqtypes) == 1: + expected = reqtypes[0].__name__ + else: + names = ', '.join(t.__name__ for t in reqtypes) + expected = f'Union[{names}]' + raise TypeError(f'Invalid value type for "{key}";' + f' expected list of "{expected}", found' + f' "{subvaluetype.__name__}".') + else: raise TypeError(f'Field type "{fieldtype}" is unsupported here.') - - # Ok, if we made it here, the value is kosher. Do the assign. - setattr(instance, key, value) - - -def dataclass_validate(instance: Any) -> None: - """Ensure values in a dataclass are correct types. - - Note that this will always fail if a dataclass contains field types - not supported by this module. - """ - # We currently simply operate by grabbing dataclass values as a dict - # and passing them through dataclass_assign(). - # It would be possible to write slightly more efficient custom code, - # this, but this keeps things simple and will allow us to easily - # incorporate things like value coercion later if we add that. - dataclass_assign(instance, dataclasses.asdict(instance)) diff --git a/tools/efrotools/code.py b/tools/efrotools/code.py index bc6a4eb7..aea849f8 100644 --- a/tools/efrotools/code.py +++ b/tools/efrotools/code.py @@ -82,15 +82,19 @@ def formatcode(projroot: Path, full: bool) -> None: def cpplint(projroot: Path, full: bool) -> None: """Run lint-checking on all code deemed lint-able.""" + # pylint: disable=too-many-locals + import tempfile from concurrent.futures import ThreadPoolExecutor from multiprocessing import cpu_count from efrotools import get_config from efro.terminal import Clr + from efro.error import CleanError os.chdir(projroot) filenames = get_code_filenames(projroot) - if any(' ' in name for name in filenames): - raise Exception('found space in path; unexpected') + for fpath in filenames: + if ' ' in fpath: + raise Exception(f'Found space in path {fpath}; unexpected.') # Check the config for a list of ones to ignore. code_blacklist: List[str] = get_config(projroot).get( @@ -114,14 +118,47 @@ def cpplint(projroot: Path, full: bool) -> None: print(f'{Clr.BLU}CppLint checking' f' {len(dirtyfiles)} file(s)...{Clr.RST}') - def lint_file(filename: str) -> None: - result = subprocess.call(['cpplint', '--root=src', filename]) - if result != 0: - raise Exception(f'Linting failed for {filename}') + # We want to do a few custom modifications to the cpplint module... + try: + import cpplint as cpplintmodule + except Exception: + raise CleanError('Unable to import cpplint') + with open(cpplintmodule.__file__) as infile: + codelines = infile.read().splitlines() + cheadersline = codelines.index('_C_HEADERS = frozenset([') - with ThreadPoolExecutor(max_workers=cpu_count()) as executor: - # Converting this to a list will propagate any errors. - list(executor.map(lint_file, dirtyfiles)) + # Extra headers we consider as valid C system headers. + c_headers = [ + 'malloc.h', 'tchar.h', 'jni.h', 'android/log.h', 'EGL/egl.h', + 'libgen.h', 'linux/netlink.h', 'linux/rtnetlink.h', 'android/bitmap.h', + 'android/log.h', 'uuid/uuid.h', 'cxxabi.h', 'direct.h', 'shellapi.h', + 'rpc.h', 'io.h' + ] + codelines.insert(cheadersline + 1, ''.join(f"'{h}'," for h in c_headers)) + + # Skip unapproved C++ headers check (it flags , , etc.) + headercheckline = codelines.index( + " if include and include.group(1) in ('cfenv',") + codelines[headercheckline] = ( + " if False and include and include.group(1) in ('cfenv',") + + def lint_file(filename: str) -> None: + result = subprocess.call(['cpplint', '--root=src', filename], env=env) + if result != 0: + raise CleanError( + f'{Clr.RED}Cpplint failed for {filename}.{Clr.RST}') + + with tempfile.TemporaryDirectory() as tmpdir: + + # Write our replacement module, make it discoverable, then run. + with open(tmpdir + '/cpplint.py', 'w') as outfile: + outfile.write('\n'.join(codelines)) + env = os.environ.copy() + env['PYTHONPATH'] = tmpdir + + with ThreadPoolExecutor(max_workers=cpu_count()) as executor: + # Converting this to a list will propagate any errors. + list(executor.map(lint_file, dirtyfiles)) if dirtyfiles: cache.mark_clean(filenames)