diff --git a/CHANGES.txt b/CHANGES.txt index 9073ec97cb..bc82207bbe 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -45,6 +45,16 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER falls back to the field, which names the XSL-NS branch outright. + From Adam Matthews: + - SConstruct and SConscript files (including one read from stdin), + site_init.py and variables files are no longer compiled with the + future flags of the SCons module that runs them. Since + "from __future__ import annotations" was added to SConscript.py, + Main.py and Variables/__init__.py, annotations in those files had + become strings, which among other things stopped dataclasses from + recognising InitVar fields ("__post_init__() missing 1 required + positional argument"). + From Andrew Morrow: - Remove the legacy job scheduler and the "legacy_sched_deprecated" experimental feature flag. diff --git a/NEWS.d/changes/NNNN.bugfix b/NEWS.d/changes/NNNN.bugfix new file mode 100644 index 0000000000..7dcc62fa36 --- /dev/null +++ b/NEWS.d/changes/NNNN.bugfix @@ -0,0 +1,5 @@ +SConstruct and SConscript files, site_init.py and variables files are no +longer compiled with the future flags of the SCons modules that run them. +Since SCons 4.11 those modules use "from __future__ import annotations", +which made annotations in build scripts strings and stopped dataclasses +from recognising InitVar fields. diff --git a/RELEASE.txt b/RELEASE.txt index 09a83ba7e7..7a0fc68b39 100644 --- a/RELEASE.txt +++ b/RELEASE.txt @@ -65,6 +65,12 @@ FIXES CC="ccache clang". Generating the ninja file failed with "Could not find tool $CC". +- SConstruct and SConscript files, site_init.py and variables files are no + longer compiled with the future flags of the SCons modules that run them. + Since SCons 4.11 those modules use "from __future__ import annotations", + which made annotations in build scripts strings and, for example, stopped + dataclasses from recognising InitVar fields. + IMPROVEMENTS ------------ diff --git a/SCons/Script/Main.py b/SCons/Script/Main.py index 3ea51bdacd..bb1a7d1b8e 100644 --- a/SCons/Script/Main.py +++ b/SCons/Script/Main.py @@ -845,7 +845,8 @@ def _load_site_scons_dir(topdir, site_dir_name=None): with open(spec.origin) as f: code = f.read() try: - codeobj = compile(code, spec.name, "exec") + # dont_inherit: site_init.py must not pick up this module's future flags + codeobj = compile(code, spec.name, "exec", dont_inherit=True) exec(codeobj, site_m) except KeyboardInterrupt: raise diff --git a/SCons/Script/SConscript.py b/SCons/Script/SConscript.py index e62563fc6a..5e62ae2c65 100644 --- a/SCons/Script/SConscript.py +++ b/SCons/Script/SConscript.py @@ -190,7 +190,12 @@ def _SConscript(fs: FS, *files: str | Node, **kw) -> Any | list[Any]: try: SCons.Script.sconscript_reading = SCons.Script.sconscript_reading + 1 if fn == "-": - exec(sys.stdin.read(), call_stack[-1].globals) + # dont_inherit: SConscripts must not pick up this module's + # "from __future__ import annotations" (or any other future flag) + exec( + compile(sys.stdin.read(), "", 'exec', dont_inherit=True), + call_stack[-1].globals, + ) else: if isinstance(fn, SCons.Node.Node): f = fn @@ -281,7 +286,10 @@ def _SConscript(fs: FS, *files: str | Node, **kw) -> Any | list[Any]: _file_.close() if SCons.Debug.sconscript_trace: print("scons: Entering "+str(scriptname)) - exec(compile(scriptdata, scriptname, 'exec'), call_stack[-1].globals) + exec( + compile(scriptdata, scriptname, 'exec', dont_inherit=True), + call_stack[-1].globals, + ) if SCons.Debug.sconscript_trace: print("scons: Exiting "+str(scriptname)) except SConscriptReturn: diff --git a/SCons/Variables/__init__.py b/SCons/Variables/__init__.py index db6c7c3047..2c3e3d90ef 100644 --- a/SCons/Variables/__init__.py +++ b/SCons/Variables/__init__.py @@ -284,7 +284,13 @@ def Update(self, env, args: dict | None = None) -> None: temp_values['__name__'] = filename with open(filename) as f: contents = f.read() - exec(contents, {}, temp_values) + # dont_inherit: variables files must not pick up this + # module's future flags + exec( + compile(contents, filename, 'exec', dont_inherit=True), + {}, + temp_values, + ) finally: if dirname: del sys.path[0] diff --git a/test/SConscript/future-flags.py b/test/SConscript/future-flags.py new file mode 100644 index 0000000000..165012d9b3 --- /dev/null +++ b/test/SConscript/future-flags.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python +# +# MIT License +# +# Copyright The SCons Foundation +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be included +# in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE +# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +""" +Test that SConstruct, SConscript, site_init.py and variables files are not +compiled with the future flags of the SCons modules that run them. + +SCons/Script/SConscript.py has "from __future__ import annotations". If a +script inherits that flag, its annotations become strings, which among +other things stops dataclasses from recognising InitVar fields. +""" + +import TestSCons + +test = TestSCons.TestSCons() + +# reports whether an annotation in the script is a type or a string +CHECK = """\ +def _f(x: int): + pass +print("%s:", type(_f.__annotations__['x']).__name__) +""" + +INITVAR = """\ +from dataclasses import InitVar, dataclass + +@dataclass +class _C: + env: InitVar[int] + name: str + + def __post_init__(self, env): + self.seen = env + +print("InitVar:", _C(env=1, name="n").seen) +""" + +test.subdir('site_scons', 'sub') +test.write(['site_scons', 'site_init.py'], CHECK % "site_init") +test.write('vars.py', CHECK % "variables") +test.write(['sub', 'SConscript'], CHECK % "SConscript" + INITVAR) +test.write('SConstruct', CHECK % "SConstruct" + """\ +Environment(tools=[], variables=Variables('vars.py')) +SConscript('sub/SConscript') +""") + +# site_init.py is read before the SConscript files +expect = "site_init: type\n" + test.wrap_stdout( + read_str="SConstruct: type\n" + "variables: type\n" + "SConscript: type\n" + "InitVar: 1\n", + build_str="scons: `.' is up to date.\n", +) +test.run(arguments='.', stdout=expect) + +# an SConstruct read from standard input +expect = "site_init: type\n" + test.wrap_stdout( + read_str="stdin: type\n", + build_str="scons: `.' is up to date.\n", +) +test.run(arguments='-f - .', stdin=CHECK % "stdin", stdout=expect) + +test.pass_test()