diff options
| author | Xavier Claessens <xavier.claessens@collabora.com> | 2024-04-05 08:55:20 -0700 |
|---|---|---|
| committer | Jussi Pakkanen <jpakkane@gmail.com> | 2024-04-14 19:40:02 +0300 |
| commit | 9f02d0a3e5a5ffc82256391c244b1af38e41ef78 (patch) | |
| tree | abd84b53919405a6ea3de5494720e88df7ae23c1 | |
| parent | 1dcffb635f3ba3dae63e6aa1b61d3cf56c9cc49b (diff) | |
| download | meson-9f02d0a3e5a5ffc82256391c244b1af38e41ef78.tar.gz | |
Clarify mutable objects usage
Only Environment and ConfigurationData are mutable. However, only
ConfigurationData becomes immutable after first use which is
inconsistent.
This deprecates modification after first use of Environment object and
clarify documentation.
| -rw-r--r-- | docs/markdown/Configuration.md | 2 | ||||
| -rw-r--r-- | docs/yaml/objects/cfg_data.yaml | 12 | ||||
| -rw-r--r-- | docs/yaml/objects/env.yaml | 13 | ||||
| -rw-r--r-- | mesonbuild/interpreter/interpreterobjects.py | 26 | ||||
| -rw-r--r-- | mesonbuild/interpreterbase/_unholder.py | 4 | ||||
| -rw-r--r-- | mesonbuild/interpreterbase/baseobjects.py | 21 | ||||
| -rwxr-xr-x | test cases/common/113 interpreter copy mutable var on assignment/check_env.py | 7 | ||||
| -rw-r--r-- | test cases/common/113 interpreter copy mutable var on assignment/meson.build | 34 | ||||
| -rw-r--r-- | test cases/failing/70 configuration immutable/input | 0 | ||||
| -rw-r--r-- | test cases/failing/70 configuration immutable/meson.build | 12 | ||||
| -rw-r--r-- | test cases/failing/70 configuration immutable/test.json | 7 |
11 files changed, 102 insertions, 36 deletions
diff --git a/docs/markdown/Configuration.md b/docs/markdown/Configuration.md index 48f071e6c..b5875e55e 100644 --- a/docs/markdown/Configuration.md +++ b/docs/markdown/Configuration.md @@ -39,7 +39,7 @@ use a single `configuration_data` object as many times as you like, but it becomes immutable after being passed to the `configure_file` function. That is, after it has been used once to generate output the `set` function becomes unusable and trying to call it causes an error. -Copy of immutable `configuration_data` is still immutable. +*Since 1.5.0* Copy of immutable `configuration_data` is however mutable. For more complex configuration file generation Meson provides a second form. To use it, put a line like this in your configuration file. diff --git a/docs/yaml/objects/cfg_data.yaml b/docs/yaml/objects/cfg_data.yaml index 03abb1709..069cadbf6 100644 --- a/docs/yaml/objects/cfg_data.yaml +++ b/docs/yaml/objects/cfg_data.yaml @@ -1,10 +1,14 @@ name: cfg_data long_name: Configuration data object description: | - This object encapsulates - configuration values to be used for generating configuration files. A - more in-depth description can be found in the [the configuration wiki - page](Configuration.md). + This object encapsulates configuration values to be used for generating + configuration files. A more in-depth description can be found in the + [the configuration page](Configuration.md). + + This object becomes immutable after first use. This means that + calling set() or merge_from() will cause an error if this object has + already been used in any function arguments. However, assignment creates a + mutable copy. methods: - name: set diff --git a/docs/yaml/objects/env.yaml b/docs/yaml/objects/env.yaml index 714da4fe4..3b2e2a851 100644 --- a/docs/yaml/objects/env.yaml +++ b/docs/yaml/objects/env.yaml @@ -9,6 +9,11 @@ description: | on the same `varname`. Earlier Meson versions would warn and only the last operation took effect. + *Since 1.5.0* This object becomes immutable after first use. This means that + calling append(), prepend() or set() will cause a deprecation warning if this + object has already been used in any function arguments. However, assignment + creates a mutable copy. + example: | ```meson env = environment() @@ -18,6 +23,14 @@ example: | env.append('MY_PATH', '2') env.append('MY_PATH', '3') env.prepend('MY_PATH', '0') + + # Deprecated since 1.5.0 + run_command('script.py', env: env) + env.append('MY_PATH', '4') + + # Allowed and only env2 is modified + env2 = env + env2.append('MY_PATH', '4') ``` methods: diff --git a/mesonbuild/interpreter/interpreterobjects.py b/mesonbuild/interpreter/interpreterobjects.py index 5f712a573..edf785c0d 100644 --- a/mesonbuild/interpreter/interpreterobjects.py +++ b/mesonbuild/interpreter/interpreterobjects.py @@ -284,6 +284,7 @@ class EnvironmentVariablesHolder(ObjectHolder[mesonlib.EnvironmentVariables], Mu def __init__(self, obj: mesonlib.EnvironmentVariables, interpreter: 'Interpreter'): super().__init__(obj, interpreter) + MutableInterpreterObject.__init__(self) self.methods.update({'set': self.set_method, 'unset': self.unset_method, 'append': self.append_method, @@ -308,12 +309,14 @@ class EnvironmentVariablesHolder(ObjectHolder[mesonlib.EnvironmentVariables], Mu @typed_kwargs('environment.set', ENV_SEPARATOR_KW) def set_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None: name, values = args + self.check_used(self.interpreter, fatal=False) self.held_object.set(name, values, kwargs['separator']) @FeatureNew('environment.unset', '1.4.0') @typed_pos_args('environment.unset', str) @noKwargs def unset_method(self, args: T.Tuple[str], kwargs: TYPE_kwargs) -> None: + self.check_used(self.interpreter, fatal=False) self.held_object.unset(args[0]) @typed_pos_args('environment.append', str, varargs=str, min_varargs=1) @@ -321,6 +324,7 @@ class EnvironmentVariablesHolder(ObjectHolder[mesonlib.EnvironmentVariables], Mu def append_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None: name, values = args self.warn_if_has_name(name) + self.check_used(self.interpreter, fatal=False) self.held_object.append(name, values, kwargs['separator']) @typed_pos_args('environment.prepend', str, varargs=str, min_varargs=1) @@ -328,6 +332,7 @@ class EnvironmentVariablesHolder(ObjectHolder[mesonlib.EnvironmentVariables], Mu def prepend_method(self, args: T.Tuple[str, T.List[str]], kwargs: 'EnvironmentSeparatorKW') -> None: name, values = args self.warn_if_has_name(name) + self.check_used(self.interpreter, fatal=False) self.held_object.prepend(name, values, kwargs['separator']) @@ -338,6 +343,7 @@ class ConfigurationDataHolder(ObjectHolder[build.ConfigurationData], MutableInte def __init__(self, obj: build.ConfigurationData, interpreter: 'Interpreter'): super().__init__(obj, interpreter) + MutableInterpreterObject.__init__(self) self.methods.update({'set': self.set_method, 'set10': self.set10_method, 'set_quoted': self.set_quoted_method, @@ -349,32 +355,31 @@ class ConfigurationDataHolder(ObjectHolder[build.ConfigurationData], MutableInte }) def __deepcopy__(self, memo: T.Dict) -> 'ConfigurationDataHolder': - return ConfigurationDataHolder(copy.deepcopy(self.held_object), self.interpreter) - - def is_used(self) -> bool: - return self.held_object.used - - def __check_used(self) -> None: + obj = ConfigurationDataHolder(copy.deepcopy(self.held_object), self.interpreter) if self.is_used(): - raise InterpreterException("Can not set values on configuration object that has been used.") + # Copies of used ConfigurationData used to be immutable. It is now + # allowed but we need this flag to print a FeatureNew warning if + # that happens. + obj.mutable_feature_new = True + return obj @typed_pos_args('configuration_data.set', str, (str, int, bool)) @typed_kwargs('configuration_data.set', _CONF_DATA_SET_KWS) def set_method(self, args: T.Tuple[str, T.Union[str, int, bool]], kwargs: 'kwargs.ConfigurationDataSet') -> None: - self.__check_used() + self.check_used(self.interpreter) self.held_object.values[args[0]] = (args[1], kwargs['description']) @typed_pos_args('configuration_data.set_quoted', str, str) @typed_kwargs('configuration_data.set_quoted', _CONF_DATA_SET_KWS) def set_quoted_method(self, args: T.Tuple[str, str], kwargs: 'kwargs.ConfigurationDataSet') -> None: - self.__check_used() + self.check_used(self.interpreter) escaped_val = '\\"'.join(args[1].split('"')) self.held_object.values[args[0]] = (f'"{escaped_val}"', kwargs['description']) @typed_pos_args('configuration_data.set10', str, (int, bool)) @typed_kwargs('configuration_data.set10', _CONF_DATA_SET_KWS) def set10_method(self, args: T.Tuple[str, T.Union[int, bool]], kwargs: 'kwargs.ConfigurationDataSet') -> None: - self.__check_used() + self.check_used(self.interpreter) # bool is a subclass of int, so we need to check for bool explicitly. # We already have typed_pos_args checking that this is either a bool or # an int. @@ -437,6 +442,7 @@ class ConfigurationDataHolder(ObjectHolder[build.ConfigurationData], MutableInte @noKwargs def merge_from_method(self, args: T.Tuple[build.ConfigurationData], kwargs: TYPE_kwargs) -> None: from_object = args[0] + self.check_used(self.interpreter) self.held_object.values.update(from_object.values) diff --git a/mesonbuild/interpreterbase/_unholder.py b/mesonbuild/interpreterbase/_unholder.py index c62aafe8e..e32b77fc7 100644 --- a/mesonbuild/interpreterbase/_unholder.py +++ b/mesonbuild/interpreterbase/_unholder.py @@ -5,7 +5,7 @@ from __future__ import annotations import typing as T -from .baseobjects import InterpreterObject, MesonInterpreterObject, ObjectHolder, HoldableTypes +from .baseobjects import InterpreterObject, MesonInterpreterObject, ObjectHolder, HoldableTypes, MutableInterpreterObject from .exceptions import InvalidArguments from ..mesonlib import HoldableObject, MesonBugException @@ -13,6 +13,8 @@ if T.TYPE_CHECKING: from .baseobjects import TYPE_var def _unholder(obj: InterpreterObject) -> TYPE_var: + if isinstance(obj, MutableInterpreterObject): + obj.mark_used() if isinstance(obj, ObjectHolder): assert isinstance(obj.held_object, HoldableTypes) return obj.held_object diff --git a/mesonbuild/interpreterbase/baseobjects.py b/mesonbuild/interpreterbase/baseobjects.py index 9a119a98a..c6864a478 100644 --- a/mesonbuild/interpreterbase/baseobjects.py +++ b/mesonbuild/interpreterbase/baseobjects.py @@ -120,6 +120,27 @@ class MesonInterpreterObject(InterpreterObject): class MutableInterpreterObject: ''' Dummy class to mark the object type as mutable ''' + def __init__(self) -> None: + self.used = False + self.mutable_feature_new = False + + def mark_used(self) -> None: + self.used = True + + def is_used(self) -> bool: + return self.used + + def check_used(self, interpreter: Interpreter, fatal: bool = True) -> None: + from .decorators import FeatureDeprecated, FeatureNew + if self.is_used(): + if fatal: + raise InvalidArguments('Can not modify object after it has been used.') + FeatureDeprecated.single_use('Modify object after it has been used', '1.5.0', + interpreter.subproject, location=interpreter.current_node) + elif self.mutable_feature_new: + FeatureNew.single_use('Modify a copy of an immutable object', '1.5.0', + interpreter.subproject, location=interpreter.current_node) + self.mutable_feature_new = False HoldableTypes = (HoldableObject, int, bool, str, list, dict) TYPE_HoldableTypes = T.Union[TYPE_elementary, HoldableObject] diff --git a/test cases/common/113 interpreter copy mutable var on assignment/check_env.py b/test cases/common/113 interpreter copy mutable var on assignment/check_env.py new file mode 100755 index 000000000..034d29243 --- /dev/null +++ b/test cases/common/113 interpreter copy mutable var on assignment/check_env.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +import os +import sys + +if sys.argv[1] not in os.environ: + exit(42) diff --git a/test cases/common/113 interpreter copy mutable var on assignment/meson.build b/test cases/common/113 interpreter copy mutable var on assignment/meson.build index d414bfc74..3d4f3b039 100644 --- a/test cases/common/113 interpreter copy mutable var on assignment/meson.build +++ b/test cases/common/113 interpreter copy mutable var on assignment/meson.build @@ -1,4 +1,4 @@ -project('foo') +project('foo', meson_version: '>=1.5') a = configuration_data() a.set('HELLO', 1) @@ -10,6 +10,15 @@ assert(b.has('HELLO'), 'Original config data should be set on copy') configure_file(output : 'b.h', configuration : b) +testcase expect_error('Can not modify object after it has been used.') + b.set('WORLD', 1) +endtestcase + +# A copy of immutable object is mutable. This should print FeatureNew warning +# if meson_version is lower than 1.5. +c = b +c.set('WORLD', 1) + # This should still work, as we didn't use the original above but a copy! a.set('WORLD', 1) @@ -17,3 +26,26 @@ assert(a.has('WORLD'), 'New config data should have been set') assert(not b.has('WORLD'), 'New config data set should not affect var copied earlier') configure_file(output : 'a.h', configuration : a) + +env1 = environment() +env1.set('FOO', '1') +env2 = env1 +env1.set('BAR', '1') + +# FOO should be in env1 and env2 +run_command('check_env.py', 'FOO', env: env1, check: true) +run_command('check_env.py', 'FOO', env: env2, check: true) + +# BAR should be only in env1 +run_command('check_env.py', 'BAR', env: env1, check: true) +assert(run_command('check_env.py', 'BAR', env: env2, check: false).returncode() == 42) + +# This should print deprecation warning but still work +env1.set('PLOP', '1') +run_command('check_env.py', 'PLOP', env: env1, check: true) + +# A copy of used env should be mutable and not print warning +env3 = env1 +env3.set('BAZ', '1') +run_command('check_env.py', 'PLOP', env: env3, check: true) +run_command('check_env.py', 'BAZ', env: env3, check: true) diff --git a/test cases/failing/70 configuration immutable/input b/test cases/failing/70 configuration immutable/input deleted file mode 100644 index e69de29bb..000000000 --- a/test cases/failing/70 configuration immutable/input +++ /dev/null diff --git a/test cases/failing/70 configuration immutable/meson.build b/test cases/failing/70 configuration immutable/meson.build deleted file mode 100644 index b6cac4126..000000000 --- a/test cases/failing/70 configuration immutable/meson.build +++ /dev/null @@ -1,12 +0,0 @@ -project('configuration_data is immutable') - -a = configuration_data() - -configure_file( - configuration : a, - input : 'input', - output : 'output', -) - -still_immutable = a -still_immutable.set('hello', 'world') diff --git a/test cases/failing/70 configuration immutable/test.json b/test cases/failing/70 configuration immutable/test.json deleted file mode 100644 index fc735fa38..000000000 --- a/test cases/failing/70 configuration immutable/test.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "stdout": [ - { - "line": "test cases/failing/70 configuration immutable/meson.build:12:16: ERROR: Can not set values on configuration object that has been used." - } - ] -} |
