Skip to content

Commit

Permalink
Merge pull request #4528 from mwichmann/maint/Variables-path
Browse files Browse the repository at this point in the history
Variables cleanup: PathVariable
  • Loading branch information
bdbaddog authored May 15, 2024
2 parents bcf7158 + a1ca685 commit e853087
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 135 deletions.
92 changes: 52 additions & 40 deletions SCons/Variables/PathVariable.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
To be used whenever a user-specified path override setting should be allowed.
Arguments to PathVariable are:
* *key* - name of this option on the command line (e.g. "prefix")
* *help* - help string for option
* *default* - default value for this option
* *validator* - [optional] validator for option value. Predefined are:
* *key* - name of this variable on the command line (e.g. "prefix")
* *help* - help string for variable
* *default* - default value for this variable
* *validator* - [optional] validator for variable value. Predefined are:
* *PathAccept* - accepts any path setting; no validation
* *PathIsDir* - path must be an existing directory
Expand All @@ -40,8 +40,8 @@
The *validator* is a function that is called and which should return
True or False to indicate if the path is valid. The arguments
to the validator function are: (*key*, *val*, *env*). *key* is the
name of the option, *val* is the path specified for the option,
and *env* is the environment to which the Options have been added.
name of the variable, *val* is the path specified for the variable,
and *env* is the environment to which the Variables have been added.
Usage example::
Expand Down Expand Up @@ -74,81 +74,93 @@

import os
import os.path
from typing import Tuple, Callable
from typing import Callable, Optional, Tuple

import SCons.Errors
import SCons.Util

__all__ = ['PathVariable',]

class _PathVariableClass:
"""Class implementing path variables.
This class exists mainly to expose the validators without code having
to import the names: they will appear as methods of ``PathVariable``,
a statically created instance of this class, which is placed in
the SConscript namespace.
Instances are callable to produce a suitable variable tuple.
"""

@staticmethod
def PathAccept(key, val, env) -> None:
"""Accepts any path, no checking done."""
pass
"""Validate path with no checking."""
return

@staticmethod
def PathIsDir(key, val, env) -> None:
"""Validator to check if Path is a directory."""
if not os.path.isdir(val):
if os.path.isfile(val):
m = 'Directory path for option %s is a file: %s'
else:
m = 'Directory path for option %s does not exist: %s'
raise SCons.Errors.UserError(m % (key, val))
"""Validate path is a directory."""
if os.path.isdir(val):
return
if os.path.isfile(val):
msg = f'Directory path for variable {key!r} is a file: {val}'
else:
msg = f'Directory path for variable {key!r} does not exist: {val}'
raise SCons.Errors.UserError(msg)

@staticmethod
def PathIsDirCreate(key, val, env) -> None:
"""Validator to check if Path is a directory,
creating it if it does not exist."""
"""Validate path is a directory, creating if needed."""
if os.path.isdir(val):
return
try:
os.makedirs(val, exist_ok=True)
except FileExistsError:
m = 'Path for option %s is a file, not a directory: %s'
raise SCons.Errors.UserError(m % (key, val))
except PermissionError:
m = 'Path for option %s could not be created: %s'
raise SCons.Errors.UserError(m % (key, val))
except OSError:
m = 'Path for option %s could not be created: %s'
raise SCons.Errors.UserError(m % (key, val))
except FileExistsError as exc:
msg = f'Path for variable {key!r} is a file, not a directory: {val}'
raise SCons.Errors.UserError(msg) from exc
except (PermissionError, OSError) as exc:
msg = f'Path for variable {key!r} could not be created: {val}'
raise SCons.Errors.UserError(msg) from exc

@staticmethod
def PathIsFile(key, val, env) -> None:
"""Validator to check if Path is a file"""
"""Validate path is a file."""
if not os.path.isfile(val):
if os.path.isdir(val):
m = 'File path for option %s is a directory: %s'
msg = f'File path for variable {key!r} is a directory: {val}'
else:
m = 'File path for option %s does not exist: %s'
raise SCons.Errors.UserError(m % (key, val))
msg = f'File path for variable {key!r} does not exist: {val}'
raise SCons.Errors.UserError(msg)

@staticmethod
def PathExists(key, val, env) -> None:
"""Validator to check if Path exists"""
"""Validate path exists."""
if not os.path.exists(val):
m = 'Path for option %s does not exist: %s'
raise SCons.Errors.UserError(m % (key, val))
msg = f'Path for variable {key!r} does not exist: {val}'
raise SCons.Errors.UserError(msg)

def __call__(self, key, help, default, validator=None) -> Tuple[str, str, str, Callable, None]:
# lint: W0622: Redefining built-in 'help' (redefined-builtin)
def __call__(
self, key, help: str, default, validator: Optional[Callable] = None
) -> Tuple[str, str, str, Callable, None]:
"""Return a tuple describing a path list SCons Variable.
The input parameters describe a 'path list' option. Returns
The input parameters describe a 'path list' variable. Returns
a tuple with the correct converter and validator appended. The
result is usable for input to :meth:`Add`.
The *default* option specifies the default path to use if the
user does not specify an override with this option.
The *default* parameter specifies the default path to use if the
user does not specify an override with this variable.
*validator* is a validator, see this file for examples
"""
if validator is None:
validator = self.PathExists

if SCons.Util.is_List(key) or SCons.Util.is_Tuple(key):
helpmsg = '%s ( /path/to/%s )' % (help, key[0])
helpmsg = f'{help} ( /path/to/{key[0]} )'
else:
helpmsg = '%s ( /path/to/%s )' % (help, key)
helpmsg = f'{help} ( /path/to/{key} )'
return (key, helpmsg, default, validator, None)


Expand Down
110 changes: 45 additions & 65 deletions SCons/Variables/PathVariableTests.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,19 @@
import SCons.Variables

import TestCmd
from TestCmd import IS_WINDOWS

class PathVariableTestCase(unittest.TestCase):
def test_PathVariable(self) -> None:
"""Test PathVariable creation"""
opts = SCons.Variables.Variables()
opts.Add(SCons.Variables.PathVariable('test',
'test option help',
'test build variable help',
'/default/path'))

o = opts.options[0]
assert o.key == 'test', o.key
assert o.help == 'test option help ( /path/to/test )', repr(o.help)
assert o.help == 'test build variable help ( /path/to/test )', repr(o.help)
assert o.default == '/default/path', o.default
assert o.validator is not None, o.validator
assert o.converter is None, o.converter
Expand All @@ -48,30 +49,27 @@ def test_PathExists(self):
"""Test the PathExists validator"""
opts = SCons.Variables.Variables()
opts.Add(SCons.Variables.PathVariable('test',
'test option help',
'test build variable help',
'/default/path',
SCons.Variables.PathVariable.PathExists))

test = TestCmd.TestCmd(workdir='')
test.write('exists', 'exists\n')

o = opts.options[0]

o.validator('X', test.workpath('exists'), {})

dne = test.workpath('does_not_exist')
try:
with self.assertRaises(SCons.Errors.UserError) as cm:
o.validator('X', dne, {})
except SCons.Errors.UserError as e:
assert str(e) == 'Path for option X does not exist: %s' % dne, e
except:
raise Exception("did not catch expected UserError")
e = cm.exception
self.assertEqual(str(e), f"Path for variable 'X' does not exist: {dne}")

def test_PathIsDir(self):
"""Test the PathIsDir validator"""
opts = SCons.Variables.Variables()
opts.Add(SCons.Variables.PathVariable('test',
'test option help',
'test build variable help',
'/default/path',
SCons.Variables.PathVariable.PathIsDir))

Expand All @@ -80,30 +78,25 @@ def test_PathIsDir(self):
test.write('file', "file\n")

o = opts.options[0]

o.validator('X', test.workpath('dir'), {})

f = test.workpath('file')
try:
with self.assertRaises(SCons.Errors.UserError) as cm:
o.validator('X', f, {})
except SCons.Errors.UserError as e:
assert str(e) == 'Directory path for option X is a file: %s' % f, e
except:
raise Exception("did not catch expected UserError")
e = cm.exception
self.assertEqual(str(e), f"Directory path for variable 'X' is a file: {f}")

dne = test.workpath('does_not_exist')
try:
with self.assertRaises(SCons.Errors.UserError) as cm:
o.validator('X', dne, {})
except SCons.Errors.UserError as e:
assert str(e) == 'Directory path for option X does not exist: %s' % dne, e
except Exception as e:
raise Exception("did not catch expected UserError") from e
e = cm.exception
self.assertEqual(str(e), f"Directory path for variable 'X' does not exist: {dne}")

def test_PathIsDirCreate(self):
"""Test the PathIsDirCreate validator"""
opts = SCons.Variables.Variables()
opts.Add(SCons.Variables.PathVariable('test',
'test option help',
'test build variable help',
'/default/path',
SCons.Variables.PathVariable.PathIsDirCreate))

Expand All @@ -117,26 +110,26 @@ def test_PathIsDirCreate(self):
assert os.path.isdir(d)

f = test.workpath('file')
try:
with self.assertRaises(SCons.Errors.UserError) as cm:
o.validator('X', f, {})
except SCons.Errors.UserError as e:
assert str(e) == 'Path for option X is a file, not a directory: %s' % f, e
except Exception as e:
raise Exception("did not catch expected UserError") from e
e = cm.exception
self.assertEqual(str(e), f"Path for variable 'X' is a file, not a directory: {f}")

f = '/yyy/zzz' # this not exists and should fail to create
try:
# pick a directory path that can't be mkdir'd
if IS_WINDOWS:
f = r'\\noserver\noshare\yyy\zzz'
else:
f = '/yyy/zzz'
with self.assertRaises(SCons.Errors.UserError) as cm:
o.validator('X', f, {})
except SCons.Errors.UserError as e:
assert str(e) == 'Path for option X could not be created: %s' % f, e
except Exception as e:
raise Exception("did not catch expected UserError") from e
e = cm.exception
self.assertEqual(str(e), f"Path for variable 'X' could not be created: {f}")

def test_PathIsFile(self):
"""Test the PathIsFile validator"""
opts = SCons.Variables.Variables()
opts.Add(SCons.Variables.PathVariable('test',
'test option help',
'test build variable help',
'/default/path',
SCons.Variables.PathVariable.PathIsFile))

Expand All @@ -145,30 +138,25 @@ def test_PathIsFile(self):
test.write('file', "file\n")

o = opts.options[0]

o.validator('X', test.workpath('file'), {})

d = test.workpath('d')
try:
with self.assertRaises(SCons.Errors.UserError) as cm:
o.validator('X', d, {})
except SCons.Errors.UserError as e:
assert str(e) == 'File path for option X does not exist: %s' % d, e
except:
raise Exception("did not catch expected UserError")
e = cm.exception
self.assertEqual(str(e), f"File path for variable 'X' does not exist: {d}")

dne = test.workpath('does_not_exist')
try:
with self.assertRaises(SCons.Errors.UserError) as cm:
o.validator('X', dne, {})
except SCons.Errors.UserError as e:
assert str(e) == 'File path for option X does not exist: %s' % dne, e
except:
raise Exception("did not catch expected UserError")
e = cm.exception
self.assertEqual(str(e), f"File path for variable 'X' does not exist: {dne}")

def test_PathAccept(self) -> None:
"""Test the PathAccept validator"""
opts = SCons.Variables.Variables()
opts.Add(SCons.Variables.PathVariable('test',
'test option help',
'test build variable help',
'/default/path',
SCons.Variables.PathVariable.PathAccept))

Expand All @@ -177,7 +165,6 @@ def test_PathAccept(self) -> None:
test.write('file', "file\n")

o = opts.options[0]

o.validator('X', test.workpath('file'), {})

d = test.workpath('d')
Expand All @@ -190,44 +177,37 @@ def test_validator(self):
"""Test the PathVariable validator argument"""
opts = SCons.Variables.Variables()
opts.Add(SCons.Variables.PathVariable('test',
'test option help',
'test variable help',
'/default/path'))

test = TestCmd.TestCmd(workdir='')
test.write('exists', 'exists\n')

o = opts.options[0]

o.validator('X', test.workpath('exists'), {})

dne = test.workpath('does_not_exist')
try:
with self.assertRaises(SCons.Errors.UserError) as cm:
o.validator('X', dne, {})
except SCons.Errors.UserError as e:
expect = 'Path for option X does not exist: %s' % dne
assert str(e) == expect, e
else:
raise Exception("did not catch expected UserError")
e = cm.exception
self.assertEqual(str(e), f"Path for variable 'X' does not exist: {dne}")

class ValidatorError(Exception):
pass

def my_validator(key, val, env):
raise Exception("my_validator() got called for %s, %s!" % (key, val))
raise ValidatorError(f"my_validator() got called for {key!r}, {val}!")

opts = SCons.Variables.Variables()
opts.Add(SCons.Variables.PathVariable('test2',
'more help',
'/default/path/again',
my_validator))

o = opts.options[0]

try:
with self.assertRaises(ValidatorError) as cm:
o.validator('Y', 'value', {})
except Exception as e:
assert str(e) == 'my_validator() got called for Y, value!', e
else:
raise Exception("did not catch expected exception from my_validator()")


e = cm.exception
self.assertEqual(str(e), f"my_validator() got called for 'Y', value!")


if __name__ == "__main__":
Expand Down
Loading

0 comments on commit e853087

Please sign in to comment.