Skip to content

Commit ebe6e9b

Browse files
authored
Merge pull request #3814 from mwichmann/addmethod-clone
Fix/update global AddMethod
2 parents 948e8af + dabd798 commit ebe6e9b

File tree

5 files changed

+129
-130
lines changed

5 files changed

+129
-130
lines changed

CHANGES.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@ RELEASE VERSION/DATE TO BE FILLED IN LATER
6565
- Make sure cProfile is used if profiling - SCons was expecting
6666
the Util module to monkeypatch in cProfile as profile if available,
6767
but this is no longer being done.
68+
- Cleanup in SCons.Util.AddMethod. If called with an environment instance
69+
as the object to modify, the method would not be correctly set up in
70+
any Clone of that instance. Now tries to detect this and calls
71+
MethodWrapper to set up the method the same way env.AddMethod does.
72+
MethodWrapper moved to Util to avoid a circular import. Fixes #3028.
6873
- Some Python 2 compatibility code dropped
6974

7075
From Simon Tegelid

SCons/Environment.py

Lines changed: 8 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
import SCons.Subst
5555
import SCons.Tool
5656
import SCons.Util
57+
from SCons.Util import MethodWrapper
5758
import SCons.Warnings
5859

5960
class _Null:
@@ -180,48 +181,17 @@ def _delete_duplicates(l, keep_last):
180181
# Shannon at the following page (there called the "transplant" class):
181182
#
182183
# ASPN : Python Cookbook : Dynamically added methods to a class
183-
# http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/81732
184+
# https://code.activestate.com/recipes/81732/
184185
#
185186
# We had independently been using the idiom as BuilderWrapper, but
186187
# factoring out the common parts into this base class, and making
187188
# BuilderWrapper a subclass that overrides __call__() to enforce specific
188189
# Builder calling conventions, simplified some of our higher-layer code.
190+
#
191+
# Note: MethodWrapper moved to SCons.Util as it was needed there
192+
# and otherwise we had a circular import problem.
189193

190-
class MethodWrapper:
191-
"""
192-
A generic Wrapper class that associates a method (which can
193-
actually be any callable) with an object. As part of creating this
194-
MethodWrapper object an attribute with the specified (by default,
195-
the name of the supplied method) is added to the underlying object.
196-
When that new "method" is called, our __call__() method adds the
197-
object as the first argument, simulating the Python behavior of
198-
supplying "self" on method calls.
199-
200-
We hang on to the name by which the method was added to the underlying
201-
base class so that we can provide a method to "clone" ourselves onto
202-
a new underlying object being copied (without which we wouldn't need
203-
to save that info).
204-
"""
205-
def __init__(self, object, method, name=None):
206-
if name is None:
207-
name = method.__name__
208-
self.object = object
209-
self.method = method
210-
self.name = name
211-
setattr(self.object, name, self)
212-
213-
def __call__(self, *args, **kwargs):
214-
nargs = (self.object,) + args
215-
return self.method(*nargs, **kwargs)
216-
217-
def clone(self, new_object):
218-
"""
219-
Returns an object that re-binds the underlying "method" to
220-
the specified new object.
221-
"""
222-
return self.__class__(new_object, self.method, self.name)
223-
224-
class BuilderWrapper(MethodWrapper):
194+
class BuilderWrapper(SCons.Util.MethodWrapper):
225195
"""
226196
A MethodWrapper subclass that that associates an environment with
227197
a Builder.
@@ -248,7 +218,7 @@ def __call__(self, target=None, source=_null, *args, **kw):
248218
target = [target]
249219
if source is not None and not SCons.Util.is_List(source):
250220
source = [source]
251-
return MethodWrapper.__call__(self, target, source, *args, **kw)
221+
return super().__call__(target, source, *args, **kw)
252222

253223
def __repr__(self):
254224
return '<BuilderWrapper %s>' % repr(self.name)
@@ -2377,7 +2347,7 @@ def __getattr__(self, name):
23772347
# Environment they are being constructed with and so will not
23782348
# have access to overrided values. So we rebuild them with the
23792349
# OverrideEnvironment so they have access to overrided values.
2380-
if isinstance(attr, (MethodWrapper, BuilderWrapper)):
2350+
if isinstance(attr, MethodWrapper):
23812351
return attr.clone(self)
23822352
else:
23832353
return attr

SCons/Environment.xml

Lines changed: 20 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -286,54 +286,40 @@ until the Action object is actually used.
286286
</arguments>
287287
<summary>
288288
<para>
289-
When called with the
290-
<function>AddMethod</function>()
291-
form,
292-
adds the specified
293-
<parameter>function</parameter>
294-
to the specified
295-
<parameter>object</parameter>
296-
as the specified method
297-
<parameter>name</parameter>.
298-
When called using the
299-
&f-env-AddMethod; form,
300-
adds the specified
301-
<parameter>function</parameter>
302-
to the construction environment
303-
<replaceable>env</replaceable>
304-
as the specified method
305-
<parameter>name</parameter>.
306-
In both cases, if
307-
<parameter>name</parameter>
308-
is omitted or
309-
<constant>None</constant>,
310-
the name of the
311-
specified
312-
<parameter>function</parameter>
313-
itself is used for the method name.
289+
Adds <parameter>function</parameter> to an object as a method.
290+
<parameter>function</parameter> will be called with an instance
291+
object as the first argument as for other methods.
292+
If <parameter>name</parameter> is given, it is used as
293+
the name of the new method, else the name of
294+
<parameter>function</parameter> is used.
295+
</para>
296+
<para>
297+
When the global function &f-AddMethod; is called,
298+
the object to add the method to must be passed as the first argument;
299+
typically this will be &Environment;,
300+
in order to create a method which applies to all &consenvs;
301+
subsequently constructed.
302+
When called using the &f-env-AddMethod; form,
303+
the method is added to the specified &consenv; only.
304+
Added methods propagate through &f-env-Clone; calls.
314305
</para>
315306

316307
<para>
317308
Examples:
318309
</para>
319310

320311
<example_commands>
321-
# Note that the first argument to the function to
322-
# be attached as a method must be the object through
323-
# which the method will be called; the Python
324-
# convention is to call it 'self'.
312+
# Function to add must accept an instance argument.
313+
# The Python convention is to call this 'self'.
325314
def my_method(self, arg):
326315
print("my_method() got", arg)
327316

328-
# Use the global AddMethod() function to add a method
329-
# to the Environment class. This
317+
# Use the global function to add a method to the Environment class:
330318
AddMethod(Environment, my_method)
331319
env = Environment()
332320
env.my_method('arg')
333321

334-
# Add the function as a method, using the function
335-
# name for the method call.
336-
env = Environment()
322+
# Use the optional name argument to set the name of the method:
337323
env.AddMethod(my_method, 'other_method_name')
338324
env.other_method_name('another arg')
339325
</example_commands>

SCons/EnvironmentTests.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,13 @@
3333
import TestCmd
3434
import TestUnit
3535

36-
from SCons.Environment import *
36+
from SCons.Environment import (
37+
Environment,
38+
NoSubstitutionProxy,
39+
OverrideEnvironment,
40+
SubstitutionEnvironment,
41+
is_valid_construction_var,
42+
)
3743
import SCons.Warnings
3844

3945
def diff_env(env1, env2):
@@ -721,7 +727,7 @@ def func2(self, arg=''):
721727
r = env4.func2()
722728
assert r == 'func2-4', r
723729

724-
# Test that clones don't re-bind an attribute that the user
730+
# Test that clones don't re-bind an attribute that the user set.
725731
env1 = Environment(FOO = '1')
726732
env1.AddMethod(func2)
727733
def replace_func2():
@@ -731,6 +737,18 @@ def replace_func2():
731737
r = env2.func2()
732738
assert r == 'replace_func2', r
733739

740+
# Test clone rebinding if using global AddMethod.
741+
env1 = Environment(FOO='1')
742+
SCons.Util.AddMethod(env1, func2)
743+
r = env1.func2()
744+
assert r == 'func2-1', r
745+
r = env1.func2('-xxx')
746+
assert r == 'func2-1-xxx', r
747+
env2 = env1.Clone(FOO='2')
748+
r = env2.func2()
749+
assert r == 'func2-2', r
750+
751+
734752
def test_Override(self):
735753
"""Test overriding construction variables"""
736754
env = SubstitutionEnvironment(ONE=1, TWO=2, THREE=3, FOUR=4)

0 commit comments

Comments
 (0)