summaryrefslogtreecommitdiff
path: root/SCons/Util/envs.py
blob: 963c963a853038450dbef004dbd4f319876c2f62 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
# SPDX-License-Identifier: MIT
#
# Copyright The SCons Foundation

"""Various SCons utility functions

Routines for working with environments and construction variables
that don't need the specifics of Environment.
"""

import os
from types import MethodType, FunctionType
from typing import Union

from .types import is_List, is_Tuple, is_String


def PrependPath(
    oldpath, newpath, sep=os.pathsep, delete_existing=True, canonicalize=None
) -> Union[list, str]:
    """Prepend *newpath* path elements to *oldpath*.

    Will only add any particular path once (leaving the first one it
    encounters and ignoring the rest, to preserve path order), and will
    :mod:`os.path.normpath` and :mod:`os.path.normcase` all paths to help
    assure this.  This can also handle the case where *oldpath*
    is a list instead of a string, in which case a list will be returned
    instead of a string. For example:

    >>> p = PrependPath("/foo/bar:/foo", "/biz/boom:/foo")
    >>> print(p)
    /biz/boom:/foo:/foo/bar

    If *delete_existing* is ``False``, then adding a path that exists will
    not move it to the beginning; it will stay where it is in the list.

    >>> p = PrependPath("/foo/bar:/foo", "/biz/boom:/foo", delete_existing=False)
    >>> print(p)
    /biz/boom:/foo/bar:/foo

    If *canonicalize* is not ``None``, it is applied to each element of
    *newpath* before use.
    """
    orig = oldpath
    is_list = True
    paths = orig
    if not is_List(orig) and not is_Tuple(orig):
        paths = paths.split(sep)
        is_list = False

    if is_String(newpath):
        newpaths = newpath.split(sep)
    elif not is_List(newpath) and not is_Tuple(newpath):
        newpaths = [newpath]  # might be a Dir
    else:
        newpaths = newpath

    if canonicalize:
        newpaths = list(map(canonicalize, newpaths))

    if not delete_existing:
        # First uniquify the old paths, making sure to
        # preserve the first instance (in Unix/Linux,
        # the first one wins), and remembering them in normpaths.
        # Then insert the new paths at the head of the list
        # if they're not already in the normpaths list.
        result = []
        normpaths = []
        for path in paths:
            if not path:
                continue
            normpath = os.path.normpath(os.path.normcase(path))
            if normpath not in normpaths:
                result.append(path)
                normpaths.append(normpath)
        newpaths.reverse()  # since we're inserting at the head
        for path in newpaths:
            if not path:
                continue
            normpath = os.path.normpath(os.path.normcase(path))
            if normpath not in normpaths:
                result.insert(0, path)
                normpaths.append(normpath)
        paths = result

    else:
        newpaths = newpaths + paths  # prepend new paths

        normpaths = []
        paths = []
        # now we add them only if they are unique
        for path in newpaths:
            normpath = os.path.normpath(os.path.normcase(path))
            if path and normpath not in normpaths:
                paths.append(path)
                normpaths.append(normpath)

    if is_list:
        return paths

    return sep.join(paths)


def AppendPath(
    oldpath, newpath, sep=os.pathsep, delete_existing=True, canonicalize=None
) -> Union[list, str]:
    """Append *newpath* path elements to *oldpath*.

    Will only add any particular path once (leaving the last one it
    encounters and ignoring the rest, to preserve path order), and will
    :mod:`os.path.normpath` and :mod:`os.path.normcase` all paths to help
    assure this.  This can also handle the case where *oldpath*
    is a list instead of a string, in which case a list will be returned
    instead of a string. For example:

    >>> p = AppendPath("/foo/bar:/foo", "/biz/boom:/foo")
    >>> print(p)
    /foo/bar:/biz/boom:/foo

    If *delete_existing* is ``False``, then adding a path that exists
    will not move it to the end; it will stay where it is in the list.

    >>> p = AppendPath("/foo/bar:/foo", "/biz/boom:/foo", delete_existing=False)
    >>> print(p)
    /foo/bar:/foo:/biz/boom

    If *canonicalize* is not ``None``, it is applied to each element of
    *newpath* before use.
    """
    orig = oldpath
    is_list = True
    paths = orig
    if not is_List(orig) and not is_Tuple(orig):
        paths = paths.split(sep)
        is_list = False

    if is_String(newpath):
        newpaths = newpath.split(sep)
    elif not is_List(newpath) and not is_Tuple(newpath):
        newpaths = [newpath]  # might be a Dir
    else:
        newpaths = newpath

    if canonicalize:
        newpaths = list(map(canonicalize, newpaths))

    if not delete_existing:
        # add old paths to result, then
        # add new paths if not already present
        # (I thought about using a dict for normpaths for speed,
        # but it's not clear hashing the strings would be faster
        # than linear searching these typically short lists.)
        result = []
        normpaths = []
        for path in paths:
            if not path:
                continue
            result.append(path)
            normpaths.append(os.path.normpath(os.path.normcase(path)))
        for path in newpaths:
            if not path:
                continue
            normpath = os.path.normpath(os.path.normcase(path))
            if normpath not in normpaths:
                result.append(path)
                normpaths.append(normpath)
        paths = result
    else:
        # start w/ new paths, add old ones if not present,
        # then reverse.
        newpaths = paths + newpaths  # append new paths
        newpaths.reverse()

        normpaths = []
        paths = []
        # now we add them only if they are unique
        for path in newpaths:
            normpath = os.path.normpath(os.path.normcase(path))
            if path and normpath not in normpaths:
                paths.append(path)
                normpaths.append(normpath)
        paths.reverse()

    if is_list:
        return paths

    return sep.join(paths)


def AddPathIfNotExists(env_dict, key, path, sep=os.pathsep):
    """Add a path element to a construction variable.

    `key` is looked up in `env_dict`, and `path` is added to it if it
    is not already present. `env_dict[key]` is assumed to be in the
    format of a PATH variable: a list of paths separated by `sep` tokens.

    >>> env = {'PATH': '/bin:/usr/bin:/usr/local/bin'}
    >>> AddPathIfNotExists(env, 'PATH', '/opt/bin')
    >>> print(env['PATH'])
    /opt/bin:/bin:/usr/bin:/usr/local/bin
    """
    try:
        is_list = True
        paths = env_dict[key]
        if not is_List(env_dict[key]):
            paths = paths.split(sep)
            is_list = False
        if os.path.normcase(path) not in list(map(os.path.normcase, paths)):
            paths = [path] + paths
        if is_list:
            env_dict[key] = paths
        else:
            env_dict[key] = sep.join(paths)
    except KeyError:
        env_dict[key] = path


class MethodWrapper:
    """A generic Wrapper class that associates a method with an object.

    As part of creating this MethodWrapper object an attribute with the
    specified name (by default, the name of the supplied method) is added
    to the underlying object.  When that new "method" is called, our
    :meth:`__call__` method adds the object as the first argument, simulating
    the Python behavior of supplying "self" on method calls.

    We hang on to the name by which the method was added to the underlying
    base class so that we can provide a method to "clone" ourselves onto
    a new underlying object being copied (without which we wouldn't need
    to save that info).
    """
    def __init__(self, obj, method, name=None):
        if name is None:
            name = method.__name__
        self.object = obj
        self.method = method
        self.name = name
        setattr(self.object, name, self)

    def __call__(self, *args, **kwargs):
        nargs = (self.object,) + args
        return self.method(*nargs, **kwargs)

    def clone(self, new_object):
        """
        Returns an object that re-binds the underlying "method" to
        the specified new object.
        """
        return self.__class__(new_object, self.method, self.name)


# The original idea for AddMethod() came from the
# following post to the ActiveState Python Cookbook:
#
# ASPN: Python Cookbook : Install bound methods in an instance
# https://code.activestate.com/recipes/223613
#
# Changed as follows:
# * Switched the installmethod() "object" and "function" arguments,
#   so the order reflects that the left-hand side is the thing being
#   "assigned to" and the right-hand side is the value being assigned.
# * The instance/class detection is changed a bit, as it's all
#   new-style classes now with Py3.
# * The by-hand construction of the function object from renamefunction()
#   is not needed, the remaining bit is now used inline in AddMethod.


def AddMethod(obj, function, name=None):
    """Add a method to an object.

    Adds *function* to *obj* if *obj* is a class object.
    Adds *function* as a bound method if *obj* is an instance object.
    If *obj* looks like an environment instance, use :class:`~SCons.Util.MethodWrapper`
    to add it.  If *name* is supplied it is used as the name of *function*.

    Although this works for any class object, the intent as a public
    API is to be used on Environment, to be able to add a method to all
    construction environments; it is preferred to use ``env.AddMethod``
    to add to an individual environment.

    >>> class A:
    ...    ...

    >>> a = A()

    >>> def f(self, x, y):
    ...    self.z = x + y

    >>> AddMethod(A, f, "add")
    >>> a.add(2, 4)
    >>> print(a.z)
    6
    >>> a.data = ['a', 'b', 'c', 'd', 'e', 'f']
    >>> AddMethod(a, lambda self, i: self.data[i], "listIndex")
    >>> print(a.listIndex(3))
    d

    """
    if name is None:
        name = function.__name__
    else:
        # "rename"
        function = FunctionType(
            function.__code__, function.__globals__, name, function.__defaults__
        )

    if hasattr(obj, '__class__') and obj.__class__ is not type:
        # obj is an instance, so it gets a bound method.
        if hasattr(obj, "added_methods"):
            method = MethodWrapper(obj, function, name)
            obj.added_methods.append(method)
        else:
            method = MethodType(function, obj)
    else:
        # obj is a class
        method = function

    setattr(obj, name, method)


# Local Variables:
# tab-width:4
# indent-tabs-mode:nil
# End:
# vim: set expandtab tabstop=4 shiftwidth=4: