Add a Memoizer metaclass to collect the logic for caching values in one location...
authorstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Wed, 29 Dec 2004 21:04:56 +0000 (21:04 +0000)
committerstevenknight <stevenknight@fdb21ef1-2011-0410-befe-b5e4ea1792b1>
Wed, 29 Dec 2004 21:04:56 +0000 (21:04 +0000)
git-svn-id: http://scons.tigris.org/svn/scons/trunk@1196 fdb21ef1-2011-0410-befe-b5e4ea1792b1

26 files changed:
src/CHANGES.txt
src/engine/MANIFEST.in
src/engine/SCons/Action.py
src/engine/SCons/ActionTests.py
src/engine/SCons/Builder.py
src/engine/SCons/BuilderTests.py
src/engine/SCons/Environment.py
src/engine/SCons/EnvironmentTests.py
src/engine/SCons/Executor.py
src/engine/SCons/ExecutorTests.py
src/engine/SCons/Memoize.py [new file with mode: 0644]
src/engine/SCons/Node/FS.py
src/engine/SCons/Node/FSTests.py
src/engine/SCons/Node/NodeTests.py
src/engine/SCons/Node/__init__.py
src/engine/SCons/Scanner/CTests.py
src/engine/SCons/Scanner/D.py
src/engine/SCons/Scanner/Fortran.py
src/engine/SCons/Scanner/Prog.py
src/engine/SCons/Scanner/ScannerTests.py
src/engine/SCons/Scanner/__init__.py
src/engine/SCons/UserTuple.py [deleted file]
src/engine/SCons/__init__.py
test/DirSource.py
test/Repository/LIBPATH.py
test/scan-once.py

index e97ea15e269711abde2637dcac4092e05588a139..6242524984a6d9894a7a520753a938381038db7d 100644 (file)
@@ -347,6 +347,10 @@ RELEASE 0.97 - XXX
   - Use the correct scanner if the same source file is used for targets in
     two different environments with the same path but different scanners.
 
+  - Collect logic for caching values in memory in a Memoizer class.
+    This cleans up a lot of special-case code in various methods and
+    caches additional values to speed up most configurations.
+
   From Levi Stephen:
 
   - Allow $JARCHDIR to be expanded to other construction variables.
index b8ce8b667f7dcc379f3c89e7c8b64ca82f80fa48..5c329bd0367577bc04eaf577b07c9627b76b9322 100644 (file)
@@ -10,6 +10,7 @@ SCons/Errors.py
 SCons/Executor.py
 SCons/Job.py
 SCons/exitfuncs.py
+SCons/Memoize.py
 SCons/Node/__init__.py
 SCons/Node/Alias.py
 SCons/Node/FS.py
@@ -129,6 +130,5 @@ SCons/Tool/tex.py
 SCons/Tool/tlib.py
 SCons/Tool/yacc.py
 SCons/Tool/zip.py
-SCons/UserTuple.py
 SCons/Util.py
 SCons/Warnings.py
index 83e4208246dc5eed6dd06fa6c02ea3b0357bc871..a3b62eb98f0d437222bd24c152601f9445da41b2 100644 (file)
@@ -215,6 +215,8 @@ class ActionBase:
     other objects (Builders, Executors, etc.)  This provides the
     common methods for manipulating and combining those actions."""
     
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
     def __cmp__(self, other):
         return cmp(self.__dict__, other)
 
@@ -239,6 +241,14 @@ class ActionBase:
         self.presub_env = None      # don't need this any more
         return lines
 
+if not SCons.Memoize.has_metaclass:
+    _Base = ActionBase
+    class ActionBase(SCons.Memoize.Memoizer, _Base):
+        "Cache-backed version of ActionBase"
+        def __init__(self, *args, **kw):
+            apply(_Base.__init__, (self,)+args, kw)
+            SCons.Memoize.Memoizer.__init__(self)
+
 
 class _ActionAction(ActionBase):
     """Base class for actions that create output objects."""
@@ -558,7 +568,10 @@ class FunctionAction(_ActionAction):
         return "%s(%s, %s)" % (name, tstr, sstr)
 
     def __str__(self):
-        return "%s(target, source, env)" % self.function_name()
+        name = self.function_name()
+        if name == 'ActionCaller':
+            return str(self.execfunction)
+        return "%s(target, source, env)" % name
 
     def execute(self, target, source, env):
         rsources = map(rfile, source)
@@ -689,6 +702,8 @@ class ActionCaller:
         args = self.subst_args(target, source, env)
         kw = self.subst_kw(target, source, env)
         return apply(self.parent.strfunc, args, kw)
+    def __str__(self):
+        return apply(self.parent.strfunc, self.args, self.kw)
 
 class ActionFactory:
     """A factory class that will wrap up an arbitrary function
@@ -705,11 +720,4 @@ class ActionFactory:
     def __call__(self, *args, **kw):
         ac = ActionCaller(self, args, kw)
         action = Action(ac, strfunction=ac.strfunction)
-        # action will be a FunctionAction; if left to its own devices,
-        # a genstr or str of this action will just show
-        # "ActionCaller(target, source, env)".  Override that with the
-        # description from strfunc.  Note that the apply is evaluated
-        # right now; __str__ is set to a (lambda) function that just
-        # returns the stored result of the evaluation whenever called.
-        action.__str__ = lambda name=apply(self.strfunc, args, kw): name
         return action
index 4c1d521b194d6a090f1c79c267f961600aac7e97..4bcb0843d4aa30a2749e778aa27070295e706675 100644 (file)
@@ -1628,12 +1628,7 @@ class ActionFactoryTestCase(unittest.TestCase):
         af = SCons.Action.ActionFactory(actfunc, strfunc)
         af(3, 6, 9)([], [], Environment())
         assert actfunc_args == [3, 6, 9], actfunc_args
-        # Note that strfunc gets evaluated twice: once when we called
-        # the actionfactory itself to get the real action
-        # (Action(ActionCaller, ...)), and once when we actually call
-        # that resulting action; since strfunc modifies the global,
-        # account for the number of times it was called.
-        assert strfunc_args == [3, 6, 9, 3, 6, 9], strfunc_args
+        assert strfunc_args == [3, 6, 9], strfunc_args
 
 
 class ActionCompareTestCase(unittest.TestCase):
@@ -1681,9 +1676,9 @@ class ActionCompareTestCase(unittest.TestCase):
                                        'BAR' : bar,
                                        'DOG' : dog} )
         
-        assert foo.get_name(env) == 'FOO'
-        assert bar.get_name(env) == 'BAR'
-        assert dog.get_name(env) == 'DOG'
+        assert foo.get_name(env) == 'FOO', foo.get_name(env)
+        assert bar.get_name(env) == 'BAR', bar.get_name(env)
+        assert dog.get_name(env) == 'DOG', dog.get_name(env)
 
 
 if __name__ == "__main__":
index ed070c4bdf3fad8a78a390107243433f8dd187d6..9ac01fab7cd6d0845859f0dc0ce8347214a67e19 100644 (file)
@@ -377,6 +377,8 @@ class BuilderBase:
     nodes (files) from input nodes (files).
     """
 
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
     def __init__(self,  action = None,
                         prefix = '',
                         suffix = '',
@@ -515,7 +517,7 @@ class BuilderBase:
             new_targets = []
             for t in tlist:
                 if not t.is_derived():
-                    t.builder = self
+                    t.builder_set(self)
                     new_targets.append(t)
 
             target, source = self.emitter(target=tlist, source=slist, env=env)
@@ -527,7 +529,7 @@ class BuilderBase:
                 if t.builder is self:
                     # Only delete the temporary builder if the emitter
                     # didn't change it on us.
-                    t.builder = None
+                    t.builder_set(None)
 
             # Have to call arg2nodes yet again, since it is legal for
             # emitters to spit out strings as well as Node instances.
@@ -631,6 +633,14 @@ class BuilderBase:
         """
         self.emitter[suffix] = emitter
 
+if not SCons.Memoize.has_metaclass:
+    _Base = BuilderBase
+    class BuilderBase(SCons.Memoize.Memoizer, _Base):
+        "Cache-backed version of BuilderBase"
+        def __init__(self, *args, **kw):
+            apply(_Base.__init__, (self,)+args, kw)
+            SCons.Memoize.Memoizer.__init__(self)
+
 class ListBuilder(SCons.Util.Proxy):
     """A Proxy to support building an array of targets (for example,
     foo.o and foo.h from foo.y) from a single Action execution.
@@ -687,27 +697,26 @@ class MultiStepBuilder(BuilderBase):
         if not SCons.Util.is_List(src_builder):
             src_builder = [ src_builder ]
         self.src_builder = src_builder
-        self.sdict = {}
-        self.cached_src_suffixes = {} # source suffixes keyed on id(env)
 
+    def _get_sdict(self, env):
+        "__cacheable__"
+        sdict = {}
+        for bld in self.src_builder:
+            if SCons.Util.is_String(bld):
+                try:
+                    bld = env['BUILDERS'][bld]
+                except KeyError:
+                    continue
+            for suf in bld.src_suffixes(env):
+                sdict[suf] = bld
+        return sdict
+        
     def _execute(self, env, target, source, overwarn={}, executor_kw={}):
         # We now assume that target and source are lists or None.
         slist = env.arg2nodes(source, self.source_factory)
         final_sources = []
 
-        try:
-            sdict = self.sdict[id(env)]
-        except KeyError:
-            sdict = {}
-            self.sdict[id(env)] = sdict
-            for bld in self.src_builder:
-                if SCons.Util.is_String(bld):
-                    try:
-                        bld = env['BUILDERS'][bld]
-                    except KeyError:
-                        continue
-                for suf in bld.src_suffixes(env):
-                    sdict[suf] = bld
+        sdict = self._get_sdict(env)
 
         src_suffixes = self.src_suffixes(env)
 
@@ -750,15 +759,12 @@ class MultiStepBuilder(BuilderBase):
     def src_suffixes(self, env):
         """Return a list of the src_suffix attributes for all
         src_builders of this Builder.
+        __cacheable__
         """
-        try:
-            return self.cached_src_suffixes[id(env)]
-        except KeyError:
-            suffixes = BuilderBase.src_suffixes(self, env)
-            for builder in self.get_src_builders(env):
-                suffixes.extend(builder.src_suffixes(env))
-            self.cached_src_suffixes[id(env)] = suffixes
-            return suffixes
+        suffixes = BuilderBase.src_suffixes(self, env)
+        for builder in self.get_src_builders(env):
+            suffixes.extend(builder.src_suffixes(env))
+        return suffixes
 
 class CompositeBuilder(SCons.Util.Proxy):
     """A Builder Proxy whose main purpose is to always have
index 5cb07e80993ed5659c43c79152261de275949751..005f3de4792c03bc6e4070ea541c41687d70390d 100644 (file)
@@ -948,9 +948,9 @@ class BuilderTestCase(unittest.TestCase):
         src = tgt.sources[0]
         assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
         assert tgt.builder.source_scanner is None, tgt.builder.source_scanner
-        assert tgt.get_source_scanner(bar_y, env1) is None, tgt.get_source_scanner(bar_y, env1)
+        assert tgt.get_source_scanner(bar_y) is None, tgt.get_source_scanner(bar_y)
         assert not src.has_builder(), src.has_builder()
-        assert src.get_source_scanner(bar_y, env1) is None, src.get_source_scanner(bar_y, env1)
+        assert src.get_source_scanner(bar_y) is None, src.get_source_scanner(bar_y)
 
         # An Environment that has suffix-specified SCANNERS should
         # provide a source scanner to the target.
@@ -974,10 +974,10 @@ class BuilderTestCase(unittest.TestCase):
         src = tgt.sources[0]
         assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
         assert not tgt.builder.source_scanner, tgt.builder.source_scanner
-        assert tgt.get_source_scanner(bar_y, env3), tgt.get_source_scanner(bar_y, env3)
-        assert str(tgt.get_source_scanner(bar_y, env3)) == 'EnvTestScanner', tgt.get_source_scanner(bar_y, env3)
+        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
+        assert str(tgt.get_source_scanner(bar_y)) == 'EnvTestScanner', tgt.get_source_scanner(bar_y)
         assert not src.has_builder(), src.has_builder()
-        assert src.get_source_scanner(bar_y, env3) is None, src.get_source_scanner(bar_y, env3)
+        assert src.get_source_scanner(bar_y) is None, src.get_source_scanner(bar_y)
 
         # Can't simply specify the scanner as a builder argument; it's
         # global to all invocations of this builder.
@@ -985,10 +985,10 @@ class BuilderTestCase(unittest.TestCase):
         src = tgt.sources[0]
         assert tgt.builder.target_scanner != scanner, tgt.builder.target_scanner
         assert not tgt.builder.source_scanner, tgt.builder.source_scanner
-        assert tgt.get_source_scanner(bar_y, env3), tgt.get_source_scanner(bar_y, env3)
-        assert str(tgt.get_source_scanner(bar_y, env3)) == 'EnvTestScanner', tgt.get_source_scanner(bar_y, env3)
+        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
+        assert str(tgt.get_source_scanner(bar_y)) == 'EnvTestScanner', tgt.get_source_scanner(bar_y)
         assert not src.has_builder(), src.has_builder()
-        assert src.get_source_scanner(bar_y, env3) is None, src.get_source_scanner(bar_y, env3)
+        assert src.get_source_scanner(bar_y) is None, src.get_source_scanner(bar_y)
 
         # Now use a builder that actually has scanners and ensure that
         # the target is set accordingly (using the specified scanner
@@ -1002,11 +1002,11 @@ class BuilderTestCase(unittest.TestCase):
         assert tgt.builder.source_scanner, tgt.builder.source_scanner
         assert tgt.builder.source_scanner == scanner, tgt.builder.source_scanner
         assert str(tgt.builder.source_scanner) == 'TestScanner', str(tgt.builder.source_scanner)
-        assert tgt.get_source_scanner(bar_y, env3), tgt.get_source_scanner(bar_y, env3)
-        assert tgt.get_source_scanner(bar_y, env3) == scanner, tgt.get_source_scanner(bar_y, env3)
-        assert str(tgt.get_source_scanner(bar_y, env3)) == 'TestScanner', tgt.get_source_scanner(bar_y, env3)
+        assert tgt.get_source_scanner(bar_y), tgt.get_source_scanner(bar_y)
+        assert tgt.get_source_scanner(bar_y) == scanner, tgt.get_source_scanner(bar_y)
+        assert str(tgt.get_source_scanner(bar_y)) == 'TestScanner', tgt.get_source_scanner(bar_y)
         assert not src.has_builder(), src.has_builder()
-        assert src.get_source_scanner(bar_y, env3) is None, src.get_source_scanner(bar_y, env3)
+        assert src.get_source_scanner(bar_y) is None, src.get_source_scanner(bar_y)
 
 
 
@@ -1418,13 +1418,25 @@ class BuilderTestCase(unittest.TestCase):
         assert b3.get_name(env) == 'bldr3', b3.get_name(env)
         assert b4.get_name(env) == 'bldr4', b4.get_name(env)
         assert b5.get_name(env) == 'builder5', b5.get_name(env)
-        assert b6.get_name(env) == 'SCons.Builder.BuilderBase', b6.get_name(env)
+        # With no name, get_name will return the class.  Allow
+        # for caching...
+        assert b6.get_name(env) in [
+            'SCons.Builder.BuilderBase',
+            "<class 'SCons.Builder.BuilderBase'>",
+            'SCons.Memoize.BuilderBase',
+            "<class 'SCons.Memoize.BuilderBase'>",
+            ], b6.get_name(env)
         assert b1.get_name(env2) == 'B1', b1.get_name(env2)
         assert b2.get_name(env2) == 'B2', b2.get_name(env2)
         assert b3.get_name(env2) == 'B3', b3.get_name(env2)
         assert b4.get_name(env2) == 'B4', b4.get_name(env2)
         assert b5.get_name(env2) == 'builder5', b5.get_name(env2)
-        assert b6.get_name(env2) == 'SCons.Builder.BuilderBase', b6.get_name(env2)
+        assert b6.get_name(env2) in [
+            'SCons.Builder.BuilderBase',
+            "<class 'SCons.Builder.BuilderBase'>",
+            'SCons.Memoize.BuilderBase',
+            "<class 'SCons.Memoize.BuilderBase'>",
+            ], b6.get_name(env2)
 
         for B in b3.get_src_builders(env):
             assert B.get_name(env) == 'bldr1'
index 4af4ced92355adb7a9d699dd72c953a0ef72b30e..2e146e3b65c7c7543e8965e4ac82e06c74966586 100644 (file)
@@ -239,6 +239,9 @@ class SubstitutionEnvironment:
     environment, we'll save that for a future refactoring when this
     class actually becomes useful.)
     """
+
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
     def __init__(self, **kw):
         """Initialization of an underlying SubstitutionEnvironment class.
         """
@@ -451,6 +454,8 @@ class Base(SubstitutionEnvironment):
     Environment.
     """
 
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
     #######################################################################
     # This is THE class for interacting with the SCons build engine,
     # and it contains a lot of stuff, so we're going to try to keep this
@@ -530,19 +535,16 @@ class Base(SubstitutionEnvironment):
     #######################################################################
 
     def get_calculator(self):
+        "__cacheable__"
         try:
-            return self._calculator
+            module = self._calc_module
+            c = apply(SCons.Sig.Calculator, (module,), CalculatorArgs)
         except AttributeError:
-            try:
-                module = self._calc_module
-                c = apply(SCons.Sig.Calculator, (module,), CalculatorArgs)
-            except AttributeError:
-                # Note that we're calling get_calculator() here, so the
-                # DefaultEnvironment() must have a _calc_module attribute
-                # to avoid infinite recursion.
-                c = SCons.Defaults.DefaultEnvironment().get_calculator()
-            self._calculator = c
-            return c
+            # Note that we're calling get_calculator() here, so the
+            # DefaultEnvironment() must have a _calc_module attribute
+            # to avoid infinite recursion.
+            c = SCons.Defaults.DefaultEnvironment().get_calculator()
+        return c
 
     def get_builder(self, name):
         """Fetch the builder with the specified name from the environment.
@@ -552,43 +554,47 @@ class Base(SubstitutionEnvironment):
         except KeyError:
             return None
 
+    def _gsm(self):
+        "__cacheable__"
+        try:
+            scanners = self._dict['SCANNERS']
+        except KeyError:
+            return None
+
+        sm = {}
+        # Reverse the scanner list so that, if multiple scanners
+        # claim they can scan the same suffix, earlier scanners
+        # in the list will overwrite later scanners, so that
+        # the result looks like a "first match" to the user.
+        if not SCons.Util.is_List(scanners):
+            scanners = [scanners]
+        else:
+            scanners = scanners[:] # copy so reverse() doesn't mod original
+        scanners.reverse()
+        for scanner in scanners:
+            for k in scanner.get_skeys(self):
+                sm[k] = scanner
+        return sm
+        
     def get_scanner(self, skey):
         """Find the appropriate scanner given a key (usually a file suffix).
+        __cacheable__
         """
-        try:
-            sm = self.scanner_map
-        except AttributeError:
-            try:
-                scanners = self._dict['SCANNERS']
-            except KeyError:
-                self.scanner_map = {}
-                return None
-            else:
-                self.scanner_map = sm = {}
-                # Reverse the scanner list so that, if multiple scanners
-                # claim they can scan the same suffix, earlier scanners
-                # in the list will overwrite later scanners, so that
-                # the result looks like a "first match" to the user.
-                if not SCons.Util.is_List(scanners):
-                    scanners = [scanners]
-                scanners.reverse()
-                for scanner in scanners:
-                    for k in scanner.get_skeys(self):
-                        sm[k] = scanner
-        try:
+        sm = self._gsm()
+        if sm.has_key(skey):
             return sm[skey]
-        except KeyError:
-            return None
+        return None
 
+    def _smd(self):
+        "__reset_cache__"
+        pass
+    
     def scanner_map_delete(self, kw=None):
         """Delete the cached scanner map (if we need to).
         """
         if not kw is None and not kw.has_key('SCANNERS'):
             return
-        try:
-            del self.scanner_map
-        except AttributeError:
-            pass
+        self._smd()
 
     def _update(self, dict):
         """Update an environment's values directly, bypassing the normal
@@ -1402,6 +1408,9 @@ class OverrideEnvironment(SubstitutionEnvironment):
     be proxied because they need *this* object's methods to fetch the
     values from the overrides dictionary.
     """
+
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
     def __init__(self, subject, overrides={}):
         if __debug__: logInstanceCreation(self, 'OverrideEnvironment')
         self.__dict__['__subject'] = subject
@@ -1519,3 +1528,12 @@ def NoSubstitutionProxy(subject):
             self.raw_to_mode(nkw)
             return apply(SCons.Util.scons_subst, nargs, nkw)
     return _NoSubstitutionProxy(subject)
+
+if not SCons.Memoize.has_metaclass:
+    _Base = Base
+    class Base(SCons.Memoize.Memoizer, _Base):
+        def __init__(self, *args, **kw):
+            SCons.Memoize.Memoizer.__init__(self)
+            apply(_Base.__init__, (self,)+args, kw)
+    Environment = Base
+
index 92b89c0eb038956caa0f08e1abc0bdffc42b666c..a0b1615ce7bc4d713aa5818b83d769d5e60ff2e8 100644 (file)
@@ -122,6 +122,9 @@ class Scanner:
     def get_skeys(self, env):
         return self.skeys
 
+    def __str__(self):
+        return self.name
+
 
 
 class CLVar(UserList.UserList):
@@ -1231,27 +1234,31 @@ def exists(env):
         assert env['BBB3'] == ['b3', 'c', 'd'], env['BBB3']
 
     def test_Copy(self):
-       """Test construction Environment copying
-
-       Update the copy independently afterwards and check that
-       the original remains intact (that is, no dangling
-       references point to objects in the copied environment).
-       Copy the original with some construction variable
-       updates and check that the original remains intact
-       and the copy has the updated values.
-       """
-       env1 = Environment(XXX = 'x', YYY = 'y')
-       env2 = env1.Copy()
-       env1copy = env1.Copy()
-       env2.Replace(YYY = 'yyy')
-       assert env1 != env2
-       assert env1 == env1copy
-
-       env3 = env1.Copy(XXX = 'x3', ZZZ = 'z3')
-       assert env3.Dictionary('XXX') == 'x3'
-       assert env3.Dictionary('YYY') == 'y'
-       assert env3.Dictionary('ZZZ') == 'z3'
-       assert env1 == env1copy
+        """Test construction Environment copying
+
+        Update the copy independently afterwards and check that
+        the original remains intact (that is, no dangling
+        references point to objects in the copied environment).
+        Copy the original with some construction variable
+        updates and check that the original remains intact
+        and the copy has the updated values.
+        """
+        env1 = Environment(XXX = 'x', YYY = 'y')
+        env2 = env1.Copy()
+        env1copy = env1.Copy()
+        assert env1copy == env1copy
+        assert env2 == env2
+        env2.Replace(YYY = 'yyy')
+        assert env2 == env2
+        assert env1 != env2
+        assert env1 == env1copy
+
+        env3 = env1.Copy(XXX = 'x3', ZZZ = 'z3')
+        assert env3 == env3
+        assert env3.Dictionary('XXX') == 'x3'
+        assert env3.Dictionary('YYY') == 'y'
+        assert env3.Dictionary('ZZZ') == 'z3'
+        assert env1 == env1copy
 
         assert env1['__env__'] is env1, env1['__env__']
         assert env2['__env__'] is env2, env2['__env__']
@@ -1277,6 +1284,8 @@ def exists(env):
         assert hasattr(env1, 'b1'), "env1.b1 was not set"
         assert env1.b1.env == env1, "b1.env doesn't point to env1"
         env2 = env1.Copy(BUILDERS = {'b2' : 2})
+        assert env2 is env2
+        assert env2 == env2
         assert hasattr(env1, 'b1'), "b1 was mistakenly cleared from env1"
         assert env1.b1.env == env1, "b1.env was changed"
         assert not hasattr(env2, 'b1'), "b1 was not cleared from env2"
index 1cb449c38c2e8b17b4bd43771ee72bf7e5160678..2a1917123c8749f63f08a34512386d2ed1e57bec 100644 (file)
@@ -43,7 +43,9 @@ class Executor:
     and sources for later processing as needed.
     """
 
-    def __init__(self, action, env=None, overridelist=[],
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
+    def __init__(self, action, env=None, overridelist=[{}],
                  targets=[], sources=[], builder_kw={}):
         if __debug__: logInstanceCreation(self)
         if not action:
@@ -58,56 +60,59 @@ class Executor:
     def get_build_env(self):
         """Fetch or create the appropriate build Environment
         for this Executor.
+        __cacheable__
         """
+        # Create the build environment instance with appropriate
+        # overrides.  These get evaluated against the current
+        # environment's construction variables so that users can
+        # add to existing values by referencing the variable in
+        # the expansion.
+        overrides = {}
+        for odict in self.overridelist:
+            overrides.update(odict)
         try:
-            return self.build_env
-        except AttributeError:
-            # Create the build environment instance with appropriate
-            # overrides.  These get evaluated against the current
-            # environment's construction variables so that users can
-            # add to existing values by referencing the variable in
-            # the expansion.
-            overrides = {}
-            for odict in self.overridelist:
-                overrides.update(odict)
-            try:
-                generate_build_dict = self.targets[0].generate_build_dict
-            except (AttributeError, IndexError):
-                pass
-            else:
-                overrides.update(generate_build_dict())
-
-            import SCons.Defaults
-            env = self.env or SCons.Defaults.DefaultEnvironment()
-            self.build_env = env.Override(overrides)
-
-            # Update the overrides with the $TARGET/$SOURCE variables for
-            # this target+source pair, so that evaluations of arbitrary
-            # Python functions have them in the __env__ environment
-            # they're passed.  Note that the underlying substitution
-            # functions also override these with their own $TARGET/$SOURCE
-            # expansions, which is *usually* duplicated effort, but covers
-            # a corner case where an Action is called directly from within
-            # a function action with different target and source lists.
-            self.build_env._update(SCons.Util.subst_dict(self.targets,
-                                                         self.sources))
-            return self.build_env
-
-    def do_nothing(self, target, errfunc, **kw):
+            generate_build_dict = self.targets[0].generate_build_dict
+        except (AttributeError, IndexError):
+            pass
+        else:
+            overrides.update(generate_build_dict())
+
+        import SCons.Defaults
+        env = self.env or SCons.Defaults.DefaultEnvironment()
+        build_env = env.Override(overrides)
+
+        # Update the overrides with the $TARGET/$SOURCE variables for
+        # this target+source pair, so that evaluations of arbitrary
+        # Python functions have them in the __env__ environment
+        # they're passed.  Note that the underlying substitution
+        # functions also override these with their own $TARGET/$SOURCE
+        # expansions, which is *usually* duplicated effort, but covers
+        # a corner case where an Action is called directly from within
+        # a function action with different target and source lists.
+        build_env._update(SCons.Util.subst_dict(self.targets, self.sources))
+
+        return build_env
+
+    def do_nothing(self, target, errfunc, kw):
         pass
 
-    def __call__(self, target, errfunc, **kw):
+    def do_execute(self, target, errfunc, kw):
         """Actually execute the action list."""
         kw = kw.copy()
         kw.update(self.builder_kw)
         apply(self.action, (self.targets, self.sources,
                             self.get_build_env(), errfunc), kw)
 
+    # use extra indirection because with new-style objects (Python 2.2
+    # and above) we can't override special methods, and nullify() needs
+    # to be able to do this.
+    
+    def __call__(self, target, errfunc, **kw):
+        self.do_execute(target, errfunc, kw)
+
     def cleanup(self):
-        try:
-            del self.build_env
-        except AttributeError:
-            pass
+        "__reset_cache__"
+        pass
 
     def add_sources(self, sources):
         """Add source files to this Executor's list.  This is necessary
@@ -116,34 +121,32 @@ class Executor:
         slist = filter(lambda x, s=self.sources: x not in s, sources)
         self.sources.extend(slist)
 
+    # another extra indirection for new-style objects and nullify...
+    
+    def my_str(self):
+        return self.action.genstring(self.targets,
+                                     self.sources,
+                                     self.get_build_env())
+
     def __str__(self):
-        try:
-            return self.string
-        except AttributeError:
-            action = self.action
-            self.string = action.genstring(self.targets,
-                                           self.sources,
-                                           self.get_build_env())
-            return self.string
+        "__cacheable__"
+        return self.my_str()
 
     def nullify(self):
-        self.__call__ = self.do_nothing
-        self.string = ''
+        "__reset_cache__"
+        self.do_execute = self.do_nothing
+        self.my_str     = lambda S=self: ''
 
     def get_contents(self):
         """Fetch the signature contents.  This, along with
         get_raw_contents(), is the real reason this class exists, so we
         can compute this once and cache it regardless of how many target
         or source Nodes there are.
+        __cacheable__
         """
-        try:
-            return self.contents
-        except AttributeError:
-            action = self.action
-            self.contents = action.get_contents(self.targets,
-                                                self.sources,
-                                                self.get_build_env())
-            return self.contents
+        return self.action.get_contents(self.targets,
+                                        self.sources,
+                                        self.get_build_env())
 
     def get_timestamp(self):
         """Fetch a time stamp for this Executor.  We don't have one, of
@@ -151,3 +154,11 @@ class Executor:
         timestamp module.
         """
         return 0
+
+if not SCons.Memoize.has_metaclass:
+    _Base = Executor
+    class Executor(SCons.Memoize.Memoizer, _Base):
+        def __init__(self, *args, **kw):
+            SCons.Memoize.Memoizer.__init__(self)
+            apply(_Base.__init__, (self,)+args, kw)
+
index 219efee92599d3cc92eee23cb4b6f9ab0365582a..d5b6dd2f312b091052296d7f4b8dda6b421f9e79 100644 (file)
@@ -96,10 +96,11 @@ class ExecutorTestCase(unittest.TestCase):
 
     def test_get_build_env(self):
         """Test fetching and generating a build environment"""
-        x = SCons.Executor.Executor(MyAction(), 'e', [], 't', ['s1', 's2'])
-        x.build_env = 'eee'
+        x = SCons.Executor.Executor(MyAction(), MyEnvironment(e=1), [],
+                                    't', ['s1', 's2'])
+        x.env = MyEnvironment(eee=1)
         be = x.get_build_env()
-        assert be == 'eee', be
+        assert be['eee'] == 1, be
 
         env = MyEnvironment(X='xxx')
         x = SCons.Executor.Executor(MyAction(),
@@ -171,17 +172,23 @@ class ExecutorTestCase(unittest.TestCase):
 
     def test_cleanup(self):
         """Test cleaning up an Executor"""
-        x = SCons.Executor.Executor('b', 'e', 'o', 't', ['s1', 's2'])
+        orig_env = MyEnvironment(e=1)
+        x = SCons.Executor.Executor('b', orig_env, [{'o':1}],
+                                    't', ['s1', 's2'])
 
+        be = x.get_build_env()
+        assert be['e'] == 1, be['e']
+        
         x.cleanup()
 
-        x.build_env = 'eee'
+        x.env = MyEnvironment(eee=1)
         be = x.get_build_env()
-        assert be == 'eee', be
+        assert be['eee'] == 1, be['eee']
 
         x.cleanup()
 
-        assert not hasattr(x, 'build_env')
+        be = x.get_build_env()
+        assert be['eee'] == 1, be['eee']
 
     def test_add_sources(self):
         """Test adding sources to an Executor"""
@@ -220,6 +227,7 @@ class ExecutorTestCase(unittest.TestCase):
         del result[:]
         x.nullify()
 
+        assert result == [], result
         x(MyNode([], []), None)
         assert result == [], result
         s = str(x)
@@ -230,13 +238,13 @@ class ExecutorTestCase(unittest.TestCase):
         env = MyEnvironment(C='contents')
 
         x = SCons.Executor.Executor(MyAction(), env, [], ['t'], ['s'])
-        x.contents = 'contents'
         c = x.get_contents()
-        assert c == 'contents', c
+        assert c == 'action1 action2 t s', c
 
-        x = SCons.Executor.Executor(MyAction(), env, [], ['t'], ['s'])
+        x = SCons.Executor.Executor(MyAction(actions=['grow']), env, [],
+                                    ['t'], ['s'])
         c = x.get_contents()
-        assert c == 'action1 action2 t s', c
+        assert c == 'grow t s', c
 
     def test_get_timestamp(self):
         """Test fetching the "timestamp" """
diff --git a/src/engine/SCons/Memoize.py b/src/engine/SCons/Memoize.py
new file mode 100644 (file)
index 0000000..ce3d9e6
--- /dev/null
@@ -0,0 +1,682 @@
+"""Memoizer
+
+Memoizer -- base class to provide automatic, optimized caching of
+method return values for subclassed objects.  Caching is activated by
+the presence of "__cacheable__" in the doc of a method (acts like a
+decorator).  The presence of "__cache_reset__" or "__reset_cache__"
+in the doc string instead indicates a method that should reset the
+cache, discarding any currently cached values.
+
+Note: current implementation is optimized for speed, not space.  The
+cache reset operation does not actually discard older results, and in
+fact, all cached results (and keys) are held indefinitely.
+
+Most of the work for this is done by copying and modifying the class
+definition itself, rather than the object instances.  This will
+therefore allow all instances of a class to get caching activated
+without requiring lengthy initialization or other management of the
+instance.
+
+[This could also be done using metaclassing (which would require
+Python 2.2) and decorators (which would require Python 2.4).  Current
+implementation is used due to Python 1.5.2 compatability requirement
+contraint.]
+
+A few notes:
+
+    * All local methods/attributes use a prefix of "_MeMoIZeR" to avoid
+      namespace collisions with the attributes of the objects
+      being cached.
+
+    * Based on performance evaluations of dictionaries, caching is
+      done by providing each object with a unique key attribute and
+      using the value of that attribute as an index for dictionary
+      lookup.  If an object doesn't have one of these attributes,
+      fallbacks are utilized (although they will be somewhat slower).
+
+      * To support this unique-value attribute correctly, it must be
+        removed whenever a __cmp__ operation is performed, and it must
+        be updated whenever a copy.copy or copy.deepcopy is performed,
+        so appropriate manipulation is provided by the Caching code
+        below.
+
+    * Cached values are stored in the class (indexed by the caching
+      key attribute, then by the name of the method called and the
+      constructed key of the arguments passed).  By storing them here
+      rather than on the instance, the instance can be compared,
+      copied, and pickled much easier.
+
+Some advantages:
+
+    * The method by which caching is implemented can be changed in a
+      single location and it will apply globally.
+
+    * Greatly simplified client code: remove lots of try...except or
+      similar handling of cached lookup.  Also usually more correct in
+      that it based caching on all input arguments whereas many
+      hand-implemented caching operations often miss arguments that
+      might affect results.
+
+    * Caching can be globally disabled very easily (for testing, etc.)
+    
+"""
+
+#
+# __COPYRIGHT__
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be included
+# in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
+# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
+# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#
+
+__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
+
+#TBD: for pickling, should probably revert object to unclassed state...
+
+import copy
+import string
+import sys
+
+#
+# Generate a key for an object that is to be used as the caching key
+# for that object.
+#
+# Current implementation: singleton generating a monotonically
+# increasing integer
+
+class MemoizerKey:
+    def __init__(self):
+        self._next_keyval = 0
+    def __call__(self):
+        r = self._next_keyval
+        self._next_keyval = self._next_keyval + 1
+        return str(r)
+Next_Memoize_Key = MemoizerKey()
+
+
+#
+# Memoized Class management.
+#
+# Classes can be manipulated just like object instances; we are going
+# to do some of that here, without the benefit of metaclassing
+# introduced in Python 2.2 (it would be nice to use that, but this
+# attempts to maintain backward compatibility to Python 1.5.2).
+#
+# The basic implementation therefore is to update the class definition
+# for any objects that we want to enable caching for.  The updated
+# definition performs caching activities for those methods
+# appropriately marked in the original class.
+#
+# When an object is created, its class is switched to this updated,
+# cache-enabled class definition, thereby enabling caching operations.
+#
+# To get an instance to used the updated, caching class, the instance
+# must declare the Memoizer as a base class and make sure to call the
+# Memoizer's __init__ during the instance's __init__.  The Memoizer's
+# __init__ will perform the class updating.
+
+# For Python 2.2 and later, where metaclassing is supported, it is
+# sufficient to provide a "__metaclass__ = Memoized_Metaclass" as part
+# of the class definition; the metaclassing will automatically invoke
+# the code herein properly.
+
+##import cPickle
+##def ALT0_MeMoIZeR_gen_key(argtuple, kwdict):
+##    return cPickle.dumps( (argtuple,kwdict) )
+
+def ALT1_MeMoIZeR_gen_key(argtuple, kwdict):
+    return repr(argtuple) + '|' + repr(kwdict)
+
+def ALT2_MeMoIZeR_gen_key(argtuple, kwdict):
+    return string.join(map(lambda A:
+                           getattr(A, '_MeMoIZeR_Key', str(A)),
+                           argtuple) + \
+                       map(lambda D:
+                           str(D[0])+
+                           getattr(D[1], '_MeMoIZeR_Key', str(D[1])),
+                           kwdict.items()),
+                       '|')
+
+def ALT3_MeMoIZeR_gen_key(argtuple, kwdict):
+    ret = []
+    for A in argtuple:
+        X = getattr(A, '_MeMoIZeR_Key', None)
+        if X:
+            ret.append(X)
+        else:
+            ret.append(str(A))
+    for K,V in kwdict.items():
+        ret.append(str(K))
+        X = getattr(V, '_MeMoIZeR_Key', None)
+        if X:
+            ret.append(X)
+        else:
+            ret.append(str(V))
+    return string.join(ret, '|')
+
+def ALT4_MeMoIZeR_gen_key(argtuple, kwdict):
+    if kwdict:
+        return string.join(map(lambda A:
+                               getattr(A, '_MeMoIZeR_Key', None) or str(A),
+                               argtuple) + \
+                           map(lambda D:
+                               str(D[0])+
+                               (getattr(D[1], '_MeMoIZeR_Key', None) or str(D[1])),
+                               kwdict.items()),
+                           '|')
+    return string.join(map(lambda A:
+                        getattr(A, '_MeMoIZeR_Key', None) or str(A),
+                        argtuple),
+                       '!')
+
+def ALT5_MeMoIZeR_gen_key(argtuple, kwdict):
+    A = string.join(map(str, argtuple), '|')
+    K = ''
+    if kwdict:
+        I = map(lambda K,D=kwdict: str(K)+'='+str(D[K]), kwdict.keys())
+        K = string.join(I, '|')
+    return string.join([A,K], '!')
+
+def ALT6_MeMoIZeR_gen_key(argtuple, kwdict):
+    A = string.join(map(str, map(id, argtuple)), '|')
+    K = ''
+    if kwdict:
+        I = map(lambda K,D=kwdict: str(K)+'='+str(id(D[K])), kwdict.keys())
+        K = string.join(I, '|')
+    return string.join([A,K], '!')
+
+def ALT7_MeMoIZeR_gen_key(argtuple, kwdict):
+    A = string.join(map(repr, argtuple), '|')
+    K = ''
+    if kwdict:
+        I = map(lambda K,D=kwdict: repr(K)+'='+repr(D[K]), kwdict.keys())
+        K = string.join(I, '|')
+    return string.join([A,K], '!')
+
+def ALT8_MeMoIZeR_gen_key(argtuple, kwdict):
+    ret = []
+    for A in argtuple:
+        X = getattr(A, '_MeMoIZeR_Key', None)
+        if X:
+            ret.append(X)
+        else:
+            ret.append(repr(A))
+    for K,V in kwdict.items():
+        ret.append(str(K))
+        X = getattr(V, '_MeMoIZeR_Key', None)
+        if X:
+            ret.append(X)
+        else:
+            ret.append(repr(V))
+    return string.join(ret, '|')
+
+def ALT9_MeMoIZeR_gen_key(argtuple, kwdict):
+    ret = []
+    for A in argtuple:
+        try:
+            X = A.__dict__.get('_MeMoIZeR_Key', None) or repr(A)
+        except (AttributeError, KeyError):
+            X = repr(A)
+        ret.append(X)
+    for K,V in kwdict.items():
+        ret.append(str(K))
+        ret.append('=')
+        try:
+            X = V.__dict__.get('_MeMoIZeR_Key', None) or repr(V)
+        except (AttributeError, KeyError):
+            X = repr(V)
+        ret.append(X)
+    return string.join(ret, '|')
+
+#_MeMoIZeR_gen_key = ALT9_MeMoIZeR_gen_key    # 8.8, 0.20
+_MeMoIZeR_gen_key = ALT8_MeMoIZeR_gen_key    # 8.5, 0.18
+#_MeMoIZeR_gen_key = ALT7_MeMoIZeR_gen_key    # 8.7, 0.17
+#_MeMoIZeR_gen_key = ALT6_MeMoIZeR_gen_key    # 
+#_MeMoIZeR_gen_key = ALT5_MeMoIZeR_gen_key    # 9.7, 0.20
+#_MeMoIZeR_gen_key = ALT4_MeMoIZeR_gen_key    # 8.6, 0.19
+#_MeMoIZeR_gen_key = ALT3_MeMoIZeR_gen_key    # 8.5, 0.20
+#_MeMoIZeR_gen_key = ALT2_MeMoIZeR_gen_key    # 10.1, 0.22
+#_MeMoIZeR_gen_key = ALT1_MeMoIZeR_gen_key    # 8.6 0.18
+
+
+
+## This is really the core worker of the Memoize module.  Any
+## __cacheable__ method ends up calling this function which tries to
+## return a previously cached value if it exists, and which calls the
+## actual function and caches the return value if it doesn't already
+## exist.
+##
+## This function should be VERY efficient: it will get called a lot
+## and its job is to be faster than what would be called.
+
+def Memoizer_cache_get(func, cdict, args, kw):
+    """Called instead of name to see if this method call's return
+    value has been cached.  If it has, just return the cached
+    value; if not, call the actual method and cache the return."""
+
+    obj = args[0]
+
+    ckey = obj._MeMoIZeR_Key + ':' + _MeMoIZeR_gen_key(args, kw)
+
+##    try:
+##        rval = cdict[ckey]
+##    except KeyError:
+##        rval = cdict[ckey] = apply(func, args, kw)
+
+    rval = cdict.get(ckey, "_MeMoIZeR")
+    if rval is "_MeMoIZeR":
+        rval = cdict[ckey] = apply(func, args, kw)
+
+##    rval = cdict.setdefault(ckey, apply(func, args, kw))
+
+##    if cdict.has_key(ckey):
+##        rval = cdict[ckey]
+##    else:
+##        rval = cdict[ckey] = apply(func, args, kw)
+
+    return rval
+
+def Memoizer_cache_get_self(func, cdict, self):
+    """Called instead of func(self) to see if this method call's
+    return value has been cached.  If it has, just return the cached
+    value; if not, call the actual method and cache the return.
+    Optimized version of Memoizer_cache_get for methods that take the
+    object instance as the only argument."""
+
+    ckey = self._MeMoIZeR_Key
+
+##    try:
+##        rval = cdict[ckey]
+##    except KeyError:
+##        rval = cdict[ckey] = func(self)
+
+    rval = cdict.get(ckey, "_MeMoIZeR")
+    if rval is "_MeMoIZeR":
+        rval = cdict[ckey] = func(self)
+
+##    rval = cdict.setdefault(ckey, func(self)))
+
+##    if cdict.has_key(ckey):
+##        rval = cdict[ckey]
+##    else:
+##        rval = cdict[ckey] = func(self)
+
+    return rval
+
+def Memoizer_cache_get_one(func, cdict, self, arg):
+    """Called instead of func(self, arg) to see if this method call's
+    return value has been cached.  If it has, just return the cached
+    value; if not, call the actual method and cache the return.
+    Optimized version of Memoizer_cache_get for methods that take the
+    object instance and one other argument only."""
+
+##    X = getattr(arg, "_MeMoIZeR_Key", None)
+##    if X:
+##        ckey = self._MeMoIZeR_Key +':'+ X
+##    else:
+##        ckey = self._MeMoIZeR_Key +':'+ str(arg)
+    ckey = self._MeMoIZeR_Key + ':' + \
+           (getattr(arg, "_MeMoIZeR_Key", None) or repr(arg))
+
+##    try:
+##        rval = cdict[ckey]
+##    except KeyError:
+##        rval = cdict[ckey] = func(self, arg)
+
+    rval = cdict.get(ckey, "_MeMoIZeR")
+    if rval is "_MeMoIZeR":
+        rval = cdict[ckey] = func(self, arg)
+
+##    rval = cdict.setdefault(ckey, func(self, arg)))
+
+##    if cdict.has_key(ckey):
+##        rval = cdict[ckey]
+##    else:
+##        rval = cdict[ckey] = func(self, arg)
+
+    return rval
+
+
+class _Memoizer_Simple:
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+        self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
+        #kwq: need to call original's setstate if it had one...
+
+    def _MeMoIZeR_reset(self):
+        self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
+        return 1
+
+
+class _Memoizer_Comparable:
+
+    def __setstate__(self, state):
+        self.__dict__.update(state)
+        self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
+        #kwq: need to call original's setstate if it had one...
+
+    def _MeMoIZeR_reset(self):
+        self.__dict__['_MeMoIZeR_Key'] = Next_Memoize_Key()
+        return 1
+
+    def __cmp__(self, other):
+        """A comparison might use the object dictionaries to
+        compare, so the dictionaries should contain caching
+        entries.  Make new dictionaries without those entries
+        to use with the underlying comparison."""
+
+        if self is other:
+            return 0
+
+        # We are here as a cached object, but cmp will flip its
+        # arguments back and forth and recurse attempting to get base
+        # arguments for the comparison, so we might have already been
+        # stripped.
+
+        try:
+            saved_d1 = self.__dict__
+            d1 = copy.copy(saved_d1)
+            del d1['_MeMoIZeR_Key']
+        except KeyError:
+            return self._MeMoIZeR_cmp(other)
+        self.__dict__ = d1
+
+        # Same thing for the other, but we should try to convert it
+        # here in case the _MeMoIZeR_cmp compares __dict__ objects
+        # directly.
+        
+        saved_other = None
+        try:
+            if other.__dict__.has_key('_MeMoIZeR_Key'):
+                saved_other = other.__dict__
+                d2 = copy.copy(saved_other)
+                del d2['_MeMoIZeR_Key']
+                other.__dict__ = d2
+        except (AttributeError, KeyError):
+            pass
+
+        # Both self and other have been prepared: perform the test,
+        # then restore the original dictionaries and exit
+        
+        rval = self._MeMoIZeR_cmp(other)
+
+        self.__dict__ = saved_d1
+        if saved_other:
+            other.__dict__ = saved_other
+
+        return rval
+
+
+def Analyze_Class(klass):
+    if klass.__dict__.has_key('_MeMoIZeR_converted'): return klass
+
+    original_name = str(klass)
+    
+    D,R,C = _analyze_classmethods(klass.__dict__, klass.__bases__)
+
+    if C:
+        modelklass = _Memoizer_Comparable
+        lcldict = {'_MeMoIZeR_cmp':C}
+    else:
+        modelklass = _Memoizer_Simple
+        lcldict = {}
+
+    klass.__dict__.update(memoize_classdict(modelklass, lcldict, D, R))
+
+    return klass
+
+
+# Note that each eval("lambda...") has a few \n's prepended to the
+# lambda, and furthermore that each of these evals has a different
+# number of \n's prepended.  This is to provide a little bit of info
+# for traceback or profile output, which generate things like 'File
+# "<string>", line X'.  X will be the number of \n's plus 1.
+
+def memoize_classdict(modelklass, new_klassdict, cacheable, resetting):
+    new_klassdict.update(modelklass.__dict__)
+    new_klassdict['_MeMoIZeR_converted'] = 1
+
+    for name,code in cacheable.items():
+        if code.func_code.co_argcount == 1 and \
+               not code.func_code.co_flags & 0xC:
+            newmethod = eval(
+                compile("\n"*1 +
+                "lambda self: Memoizer_cache_get_self(methcode, methcached, self)",
+                        "Memoizer_cache_get_self_lambda",
+                        "eval"),
+                {'methcode':code, 'methcached':{},
+                 'Memoizer_cache_get_self':Memoizer_cache_get_self},
+                {})
+        elif code.func_code.co_argcount == 2 and \
+               not code.func_code.co_flags & 0xC:
+            newmethod = eval(
+                compile("\n"*2 +
+                "lambda self, arg: Memoizer_cache_get_one(methcode, methcached, self, arg)",
+                        "Memoizer_cache_get_one_lambda",
+                        "eval"),
+                {'methcode':code, 'methcached':{},
+                 'Memoizer_cache_get_one':Memoizer_cache_get_one},
+                {})
+        else:
+            newmethod = eval(
+                compile("\n"*3 +
+                "lambda *args, **kw: Memoizer_cache_get(methcode, methcached, args, kw)",
+                        "Memoizer_cache_get_lambda",
+                        "eval"),
+                {'methcode':code, 'methcached':{},
+                 'Memoizer_cache_get':Memoizer_cache_get}, {})
+        new_klassdict[name] = newmethod
+
+    for name,code in resetting.items():
+        newmethod = eval("lambda obj_self, *args, **kw: (obj_self._MeMoIZeR_reset(), apply(rmethcode, (obj_self,)+args, kw))[1]",
+                         {'rmethcode':code}, {})
+        new_klassdict[name] = newmethod
+
+    return new_klassdict
+        
+
+def _analyze_classmethods(klassdict, klassbases):
+    """Given a class, performs a scan of methods for that class and
+    all its base classes (recursively). Returns aggregated results of
+    _scan_classdict calls where subclass methods are superimposed over
+    base class methods of the same name (emulating instance->class
+    method lookup)."""
+
+    D = {}
+    R = {}
+    C = None
+    
+    # Get cache/reset/cmp methods from subclasses
+
+    for K in klassbases:
+        if K.__dict__.has_key('_MeMoIZeR_converted'): continue
+        d,r,c = _analyze_classmethods(K.__dict__, K.__bases__)
+        D.update(d)
+        R.update(r)
+        C = c or C
+
+    # Delete base method info if current class has an override
+
+    for M in D.keys():
+        if M == '__cmp__': continue
+        if klassdict.has_key(M):
+            del D[M]
+    for M in R.keys():
+        if M == '__cmp__': continue
+        if klassdict.has_key(M):
+            del R[M]
+
+    # Get cache/reset/cmp from current class
+
+    d,r,c = _scan_classdict(klassdict)
+
+    # Update accumulated cache/reset/cmp methods
+
+    D.update(d)
+    R.update(r)
+    C = c or C
+
+    return D,R,C
+
+
+def _scan_classdict(klassdict):
+    """Scans the method dictionary of a class to find all methods
+    interesting to caching operations.  Returns a tuple of these
+    interesting methods:
+
+      ( dict-of-cachable-methods,
+        dict-of-cache-resetting-methods,
+        cmp_method_val or None)
+
+    Each dict has the name of the method as a key and the corresponding
+    value is the method body."""
+    
+    cache_setters = {}
+    cache_resetters = {}
+    cmp_if_exists = None
+    already_cache_modified = 0
+
+    for attr,val in klassdict.items():
+        if not callable(val): continue
+        if attr == '__cmp__':
+            cmp_if_exists = val
+            continue  # cmp can't be cached and can't reset cache
+        if attr == '_MeMoIZeR_cmp':
+            already_cache_modified = 1
+            continue
+        if not val.__doc__: continue
+        if string.find(val.__doc__, '__cache_reset__') > -1:
+            cache_resetters[attr] = val
+            continue
+        if string.find(val.__doc__, '__reset_cache__') > -1:
+            cache_resetters[attr] = val
+            continue
+        if string.find(val.__doc__, '__cacheable__') > -1:
+            cache_setters[attr] = val
+            continue
+    if already_cache_modified: cmp_if_exists = 'already_cache_modified'
+    return cache_setters, cache_resetters, cmp_if_exists
+        
+#
+# Primary Memoizer class.  This should be a base-class for any class
+# that wants method call results to be cached.  The sub-class should
+# call this parent class's __init__ method, but no other requirements
+# are made on the subclass (other than appropriate decoration).
+
+class Memoizer:
+    """Object which performs caching of method calls for its 'primary'
+    instance."""
+
+    def __init__(self):
+        self.__class__ = Analyze_Class(self.__class__)
+        self._MeMoIZeR_Key =  Next_Memoize_Key()
+    
+
+has_metaclass = 1
+# Find out if we are pre-2.2
+
+try:
+    vinfo = sys.version_info
+except AttributeError:
+    """Split an old-style version string into major and minor parts.  This
+    is complicated by the fact that a version string can be something
+    like 3.2b1."""
+    import re
+    version = string.split(string.split(sys.version, ' ')[0], '.')
+    vinfo = (int(version[0]), int(re.match('\d+', version[1]).group()))
+    del re
+
+need_version = (2, 2) # actual
+#need_version = (33, 0)  # always
+#need_version = (0, 0)  # never
+if vinfo[0] < need_version[0] or \
+   (vinfo[0] == need_version[0] and
+    vinfo[1] < need_version[1]):
+    has_metaclass = 0
+    class Memoized_Metaclass:
+        # Just a place-holder so pre-metaclass Python versions don't
+        # have to have special code for the Memoized classes.
+        pass
+else:
+
+    # Initialization is a wee bit of a hassle.  We want to do some of
+    # our own work for initialization, then pass on to the actual
+    # initialization function.  However, we have to be careful we
+    # don't interfere with (a) the super()'s initialization call of
+    # it's superclass's __init__, and (b) classes we are Memoizing
+    # that don't have their own __init__ but which have a super that
+    # has an __init__.  To do (a), we eval a lambda below where the
+    # actual init code is locally bound and the __init__ entry in the
+    # class's dictionary is replaced with the _MeMoIZeR_init call.  To
+    # do (b), we use _MeMoIZeR_superinit as a fallback if the class
+    # doesn't have it's own __init__.  Note that we don't use getattr
+    # to obtain the __init__ because we don't want to re-instrument
+    # parent-class __init__ operations (and we want to avoid the
+    # Object object's slot init if the class has no __init__).
+    
+    def _MeMoIZeR_init(actual_init, self, args, kw):
+        self.__dict__['_MeMoIZeR_Key'] =  Next_Memoize_Key()
+        apply(actual_init, (self,)+args, kw)
+
+    def _MeMoIZeR_superinit(self, cls, args, kw):
+        apply(super(cls, self).__init__, args, kw)
+        
+    class Memoized_Metaclass(type):
+        def __init__(cls, name, bases, cls_dict):
+            # Note that cls_dict apparently contains a *copy* of the
+            # attribute dictionary of the class; modifying cls_dict
+            # has no effect on the actual class itself.
+            D,R,C = _analyze_classmethods(cls_dict, bases)
+            if C:
+                modelklass = _Memoizer_Comparable
+                cls_dict['_MeMoIZeR_cmp'] = C
+            else:
+                modelklass = _Memoizer_Simple
+            klassdict = memoize_classdict(modelklass, cls_dict, D, R)
+
+            init = klassdict.get('__init__', None)
+            if not init:
+                # Make sure filename has os.sep+'SCons'+os.sep so that
+                # SCons.Script.find_deepest_user_frame doesn't stop here
+                import inspect # It's OK, can't get here for Python < 2.1
+                superinitcode = compile(
+                    "lambda self, *args, **kw: MPI(self, cls, args, kw)",
+                    inspect.getsourcefile(_MeMoIZeR_superinit),
+                    "eval")
+                superinit = eval(superinitcode,
+                                 {'cls':cls,
+                                  'MPI':_MeMoIZeR_superinit})
+                init = superinit
+                
+            newinitcode = compile(
+                "\n"*(init.func_code.co_firstlineno-1) +
+                "lambda self, args, kw: _MeMoIZeR_init(real_init, self, args, kw)",
+                init.func_code.co_filename, 'eval')
+            newinit = eval(newinitcode,
+                           {'real_init':init,
+                            '_MeMoIZeR_init':_MeMoIZeR_init},
+                           {})
+            klassdict['__init__'] = lambda self, *args, **kw: newinit(self, args, kw)
+
+            super(Memoized_Metaclass, cls).__init__(name, bases, klassdict)
+            # Now, since klassdict doesn't seem to have affected the class
+            # definition itself, apply klassdict.
+            for attr in klassdict.keys():
+                setattr(cls, attr, klassdict[attr])
+                
index 60ebb7914d388f9c92afab5b587c13cb011f1fc1..a67fa76cf262972b9971c0c12c09290089e4876e 100644 (file)
@@ -419,6 +419,12 @@ class EntryProxy(SCons.Util.Proxy):
             except AttributeError:
                 entry = self.get()
                 classname = string.split(str(entry.__class__), '.')[-1]
+                if classname[-2:] == "'>":
+                    # new-style classes report their name as:
+                    #   "<class 'something'>"
+                    # instead of the classic classes:
+                    #   "something"
+                    classname = classname[:-2]
                 raise AttributeError, "%s instance '%s' has no attribute '%s'" % (classname, entry.name, name)
             return attr
 
@@ -447,7 +453,6 @@ class Base(SCons.Node.Node):
 
         self.name = name
         self.fs = fs
-        self.relpath = {self : '.'}
 
         assert directory, "A directory must be provided"
 
@@ -465,31 +470,16 @@ class Base(SCons.Node.Node):
         """Completely clear a Node.FS.Base object of all its cached
         state (so that it can be re-evaluated by interfaces that do
         continuous integration builds).
+        __cache_reset__
         """
         SCons.Node.Node.clear(self)
-        try:
-            delattr(self, '_exists')
-        except AttributeError:
-            pass
-        try:
-            delattr(self, '_rexists')
-        except AttributeError:
-            pass
-        try:
-            delattr(self, '_str_val')
-        except AttributeError:
-            pass
-        self.relpath = {self : '.'}
 
     def get_dir(self):
         return self.dir
 
     def get_suffix(self):
-        try:
-            return self.ext
-        except AttributeError:
-            self.ext = SCons.Util.splitext(self.name)[1]
-            return self.ext
+        "__cacheable__"
+        return SCons.Util.splitext(self.name)[1]
 
     def rfile(self):
         return self
@@ -497,33 +487,29 @@ class Base(SCons.Node.Node):
     def __str__(self):
         """A Node.FS.Base object's string representation is its path
         name."""
-        try:
-            return self._str_val
-        except AttributeError:
-            global Save_Strings
-            if self.duplicate or self.is_derived():
-                str_val = self.get_path()
-            else:
-                str_val = self.srcnode().get_path()
-            if Save_Strings:
-                self._str_val = str_val
-            return str_val
+        global Save_Strings
+        if Save_Strings:
+            return self._save_str()
+        return self._get_str()
+
+    def _save_str(self):
+        "__cacheable__"
+        return self._get_str()
+
+    def _get_str(self):
+        if self.duplicate or self.is_derived():
+            return self.get_path()
+        return self.srcnode().get_path()
 
     rstr = __str__
 
     def exists(self):
-        try:
-            return self._exists
-        except AttributeError:
-            self._exists = self.fs.exists(self.abspath)
-            return self._exists
+        "__cacheable__"
+        return self.fs.exists(self.abspath)
 
     def rexists(self):
-        try:
-            return self._rexists
-        except AttributeError:
-            self._rexists = self.rfile().exists()
-            return self._rexists
+        "__cacheable__"
+        return self.rfile().exists()
 
     def is_under(self, dir):
         if self is dir:
@@ -537,44 +523,40 @@ class Base(SCons.Node.Node):
     def srcnode(self):
         """If this node is in a build path, return the node
         corresponding to its source file.  Otherwise, return
-        ourself."""
-        try:
-            return self._srcnode
-        except AttributeError:
-            dir=self.dir
-            name=self.name
-            while dir:
-                if dir.srcdir:
-                    self._srcnode = self.fs.Entry(name, dir.srcdir,
-                                                  klass=self.__class__)
-                    if self._srcnode.is_under(dir):
-                        # Shouldn't source from something in the build
-                        # path: probably means build_dir is under
-                        # src_dir and we are reflecting.
-                        break
-                    return self._srcnode
-                name = dir.name + os.sep + name
-                dir=dir.get_dir()
-            self._srcnode = self
-            return self._srcnode
+        ourself.
+        __cacheable__"""
+        dir=self.dir
+        name=self.name
+        while dir:
+            if dir.srcdir:
+                srcnode = self.fs.Entry(name, dir.srcdir,
+                                        klass=self.__class__)
+                if srcnode.is_under(dir):
+                    # Shouldn't source from something in the build
+                    # path: probably means build_dir is under
+                    # src_dir and we are reflecting.
+                    break
+                return srcnode
+            name = dir.name + os.sep + name
+            dir=dir.get_dir()
+        return self
 
     def get_path(self, dir=None):
         """Return path relative to the current working directory of the
         Node.FS.Base object that owns us."""
         if not dir:
             dir = self.fs.getcwd()
-        try:
-            return self.relpath[dir]
-        except KeyError:
-            path_elems = []
-            d = self
+        path_elems = []
+        d = self
+        if d == dir:
+            path_elems.append('.')
+        else:
             while d != dir and not isinstance(d, ParentOfRoot):
                 path_elems.append(d.name)
                 d = d.dir
             path_elems.reverse()
-            ret = string.join(path_elems, os.sep)
-            self.relpath[dir] = ret
-            return ret
+        ret = string.join(path_elems, os.sep)
+        return ret
             
     def set_src_builder(self, builder):
         """Set the source code builder for this node."""
@@ -1142,7 +1124,8 @@ class Dir(Base):
 
         Set up this directory's entries and hook it into the file
         system tree.  Specify that directories (this Node) don't use
-        signatures for calculating whether they're current."""
+        signatures for calculating whether they're current.
+        __cache_reset__"""
 
         self.repositories = []
         self.srcdir = None
@@ -1166,30 +1149,11 @@ class Dir(Base):
                 if node != self and isinstance(node, Dir):
                     node.__clearRepositoryCache(duplicate)
                 else:
+                    node.clear()
                     try:
                         del node._srcreps
                     except AttributeError:
                         pass
-                    try:
-                        del node._rfile
-                    except AttributeError:
-                        pass
-                    try:
-                        del node._rexists
-                    except AttributeError:
-                        pass
-                    try:
-                        del node._exists
-                    except AttributeError:
-                        pass
-                    try:
-                        del node._srcnode
-                    except AttributeError:
-                        pass
-                    try:
-                        del node._str_val
-                    except AttributeError:
-                        pass
                     if duplicate != None:
                         node.duplicate=duplicate
     
@@ -1311,15 +1275,13 @@ class Dir(Base):
             return 0
 
     def rdir(self):
-        try:
-            return self._rdir
-        except AttributeError:
-            self._rdir = self
-            if not self.exists():
-                n = self.fs.Rsearch(self.path, clazz=Dir, cwd=self.fs.Top)
-                if n:
-                    self._rdir = n
-            return self._rdir
+        "__cacheable__"
+        rdir = self
+        if not self.exists():
+            n = self.fs.Rsearch(self.path, clazz=Dir, cwd=self.fs.Top)
+            if n:
+                rdir = n
+        return rdir
 
     def sconsign(self):
         """Return the .sconsign file info for this directory,
@@ -1420,9 +1382,8 @@ class File(Base):
                 'RDirs' : self.RDirs}
 
     def _morph(self):
-        """Turn a file system node into a File object."""
+        """Turn a file system node into a File object.  __cache_reset__"""
         self.scanner_paths = {}
-        self.found_includes = {}
         if not hasattr(self, '_local'):
             self._local = 0
 
@@ -1501,14 +1462,7 @@ class File(Base):
             path = scanner.path(env, target.cwd)
             target.scanner_paths[scanner] = path
 
-        key = str(id(env)) + '|' + str(id(scanner)) + '|' + string.join(map(str,path), ':')
-        try:
-            includes = self.found_includes[key]
-        except KeyError:
-            includes = scanner(self, env, path)
-            self.found_includes[key] = includes
-
-        return includes
+        return scanner(self, env, path)
 
     def _createDir(self):
         # ensure that the directories for this node are
@@ -1537,14 +1491,7 @@ class File(Base):
                 # created the directory, depending on whether the -n
                 # option was used or not.  Delete the _exists and
                 # _rexists attributes so they can be reevaluated.
-                try:
-                    delattr(dirnode, '_exists')
-                except AttributeError:
-                    pass
-                try:
-                    delattr(dirnode, '_rexists')
-                except AttributeError:
-                    pass
+                dirnode.clear()
             except OSError:
                 pass
 
@@ -1589,22 +1536,14 @@ class File(Base):
         return None
 
     def built(self):
-        """Called just after this node is sucessfully built."""
+        """Called just after this node is successfully built.
+        __cache_reset__"""
         # Push this file out to cache before the superclass Node.built()
         # method has a chance to clear the build signature, which it
         # will do if this file has a source scanner.
         if self.fs.CachePath and self.fs.exists(self.path):
             CachePush(self, [], None)
         SCons.Node.Node.built(self)
-        self.found_includes = {}
-        try:
-            delattr(self, '_exists')
-        except AttributeError:
-            pass
-        try:
-            delattr(self, '_rexists')
-        except AttributeError:
-            pass
 
     def visited(self):
         if self.fs.CachePath and self.fs.cache_force and self.fs.exists(self.path):
@@ -1656,7 +1595,11 @@ class File(Base):
 
     def is_pseudo_derived(self):
         return self.has_src_builder()
-    
+
+    def _rmv_existing(self):
+        '__cache_reset__'
+        Unlink(self, [], None)
+        
     def prepare(self):
         """Prepare for this file to be created."""
         SCons.Node.Node.prepare(self)
@@ -1664,11 +1607,7 @@ class File(Base):
         if self.get_state() != SCons.Node.up_to_date:
             if self.exists():
                 if self.is_derived() and not self.precious:
-                    Unlink(self, [], None)
-                    try:
-                        delattr(self, '_exists')
-                    except AttributeError:
-                        pass
+                    self._rmv_existing()
             else:
                 try:
                     self._createDir()
@@ -1684,9 +1623,13 @@ class File(Base):
         return None
 
     def exists(self):
+        "__cacheable__"
         # Duplicate from source path if we are set up to do this.
         if self.duplicate and not self.is_derived() and not self.linked:
-            src=self.srcnode().rfile()
+            src=self.srcnode()
+            if src is self:
+                return Base.exists(self)
+            src = src.rfile()
             if src.abspath != self.abspath and src.exists():
                 self._createDir()
                 try:
@@ -1703,14 +1646,7 @@ class File(Base):
                 # created the file, depending on whether the -n
                 # option was used or not.  Delete the _exists and
                 # _rexists attributes so they can be reevaluated.
-                try:
-                    delattr(self, '_exists')
-                except AttributeError:
-                    pass
-                try:
-                    delattr(self, '_rexists')
-                except AttributeError:
-                    pass
+                self.clear()
         return Base.exists(self)
 
     def new_binfo(self):
@@ -1791,23 +1727,20 @@ class File(Base):
                         LocalCopy(self, r, None)
                         self.store_info(self.binfo)
                     return 1
-            self._rfile = self
             return None
         else:
             old = self.get_stored_info()
             return (old == self.binfo)
 
     def rfile(self):
-        try:
-            return self._rfile
-        except AttributeError:
-            self._rfile = self
-            if not self.exists():
-                n = self.fs.Rsearch(self.path, clazz=File,
-                                    cwd=self.fs.Top)
-                if n:
-                    self._rfile = n
-            return self._rfile
+        "__cacheable__"
+        rfile = self
+        if not self.exists():
+            n = self.fs.Rsearch(self.path, clazz=File,
+                                cwd=self.fs.Top)
+            if n:
+                rfile = n
+        return rfile
 
     def rstr(self):
         return str(self.rfile())
@@ -1840,7 +1773,9 @@ def find_file(filename, paths, node_factory=default_fs.File, verbose=None):
     find_file(str, [Dir()]) -> [nodes]
 
     filename - a filename to find
-    paths - a list of directory path *nodes* to search in
+    paths - a list of directory path *nodes* to search in.  Can be
+            represented as a list, a tuple, or a callable that is
+            called with no arguments and returns the list or tuple.
 
     returns - the node created from the found file.
 
@@ -1853,6 +1788,10 @@ def find_file(filename, paths, node_factory=default_fs.File, verbose=None):
     if verbose and not SCons.Util.is_String(verbose):
         verbose = "find_file"
     retval = None
+
+    if callable(paths):
+        paths = paths()
+
     for dir in paths:
         if verbose:
             sys.stdout.write("  %s: looking for '%s' in '%s' ...\n" % (verbose, filename, dir))
index c829f2223b0d0799ea7e1cffec3811db1589b197..e71093f66337552300d9242f6f95f1e163129907 100644 (file)
@@ -211,7 +211,7 @@ class BuildDirTestCase(unittest.TestCase):
         assert str(f2) == os.path.normpath('build/var2/test.in'), str(f2)
         # Build path exists
         assert f2.exists()
-        # ...and should copy the file from src to build path
+        # ...and exists() should copy the file from src to build path
         assert test.read(['work', 'build', 'var2', 'test.in']) == 'test.in',\
                test.read(['work', 'build', 'var2', 'test.in'])
         # Since exists() is true, so should rexists() be
@@ -898,10 +898,6 @@ class FSTestCase(unittest.TestCase):
         assert deps == [xyz], deps
         assert s.call_count == 1, s.call_count
 
-        deps = f12.get_found_includes(env, s, t1)
-        assert deps == [xyz], deps
-        assert s.call_count == 1, s.call_count
-
         f12.built()
 
         deps = f12.get_found_includes(env, s, t1)
@@ -946,9 +942,9 @@ class FSTestCase(unittest.TestCase):
         f1 = fs.File(test.workpath("do_i_exist"))
         assert not f1.exists()
         test.write("do_i_exist","\n")
-        assert not f1.exists()
+        assert not f1.exists(), "exists() call not cached"
         f1.built()
-        assert f1.exists()
+        assert f1.exists(), "exists() call caching not reset"
         test.unlink("do_i_exist")
         assert f1.exists()
         f1.built()
@@ -1822,33 +1818,48 @@ class clearTestCase(unittest.TestCase):
     def runTest(self):
         """Test clearing FS nodes of cached data."""
         fs = SCons.Node.FS.FS()
+        test = TestCmd(workdir='')
 
         e = fs.Entry('e')
-        e._exists = 1
-        e._rexists = 1
-        e._str_val = 'e'
+        assert not e.exists()
+        assert not e.rexists()
+        assert str(e) == 'e', str(d)
         e.clear()
-        assert not hasattr(e, '_exists')
-        assert not hasattr(e, '_rexists')
-        assert not hasattr(e, '_str_val')
+        assert not e.exists()
+        assert not e.rexists()
+        assert str(e) == 'e', str(d)
 
-        d = fs.Dir('d')
-        d._exists = 1
-        d._rexists = 1
-        d._str_val = 'd'
+        d = fs.Dir(test.workpath('d'))
+        test.subdir('d')
+        assert d.exists()
+        assert d.rexists()
+        assert str(d) == test.workpath('d'), str(d)
+        fs.rename(test.workpath('d'), test.workpath('gone'))
+        # Verify caching is active
+        assert d.exists(), 'caching not active'
+        assert d.rexists()
+        assert str(d) == test.workpath('d'), str(d)
+        # Now verify clear() resets the cache
         d.clear()
-        assert not hasattr(d, '_exists')
-        assert not hasattr(d, '_rexists')
-        assert not hasattr(d, '_str_val')
-
-        f = fs.File('f')
-        f._exists = 1
-        f._rexists = 1
-        f._str_val = 'f'
+        assert not d.exists()      
+        assert not d.rexists()
+        assert str(d) == test.workpath('d'), str(d)
+        
+        f = fs.File(test.workpath('f'))
+        test.write(test.workpath('f'), 'file f')
+        assert f.exists()
+        assert f.rexists()
+        assert str(f) == test.workpath('f'), str(f)
+        # Verify caching is active
+        test.unlink(test.workpath('f'))
+        assert f.exists()
+        assert f.rexists()
+        assert str(f) == test.workpath('f'), str(f)
+        # Now verify clear() resets the cache
         f.clear()
-        assert not hasattr(f, '_exists')
-        assert not hasattr(f, '_rexists')
-        assert not hasattr(f, '_str_val')
+        assert not f.exists()
+        assert not f.rexists()
+        assert str(f) == test.workpath('f'), str(f)
 
 class postprocessTestCase(unittest.TestCase):
     def runTest(self):
@@ -2066,7 +2077,7 @@ class SaveStringsTestCase(unittest.TestCase):
 
         s = map(str, nodes)
         expect = map(os.path.normpath, ['src/f', 'd1/f', 'd0/b', 'd1/b'])
-        assert s == expect, s
+        assert s == expect, 'node str() not cached: %s'%s
 
 if __name__ == "__main__":
     suite = unittest.TestSuite()
index 3c402d03fbd35eeaee964c426336e0d5ecb38e51..106e44e1e42d255ef78301f6b5f5aa89f7002eac 100644 (file)
@@ -525,13 +525,18 @@ class NodeTestCase(unittest.TestCase):
     def test_explain(self):
         """Test explaining why a Node must be rebuilt
         """
-        node = SCons.Node.Node()
+        class testNode(SCons.Node.Node):
+            def __str__(self): return 'xyzzy'
+        node = testNode()
         node.exists = lambda: None
-        node.__str__ = lambda: 'xyzzy'
+        # Can't do this with new-style classes (python bug #1066490)
+        #node.__str__ = lambda: 'xyzzy'
         result = node.explain()
         assert result == "building `xyzzy' because it doesn't exist\n", result
 
-        node = SCons.Node.Node()
+        class testNode2(SCons.Node.Node):
+            def __str__(self): return 'null_binfo'
+        node = testNode2()
         result = node.explain()
         assert result == None, result
 
@@ -540,7 +545,7 @@ class NodeTestCase(unittest.TestCase):
                 pass
 
         node.get_stored_info = Null_BInfo
-        node.__str__ = lambda: 'null_binfo'
+        #see above: node.__str__ = lambda: 'null_binfo'
         result = node.explain()
         assert result == "Cannot explain why `null_binfo' is being rebuilt: No previous build information found\n", result
 
@@ -802,7 +807,7 @@ class NodeTestCase(unittest.TestCase):
         """
         target = SCons.Node.Node()
         source = SCons.Node.Node()
-        s = target.get_source_scanner(source, None)
+        s = target.get_source_scanner(source)
         assert s is None, s
 
         ts1 = Scanner()
@@ -821,19 +826,19 @@ class NodeTestCase(unittest.TestCase):
         builder = Builder2(ts1)
             
         targets = builder([source])
-        s = targets[0].get_source_scanner(source, None)
+        s = targets[0].get_source_scanner(source)
         assert s is ts1, s
 
         target.builder_set(Builder2(ts1))
         target.builder.source_scanner = ts2
-        s = target.get_source_scanner(source, None)
+        s = target.get_source_scanner(source)
         assert s is ts2, s
 
         builder = Builder1(env=Environment(SCANNERS = [ts3]))
 
         targets = builder([source])
         
-        s = targets[0].get_source_scanner(source, builder.env)
+        s = targets[0].get_source_scanner(source)
         assert s is ts3, s
 
 
@@ -880,14 +885,13 @@ class NodeTestCase(unittest.TestCase):
         SCons.Node.implicit_deps_unchanged = None
         try:
             sn = StoredNode("eee")
-            sn._children = ['fake']
             sn.builder_set(Builder())
             sn.builder.target_scanner = s
 
             sn.scan()
 
             assert sn.implicit == [], sn.implicit
-            assert sn._children == [], sn._children
+            assert sn.children() == [], sn.children()
 
         finally:
             SCons.Sig.default_calc = save_default_calc
@@ -1100,7 +1104,6 @@ class NodeTestCase(unittest.TestCase):
 
         n.clear()
 
-        assert n.get_state() is None, n.get_state()
         assert not hasattr(n, 'binfo'), n.bsig
         assert n.includes is None, n.includes
         assert n.found_includes == {}, n.found_includes
@@ -1176,8 +1179,13 @@ class NodeListTestCase(unittest.TestCase):
             assert s == "['n3', 'n2', 'n1']", s
 
         r = repr(nl)
-        r = re.sub('at (0x)?[0-9A-Fa-f]+', 'at 0x', repr(nl))
-        l = string.join(["<__main__.MyNode instance at 0x>"]*3, ", ")
+        r = re.sub('at (0x)?[0-9a-z]+', 'at 0x', r)
+        # Don't care about ancestry: just leaf value of MyNode
+        r = re.sub('<.*?\.MyNode', '<MyNode', r)
+        # New-style classes report as "object"; classic classes report
+        # as "instance"...
+        r = re.sub("object", "instance", r)
+        l = string.join(["<MyNode instance at 0x>"]*3, ", ")
         assert r == '[%s]' % l, r
 
 
index c7a652de8d195a2cf9311443a8ddbdea209ad19b..e239e93d45b275b4942fd2f6009a3ccab41234d3 100644 (file)
@@ -92,6 +92,8 @@ class Node:
     build, or use to build other Nodes.
     """
 
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
     class Attrs:
         pass
 
@@ -147,18 +149,9 @@ class Node:
         return {}
 
     def get_build_env(self):
-        """Fetch the appropriate Environment to build this node."""
-        try:
-            build_env = self._build_env
-        except AttributeError:
-            # This gets called a lot, so cache it. A node gets created
-            # in the context of a specific environment and it doesn't
-            # get "moved" to a different environment, so caching this
-            # value is safe.
-            executor = self.get_executor()
-            build_env = executor.get_build_env()
-            self._build_env = build_env
-        return self._build_env
+        """Fetch the appropriate Environment to build this node.
+        __cacheable__"""
+        return self.get_executor().get_build_env()
 
     def set_executor(self, executor):
         """Set the action executor for this node."""
@@ -219,7 +212,7 @@ class Node:
         apply(executor, (self, errfunc), kw)
 
     def built(self):
-        """Called just after this node is sucessfully built."""
+        """Called just after this node is successfully built."""
 
         # Clear the implicit dependency caches of any Nodes
         # waiting for this Node to be built.
@@ -237,9 +230,7 @@ class Node:
 
         # Reset this Node's cached state since it was just built and
         # various state has changed.
-        save_state = self.get_state()
         self.clear()
-        self.set_state(save_state)
 
         # Had build info, so it should be stored in the signature
         # cache.  However, if the build info included a content
@@ -275,8 +266,8 @@ class Node:
         """Completely clear a Node of all its cached state (so that it
         can be re-evaluated by interfaces that do continuous integration
         builds).
+        __reset_cache__
         """
-        self.set_state(None)
         self.del_binfo()
         self.del_cinfo()
         try:
@@ -299,8 +290,8 @@ class Node:
         return reduce(lambda D,N,C=self.children(): D or (N in C), nodes, 0)
 
     def builder_set(self, builder):
+        "__cache_reset__"
         self.builder = builder
-        self._src_scanners = {}  # cached scanners are based on the builder
 
     def has_builder(self):
         """Return whether this Node has a builder or not.
@@ -312,6 +303,7 @@ class Node:
         and __nonzero__ attributes on instances of our Builder Proxy
         class(es), generating a bazillion extra calls and slowing
         things down immensely.
+        __cacheable__
         """
         try:
             b = self.builder
@@ -328,7 +320,8 @@ class Node:
         This allows an internal Builder created by SCons to be marked
         non-explicit, so that it can be overridden by an explicit
         builder that the user supplies (the canonical example being
-        directories)."""
+        directories).
+        __cacheable__"""
         return self.has_builder() and self.builder.is_explicit
 
     def get_builder(self, default_builder=None):
@@ -411,37 +404,28 @@ class Node:
 
         return deps
 
-    # cache used to make implicit_factory fast.
-    implicit_factory_cache = {}
-    
     def implicit_factory(self, path):
         """
         Turn a cache implicit dependency path into a node.
         This is called so many times that doing caching
         here is a significant performance boost.
+        __cacheable__
         """
-        try:
-            return self.implicit_factory_cache[path]
-        except KeyError:
-            n = self.builder.source_factory(path)
-            self.implicit_factory_cache[path] = n
-            return n
+        return self.builder.source_factory(path)
+
 
-    def get_source_scanner(self, node, build_env):
+    def get_source_scanner(self, node):
         """Fetch the source scanner for the specified node
 
         NOTE:  "self" is the target being built, "node" is
         the source file for which we want to fetch the scanner.
 
-        build_env is the build environment (it's self.get_build_env(),
-                  but the caller always knows this so it can give it
-                  to us).
-
         Implies self.has_builder() is true; again, expect to only be
         called from locations where this is already verified.
 
         This function may be called very often; it attempts to cache
         the scanner found to improve performance.
+        __cacheable__
         """
         # Called from scan() for each child (node) of this node
         # (self).  The scan() may be called multiple times, so this
@@ -451,22 +435,12 @@ class Node:
         # as an optimization of an already-determined value, not as a
         # changing parameter.
 
-        key = str(id(node)) + '|' + str(id(build_env))
-        try:
-            return self._src_scanners[key]
-        except AttributeError:
-            self._src_scanners = {}
-        except KeyError:
-            pass
-
         if not self.has_builder():
-            self._src_scanners[key] = None
             return None
         
         try:
             scanner = self.builder.source_scanner
             if scanner:
-                self._src_scanners[key] = scanner
                 return scanner
         except AttributeError:
             pass
@@ -475,8 +449,7 @@ class Node:
         # based on the node's scanner key (usually the file
         # extension).
         
-        scanner = build_env.get_scanner(node.scanner_key())
-        self._src_scanners[key] = scanner
+        scanner = self.get_build_env().get_scanner(node.scanner_key())
         return scanner
 
     def scan(self):
@@ -513,7 +486,7 @@ class Node:
                     self.del_binfo()
 
         for child in self.children(scan=0):
-            scanner = self.get_source_scanner(child, build_env)
+            scanner = self.get_source_scanner(child)
             if scanner:
                 deps = child.get_implicit_deps(build_env, scanner, self)
                 self._add_child(self.implicit, self.implicit_dict, deps)
@@ -545,28 +518,21 @@ class Node:
     def calc_signature(self, calc=None):
         """
         Select and calculate the appropriate build signature for a node.
+        __cacheable__
 
         self - the node
         calc - the signature calculation module
         returns - the signature
         """
-        try:
-            return self._calculated_sig
-        except AttributeError:
-            if self.is_derived():
-                import SCons.Defaults
-                
-                env = self.env or SCons.Defaults.DefaultEnvironment()
-                if env.use_build_signature():
-                    sig = self.calc_bsig(calc)
-                else:
-                    sig = self.calc_csig(calc)
-            elif not self.rexists():
-                sig = None
-            else:
-                sig = self.calc_csig(calc)
-            self._calculated_sig = sig
-            return sig
+        if self.is_derived():
+            import SCons.Defaults
+
+            env = self.env or SCons.Defaults.DefaultEnvironment()
+            if env.use_build_signature():
+                return self.calc_bsig(calc)
+        elif not self.rexists():
+            return None
+        return self.calc_csig(calc)
 
     def new_binfo(self):
         return BuildInfo()
@@ -769,10 +735,8 @@ class Node:
             self.wkids.append(wkid)
 
     def _children_reset(self):
-        try:
-            delattr(self, '_children')
-        except AttributeError:
-            pass
+        "__cache_reset__"
+        pass
 
     def filter_ignore(self, nodelist):
         ignore = self.ignore
@@ -782,17 +746,16 @@ class Node:
                 result.append(node)
         return result
 
+    def _children_get(self):
+        "__cacheable__"
+        return self.filter_ignore(self.all_children(scan=0))
+        
     def children(self, scan=1):
         """Return a list of the node's direct children, minus those
         that are ignored by this node."""
         if scan:
             self.scan()
-        try:
-            return self._children
-        except AttributeError:
-            c = self.all_children(scan=0)
-            self._children = self.filter_ignore(c)
-            return self._children
+        return self._children_get()
 
     def all_children(self, scan=1):
         """Return a list of all the node's direct children."""
@@ -875,7 +838,7 @@ class Node:
         if self.is_derived() and self.env:
             env = self.get_build_env()
             for s in self.sources:
-                scanner = self.get_source_scanner(s, env)
+                scanner = self.get_source_scanner(s)
                 def f(node, env=env, scanner=scanner, target=self):
                     return node.get_found_includes(env, scanner, target)
                 return SCons.Util.render_tree(s, f, 1)
@@ -1022,6 +985,14 @@ else:
 del l
 del ul
 
+if not SCons.Memoize.has_metaclass:
+    _Base = Node
+    class Node(SCons.Memoize.Memoizer, _Base):
+        def __init__(self, *args, **kw):
+            apply(_Base.__init__, (self,)+args, kw)
+            SCons.Memoize.Memoizer.__init__(self)
+
+
 def get_children(node, parent): return node.children()
 def ignore_cycle(node, stack): pass
 def do_nothing(node, parent): pass
index e5830a06348c8af55f509a5218641b1d040e4ad6..5296f93c31eb1eba0c5fae4e2f64df9186f797a2 100644 (file)
@@ -333,8 +333,8 @@ class CScannerTestCase10(unittest.TestCase):
         s = SCons.Scanner.C.CScan(fs=fs)
         path = s.path(env)
         test.write('include/fa.cpp', test.read('fa.cpp'))
-        deps = s(fs.File('#include/fa.cpp'), env, path)
         fs.chdir(fs.Dir('..'))
+        deps = s(fs.File('#include/fa.cpp'), env, path)
         deps_match(self, deps, [ 'include/fa.h', 'include/fb.h' ])
         test.unlink('include/fa.cpp')
 
index 27ba165f41edf4618d9d03c1c39cd6f4a0877f1a..fba973f10df26eb05b1ed8eabf7efc4568100002 100644 (file)
@@ -47,6 +47,7 @@ def DScan(fs = SCons.Node.FS.default_fs):
 
 class DScanner(SCons.Scanner.Classic):
     def find_include(self, include, source_dir, path):
+        if callable(path): path=path()
         # translate dots (package separators) to slashes
         inc = string.replace(include, '.', '/')
 
index 6ab878f4a58570f7efc28dd38c5893777d6b976e..4da0914d3d3943e756599164e23a741dd241a747 100644 (file)
@@ -62,6 +62,11 @@ class F90Scanner(SCons.Scanner.Classic):
         self.fs = fs
 
         def _scan(node, env, path, self=self, fs=fs):
+            node = node.rfile()
+
+            if not node.exists():
+                return []
+
             return self.scan(node, env, path)
 
         kw['function'] = _scan
@@ -73,11 +78,8 @@ class F90Scanner(SCons.Scanner.Classic):
         apply(SCons.Scanner.Current.__init__, (self,) + args, kw)
 
     def scan(self, node, env, path=()):
-        node = node.rfile()
-
-        if not node.exists():
-            return []
-
+        "__cacheable__"
+        
         # cache the includes list in node so we only scan it once:
         if node.includes != None:
             mods_and_includes = node.includes
index 512e5128bc4bfadaaaaaabea6da5ef4eceb5c13a..d9b57b90b31005713e632cb586288c62ee2b72a6 100644 (file)
@@ -80,6 +80,8 @@ def scan(node, env, libpath = (), fs = SCons.Node.FS.default_fs):
 
     result = []
 
+    if callable(libpath): libpath = libpath()
+    
     find_file = SCons.Node.FS.find_file
     adjustixes = SCons.Util.adjustixes
     for lib in libs:
index 00ad7fb592cb314de2b58669ba0a6ced218a2d68..e418c1778cfc01fefd0309f51118dbeca3a08b4b 100644 (file)
@@ -69,7 +69,7 @@ class FindPathDirsTestCase(unittest.TestCase):
 
         fpd = SCons.Scanner.FindPathDirs('LIBPATH', FS())
         result = fpd(env, dir)
-        assert result == ('xxx', 'foo'), result
+        assert str(result) == "('xxx', 'foo')", result
 
 class ScannerTestCase(unittest.TestCase):
 
@@ -434,7 +434,7 @@ class ClassicTestCase(unittest.TestCase):
         # Verify that overall scan results are cached even if individual
         # results are de-cached
         ret = s.function(n, env, ('foo2',))
-        assert ret == ['abc'], ret
+        assert ret == ['abc'], 'caching inactive; got: %s'%ret
 
         # Verify that it sorts what it finds.
         n.includes = ['xyz', 'uvw']
@@ -459,6 +459,8 @@ class ClassicCPPTestCase(unittest.TestCase):
         s = SCons.Scanner.ClassicCPP("Test", [], None, "")
 
         def _find_file(filename, paths, factory):
+            if callable(paths):
+                paths = paths()
             return paths[0]+'/'+filename
 
         save = SCons.Node.FS.find_file
@@ -474,7 +476,7 @@ class ClassicCPPTestCase(unittest.TestCase):
             assert i == 'bbb', i
 
         finally:
-            SCons.Node.FS.find_file = _find_file
+            SCons.Node.FS.find_file = save
 
 def suite():
     suite = unittest.TestSuite()
index cbab50c3c47d185730ceff59533fc8455736636f..1968a9e317ede3729d56e1cf36d84a72b7a0381f 100644 (file)
@@ -34,7 +34,6 @@ import string
 
 import SCons.Node.FS
 import SCons.Sig
-import SCons.UserTuple
 import SCons.Util
 
 
@@ -54,51 +53,51 @@ def Scanner(function, *args, **kw):
     else:
         return apply(Base, (function,) + args, kw)
 
-# Important, important, important performance optimization:
-#
-# The paths of Nodes returned from a FindPathDirs will be used to index
-# a dictionary that caches the values, so we don't have to look up the
-# same path over and over and over.  If FindPathDir returns just a tuple,
-# though, then the time it takes to compute the hash of the tuple grows
-# proportionally to the length of the tuple itself--and some people can
-# have very, very long strings of include directories...
-#
-# The solution is to wrap the tuple in an object, a UserTuple class
-# whose *id()* our caller can use to cache the appropriate value.
-# This means we have to guarantee that these ids genuinely represent
-# unique values, which we do by maintaining our own cache that maps the
-# expensive-to-hash tuple values to the easy-to-hash UniqueUserTuple
-# values that our caller uses.
-#
-# *Major* kudos to Eric Frias and his colleagues for finding this.
-class UniqueUserTuple(SCons.UserTuple.UserTuple):
-    def __hash__(self):
-        return id(self)
-
-PathCache = {}
 
+class Binder:
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+    def __init__(self, bindval):
+        self._val = bindval
+    def __call__(self):
+        return self._val
+    def __str__(self):
+        return str(self._val)
+        #debug: return 'B<%s>'%str(self._val)
+    
 class FindPathDirs:
     """A class to bind a specific *PATH variable name and the fs object
     to a function that will return all of the *path directories."""
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
     def __init__(self, variable, fs):
         self.variable = variable
         self.fs = fs
     def __call__(self, env, dir, argument=None):
+        "__cacheable__"
         try:
             path = env[self.variable]
         except KeyError:
             return ()
 
         path_tuple = tuple(self.fs.Rsearchall(env.subst_path(path),
-                                              must_exist = 0,
+                                              must_exist = 0, #kwq!
                                               clazz = SCons.Node.FS.Dir,
                                               cwd = dir))
-        try:
-            return PathCache[path_tuple]
-        except KeyError:
-            path_UserTuple = UniqueUserTuple(path_tuple)
-            PathCache[path_tuple] = path_UserTuple
-            return path_UserTuple
+        return Binder(path_tuple)
+
+if not SCons.Memoize.has_metaclass:
+    _FPD_Base = FindPathDirs
+    class FindPathDirs(SCons.Memoize.Memoizer, _FPD_Base):
+        "Cache-backed version of FindPathDirs"
+        def __init__(self, *args, **kw):
+            apply(_FPD_Base.__init__, (self,)+args, kw)
+            SCons.Memoize.Memoizer.__init__(self)
+    _BinderBase = Binder
+    class Binder(SCons.Memoize.Memoizer, _BinderBase):
+        "Cache-backed version of Binder"
+        def __init__(self, *args, **kw):
+            apply(_BinderBase.__init__, (self,)+args, kw)
+            SCons.Memoize.Memoizer.__init__(self)
+
 
 class Base:
     """
@@ -106,6 +105,8 @@ class Base:
     straightforward, single-pass scanning of a single file.
     """
 
+    __metaclass__ = SCons.Memoize.Memoized_Metaclass
+
     def __init__(self,
                  function,
                  name = "NONE",
@@ -135,6 +136,8 @@ class Base:
         (a construction environment, optional directory, and optional
         argument for this instance) and returns a tuple of the
         directories that can be searched for implicit dependency files.
+        May also return a callable() which is called with no args and
+        returns the tuple (supporting Bindable class).
 
         'node_class' - the class of Nodes which this scan will return.
         If node_class is None, then this scanner will not enforce any
@@ -186,6 +189,7 @@ class Base:
         self.recursive = recursive
 
     def path(self, env, dir = None):
+        "__cacheable__"
         if not self.path_function:
             return ()
         if not self.argument is _null:
@@ -242,6 +246,14 @@ class Base:
     def select(self, node):
         return self
 
+if not SCons.Memoize.has_metaclass:
+    _Base = Base
+    class Base(SCons.Memoize.Memoizer, _Base):
+        "Cache-backed version of Scanner Base"
+        def __init__(self, *args, **kw):
+            apply(_Base.__init__, (self,)+args, kw)
+            SCons.Memoize.Memoizer.__init__(self)
+
 
 class Selector(Base):
     """
@@ -296,22 +308,12 @@ class Classic(Current):
 
         self.cre = re.compile(regex, re.M)
         self.fs = fs
-        self._cached = {}
 
         def _scan(node, env, path=(), self=self):
             node = node.rfile()
-
             if not node.exists():
                 return []
-
-            key = str(id(node)) + '|' + string.join(map(str, path), ':')
-            try:
-                return self._cached[key]
-            except KeyError:
-                pass
-
-            self._cached[key] = scan_result = self.scan(node, path)
-            return scan_result
+            return self.scan(node, path)
 
         kw['function'] = _scan
         kw['path_function'] = FindPathDirs(path_variable, fs)
@@ -322,6 +324,8 @@ class Classic(Current):
         apply(Current.__init__, (self,) + args, kw)
 
     def find_include(self, include, source_dir, path):
+        "__cacheable__"
+        if callable(path): path = path()
         n = SCons.Node.FS.find_file(include,
                                     (source_dir,) + tuple(path),
                                     self.fs.File)
@@ -331,6 +335,7 @@ class Classic(Current):
         return SCons.Node.FS._my_normcase(include)
 
     def scan(self, node, path=()):
+        "__cacheable__"
 
         # cache the includes list in node so we only scan it once:
         if node.includes != None:
@@ -372,14 +377,19 @@ class ClassicCPP(Classic):
     the contained filename in group 1.
     """
     def find_include(self, include, source_dir, path):
+        "__cacheable__"
+        if callable(path):
+            path = path()   #kwq: extend callable to find_file...
+
         if include[0] == '"':
-            n = SCons.Node.FS.find_file(include[1],
-                                        (source_dir,) + tuple(path),
-                                        self.fs.File)
+            paths = Binder( (source_dir,) + tuple(path) )
         else:
-            n = SCons.Node.FS.find_file(include[1],
-                                        tuple(path) + (source_dir,),
-                                        self.fs.File)
+            paths = Binder( tuple(path) + (source_dir,) )
+
+        n = SCons.Node.FS.find_file(include[1],
+                                    paths,
+                                    self.fs.File)
+
         return n, include[1]
 
     def sort_key(self, include):
diff --git a/src/engine/SCons/UserTuple.py b/src/engine/SCons/UserTuple.py
deleted file mode 100644 (file)
index 8682783..0000000
+++ /dev/null
@@ -1,185 +0,0 @@
-"""SCons.UserTuple
-
-A more or less complete user-defined wrapper around tuple objects.
-
-This is basically cut-and-pasted from UserList, but it wraps an immutable
-tuple instead of a mutable list, primarily so that the wrapper object can
-be used as the hash of a dictionary.  The time it takes to compute the
-hash value of a builtin tuple grows as the length of the tuple grows, but
-the time it takes to compute hash value of an object can stay constant.
-
-"""
-
-#
-# __COPYRIGHT__
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be included
-# in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
-# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
-# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#
-
-__revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__"
-
-class UserTuple:
-    def __init__(self, inittuple=None):
-        self.data = ()
-        if inittuple is not None:
-            # XXX should this accept an arbitrary sequence?
-            if type(inittuple) == type(self.data):
-                self.data = inittuple[:]
-            elif isinstance(inittuple, UserTuple):
-                self.data = tuple(inittuple.data[:])
-            else:
-                self.data = tuple(inittuple)
-    def __str__(self): return str(self.data)
-    def __lt__(self, other): return self.data <  self.__cast(other)
-    def __le__(self, other): return self.data <= self.__cast(other)
-    def __eq__(self, other): return self.data == self.__cast(other)
-    def __ne__(self, other): return self.data != self.__cast(other)
-    def __gt__(self, other): return self.data >  self.__cast(other)
-    def __ge__(self, other): return self.data >= self.__cast(other)
-    def __cast(self, other):
-        if isinstance(other, UserTuple): return other.data
-        else: return other
-    def __cmp__(self, other):
-        return cmp(self.data, self.__cast(other))
-    def __contains__(self, item): return item in self.data
-    def __len__(self): return len(self.data)
-    def __getitem__(self, i): return self.data[i]
-    def __setitem__(self, i, item):
-        raise TypeError, "object doesn't support item assignment"
-    def __delitem__(self, i):
-        raise TypeError, "object doesn't support item deletion"
-    def __getslice__(self, i, j):
-        i = max(i, 0); j = max(j, 0)
-        return self.__class__(self.data[i:j])
-    def __setslice__(self, i, j, other):
-        raise TypeError, "object doesn't support slice assignment"
-    def __delslice__(self, i, j):
-        raise TypeError, "object doesn't support slice deletion"
-    def __add__(self, other):
-        if isinstance(other, UserTuple):
-            return self.__class__(self.data + other.data)
-        elif isinstance(other, type(self.data)):
-            return self.__class__(self.data + other)
-        else:
-            return self.__class__(self.data + tuple(other))
-    def __radd__(self, other):
-        if isinstance(other, UserTuple):
-            return self.__class__(other.data + self.data)
-        elif isinstance(other, type(self.data)):
-            return self.__class__(other + self.data)
-        else:
-            return self.__class__(tuple(other) + self.data)
-    def __mul__(self, n):
-        return self.__class__(self.data*n)
-    __rmul__ = __mul__
-    def __iter__(self):
-        return iter(self.data)
-    def __hash__(self):
-        return hash(self.data)
-
-if (__name__ == "__main__"):
-    t = UserTuple((1, 2, 3))
-    assert isinstance(t, UserTuple)
-    t2 = UserTuple(t)
-    assert isinstance(t2, UserTuple)
-    t3 = UserTuple([1, 2, 3])
-    assert isinstance(t3, UserTuple)
-    assert t == t2
-    assert t == t3
-    assert str(t) == '(1, 2, 3)', str(t)
-    assert t < UserTuple((2, 2, 3))
-    assert t <= UserTuple((2, 2, 3))
-    assert t == UserTuple((1, 2, 3))
-    assert t != UserTuple((3, 2, 1))
-    assert t > UserTuple((0, 2, 3))
-    assert t >= UserTuple((0, 2, 3))
-    assert cmp(t, UserTuple((0,))) == 1
-    assert cmp(t, UserTuple((1, 2, 3))) == 0
-    assert cmp(t, UserTuple((2,))) == -1
-    assert t < (2, 2, 3)
-    assert t <= (2, 2, 3)
-    assert t == (1, 2, 3)
-    assert t != (3, 2, 1)
-    assert t > (0, 2, 3)
-    assert t >= (0, 2, 3)
-    assert cmp(t, (0,)) == 1
-    assert cmp(t, (1, 2, 3)) == 0
-    assert cmp(t, (2,)) == -1
-    assert 3 in t
-    assert len(t) == 3
-    assert t[0] == 1
-    assert t[1] == 2
-    assert t[2] == 3
-    try:
-        t[0] = 4
-    except TypeError, e:
-        assert str(e) == "object doesn't support item assignment"
-    else:
-        raise "Did not catch expected TypeError"
-    try:
-        del t[0]
-    except TypeError, e:
-        assert str(e) == "object doesn't support item deletion"
-    else:
-        raise "Did not catch expected TypeError"
-    assert t[1:2] == (2,)
-    try:
-        t[0:2] = (4, 5)
-    except TypeError, e:
-        assert str(e) == "object doesn't support slice assignment", e
-    else:
-        raise "Did not catch expected TypeError"
-    try:
-        del t[0:2]
-    except TypeError, e:
-        assert str(e) == "object doesn't support slice deletion"
-    else:
-        raise "Did not catch expected TypeError"
-    assert t + UserTuple((4, 5)) == (1, 2, 3, 4, 5)
-    assert t + (4, 5) == (1, 2, 3, 4, 5)
-    assert t + [4, 5] == (1, 2, 3, 4, 5)
-    assert UserTuple((-1, 0)) + t == (-1, 0, 1, 2, 3)
-    assert (-1, 0) + t == (-1, 0, 1, 2, 3)
-    assert [-1, 0] + t == (-1, 0, 1, 2, 3)
-    assert t * 2 == (1, 2, 3, 1, 2, 3)
-    assert 2 * t == (1, 2, 3, 1, 2, 3)
-
-    t1 = UserTuple((1,))
-    t1a = UserTuple((1,))
-    t1b = UserTuple((1,))
-    t2 = UserTuple((2,))
-    t3 = UserTuple((3,))
-    d = {}
-    d[t1] = 't1'
-    d[t2] = 't2'
-    d[t3] = 't3'
-    assert d[t1] == 't1'
-    assert d[t1a] == 't1'
-    assert d[t1b] == 't1'
-    assert d[t2] == 't2'
-    assert d[t3] == 't3'
-    d[t1a] = 't1a'
-    assert d[t1] == 't1a'
-    assert d[t1a] == 't1a'
-    assert d[t1b] == 't1a'
-    d[t1b] = 't1b'
-    assert d[t1] == 't1b'
-    assert d[t1a] == 't1b'
-    assert d[t1b] == 't1b'
index b5488419fd23a5f526ed080f82dcb7cc535ab0d7..20e47344e5d18c536d73e57d6fede40dfa7307eb 100644 (file)
@@ -38,3 +38,5 @@ __buildsys__ = "__BUILDSYS__"
 __date__ = "__DATE__"
 
 __developer__ = "__DEVELOPER__"
+
+import SCons.Memoize
index 5c0291e44e58844aba656eccb243671ceba4845a..84d81850f99451ed55f1cff7e37219ec6ba843fc 100644 (file)
@@ -69,8 +69,8 @@ env_csig.TestDir(source='csig', target='csig.out')
 """)
 
 test.run(arguments=".", stderr=None)
-test.fail_test(test.read('bsig.out') != 'stuff\n')
-test.fail_test(test.read('csig.out') != 'stuff\n')
+test.must_match('bsig.out', 'stuff\n')
+test.must_match('csig.out', 'stuff\n')
 
 test.up_to_date(arguments='bsig.out')
 test.up_to_date(arguments='csig.out')
index 96b198e1df280cb3e50592331af2b22a25f830db..4b249e52758d8196a0400d624b99c510de7b904d 100644 (file)
@@ -49,7 +49,7 @@ def write_LIBDIRFLAGS(env, target, source):
     pre = env.subst('$LIBDIRPREFIX')
     suf = env.subst('$LIBDIRSUFFIX')
     f = open(str(target[0]), 'wb')
-    for arg in string.split(env.subst('$_LIBDIRFLAGS')):
+    for arg in string.split(env.subst('$_LIBDIRFLAGS', target=target)):
        if arg[:len(pre)] == pre:
            arg = arg[len(pre):]
        if arg[-len(suf):] == suf:
index 7019e2356ed935eef535aceef743b6d6844a89e2..44363106afecedf3f9a966858729f6dab14ee542 100644 (file)
@@ -481,14 +481,21 @@ test.run(arguments = 'SLF',
 # XXX Note that the generated .h files still get scanned twice,
 # once before they're generated and once after.  That's the
 # next thing to fix here.
-test.fail_test(test.read("MyCScan.out", "rb") != """\
+
+# Note KWQ 01 Nov 2004: used to check for a one for all counts below;
+# this was indirectly a test that the caching method in use at the
+# time was working.  With the introduction of Memoize-based caching,
+# the caching is performed right at the interface level, so the test
+# here cannot be run the same way; ergo real counts are used below.
+
+test.must_match("MyCScan.out", """\
 libg_1.c: 1
 libg_2.c: 1
 libg_3.c: 1
-libg_gx.h: 1
+libg_gx.h: 3
 libg_gy.h: 1
 libg_gz.h: 1
-libg_w.h: 1
+libg_w.h: 3
 """)
 
 test.pass_test()