Repository: SCons/scons

Issue:
Change SConscript() default behavoir to error if SConscript is not present.
This is the completion of deprecation of old behavior.
When we introduced `must_exist` flag, the intent was to flip the default to error out and allow users to specify `must_exist=False` in the case it was ok for the file to not exist.


Discussion:
Should this completely clean out the warnings setup leaving just "fail unless `must_exist` is false", or leave in the warnings setup and flip the default, so that the cmdline options are still accepted but maybe don't mean anything?  (I'd need ot think about the latter)
Ok so we added warnings in 4.2.0 and that was 07/31/2021

I'd say flip the default and remove this bit? (from SConscript.py `handle_missing_SConscript()`)

```        if SCons.Script._warn_missing_sconscript_deprecated:
            msg = (
                "Calling missing SConscript without error is deprecated.\n"
                "Transition by adding must_exist=False to SConscript calls.\n"
                "Missing SConscript '%s'" % f.get_internal_path()
            )
```

I think that means next version must be 4.4.0

https://github.com/SCons/scons/wiki/DeprecationCycle
?
Sure, this part of the change is easy enough:
```python
diff --git a/SCons/Script/SConscript.py b/SCons/Script/SConscript.py
index 61881515e..992cf95cc 100644
--- a/SCons/Script/SConscript.py
+++ b/SCons/Script/SConscript.py
@@ -145,7 +145,7 @@ def Return(*vars, **kw):
 
 stack_bottom = '% Stack boTTom %' # hard to define a variable w/this name :)
 
-def handle_missing_SConscript(f, must_exist=None):
+def handle_missing_SConscript(f, must_exist=True):
     """Take appropriate action on missing file in SConscript() call.
 
     Print a warning or raise an exception on missing file, unless
@@ -163,22 +163,10 @@ def handle_missing_SConscript(f, must_exist=None):
           :data:`SCons.Script._no_missing_sconscript` is true.
     """
 
-    if must_exist or (SCons.Script._no_missing_sconscript and must_exist is not False):
-        msg = "Fatal: missing SConscript '%s'" % f.get_internal_path()
+    if must_exist:
+        msg = f"Fatal: missing SConscript '{f.get_internal_path()}'"
         raise SCons.Errors.UserError(msg)
 
-    if must_exist is None:
-        if SCons.Script._warn_missing_sconscript_deprecated:
-            msg = (
-                "Calling missing SConscript without error is deprecated.\n"
-                "Transition by adding must_exist=False to SConscript calls.\n"
-                "Missing SConscript '%s'" % f.get_internal_path()
-            )
-            SCons.Warnings.warn(SCons.Warnings.MissingSConscriptWarning, msg)
-            SCons.Script._warn_missing_sconscript_deprecated = False
-        else:
-            msg = "Ignoring missing SConscript '%s'" % f.get_internal_path()
-            SCons.Warnings.warn(SCons.Warnings.MissingSConscriptWarning, msg)
 
 def _SConscript(fs, *files, **kw):
     top = fs.Top
```

But leaves the question of what to do with other bits: the logic around `SCons.Script._no_missing_sconscript` - the flag setting and warning settings; and the command-line option to fiddle it.

Resolving patch:
diff --git a/SCons/Script/SConscript.py b/SCons/Script/SConscript.py
--- a/SCons/Script/SConscript.py
+++ b/SCons/Script/SConscript.py
@@ -145,40 +145,32 @@ def Return(*vars, **kw):
 
 stack_bottom = '% Stack boTTom %' # hard to define a variable w/this name :)
 
-def handle_missing_SConscript(f, must_exist=None):
+def handle_missing_SConscript(f: str, must_exist: bool = True) -> None:
     """Take appropriate action on missing file in SConscript() call.
 
     Print a warning or raise an exception on missing file, unless
-    missing is explicitly allowed by the *must_exist* value.
-    On first warning, print a deprecation message.
+    missing is explicitly allowed by the *must_exist* parameter or by
+    a global flag.
 
     Args:
-        f (str): path of missing configuration file
-        must_exist (bool): if true, fail.  If false, but not ``None``,
-          allow the file to be missing.  The default is ``None``,
-          which means issue the warning.  The default is deprecated.
+        f: path to missing configuration file
+        must_exist: if true (the default), fail.  If false
+          do nothing, allowing a build to declare it's okay to be missing.
 
     Raises:
-        UserError: if *must_exist* is true or if global
+       UserError: if *must_exist* is true or if global
           :data:`SCons.Script._no_missing_sconscript` is true.
+
+    .. versionchanged: 4.6.0
+       Changed default from False.
     """
+    if not must_exist:  # explicitly set False: ok
+        return
+    if not SCons.Script._no_missing_sconscript:  # system default changed: ok
+        return
+    msg = f"missing SConscript file {f.get_internal_path()!r}"
+    raise SCons.Errors.UserError(msg)
 
-    if must_exist or (SCons.Script._no_missing_sconscript and must_exist is not False):
-        msg = "Fatal: missing SConscript '%s'" % f.get_internal_path()
-        raise SCons.Errors.UserError(msg)
-
-    if must_exist is None:
-        if SCons.Script._warn_missing_sconscript_deprecated:
-            msg = (
-                "Calling missing SConscript without error is deprecated.\n"
-                "Transition by adding must_exist=False to SConscript calls.\n"
-                "Missing SConscript '%s'" % f.get_internal_path()
-            )
-            SCons.Warnings.warn(SCons.Warnings.MissingSConscriptWarning, msg)
-            SCons.Script._warn_missing_sconscript_deprecated = False
-        else:
-            msg = "Ignoring missing SConscript '%s'" % f.get_internal_path()
-            SCons.Warnings.warn(SCons.Warnings.MissingSConscriptWarning, msg)
 
 def _SConscript(fs, *files, **kw):
     top = fs.Top
@@ -294,7 +286,7 @@ def _SConscript(fs, *files, **kw):
                             call_stack[-1].globals.update({__file__:old_file})
 
                 else:
-                    handle_missing_SConscript(f, kw.get('must_exist', None))
+                    handle_missing_SConscript(f, kw.get('must_exist', True))
 
         finally:
             SCons.Script.sconscript_reading = SCons.Script.sconscript_reading - 1
diff --git a/SCons/Script/__init__.py b/SCons/Script/__init__.py
--- a/SCons/Script/__init__.py
+++ b/SCons/Script/__init__.py
@@ -269,10 +269,10 @@ def HelpFunction(text, append: bool=False) -> None:
 # Will be non-zero if we are reading an SConscript file.
 sconscript_reading = 0
 
-_no_missing_sconscript = False
-_warn_missing_sconscript_deprecated = True
+_no_missing_sconscript = True
+_warn_missing_sconscript_deprecated = False  # TODO: now unused
 
-def set_missing_sconscript_error(flag: int=1):
+def set_missing_sconscript_error(flag: bool = True) -> bool:
     """Set behavior on missing file in SConscript() call.
 
     Returns:
diff --git a/SCons/Warnings.py b/SCons/Warnings.py
--- a/SCons/Warnings.py
+++ b/SCons/Warnings.py
@@ -70,6 +70,7 @@ class LinkWarning(WarningOnByDefault):
 class MisleadingKeywordsWarning(WarningOnByDefault):
     pass
 
+# TODO: no longer needed, now an error instead of warning. Leave for a bit.
 class MissingSConscriptWarning(WarningOnByDefault):
     pass
 


Test patch:
diff --git a/test/SConscript/must_exist_deprecation.py b/test/Removed/Old/SConscript-must_exist_deprecation.py
similarity index 100%
rename from test/SConscript/must_exist_deprecation.py
rename to test/Removed/Old/SConscript-must_exist_deprecation.py
diff --git a/test/option/warn-missing-sconscript.py b/test/Removed/Old/warn-missing-sconscript.py
similarity index 88%
rename from test/option/warn-missing-sconscript.py
rename to test/Removed/Old/warn-missing-sconscript.py
--- a/test/option/warn-missing-sconscript.py
+++ b/test/Removed/Old/warn-missing-sconscript.py
@@ -61,14 +61,10 @@ def build(target, source, env):
 #scons: warning: Ignoring missing SConscript 'no_such_file'
 "" + TestSCons.file_expr
 
-test.run(arguments = '--warn=missing-sconscript .', stderr = expect)
-
-test.run(arguments = '--warn=no-missing-sconscript .', stderr = "")
-
-test.run(arguments = 'WARN=missing-sconscript .', stderr = expect)
-
-test.run(arguments = 'WARN=no-missing-sconscript .', stderr = "")
-
+test.run(arguments='--warn=missing-sconscript .', stderr=expect)
+test.run(arguments='--warn=no-missing-sconscript .', stderr="")
+test.run(arguments='WARN=missing-sconscript .', stderr=expect)
+test.run(arguments='WARN=no-missing-sconscript .', stderr="")
 
 test.pass_test()
 
diff --git a/test/SConscript/must_exist.py b/test/SConscript/must_exist.py
--- a/test/SConscript/must_exist.py
+++ b/test/SConscript/must_exist.py
@@ -37,76 +37,93 @@
 # this gives us traceability of the line responsible
 SConstruct_path = test.workpath('SConstruct')
 test.write(SConstruct_path, """\
-import SCons
-from SCons.Warnings import _warningOut
 import sys
+import traceback
+
+import SCons.Script
+from SCons.Errors import UserError
+from SCons.Script.Main import find_deepest_user_frame
 
 DefaultEnvironment(tools=[])
-# 1. 1st default call should succeed with deprecation warning
-try:
-    SConscript('missing/SConscript')
-except SCons.Errors.UserError as e:
-    if _warningOut:
-        _warningOut(e)
-# 2. 2nd default call should succeed with warning (no depr)
+
+def user_error(e):
+    "Synthesize msg from UserError."
+    # Borrowed from SCons.Script._scons_user_error which we don't use
+    # because it exits - we only want the message.
+    etype, value, tb = sys.exc_info()
+    filename, lineno, routine, _ = find_deepest_user_frame(traceback.extract_tb(tb))
+    sys.stderr.write(f"\\nscons: *** {value}\\n")
+    sys.stderr.write(f'File "{filename}", line {lineno}, in {routine}\\n')
+
+# 1. Call with defaults raises exception
 try:
-    SConscript('missing/SConscript')
-except SCons.Errors.UserError as e:
-    if _warningOut:
-        _warningOut(e)
-# 3. must_exist True call should raise exception
+    SConscript("missing/SConscript")
+except UserError as e:
+    user_error(e)
+
+# 2. Call with must_exist=True raises exception
 try:
-    SConscript('missing/SConscript', must_exist=True)
-except SCons.Errors.UserError as e:
-    if _warningOut:
-        _warningOut(e)
-# 4. must_exist False call should succeed silently
+    SConscript("missing/SConscript", must_exist=True)
+except UserError as e:
+    user_error(e)
+
+# 3. Call with must_exist=False call should succeed silently
 try:
-    SConscript('missing/SConscript', must_exist=False)
-except SCons.Errors.UserError as e:
-    if _warningOut:
-        _warningOut(e)
-# 5. with system setting changed, should raise exception
-SCons.Script.set_missing_sconscript_error()
+    SConscript("missing/SConscript", must_exist=False)
+except UserError as e:
+    user_error(e)
+
+# 4. with system setting changed, should succeed silently
+SCons.Script.set_missing_sconscript_error(flag=False)
 try:
-    SConscript('missing/SConscript')
-except SCons.Errors.UserError as e:
-    if _warningOut:
-        _warningOut(e)
-# 6. must_exist=False overrides system setting, should emit warning
+    SConscript("missing/SConscript")
+except UserError as e:
+    user_error(e)
+
+# 5. must_exist=True "wins" over system setting
 try:
-    SConscript('missing/SConscript', must_exist=False)
-except SCons.Errors.UserError as e:
-    if _warningOut:
-        _warningOut(e)
-""")
-
-# we should see two exceptions as "Fatal" and
-# and see four warnings, the first having the depr message
-# need to build the path in the expected msg in an OS-agnostic way
-missing = os.path.normpath('missing/SConscript')
-warn1 = """
-scons: warning: Calling missing SConscript without error is deprecated.
-Transition by adding must_exist=False to SConscript calls.
-Missing SConscript '{}'
-""".format(missing) + test.python_file_line(SConstruct_path, 8)
-
-warn2 = """
-scons: warning: Ignoring missing SConscript '{}'
-""".format(missing) + test.python_file_line(SConstruct_path, 14)
-
-err1 = """
-scons: warning: Fatal: missing SConscript '{}'
-""".format(missing) + test.python_file_line(SConstruct_path, 23)
-
-err2 = """
-scons: warning: Fatal: missing SConscript '{}'
-""".format(missing) + test.python_file_line(SConstruct_path, 36)
+    SConscript("missing/SConscript", must_exist=True)
+except UserError as e:
+    user_error(e)
+""",
+)
+
+missing = os.path.join("missing", "SConscript")
+err1 = f"""
+scons: *** missing SConscript file {missing!r}
+""" + test.python_file_line(
+    SConstruct_path, 21
+)
+
+err2 = f"""
+scons: *** missing SConscript file {missing!r}
+""" + test.python_file_line(
+    SConstruct_path, 27
+)
+
+err3 = f"""
+scons: *** missing SConscript file {missing!r}
+""" + test.python_file_line(
+    SConstruct_path, 33
+)
+
+err4 = f"""
+scons: *** missing SConscript file {missing!r}
+""" + test.python_file_line(
+    SConstruct_path, 40
+)
+
+err5 = f"""
+scons: *** missing SConscript file {missing!r}
+""" + test.python_file_line(
+    SConstruct_path, 46
+)
 
 nowarn = ""
 
-expect_stderr = warn1 + warn2 + err1 + nowarn + err2 + nowarn
-test.run(arguments = ".", stderr = expect_stderr)
+# of the five tests, we actually expect fails from 1 and 2
+expect_stderr = err1 + err2 + nowarn + nowarn + nowarn
+test.run(arguments=".", stderr=expect_stderr)
 test.pass_test()
 
 # Local Variables:
diff --git a/test/option/option-f.py b/test/option/option-f.py
--- a/test/option/option-f.py
+++ b/test/option/option-f.py
@@ -34,21 +34,24 @@
 subdir_BuildThis = os.path.join('subdir', 'Buildthis')
 
 test.write('SConscript', """
-DefaultEnvironment(tools=[])
 import os
+
+DefaultEnvironment(tools=[])
 print("SConscript " + os.getcwd())
 """)
 
 test.write(subdir_BuildThis, """
-DefaultEnvironment(tools=[])
 import os
-print("subdir/BuildThis "+ os.getcwd())
+
+DefaultEnvironment(tools=[])
+print("subdir/BuildThis " + os.getcwd())
 """)
 
 test.write('Build2', """
-DefaultEnvironment(tools=[])
 import os
-print("Build2 "+ os.getcwd())
+
+DefaultEnvironment(tools=[])
+print("Build2 " + os.getcwd())
 """)
 
 wpath = test.workpath()
@@ -56,14 +59,15 @@
 test.run(
     arguments='-f SConscript .',
     stdout=test.wrap_stdout(
-        read_str='SConscript %s\n' % wpath, build_str="scons: `.' is up to date.\n"
+        read_str=f'SConscript {wpath}\n',
+        build_str="scons: `.' is up to date.\n"
     ),
 )
 
 test.run(
-    arguments='-f %s .' % subdir_BuildThis,
+    arguments=f'-f {subdir_BuildThis} .',
     stdout=test.wrap_stdout(
-        read_str='subdir/BuildThis %s\n' % wpath,
+        read_str=f'subdir/BuildThis {wpath}\n',
         build_str="scons: `.' is up to date.\n",
     ),
 )
@@ -71,14 +75,15 @@
 test.run(
     arguments='--file=SConscript .',
     stdout=test.wrap_stdout(
-        read_str='SConscript %s\n' % wpath, build_str="scons: `.' is up to date.\n"
+        read_str=f'SConscript {wpath}\n',
+        build_str="scons: `.' is up to date.\n"
     ),
 )
 
 test.run(
-    arguments='--file=%s .' % subdir_BuildThis,
+    arguments=f'--file={subdir_BuildThis} .',
     stdout=test.wrap_stdout(
-        read_str='subdir/BuildThis %s\n' % wpath,
+        read_str=f'subdir/BuildThis {wpath}\n',
         build_str="scons: `.' is up to date.\n",
     ),
 )
@@ -86,14 +91,15 @@
 test.run(
     arguments='--makefile=SConscript .',
     stdout=test.wrap_stdout(
-        read_str='SConscript %s\n' % wpath, build_str="scons: `.' is up to date.\n"
+        read_str=f'SConscript {wpath}\n',
+        build_str="scons: `.' is up to date.\n"
     ),
 )
 
 test.run(
-    arguments='--makefile=%s .' % subdir_BuildThis,
+    arguments=f'--makefile={subdir_BuildThis} .',
     stdout=test.wrap_stdout(
-        read_str='subdir/BuildThis %s\n' % wpath,
+        read_str=f'subdir/BuildThis {wpath}\n',
         build_str="scons: `.' is up to date.\n",
     ),
 )
@@ -101,14 +107,15 @@
 test.run(
     arguments='--sconstruct=SConscript .',
     stdout=test.wrap_stdout(
-        read_str='SConscript %s\n' % wpath, build_str="scons: `.' is up to date.\n"
+        read_str=f'SConscript {wpath}\n',
+        build_str="scons: `.' is up to date.\n"
     ),
 )
 
 test.run(
-    arguments='--sconstruct=%s .' % subdir_BuildThis,
+    arguments=f'--sconstruct={subdir_BuildThis} .',
     stdout=test.wrap_stdout(
-        read_str='subdir/BuildThis %s\n' % wpath,
+        read_str=f'subdir/BuildThis {wpath}\n',
         build_str="scons: `.' is up to date.\n",
     ),
 )
@@ -121,28 +128,21 @@
 print("STDIN " + os.getcwd())
 """,
     stdout=test.wrap_stdout(
-        read_str='STDIN %s\n' % wpath, build_str="scons: `.' is up to date.\n"
+        read_str=f'STDIN {wpath}\n',
+        build_str="scons: `.' is up to date.\n"
     ),
 )
 
 expect = test.wrap_stdout(
-    read_str='Build2 %s\nSConscript %s\n' % (wpath, wpath),
+    read_str=f'Build2 {wpath}\nSConscript {wpath}\n',
     build_str="scons: `.' is up to date.\n",
 )
 test.run(arguments='-f Build2 -f SConscript .', stdout=expect)
 
-test.run(
-    arguments='-f no_such_file .',
-    stdout=test.wrap_stdout("scons: `.' is up to date.\n"),
-    stderr=None,
-)
-
-expect = """
-scons: warning: Calling missing SConscript without error is deprecated.
-Transition by adding must_exist=False to SConscript calls.
-Missing SConscript 'no_such_file'"""
-stderr = test.stderr()
-test.must_contain_all(test.stderr(), expect)
+missing = "no_such_file"
+test.run(arguments=f"-f {missing} .", status=2, stderr=None)
+expect = [f"scons: *** missing SConscript file {missing!r}"]
+test.must_contain_all_lines(test.stderr(), expect)
 
 test.pass_test()