From 23f666b497a79fe8e4187754ebfe2cd9d53d183c Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 28 Jan 2022 12:16:55 -0800 Subject: [PATCH] interpreter: use typed_kwargs for configure_file --- mesonbuild/interpreter/interpreter.py | 143 ++++++++++++-------------- mesonbuild/interpreter/kwargs.py | 18 ++++ 2 files changed, 82 insertions(+), 79 deletions(-) diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index 4f1b0ed60..655a67b67 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -11,6 +11,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +from __future__ import annotations from .. import mparser from .. import environment from .. import coredata @@ -2223,67 +2224,67 @@ external dependencies (including libraries) must go to "dependencies".''') self.build.install_dirs.append(idir) return idir - @FeatureNewKwargs('configure_file', '0.47.0', ['copy', 'output_format', 'install_mode', 'encoding']) - @FeatureNewKwargs('configure_file', '0.46.0', ['format']) - @FeatureNewKwargs('configure_file', '0.41.0', ['capture']) - @FeatureNewKwargs('configure_file', '0.50.0', ['install']) - @FeatureNewKwargs('configure_file', '0.52.0', ['depfile']) - @FeatureNewKwargs('configure_file', '0.60.0', ['install_tag']) - @permittedKwargs({'input', 'output', 'configuration', 'command', 'copy', 'depfile', - 'install_dir', 'install_mode', 'capture', 'install', 'format', - 'output_format', 'encoding', 'install_tag'}) @noPosargs - def func_configure_file(self, node, args, kwargs): - if 'output' not in kwargs: - raise InterpreterException('Required keyword argument "output" not defined.') - actions = {'configuration', 'command', 'copy'}.intersection(kwargs.keys()) - if len(actions) == 0: + @typed_kwargs( + 'configure_file', + DEPFILE_KW.evolve(since='0.52.0'), + INSTALL_MODE_KW.evolve(since='0.47.0,'), + INSTALL_TAG_KW.evolve(since='0.60.0'), + KwargInfo('capture', bool, default=False, since='0.41.0'), + KwargInfo( + 'command', + (ContainerTypeInfo(list, (build.Executable, ExternalProgram, compilers.Compiler, mesonlib.File, str), allow_empty=False), NoneType), + listify=True, + ), + KwargInfo( + 'configuration', + (ContainerTypeInfo(dict, (str, int, bool)), build.ConfigurationData, NoneType), + ), + KwargInfo('copy', bool, default=False, since='0.47.0'), + KwargInfo('encoding', str, default='utf-8', since='0.47.0'), + KwargInfo('format', str, default='meson', since='0.46.0', + validator=in_set_validator({'meson', 'cmake', 'cmake@'})), + KwargInfo( + 'input', + ContainerTypeInfo(list, (mesonlib.File, str)), + listify=True, + default=[], + ), + # Cannot use shared implementation until None backwards compat is dropped + KwargInfo('install', (bool, NoneType), since='0.50.0'), + KwargInfo('install_dir', (str, bool), default='', + validator=lambda x: 'must be `false` if boolean' if x is True else None), + KwargInfo('output', str, required=True), + KwargInfo('output_format', str, default='c', since='0.47.0', + validator=in_set_validator({'c', 'nasm'})), + ) + def func_configure_file(self, node: mparser.BaseNode, args: T.List[TYPE_var], + kwargs: kwargs.ConfigureFile): + actions = sorted(x for x in {'configuration', 'command', 'copy'} + if kwargs[x] not in [None, False]) + num_actions = len(actions) + if num_actions == 0: raise InterpreterException('Must specify an action with one of these ' 'keyword arguments: \'configuration\', ' '\'command\', or \'copy\'.') - elif len(actions) == 2: + elif num_actions == 2: raise InterpreterException('Must not specify both {!r} and {!r} ' 'keyword arguments since they are ' 'mutually exclusive.'.format(*actions)) - elif len(actions) == 3: + elif num_actions == 3: raise InterpreterException('Must specify one of {!r}, {!r}, and ' '{!r} keyword arguments since they are ' 'mutually exclusive.'.format(*actions)) - if 'capture' in kwargs: - if not isinstance(kwargs['capture'], bool): - raise InterpreterException('"capture" keyword must be a boolean.') - if 'command' not in kwargs: - raise InterpreterException('"capture" keyword requires "command" keyword.') - - if 'format' in kwargs: - fmt = kwargs['format'] - if not isinstance(fmt, str): - raise InterpreterException('"format" keyword must be a string.') - else: - fmt = 'meson' - - if fmt not in ('meson', 'cmake', 'cmake@'): - raise InterpreterException('"format" possible values are "meson", "cmake" or "cmake@".') - if 'output_format' in kwargs: - output_format = kwargs['output_format'] - if not isinstance(output_format, str): - raise InterpreterException('"output_format" keyword must be a string.') - else: - output_format = 'c' - - if output_format not in ('c', 'nasm'): - raise InterpreterException('"format" possible values are "c" or "nasm".') + if kwargs['capture'] and not kwargs['command']: + raise InvalidArguments('configure_file: "capture" keyword requires "command" keyword.') - if 'depfile' in kwargs: - depfile = kwargs['depfile'] - if not isinstance(depfile, str): - raise InterpreterException('depfile file name must be a string') - else: - depfile = None + fmt = kwargs['format'] + output_format = kwargs['output_format'] + depfile = kwargs['depfile'] # Validate input - inputs = self.source_strings_to_files(extract_as_list(kwargs, 'input')) + inputs = self.source_strings_to_files(kwargs['input']) inputs_abs = [] for f in inputs: if isinstance(f, mesonlib.File): @@ -2292,10 +2293,9 @@ external dependencies (including libraries) must go to "dependencies".''') self.add_build_def_file(f) else: raise InterpreterException('Inputs can only be strings or file objects') + # Validate output output = kwargs['output'] - if not isinstance(output, str): - raise InterpreterException('Output file name must be a string') if inputs_abs: values = mesonlib.get_filenames_templates_dict(inputs_abs, None) outputs = mesonlib.substitute_values([output], values) @@ -2314,8 +2314,9 @@ external dependencies (including libraries) must go to "dependencies".''') raise InterpreterException('Output file name must not contain a subdirectory.') (ofile_path, ofile_fname) = os.path.split(os.path.join(self.subdir, output)) ofile_abs = os.path.join(self.environment.build_dir, ofile_path, ofile_fname) + # Perform the appropriate action - if 'configuration' in kwargs: + if kwargs['configuration'] is not None: conf = kwargs['configuration'] if isinstance(conf, dict): FeatureNew.single_use('configure_file.configuration dictionary', '0.49.0', self.subproject, location=node) @@ -2324,14 +2325,12 @@ external dependencies (including libraries) must go to "dependencies".''') raise InvalidArguments( f'"configuration_data": initial value dictionary key "{k!r}"" must be "str | int | bool", not "{v!r}"') conf = build.ConfigurationData(conf) - elif not isinstance(conf, build.ConfigurationData): - raise InterpreterException('Argument "configuration" is not of type configuration_data') mlog.log('Configuring', mlog.bold(output), 'using configuration') if len(inputs) > 1: raise InterpreterException('At most one input file can given in configuration mode') if inputs: os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True) - file_encoding = kwargs.setdefault('encoding', 'utf-8') + file_encoding = kwargs['encoding'] missing_variables, confdata_useless = \ mesonlib.do_conf_file(inputs_abs[0], ofile_abs, conf, fmt, file_encoding) @@ -2351,7 +2350,7 @@ external dependencies (including libraries) must go to "dependencies".''') else: mesonlib.dump_conf_header(ofile_abs, conf, output_format) conf.used = True - elif 'command' in kwargs: + elif kwargs['command'] is not None: if len(inputs) > 1: FeatureNew.single_use('multiple inputs in configure_file()', '0.52.0', self.subproject, location=node) # We use absolute paths for input and output here because the cwd @@ -2364,13 +2363,13 @@ external dependencies (including libraries) must go to "dependencies".''') # Substitute @INPUT@, @OUTPUT@, etc here. _cmd = mesonlib.substitute_values(kwargs['command'], values) mlog.log('Configuring', mlog.bold(output), 'with command') - cmd, *args = mesonlib.listify(_cmd) + cmd, *args = _cmd res = self.run_command_impl(node, (cmd, args), {'capture': True, 'check': True, 'env': build.EnvironmentVariables()}, True) - if 'capture' in kwargs and kwargs['capture']: + if kwargs['capture']: dst_tmp = ofile_abs + '~' - file_encoding = kwargs.setdefault('encoding', 'utf-8') + file_encoding = kwargs['encoding'] with open(dst_tmp, 'w', encoding=file_encoding) as f: f.writelines(res.stdout) if inputs_abs: @@ -2384,42 +2383,28 @@ external dependencies (including libraries) must go to "dependencies".''') for dep in deps: self.add_build_def_file(dep) - elif 'copy' in kwargs: + elif kwargs['copy']: if len(inputs_abs) != 1: raise InterpreterException('Exactly one input file must be given in copy mode') os.makedirs(os.path.join(self.environment.build_dir, self.subdir), exist_ok=True) shutil.copy2(inputs_abs[0], ofile_abs) - else: - # Not reachable - raise AssertionError + # Install file if requested, we check for the empty string # for backwards compatibility. That was the behaviour before # 0.45.0 so preserve it. - idir = kwargs.get('install_dir', '') + idir = kwargs['install_dir'] if idir is False: idir = '' FeatureDeprecated.single_use('configure_file install_dir: false', '0.50.0', self.subproject, 'Use the `install:` kwarg instead', location=node) - if not isinstance(idir, str): - if isinstance(idir, list) and len(idir) == 0: - mlog.deprecation('install_dir: kwarg must be a string and not an empty array. ' - 'Please use the install: kwarg to enable or disable installation. ' - 'This will be a hard error in the next release.') - else: - raise InterpreterException('"install_dir" must be a string') - install = kwargs.get('install', idir != '') - if not isinstance(install, bool): - raise InterpreterException('"install" must be a boolean') + install = kwargs['install'] if kwargs['install'] is not None else idir != '' if install: if not idir: - raise InterpreterException('"install_dir" must be specified ' - 'when "install" in a configure_file ' - 'is true') + raise InterpreterException( + '"install_dir" must be specified when "install" in a configure_file is true') cfile = mesonlib.File.from_built_file(ofile_path, ofile_fname) - install_mode = self._get_kwarg_install_mode(kwargs) - install_tag = kwargs.get('install_tag') - if install_tag is not None and not isinstance(install_tag, str): - raise InvalidArguments('install_tag keyword argument must be string') + install_mode = kwargs['install_mode'] + install_tag = kwargs['install_tag'] self.build.data.append(build.Data([cfile], idir, idir, install_mode, self.subproject, install_tag=install_tag, data_type='configure')) return mesonlib.File.from_built_file(self.subdir, output) diff --git a/mesonbuild/interpreter/kwargs.py b/mesonbuild/interpreter/kwargs.py index dff9e1254..981f50d27 100644 --- a/mesonbuild/interpreter/kwargs.py +++ b/mesonbuild/interpreter/kwargs.py @@ -10,6 +10,7 @@ from typing_extensions import TypedDict, Literal, Protocol from .. import build from .. import coredata +from ..compilers import Compiler from ..mesonlib import MachineChoice, File, FileMode, FileOrString, OptionKey from ..programs import ExternalProgram @@ -275,3 +276,20 @@ class VcsTag(TypedDict): build.ExtractedObjects, build.GeneratedList, ExternalProgram, File]] output: T.List[str] replace_string: str + + +class ConfigureFile(TypedDict): + + output: str + capture: bool + format: T.Literal['meson', 'cmake', 'cmake@'] + output_format: T.Literal['c', 'nasm'] + depfile: T.Optional[str] + install: T.Optional[bool] + install_dir: T.Union[str, T.Literal[False]] + install_mode: FileMode + install_tag: T.Optional[str] + encoding: str + command: T.Optional[T.List[T.Union[build.Executable, ExternalProgram, Compiler, File, str]]] + input: T.List[FileOrString] + configuration: T.Optional[T.Union[T.Dict[str, T.Union[str, int, bool]], build.ConfigurationData]]