summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorXavier Claessens <xavier.claessens@collabora.com>2024-04-05 08:55:20 -0700
committerJussi Pakkanen <jpakkane@gmail.com>2024-04-14 19:40:02 +0300
commit9f02d0a3e5a5ffc82256391c244b1af38e41ef78 (patch)
treeabd84b53919405a6ea3de5494720e88df7ae23c1
parent1dcffb635f3ba3dae63e6aa1b61d3cf56c9cc49b (diff)
downloadmeson-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.md2
-rw-r--r--docs/yaml/objects/cfg_data.yaml12
-rw-r--r--docs/yaml/objects/env.yaml13
-rw-r--r--mesonbuild/interpreter/interpreterobjects.py26
-rw-r--r--mesonbuild/interpreterbase/_unholder.py4
-rw-r--r--mesonbuild/interpreterbase/baseobjects.py21
-rwxr-xr-xtest cases/common/113 interpreter copy mutable var on assignment/check_env.py7
-rw-r--r--test cases/common/113 interpreter copy mutable var on assignment/meson.build34
-rw-r--r--test cases/failing/70 configuration immutable/input0
-rw-r--r--test cases/failing/70 configuration immutable/meson.build12
-rw-r--r--test cases/failing/70 configuration immutable/test.json7
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."
- }
- ]
-}