Variables testing: confirm space-containing values
[scons.git] / SCons / Util / envs.py
blob2640ef5c195265cb2577e9ac11c4fbd0a9eeedb6
1 # SPDX-License-Identifier: MIT
3 # Copyright The SCons Foundation
5 """
6 SCons environment utility functions.
8 Routines for working with environments and construction variables
9 that don't need the specifics of the Environment class.
10 """
12 import re
13 import os
14 from types import MethodType, FunctionType
15 from typing import Union, Callable, Optional, Any
17 from .sctypes import is_List, is_Tuple, is_String
20 def PrependPath(
21 oldpath,
22 newpath,
23 sep=os.pathsep,
24 delete_existing: bool = True,
25 canonicalize: Optional[Callable] = None,
26 ) -> Union[list, str]:
27 """Prepend *newpath* path elements to *oldpath*.
29 Will only add any particular path once (leaving the first one it
30 encounters and ignoring the rest, to preserve path order), and will
31 :mod:`os.path.normpath` and :mod:`os.path.normcase` all paths to help
32 assure this. This can also handle the case where *oldpath*
33 is a list instead of a string, in which case a list will be returned
34 instead of a string. For example:
36 >>> p = PrependPath("/foo/bar:/foo", "/biz/boom:/foo")
37 >>> print(p)
38 /biz/boom:/foo:/foo/bar
40 If *delete_existing* is ``False``, then adding a path that exists will
41 not move it to the beginning; it will stay where it is in the list.
43 >>> p = PrependPath("/foo/bar:/foo", "/biz/boom:/foo", delete_existing=False)
44 >>> print(p)
45 /biz/boom:/foo/bar:/foo
47 If *canonicalize* is not ``None``, it is applied to each element of
48 *newpath* before use.
49 """
50 orig = oldpath
51 is_list = True
52 paths = orig
53 if not is_List(orig) and not is_Tuple(orig):
54 paths = paths.split(sep)
55 is_list = False
57 if is_String(newpath):
58 newpaths = newpath.split(sep)
59 elif is_List(newpath) or is_Tuple(newpath):
60 newpaths = newpath
61 else:
62 newpaths = [newpath] # might be a Dir
64 if canonicalize:
65 newpaths = list(map(canonicalize, newpaths))
67 if not delete_existing:
68 # First uniquify the old paths, making sure to
69 # preserve the first instance (in Unix/Linux,
70 # the first one wins), and remembering them in normpaths.
71 # Then insert the new paths at the head of the list
72 # if they're not already in the normpaths list.
73 result = []
74 normpaths = []
75 for path in paths:
76 if not path:
77 continue
78 normpath = os.path.normpath(os.path.normcase(path))
79 if normpath not in normpaths:
80 result.append(path)
81 normpaths.append(normpath)
82 newpaths.reverse() # since we're inserting at the head
83 for path in newpaths:
84 if not path:
85 continue
86 normpath = os.path.normpath(os.path.normcase(path))
87 if normpath not in normpaths:
88 result.insert(0, path)
89 normpaths.append(normpath)
90 paths = result
92 else:
93 newpaths = newpaths + paths # prepend new paths
95 normpaths = []
96 paths = []
97 # now we add them only if they are unique
98 for path in newpaths:
99 normpath = os.path.normpath(os.path.normcase(path))
100 if path and normpath not in normpaths:
101 paths.append(path)
102 normpaths.append(normpath)
104 if is_list:
105 return paths
107 return sep.join(paths)
110 def AppendPath(
111 oldpath,
112 newpath,
113 sep=os.pathsep,
114 delete_existing: bool = True,
115 canonicalize: Optional[Callable] = None,
116 ) -> Union[list, str]:
117 """Append *newpath* path elements to *oldpath*.
119 Will only add any particular path once (leaving the last one it
120 encounters and ignoring the rest, to preserve path order), and will
121 :mod:`os.path.normpath` and :mod:`os.path.normcase` all paths to help
122 assure this. This can also handle the case where *oldpath*
123 is a list instead of a string, in which case a list will be returned
124 instead of a string. For example:
126 >>> p = AppendPath("/foo/bar:/foo", "/biz/boom:/foo")
127 >>> print(p)
128 /foo/bar:/biz/boom:/foo
130 If *delete_existing* is ``False``, then adding a path that exists
131 will not move it to the end; it will stay where it is in the list.
133 >>> p = AppendPath("/foo/bar:/foo", "/biz/boom:/foo", delete_existing=False)
134 >>> print(p)
135 /foo/bar:/foo:/biz/boom
137 If *canonicalize* is not ``None``, it is applied to each element of
138 *newpath* before use.
140 orig = oldpath
141 is_list = True
142 paths = orig
143 if not is_List(orig) and not is_Tuple(orig):
144 paths = paths.split(sep)
145 is_list = False
147 if is_String(newpath):
148 newpaths = newpath.split(sep)
149 elif is_List(newpath) or is_Tuple(newpath):
150 newpaths = newpath
151 else:
152 newpaths = [newpath] # might be a Dir
154 if canonicalize:
155 newpaths = list(map(canonicalize, newpaths))
157 if not delete_existing:
158 # add old paths to result, then
159 # add new paths if not already present
160 # (I thought about using a dict for normpaths for speed,
161 # but it's not clear hashing the strings would be faster
162 # than linear searching these typically short lists.)
163 result = []
164 normpaths = []
165 for path in paths:
166 if not path:
167 continue
168 result.append(path)
169 normpaths.append(os.path.normpath(os.path.normcase(path)))
170 for path in newpaths:
171 if not path:
172 continue
173 normpath = os.path.normpath(os.path.normcase(path))
174 if normpath not in normpaths:
175 result.append(path)
176 normpaths.append(normpath)
177 paths = result
178 else:
179 # start w/ new paths, add old ones if not present,
180 # then reverse.
181 newpaths = paths + newpaths # append new paths
182 newpaths.reverse()
184 normpaths = []
185 paths = []
186 # now we add them only if they are unique
187 for path in newpaths:
188 normpath = os.path.normpath(os.path.normcase(path))
189 if path and normpath not in normpaths:
190 paths.append(path)
191 normpaths.append(normpath)
192 paths.reverse()
194 if is_list:
195 return paths
197 return sep.join(paths)
200 def AddPathIfNotExists(env_dict, key, path, sep: str = os.pathsep) -> None:
201 """Add a path element to a construction variable.
203 `key` is looked up in `env_dict`, and `path` is added to it if it
204 is not already present. `env_dict[key]` is assumed to be in the
205 format of a PATH variable: a list of paths separated by `sep` tokens.
207 >>> env = {'PATH': '/bin:/usr/bin:/usr/local/bin'}
208 >>> AddPathIfNotExists(env, 'PATH', '/opt/bin')
209 >>> print(env['PATH'])
210 /opt/bin:/bin:/usr/bin:/usr/local/bin
212 try:
213 is_list = True
214 paths = env_dict[key]
215 if not is_List(env_dict[key]):
216 paths = paths.split(sep)
217 is_list = False
218 if os.path.normcase(path) not in list(map(os.path.normcase, paths)):
219 paths = [path] + paths
220 if is_list:
221 env_dict[key] = paths
222 else:
223 env_dict[key] = sep.join(paths)
224 except KeyError:
225 env_dict[key] = path
228 class MethodWrapper:
229 """A generic Wrapper class that associates a method with an object.
231 As part of creating this MethodWrapper object an attribute with the
232 specified name (by default, the name of the supplied method) is added
233 to the underlying object. When that new "method" is called, our
234 :meth:`__call__` method adds the object as the first argument, simulating
235 the Python behavior of supplying "self" on method calls.
237 We hang on to the name by which the method was added to the underlying
238 base class so that we can provide a method to "clone" ourselves onto
239 a new underlying object being copied (without which we wouldn't need
240 to save that info).
242 def __init__(self, obj: Any, method: Callable, name: Optional[str] = None) -> None:
243 if name is None:
244 name = method.__name__
245 self.object = obj
246 self.method = method
247 self.name: str = name
248 setattr(self.object, name, self)
250 def __call__(self, *args, **kwargs):
251 nargs = (self.object,) + args
252 return self.method(*nargs, **kwargs)
254 def clone(self, new_object):
256 Returns an object that re-binds the underlying "method" to
257 the specified new object.
259 return self.__class__(new_object, self.method, self.name)
262 # The original idea for AddMethod() came from the
263 # following post to the ActiveState Python Cookbook:
265 # ASPN: Python Cookbook : Install bound methods in an instance
266 # https://code.activestate.com/recipes/223613
268 # Changed as follows:
269 # * Switched the installmethod() "object" and "function" arguments,
270 # so the order reflects that the left-hand side is the thing being
271 # "assigned to" and the right-hand side is the value being assigned.
272 # * The instance/class detection is changed a bit, as it's all
273 # new-style classes now with Py3.
274 # * The by-hand construction of the function object from renamefunction()
275 # is not needed, the remaining bit is now used inline in AddMethod.
278 def AddMethod(obj, function: Callable, name: Optional[str] = None) -> None:
279 """Add a method to an object.
281 Adds *function* to *obj* if *obj* is a class object.
282 Adds *function* as a bound method if *obj* is an instance object.
283 If *obj* looks like an environment instance, use :class:`~SCons.Util.MethodWrapper`
284 to add it. If *name* is supplied it is used as the name of *function*.
286 Although this works for any class object, the intent as a public
287 API is to be used on Environment, to be able to add a method to all
288 construction environments; it is preferred to use ``env.AddMethod``
289 to add to an individual environment.
291 >>> class A:
292 ... ...
294 >>> a = A()
296 >>> def f(self, x, y):
297 ... self.z = x + y
299 >>> AddMethod(A, f, "add")
300 >>> a.add(2, 4)
301 >>> print(a.z)
303 >>> a.data = ['a', 'b', 'c', 'd', 'e', 'f']
304 >>> AddMethod(a, lambda self, i: self.data[i], "listIndex")
305 >>> print(a.listIndex(3))
309 if name is None:
310 name = function.__name__
311 else:
312 # "rename"
313 function = FunctionType(
314 function.__code__, function.__globals__, name, function.__defaults__
317 method: Union[MethodType, MethodWrapper, Callable]
319 if hasattr(obj, '__class__') and obj.__class__ is not type:
320 # obj is an instance, so it gets a bound method.
321 if hasattr(obj, "added_methods"):
322 method = MethodWrapper(obj, function, name)
323 obj.added_methods.append(method)
324 else:
325 method = MethodType(function, obj)
326 else:
327 # obj is a class
328 method = function
330 setattr(obj, name, method)
333 # This routine is used to validate that a construction var name can be used
334 # as a Python identifier, which we require. However, Python 3 introduced an
335 # isidentifier() string method so there's really not any need for it now.
336 _is_valid_var_re = re.compile(r'[_a-zA-Z]\w*$')
338 def is_valid_construction_var(varstr: str) -> bool:
339 """Return True if *varstr* is a legitimate name of a construction variable."""
340 return bool(_is_valid_var_re.match(varstr))
342 # Local Variables:
343 # tab-width:4
344 # indent-tabs-mode:nil
345 # End:
346 # vim: set expandtab tabstop=4 shiftwidth=4: