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.
pull/13087/head
Xavier Claessens 4 weeks ago committed by Jussi Pakkanen
parent 1dcffb635f
commit 9f02d0a3e5
  1. 2
      docs/markdown/Configuration.md
  2. 12
      docs/yaml/objects/cfg_data.yaml
  3. 13
      docs/yaml/objects/env.yaml
  4. 26
      mesonbuild/interpreter/interpreterobjects.py
  5. 4
      mesonbuild/interpreterbase/_unholder.py
  6. 21
      mesonbuild/interpreterbase/baseobjects.py
  7. 7
      test cases/common/113 interpreter copy mutable var on assignment/check_env.py
  8. 34
      test cases/common/113 interpreter copy mutable var on assignment/meson.build
  9. 0
      test cases/failing/70 configuration immutable/input
  10. 12
      test cases/failing/70 configuration immutable/meson.build
  11. 7
      test cases/failing/70 configuration immutable/test.json

@ -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.

@ -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

@ -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:

@ -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)

@ -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

@ -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]

@ -0,0 +1,7 @@
#!/usr/bin/env python3
import os
import sys
if sys.argv[1] not in os.environ:
exit(42)

@ -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)

@ -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')

@ -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."
}
]
}
Loading…
Cancel
Save