1 # SPDX-License-Identifier: MIT
3 # Copyright The SCons Foundation
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.
14 from types
import MethodType
, FunctionType
15 from typing
import Union
, Callable
, Optional
, Any
17 from .sctypes
import is_List
, is_Tuple
, is_String
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")
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)
45 /biz/boom:/foo/bar:/foo
47 If *canonicalize* is not ``None``, it is applied to each element of
53 if not is_List(orig
) and not is_Tuple(orig
):
54 paths
= paths
.split(sep
)
57 if is_String(newpath
):
58 newpaths
= newpath
.split(sep
)
59 elif is_List(newpath
) or is_Tuple(newpath
):
62 newpaths
= [newpath
] # might be a Dir
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.
78 normpath
= os
.path
.normpath(os
.path
.normcase(path
))
79 if normpath
not in normpaths
:
81 normpaths
.append(normpath
)
82 newpaths
.reverse() # since we're inserting at the head
86 normpath
= os
.path
.normpath(os
.path
.normcase(path
))
87 if normpath
not in normpaths
:
88 result
.insert(0, path
)
89 normpaths
.append(normpath
)
93 newpaths
= newpaths
+ paths
# prepend new paths
97 # now we add them only if they are unique
99 normpath
= os
.path
.normpath(os
.path
.normcase(path
))
100 if path
and normpath
not in normpaths
:
102 normpaths
.append(normpath
)
107 return sep
.join(paths
)
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")
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)
135 /foo/bar:/foo:/biz/boom
137 If *canonicalize* is not ``None``, it is applied to each element of
138 *newpath* before use.
143 if not is_List(orig
) and not is_Tuple(orig
):
144 paths
= paths
.split(sep
)
147 if is_String(newpath
):
148 newpaths
= newpath
.split(sep
)
149 elif is_List(newpath
) or is_Tuple(newpath
):
152 newpaths
= [newpath
] # might be a Dir
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.)
169 normpaths
.append(os
.path
.normpath(os
.path
.normcase(path
)))
170 for path
in newpaths
:
173 normpath
= os
.path
.normpath(os
.path
.normcase(path
))
174 if normpath
not in normpaths
:
176 normpaths
.append(normpath
)
179 # start w/ new paths, add old ones if not present,
181 newpaths
= paths
+ newpaths
# append new 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
:
191 normpaths
.append(normpath
)
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
214 paths
= env_dict
[key
]
215 if not is_List(env_dict
[key
]):
216 paths
= paths
.split(sep
)
218 if os
.path
.normcase(path
) not in list(map(os
.path
.normcase
, paths
)):
219 paths
= [path
] + paths
221 env_dict
[key
] = paths
223 env_dict
[key
] = sep
.join(paths
)
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
242 def __init__(self
, obj
: Any
, method
: Callable
, name
: Optional
[str] = None) -> None:
244 name
= method
.__name
__
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.
296 >>> def f(self, x, y):
299 >>> AddMethod(A, f, "add")
303 >>> a.data = ['a', 'b', 'c', 'd', 'e', 'f']
304 >>> AddMethod(a, lambda self, i: self.data[i], "listIndex")
305 >>> print(a.listIndex(3))
310 name
= function
.__name
__
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
)
325 method
= MethodType(function
, obj
)
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
))
344 # indent-tabs-mode:nil
346 # vim: set expandtab tabstop=4 shiftwidth=4: