From c0b8e02d9fb1b58551b18e68ae188263dd90eb4c Mon Sep 17 00:00:00 2001 From: Eli Schwartz Date: Thu, 10 Feb 2022 21:39:50 -0500 Subject: [PATCH] FeatureNew: add mypy type annotations for subproject arg Use a derived type when passing `subproject` around, so that mypy knows it's actually a SubProject, not a str. This means that passing anything other than a handle to the interpreter state's subproject attribute becomes a type violation, specifically when the order of the *four* different str arguments is typoed. --- mesonbuild/build.py | 5 +++-- mesonbuild/interpreter/interpreterobjects.py | 3 ++- mesonbuild/interpreter/primitives/range.py | 5 ++++- mesonbuild/interpreterbase/__init__.py | 4 ++++ mesonbuild/interpreterbase/baseobjects.py | 6 ++++-- mesonbuild/interpreterbase/decorators.py | 7 ++++--- mesonbuild/interpreterbase/interpreterbase.py | 4 +++- mesonbuild/optinterpreter.py | 3 ++- mesonbuild/wrap/wrap.py | 3 ++- 9 files changed, 28 insertions(+), 12 deletions(-) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 782f59ca3..27e6d3698 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -49,6 +49,7 @@ if T.TYPE_CHECKING: from ._typing import ImmutableListProtocol, ImmutableSetProtocol from .backend.backends import Backend, ExecutableSerialisation from .interpreter.interpreter import Test, SourceOutputs, Interpreter + from .interpreterbase import SubProject from .mesonlib import FileMode, FileOrString from .modules import ModuleState from .mparser import BaseNode @@ -514,7 +515,7 @@ class Target(HoldableObject): name: str subdir: str - subproject: str + subproject: 'SubProject' build_by_default: bool for_machine: MachineChoice @@ -675,7 +676,7 @@ class BuildTarget(Target): install_dir: T.List[T.Union[str, bool]] - def __init__(self, name: str, subdir: str, subproject: str, for_machine: MachineChoice, + def __init__(self, name: str, subdir: str, subproject: 'SubProject', for_machine: MachineChoice, sources: T.List['SourceOutputs'], objects, environment: environment.Environment, kwargs): super().__init__(name, subdir, subproject, True, for_machine) unity_opt = environment.coredata.get_option(OptionKey('unity')) diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index 6b7cbcdd1..989877e67 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -31,6 +31,7 @@ if T.TYPE_CHECKING: from . import kwargs from .interpreter import Interpreter from ..envconfig import MachineInfo + from ..interpreterbase import SubProject from typing_extensions import TypedDict @@ -40,7 +41,7 @@ if T.TYPE_CHECKING: def extract_required_kwarg(kwargs: 'kwargs.ExtractRequired', - subproject: str, + subproject: 'SubProject', feature_check: T.Optional[FeatureCheckBase] = None, default: bool = True) -> T.Tuple[bool, bool, T.Optional[str]]: val = kwargs.get('required', default) diff --git a/mesonbuild/interpreter/primitives/range.py b/mesonbuild/interpreter/primitives/range.py index 9054278b2..a35470c8b 100644 --- a/mesonbuild/interpreter/primitives/range.py +++ b/mesonbuild/interpreter/primitives/range.py @@ -10,8 +10,11 @@ from ...interpreterbase import ( InvalidArguments, ) +if T.TYPE_CHECKING: + from ...interpreterbase import SubProject + class RangeHolder(MesonInterpreterObject, IterableObject): - def __init__(self, start: int, stop: int, step: int, *, subproject: str) -> None: + def __init__(self, start: int, stop: int, step: int, *, subproject: 'SubProject') -> None: super().__init__(subproject=subproject) self.range = range(start, stop, step) self.operators.update({ diff --git a/mesonbuild/interpreterbase/__init__.py b/mesonbuild/interpreterbase/__init__.py index 7c4b1db3b..13f55e5d3 100644 --- a/mesonbuild/interpreterbase/__init__.py +++ b/mesonbuild/interpreterbase/__init__.py @@ -57,6 +57,8 @@ __all__ = [ 'InterpreterBase', + 'SubProject', + 'TV_fw_var', 'TV_fw_args', 'TV_fw_kwargs', @@ -91,6 +93,8 @@ from .baseobjects import ( TYPE_key_resolver, TYPE_HoldableTypes, + SubProject, + HoldableTypes, ) diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py index ca17481a1..7186001f7 100644 --- a/mesonbuild/interpreterbase/baseobjects.py +++ b/mesonbuild/interpreterbase/baseobjects.py @@ -39,6 +39,8 @@ TYPE_kwargs = T.Dict[str, TYPE_var] TYPE_nkwargs = T.Dict[str, TYPE_nvar] TYPE_key_resolver = T.Callable[[mparser.BaseNode], str] +SubProject = T.NewType('SubProject', str) + if T.TYPE_CHECKING: from typing_extensions import Protocol __T = T.TypeVar('__T', bound=TYPE_var, contravariant=True) @@ -47,7 +49,7 @@ if T.TYPE_CHECKING: def __call__(self, other: __T) -> TYPE_var: ... class InterpreterObject: - def __init__(self, *, subproject: T.Optional[str] = None) -> None: + def __init__(self, *, subproject: T.Optional['SubProject'] = None) -> None: self.methods: T.Dict[ str, T.Callable[[T.List[TYPE_var], TYPE_kwargs], TYPE_var] @@ -63,7 +65,7 @@ class InterpreterObject: # Current node set during a method call. This can be used as location # when printing a warning message during a method call. self.current_node: mparser.BaseNode = None - self.subproject: str = subproject or '' + self.subproject = subproject or SubProject('') # Some default operators supported by all objects self.operators.update({ diff --git a/mesonbuild/interpreterbase/decorators.py b/mesonbuild/interpreterbase/decorators.py index c6fcaa018..f104ac6ba 100644 --- a/mesonbuild/interpreterbase/decorators.py +++ b/mesonbuild/interpreterbase/decorators.py @@ -27,8 +27,9 @@ import copy import typing as T if T.TYPE_CHECKING: from .. import mparser + from .interpreterbase import SubProject -def get_callee_args(wrapped_args: T.Sequence[T.Any]) -> T.Tuple['mparser.BaseNode', T.List['TYPE_var'], 'TYPE_kwargs', str]: +def get_callee_args(wrapped_args: T.Sequence[T.Any]) -> T.Tuple['mparser.BaseNode', T.List['TYPE_var'], 'TYPE_kwargs', 'SubProject']: # First argument could be InterpreterBase, InterpreterObject or ModuleObject. # In the case of a ModuleObject it is the 2nd argument (ModuleState) that # contains the needed information. @@ -600,7 +601,7 @@ class FeatureCheckBase(metaclass=abc.ABCMeta): def check_version(target_version: str, feature_version: str) -> bool: pass - def use(self, subproject: str) -> None: + def use(self, subproject: 'SubProject') -> None: tv = self.get_target_version(subproject) # No target version if tv == '': @@ -668,7 +669,7 @@ class FeatureCheckBase(metaclass=abc.ABCMeta): return T.cast(TV_func, wrapped) @classmethod - def single_use(cls, feature_name: str, version: str, subproject: str, + def single_use(cls, feature_name: str, version: str, subproject: 'SubProject', extra_message: str = '', location: T.Optional['mparser.BaseNode'] = None) -> None: """Oneline version that instantiates and calls use().""" cls(feature_name, version, extra_message, location).use(subproject) diff --git a/mesonbuild/interpreterbase/interpreterbase.py b/mesonbuild/interpreterbase/interpreterbase.py index 99f661921..bd7d7f908 100644 --- a/mesonbuild/interpreterbase/interpreterbase.py +++ b/mesonbuild/interpreterbase/interpreterbase.py @@ -26,6 +26,8 @@ from .baseobjects import ( ObjectHolder, IterableObject, + SubProject, + TYPE_var, TYPE_kwargs, @@ -73,7 +75,7 @@ FunctionType = T.Dict[ ] class InterpreterBase: - def __init__(self, source_root: str, subdir: str, subproject: str): + def __init__(self, source_root: str, subdir: str, subproject: 'SubProject'): self.source_root = source_root self.funcs: FunctionType = {} self.builtin: T.Dict[str, InterpreterObject] = {} diff --git a/mesonbuild/optinterpreter.py b/mesonbuild/optinterpreter.py index 6155c2fa5..46a4445f2 100644 --- a/mesonbuild/optinterpreter.py +++ b/mesonbuild/optinterpreter.py @@ -22,6 +22,7 @@ from . import mlog from .interpreterbase import FeatureNew, typed_pos_args, typed_kwargs, ContainerTypeInfo, KwargInfo, permittedKwargs if T.TYPE_CHECKING: from .interpreterbase import TYPE_var, TYPE_kwargs + from .interpreterbase import SubProject from typing_extensions import TypedDict FuncOptionArgs = TypedDict('FuncOptionArgs', { 'type': str, @@ -50,7 +51,7 @@ optname_regex = re.compile('[^a-zA-Z0-9_-]') class OptionInterpreter: - def __init__(self, subproject: str) -> None: + def __init__(self, subproject: 'SubProject') -> None: self.options: 'coredata.KeyedOptionDictType' = {} self.subproject = subproject self.option_types = {'string': self.string_parser, diff --git a/mesonbuild/wrap/wrap.py b/mesonbuild/wrap/wrap.py index 21eb9d613..51632fdf2 100644 --- a/mesonbuild/wrap/wrap.py +++ b/mesonbuild/wrap/wrap.py @@ -35,6 +35,7 @@ from . import WrapMode from .. import coredata from ..mesonlib import quiet_git, GIT, ProgressBar, MesonException, windows_proof_rmtree from ..interpreterbase import FeatureNew +from ..interpreterbase import SubProject from .. import mesonlib if T.TYPE_CHECKING: @@ -95,7 +96,7 @@ class WrapNotFoundException(WrapException): class PackageDefinition: def __init__(self, fname: str, subproject: str = ''): self.filename = fname - self.subproject = subproject + self.subproject = SubProject(subproject) self.type = None # type: T.Optional[str] self.values = {} # type: T.Dict[str, str] self.provided_deps = {} # type: T.Dict[str, T.Optional[str]]