diff options
author | Steven Knight <knight@baldmt.com> | 2006-12-16 01:43:01 (GMT) |
---|---|---|
committer | Steven Knight <knight@baldmt.com> | 2006-12-16 01:43:01 (GMT) |
commit | c4d04b3b45e7b71a1b28053b90084bcf2fdf9c0e (patch) | |
tree | 8a0d07c078ac21bf1ab689eacf06577069bb9231 /src/engine/SCons/Node | |
parent | b32cd624a5ad9526d28584b8e6c4a7958f436424 (diff) | |
download | SCons-c4d04b3b45e7b71a1b28053b90084bcf2fdf9c0e.zip SCons-c4d04b3b45e7b71a1b28053b90084bcf2fdf9c0e.tar.gz SCons-c4d04b3b45e7b71a1b28053b90084bcf2fdf9c0e.tar.bz2 |
Merged revisions 1675-1736 via svnmerge from
http://scons.tigris.org/svn/scons/branches/core
........
r1689 | stevenknight | 2006-11-06 20:56:29 -0600 (Mon, 06 Nov 2006) | 1 line
0.96.D483 - Merge changes for 0.96.93 packaging from the subsidiary branch.
........
r1690 | stevenknight | 2006-11-06 20:59:30 -0600 (Mon, 06 Nov 2006) | 1 line
0.96.D484 - Update HOWTO for releases. Fix name type in src/CHANGES.txt.
........
r1691 | stevenknight | 2006-11-08 13:55:36 -0600 (Wed, 08 Nov 2006) | 1 line
0.96.D485 - Fix MergeFlags() handling of None values. (John Pye)
........
r1692 | stevenknight | 2006-11-08 17:15:05 -0600 (Wed, 08 Nov 2006) | 1 line
0.96.D486 - Directly execute commands on Windows when possible. (Jay Kint)
........
r1693 | stevenknight | 2006-11-08 18:54:49 -0600 (Wed, 08 Nov 2006) | 1 line
0.96.D487 - Remove the semi-colon from the list of characters that determine when we use cmd
........
r1694 | stevenknight | 2006-11-09 01:34:06 -0600 (Thu, 09 Nov 2006) | 1 line
0.96.D488 - Pick up latex/bibtex 'Rerun to get citations correct' messages. (Dmitry Mikhin)
........
r1695 | stevenknight | 2006-11-11 08:36:33 -0600 (Sat, 11 Nov 2006) | 1 line
0.96.D489 - Back out the direct-execution-on-Windows change until we solve a corner case.
........
r1696 | stevenknight | 2006-11-15 10:33:10 -0600 (Wed, 15 Nov 2006) | 1 line
0.96.D490 - Fix the sconsign script when the .sconsign.dblite file is specified with its suf
........
r1697 | stevenknight | 2006-11-18 10:45:50 -0600 (Sat, 18 Nov 2006) | 4 lines
Complete move of test/sconsign/script.py to underneath test/sconsign/script/.
(This got left out of the previous checkin due to an error in the
script that resubmits Aegis changes to Subversion.)
........
r1698 | stevenknight | 2006-11-18 11:05:26 -0600 (Sat, 18 Nov 2006) | 1 line
0.96.D491 - Allow an Options converter to take the construction environment as a parameter.
........
r1699 | stevenknight | 2006-11-30 15:34:37 -0600 (Thu, 30 Nov 2006) | 1 line
0.96.D492 - Reverse the order in which we try the arguments Options converters, first a sing
........
r1700 | stevenknight | 2006-11-30 16:03:09 -0600 (Thu, 30 Nov 2006) | 1 line
0.96.D493 - Speed up rel_path() by avoiding recomputation of intermediate directory relative
........
r1701 | stevenknight | 2006-11-30 16:14:16 -0600 (Thu, 30 Nov 2006) | 1 line
0.96.D494 - More efficient get_suffix(): compute it once when we set the name.
........
r1702 | stevenknight | 2006-11-30 16:22:55 -0600 (Thu, 30 Nov 2006) | 1 line
0.96.D495 - Fix missing XML end tags.
........
r1703 | stevenknight | 2006-11-30 17:15:25 -0600 (Thu, 30 Nov 2006) | 1 line
0.96.D496 - Turn Memoizer into a simple counter for --debug=memoizer, not something that doe
........
r1704 | stevenknight | 2006-11-30 20:30:50 -0600 (Thu, 30 Nov 2006) | 1 line
0.96.D497 - Add the scons-time script, with doc and tests.
........
r1705 | stevenknight | 2006-11-30 23:28:20 -0600 (Thu, 30 Nov 2006) | 1 line
0.96.D498 - Update the copyright years string.
........
r1706 | stevenknight | 2006-12-01 11:54:22 -0600 (Fri, 01 Dec 2006) | 1 line
0.96.D499 - Fix _do_Lookup => _doLookup value-caching misspellings. (Ben Leslie)
........
r1707 | stevenknight | 2006-12-01 12:03:46 -0600 (Fri, 01 Dec 2006) | 1 line
0.96.D500 - Fix copyright test against debian build. (Walter Franzini)
........
r1708 | stevenknight | 2006-12-01 14:23:29 -0600 (Fri, 01 Dec 2006) | 1 line
0.96.D501 - Add #include lines for test portability. (Gary Oberbrunner)
........
r1709 | stevenknight | 2006-12-01 14:51:12 -0600 (Fri, 01 Dec 2006) | 1 line
0.96.D502 - Fix tests under Python versions with no profiler (pstats module).
........
r1710 | stevenknight | 2006-12-01 20:04:49 -0600 (Fri, 01 Dec 2006) | 1 line
0.96.D503 - Remove unnecessary os.path.normpath() calls. (Gary Oberbrunner)
........
r1711 | stevenknight | 2006-12-01 20:34:31 -0600 (Fri, 01 Dec 2006) | 1 line
0.96.D504 - Accomodate arbitray white space after a SWIG %module keyword. (Anonymous)
........
r1712 | stevenknight | 2006-12-05 14:49:54 -0600 (Tue, 05 Dec 2006) | 1 line
0.96.D506 - Cache substitutions of of Builder source suffixes. Use a new PathList module, and a refactor Node.FS.Rfindalldirs() method, to cache calculations of values like CPPPATH.
........
r1713 | stevenknight | 2006-12-05 18:43:36 -0600 (Tue, 05 Dec 2006) | 1 line
0.96.D507 - Use cached stat() values in diskchecks.
........
r1714 | stevenknight | 2006-12-05 21:11:24 -0600 (Tue, 05 Dec 2006) | 1 line
0.96.D508 - Fix Memoizer hit counts for methods memoizing simple values. Clean up the code for memoizing return values in a dictionary. Fix comments.
........
r1715 | stevenknight | 2006-12-06 07:23:18 -0600 (Wed, 06 Dec 2006) | 1 line
0.96.D369 - More efficient Node.FS.Dir.current() check. Fix some Windows test portability issues.
........
r1716 | stevenknight | 2006-12-06 12:24:32 -0600 (Wed, 06 Dec 2006) | 2 lines
Undo previous checkin (distributed incorrect Aegis change number).
........
r1717 | stevenknight | 2006-12-06 12:34:53 -0600 (Wed, 06 Dec 2006) | 1 line
0.96.D505 - Update ae-{cvs,svn}-ci for newer versions of aetar, and to not truncate descriptions.
........
r1718 | stevenknight | 2006-12-07 23:01:41 -0600 (Thu, 07 Dec 2006) | 1 line
0.96.D509 - Only look for mslink on Windows systems. (Sohail Somani)
........
r1719 | stevenknight | 2006-12-07 23:18:33 -0600 (Thu, 07 Dec 2006) | 1 line
0.96.D510 - Have the D compiler Tool use the same logic for shared libraries, too. (Paolo Invernizzi)
........
r1720 | stevenknight | 2006-12-07 23:29:47 -0600 (Thu, 07 Dec 2006) | 1 line
0.96.D511 - Generalize a JobTests.py test so it doesn't assume a specific order in which the operating system executes the threads.
........
r1721 | stevenknight | 2006-12-07 23:39:37 -0600 (Thu, 07 Dec 2006) | 1 line
0.96.D512 - Back out the Tool/dmd.py change; it breaks shared library linking for other lanuages beside D in the construction environment.
........
r1722 | stevenknight | 2006-12-07 23:47:11 -0600 (Thu, 07 Dec 2006) | 1 line
0.96.D513 - Test fixes: Windows portability, handle changes to Python 2.5 messages.
........
r1723 | stevenknight | 2006-12-08 00:00:13 -0600 (Fri, 08 Dec 2006) | 1 line
0.96.D514 - Change how the 'as' Tool is imported to accomodate the Python 2.6 'as' keyword.
........
r1724 | stevenknight | 2006-12-08 11:19:27 -0600 (Fri, 08 Dec 2006) | 1 line
0.96.D515 - Cache both Node.FS.find_file() and Node.FS.Dri.srcdir_find_file().
........
r1725 | stevenknight | 2006-12-08 17:27:35 -0600 (Fri, 08 Dec 2006) | 1 line
0.96.D516 - Better error when we try to fetch contents from an Entry that doesn't exist. (Tom Parker)
........
r1726 | stevenknight | 2006-12-08 23:28:55 -0600 (Fri, 08 Dec 2006) | 1 line
0.96.D517 - Make sure we pick up the scons-local directory regardless of where we chdir internally.
........
r1727 | stevenknight | 2006-12-11 16:25:53 -0600 (Mon, 11 Dec 2006) | 1 line
0.96.D518 - Cache results of Executor.get_unignored_sources() and Executor.process_sources(). Eliminate some map() and disambiguate() calls when scanning for implicit dependencies.
........
r1728 | stevenknight | 2006-12-12 14:32:22 -0600 (Tue, 12 Dec 2006) | 1 line
0.96.D519 - Fix SideEffect() when -j is used.
........
r1729 | stevenknight | 2006-12-12 16:58:15 -0600 (Tue, 12 Dec 2006) | 1 line
0.96.D520 - Add a srcdir keyword to Builder calls.
........
r1730 | stevenknight | 2006-12-12 21:40:59 -0600 (Tue, 12 Dec 2006) | 1 line
0.96.D521 - TeX/LaTeX updates, including handling files in subdirectories. (Joel B. Mohler, Rob Managan, Dmitry Mikhin)
........
r1731 | stevenknight | 2006-12-14 15:01:02 -0600 (Thu, 14 Dec 2006) | 1 line
0.96.D522 - Propogate TypeErrors during variable substitution for display to the user.
........
r1732 | stevenknight | 2006-12-14 20:01:49 -0600 (Thu, 14 Dec 2006) | 1 line
0.96.D523 - Fix the os.path.join() calls in EnvironmentTests.py.
........
r1733 | stevenknight | 2006-12-15 07:48:22 -0600 (Fri, 15 Dec 2006) | 1 line
0.96.D524 - Fix source directories as dependencies of an Alias (0.96.93 problem found by LilyPond).
........
r1735 | stevenknight | 2006-12-15 12:43:45 -0600 (Fri, 15 Dec 2006) | 1 line
0.96.D525 - Allow printing Debug.caller() output (or other end-of-run debugging info) when using -h.
........
r1736 | stevenknight | 2006-12-15 16:30:08 -0600 (Fri, 15 Dec 2006) | 1 line
0.96.D526 - Add an option to debug IndexError and NameError exceptions during variable substitution.
........
Diffstat (limited to 'src/engine/SCons/Node')
-rw-r--r-- | src/engine/SCons/Node/FS.py | 554 | ||||
-rw-r--r-- | src/engine/SCons/Node/FSTests.py | 83 | ||||
-rw-r--r-- | src/engine/SCons/Node/__init__.py | 54 |
3 files changed, 461 insertions, 230 deletions
diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index 382bca3..08b8d7d 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -346,9 +346,20 @@ class DiskChecker: self.set_ignore() def do_diskcheck_match(node, predicate, errorfmt): - path = node.abspath - if predicate(path): - raise TypeError, errorfmt % path + result = predicate() + try: + # If calling the predicate() cached a None value from stat(), + # remove it so it doesn't interfere with later attempts to + # build this Node as we walk the DAG. (This isn't a great way + # to do this, we're reaching into an interface that doesn't + # really belong to us, but it's all about performance, so + # for now we'll just document the dependency...) + if node._memo['stat'] is None: + del node._memo['stat'] + except (AttributeError, KeyError): + pass + if result: + raise TypeError, errorfmt % node.abspath def ignore_diskcheck_match(node, predicate, errorfmt): pass @@ -520,6 +531,8 @@ class Base(SCons.Node.Node): object identity comparisons. """ + memoizer_counters = [] + def __init__(self, name, directory, fs): """Initialize a generic Node.FS.Base object. @@ -531,6 +544,7 @@ class Base(SCons.Node.Node): SCons.Node.Node.__init__(self) self.name = name + self.suffix = SCons.Util.splitext(name)[1] self.fs = fs assert directory, "A directory must be provided" @@ -550,20 +564,11 @@ class Base(SCons.Node.Node): self.cwd = None # will hold the SConscript directory for target nodes self.duplicate = directory.duplicate - def clear(self): - """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) - def get_dir(self): return self.dir def get_suffix(self): - "__cacheable__" - return SCons.Util.splitext(self.name)[1] + return self.suffix def rfile(self): return self @@ -576,9 +581,16 @@ class Base(SCons.Node.Node): return self._save_str() return self._get_str() + memoizer_counters.append(SCons.Memoize.CountValue('_save_str')) + def _save_str(self): - "__cacheable__" - return self._get_str() + try: + return self._memo['_save_str'] + except KeyError: + pass + result = self._get_str() + self._memo['_save_str'] = result + return result def _get_str(self): if self.duplicate or self.is_derived(): @@ -587,17 +599,20 @@ class Base(SCons.Node.Node): rstr = __str__ + memoizer_counters.append(SCons.Memoize.CountValue('stat')) + def stat(self): - "__cacheable__" - try: return self.fs.stat(self.abspath) - except os.error: return None + try: return self._memo['stat'] + except KeyError: pass + try: result = self.fs.stat(self.abspath) + except os.error: result = None + self._memo['stat'] = result + return result def exists(self): - "__cacheable__" return not self.stat() is None def rexists(self): - "__cacheable__" return self.rfile().exists() def getmtime(self): @@ -640,7 +655,7 @@ class Base(SCons.Node.Node): """If this node is in a build path, return the node corresponding to its source file. Otherwise, return ourself. - __cacheable__""" + """ dir=self.dir name=self.name while dir: @@ -707,9 +722,48 @@ class Base(SCons.Node.Node): def target_from_source(self, prefix, suffix, splitext=SCons.Util.splitext): return self.dir.Entry(prefix + splitext(self.name)[0] + suffix) + def _Rfindalldirs_key(self, pathlist): + return pathlist + + memoizer_counters.append(SCons.Memoize.CountDict('Rfindalldirs', _Rfindalldirs_key)) + + def Rfindalldirs(self, pathlist): + """ + Return all of the directories for a given path list, including + corresponding "backing" directories in any repositories. + + The Node lookups are relative to this Node (typically a + directory), so memoizing result saves cycles from looking + up the same path for each target in a given directory. + """ + try: + memo_dict = self._memo['Rfindalldirs'] + except KeyError: + memo_dict = {} + self._memo['Rfindalldirs'] = memo_dict + else: + try: + return memo_dict[pathlist] + except KeyError: + pass + + create_dir_relative_to_self = self.Dir + result = [] + for path in pathlist: + if isinstance(path, SCons.Node.Node): + result.append(path) + else: + dir = create_dir_relative_to_self(path) + result.extend(dir.get_all_rdirs()) + + memo_dict[pathlist] = result + + return result + def RDirs(self, pathlist): """Search for a list of directories in the Repository list.""" - return self.fs.Rfindalldirs(pathlist, self.cwd) + cwd = self.cwd or self.fs._cwd + return cwd.Rfindalldirs(pathlist) class Entry(Base): """This is the class for generic Node.FS entries--that is, things @@ -723,13 +777,35 @@ class Entry(Base): pass def disambiguate(self): - if self.isdir() or self.srcnode().isdir(): + """ + """ + if self.isdir(): self.__class__ = Dir self._morph() - else: + elif self.isfile(): self.__class__ = File self._morph() self.clear() + else: + # There was nothing on-disk at this location, so look in + # the src directory. + # + # We can't just use self.srcnode() straight away because + # that would create an actual Node for this file in the src + # directory, and there might not be one. Instead, use the + # dir_on_disk() method to see if there's something on-disk + # with that name, in which case we can go ahead and call + # self.srcnode() to create the right type of entry. + srcdir = self.dir.srcnode() + if srcdir != self.dir and \ + srcdir.entry_exists_on_disk(self.name) and \ + self.srcnode().isdir(): + self.__class__ = Dir + self._morph() + else: + self.__class__ = File + self._morph() + self.clear() return self def rfile(self): @@ -759,7 +835,8 @@ class Entry(Base): return self.get_contents() if self.islink(): return '' # avoid errors for dangling symlinks - raise AttributeError + msg = "No such file or directory: '%s'" % self.abspath + raise SCons.Errors.UserError, msg def must_be_a_Dir(self): """Called to make sure a Node is a Dir. Since we're an @@ -867,14 +944,6 @@ class LocalFS: return '' -if SCons.Memoize.use_old_memoization(): - _FSBase = LocalFS - class LocalFS(SCons.Memoize.Memoizer, _FSBase): - def __init__(self, *args, **kw): - apply(_FSBase.__init__, (self,)+args, kw) - SCons.Memoize.Memoizer.__init__(self) - - #class RemoteFS: # # Skeleton for the obvious methods we might need from the # # abstraction layer for a remote filesystem. @@ -886,6 +955,8 @@ if SCons.Memoize.use_old_memoization(): class FS(LocalFS): + memoizer_counters = [] + def __init__(self, path = None): """Initialize the Node.FS subsystem. @@ -897,6 +968,8 @@ class FS(LocalFS): """ if __debug__: logInstanceCreation(self, 'Node.FS') + self._memo = {} + self.Root = {} self.SConstruct_dir = None self.CachePath = None @@ -915,10 +988,6 @@ class FS(LocalFS): self.Top.path = '.' self.Top.tpath = '.' self._cwd = self.Top - - def clear_cache(self): - "__cache_reset__" - pass def set_SConstruct_dir(self, dir): self.SConstruct_dir = dir @@ -942,6 +1011,11 @@ class FS(LocalFS): raise TypeError, "Tried to lookup %s '%s' as a %s." % \ (node.__class__.__name__, node.path, klass.__name__) + def _doLookup_key(self, fsclass, name, directory = None, create = 1): + return (fsclass, name, directory) + + memoizer_counters.append(SCons.Memoize.CountDict('_doLookup', _doLookup_key)) + def _doLookup(self, fsclass, name, directory = None, create = 1): """This method differs from the File and Dir factory methods in one important way: the meaning of the directory parameter. @@ -949,7 +1023,18 @@ class FS(LocalFS): name is expected to be an absolute path. If you try to look up a relative path with directory=None, then an AssertionError will be raised. - __cacheable__""" + """ + memo_key = (fsclass, name, directory) + try: + memo_dict = self._memo['_doLookup'] + except KeyError: + memo_dict = {} + self._memo['_doLookup'] = memo_dict + else: + try: + return memo_dict[memo_key] + except KeyError: + pass if not name: # This is a stupid hack to compensate for the fact that the @@ -990,6 +1075,7 @@ class FS(LocalFS): self.Root[''] = directory if not path_orig: + memo_dict[memo_key] = directory return directory last_orig = path_orig.pop() # strip last element @@ -1040,6 +1126,9 @@ class FS(LocalFS): directory.add_wkid(result) else: result = self.__checkClass(e, fsclass) + + memo_dict[memo_key] = result + return result def _transformPath(self, name, directory): @@ -1067,7 +1156,8 @@ class FS(LocalFS): # Correct such that '#/foo' is equivalent # to '#foo'. name = name[1:] - name = os.path.join('.', os.path.normpath(name)) + name = os.path.normpath(os.path.join('.', name)) + return (name, directory) elif not directory: directory = self._cwd return (os.path.normpath(name), directory) @@ -1116,7 +1206,6 @@ class FS(LocalFS): This method will raise TypeError if a directory is found at the specified path. """ - return self.Entry(name, directory, create, File) def Dir(self, name, directory = None, create = 1): @@ -1129,7 +1218,6 @@ class FS(LocalFS): This method will raise TypeError if a normal file is found at the specified path. """ - return self.Entry(name, directory, create, Dir) def BuildDir(self, build_dir, src_dir, duplicate=1): @@ -1155,22 +1243,6 @@ class FS(LocalFS): d = self.Dir(d) self.Top.addRepository(d) - def Rfindalldirs(self, pathlist, cwd): - """__cacheable__""" - if SCons.Util.is_String(pathlist): - pathlist = string.split(pathlist, os.pathsep) - if not SCons.Util.is_List(pathlist): - pathlist = [pathlist] - result = [] - for path in filter(None, pathlist): - if isinstance(path, SCons.Node.Node): - result.append(path) - continue - path, dir = self._transformPath(path, cwd) - dir = dir.Dir(path) - result.extend(dir.get_all_rdirs()) - return result - def CacheDebugWrite(self, fmt, target, cachefile): self.CacheDebugFP.write(fmt % (target, os.path.split(cachefile)[1])) @@ -1194,7 +1266,10 @@ class FS(LocalFS): Climb the directory tree, and look up path names relative to any linked build directories we find. - __cacheable__ + + Even though this loops and walks up the tree, we don't memoize + the return value because this is really only used to process + the command-line targets. """ targets = [] message = None @@ -1223,6 +1298,8 @@ class Dir(Base): """A class for directories in a file system. """ + memoizer_counters = [] + NodeInfo = DirNodeInfo BuildInfo = DirBuildInfo @@ -1238,7 +1315,7 @@ 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. - __cache_reset__""" + """ self.repositories = [] self.srcdir = None @@ -1258,8 +1335,8 @@ class Dir(Base): self.get_executor().set_action_list(self.builder.action) def diskcheck_match(self): - diskcheck_match(self, self.fs.isfile, - "File %s found where directory expected.") + diskcheck_match(self, self.isfile, + "File %s found where directory expected.") def __clearRepositoryCache(self, duplicate=None): """Called when we change the repository(ies) for a directory. @@ -1305,13 +1382,19 @@ class Dir(Base): def getRepositories(self): """Returns a list of repositories for this directory. - __cacheable__""" + """ if self.srcdir and not self.duplicate: return self.srcdir.get_all_rdirs() + self.repositories return self.repositories + memoizer_counters.append(SCons.Memoize.CountValue('get_all_rdirs')) + def get_all_rdirs(self): - """__cacheable__""" + try: + return self._memo['get_all_rdirs'] + except KeyError: + pass + result = [self] fname = '.' dir = self @@ -1320,6 +1403,9 @@ class Dir(Base): result.append(rep.Dir(fname)) fname = dir.name + os.sep + fname dir = dir.up() + + self._memo['get_all_rdirs'] = result + return result def addRepository(self, dir): @@ -1331,29 +1417,54 @@ class Dir(Base): def up(self): return self.entries['..'] + def _rel_path_key(self, other): + return str(other) + + memoizer_counters.append(SCons.Memoize.CountDict('rel_path', _rel_path_key)) + def rel_path(self, other): """Return a path to "other" relative to this directory. - __cacheable__""" - if isinstance(other, Dir): - name = [] + """ + try: + memo_dict = self._memo['rel_path'] + except KeyError: + memo_dict = {} + self._memo['rel_path'] = memo_dict else: try: - name = [other.name] - other = other.dir - except AttributeError: - return str(other) + return memo_dict[other] + except KeyError: + pass + if self is other: - return name and name[0] or '.' - i = 0 - for x, y in map(None, self.path_elements, other.path_elements): - if not x is y: - break - i = i + 1 - path_elems = ['..']*(len(self.path_elements)-i) \ - + map(lambda n: n.name, other.path_elements[i:]) \ - + name + + result = '.' + + elif not other in self.path_elements: + + try: + other_dir = other.dir + except AttributeError: + result = str(other) + else: + dir_rel_path = self.rel_path(other_dir) + if dir_rel_path == '.': + result = other.name + else: + result = dir_rel_path + os.sep + other.name + + else: + + i = self.path_elements.index(other) + 1 + + path_elems = ['..'] * (len(self.path_elements) - i) \ + + map(lambda n: n.name, other.path_elements[i:]) - return string.join(path_elems, os.sep) + result = string.join(path_elems, os.sep) + + memo_dict[other] = result + + return result def get_env_scanner(self, env, kw={}): return SCons.Defaults.DirEntryScanner @@ -1362,10 +1473,13 @@ class Dir(Base): return SCons.Defaults.DirEntryScanner def get_found_includes(self, env, scanner, path): - """Return the included implicit dependencies in this file. - Cache results so we only scan the file once per path - regardless of how many times this information is requested. - __cacheable__""" + """Return this directory's implicit dependencies. + + We don't bother caching the results because the scan typically + shouldn't be requested more than once (as opposed to scanning + .h file contents, which can be requested as many times as the + files is #included by other files). + """ if not scanner: return [] # Clear cached info for this Dir. If we already visited this @@ -1451,7 +1565,6 @@ class Dir(Base): return 1 def rdir(self): - "__cacheable__" if not self.exists(): norm_name = _my_normcase(self.name) for dir in self.dir.get_all_rdirs(): @@ -1500,7 +1613,6 @@ class Dir(Base): return self def entry_exists_on_disk(self, name): - """__cacheable__""" try: d = self.on_disk_entries except AttributeError: @@ -1515,8 +1627,14 @@ class Dir(Base): self.on_disk_entries = d return d.has_key(_my_normcase(name)) + memoizer_counters.append(SCons.Memoize.CountValue('srcdir_list')) + def srcdir_list(self): - """__cacheable__""" + try: + return self._memo['srcdir_list'] + except KeyError: + pass + result = [] dirname = '.' @@ -1533,6 +1651,8 @@ class Dir(Base): dirname = dir.name + os.sep + dirname dir = dir.up() + self._memo['srcdir_list'] = result + return result def srcdir_duplicate(self, name): @@ -1547,8 +1667,23 @@ class Dir(Base): return srcnode return None + def _srcdir_find_file_key(self, filename): + return filename + + memoizer_counters.append(SCons.Memoize.CountDict('srcdir_find_file', _srcdir_find_file_key)) + def srcdir_find_file(self, filename): - """__cacheable__""" + try: + memo_dict = self._memo['srcdir_find_file'] + except KeyError: + memo_dict = {} + self._memo['srcdir_find_file'] = memo_dict + else: + try: + return memo_dict[filename] + except KeyError: + pass + def func(node): if (isinstance(node, File) or isinstance(node, Entry)) and \ (node.is_derived() or node.is_pseudo_derived() or node.exists()): @@ -1562,7 +1697,9 @@ class Dir(Base): except KeyError: node = rdir.file_on_disk(filename) else: node = func(node) if node: - return node, self + result = (node, self) + memo_dict[filename] = result + return result for srcdir in self.srcdir_list(): for rdir in srcdir.get_all_rdirs(): @@ -1570,9 +1707,13 @@ class Dir(Base): except KeyError: node = rdir.file_on_disk(filename) else: node = func(node) if node: - return File(filename, self, self.fs), srcdir + result = (File(filename, self, self.fs), srcdir) + memo_dict[filename] = result + return result - return None, None + result = (None, None) + memo_dict[filename] = result + return result def dir_on_disk(self, name): if self.entry_exists_on_disk(name): @@ -1720,12 +1861,14 @@ class File(Base): """A class for files in a file system. """ + memoizer_counters = [] + NodeInfo = FileNodeInfo BuildInfo = FileBuildInfo def diskcheck_match(self): - diskcheck_match(self, self.fs.isdir, - "Directory %s found where file expected.") + diskcheck_match(self, self.isdir, + "Directory %s found where file expected.") def __init__(self, name, directory, fs): if __debug__: logInstanceCreation(self, 'Node.FS.File') @@ -1760,7 +1903,7 @@ class File(Base): # 'RDirs' : self.RDirs} def _morph(self): - """Turn a file system node into a File object. __cache_reset__""" + """Turn a file system node into a File object.""" self.scanner_paths = {} if not hasattr(self, '_local'): self._local = 0 @@ -1789,7 +1932,6 @@ class File(Base): self.dir.sconsign().set_entry(self.name, entry) def get_stored_info(self): - "__cacheable__" try: stored = self.dir.sconsign().get_entry(self.name) except (KeyError, OSError): @@ -1816,14 +1958,37 @@ class File(Base): def rel_path(self, other): return self.dir.rel_path(other) + def _get_found_includes_key(self, env, scanner, path): + return (id(env), id(scanner), path) + + memoizer_counters.append(SCons.Memoize.CountDict('get_found_includes', _get_found_includes_key)) + def get_found_includes(self, env, scanner, path): """Return the included implicit dependencies in this file. Cache results so we only scan the file once per path regardless of how many times this information is requested. - __cacheable__""" - if not scanner: - return [] - return scanner(self, env, path) + """ + memo_key = (id(env), id(scanner), path) + try: + memo_dict = self._memo['get_found_includes'] + except KeyError: + memo_dict = {} + self._memo['get_found_includes'] = memo_dict + else: + try: + return memo_dict[memo_key] + except KeyError: + pass + + if scanner: + result = scanner(self, env, path) + result = map(lambda N: N.disambiguate(), result) + else: + result = [] + + memo_dict[memo_key] = result + + return result def _createDir(self): # ensure that the directories for this node are @@ -1875,13 +2040,17 @@ class File(Base): def built(self): """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. + # + # We have to clear the memoized values *before* we push it to + # cache so that the memoization of the self.exists() return + # value doesn't interfere. + self.clear_memoized_values() if self.fs.CachePath and self.exists(): CachePush(self, [], None) - self.fs.clear_cache() SCons.Node.Node.built(self) def visited(self): @@ -1926,11 +2095,10 @@ class File(Base): return self.fs.build_dir_target_climb(self, self.dir, [self.name]) def is_pseudo_derived(self): - "__cacheable__" return self.has_src_builder() def _rmv_existing(self): - '__cache_reset__' + self.clear_memoized_values() Unlink(self, [], None) def prepare(self): @@ -1973,29 +2141,36 @@ class File(Base): # _rexists attributes so they can be reevaluated. self.clear() + memoizer_counters.append(SCons.Memoize.CountValue('exists')) + def exists(self): - "__cacheable__" + try: + return self._memo['exists'] + except KeyError: + pass # 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() - if src is self: - return Base.exists(self) - # At this point, src is meant to be copied in a build directory. - src = src.rfile() - if src.abspath != self.abspath: - if src.exists(): - self.do_duplicate(src) - # Can't return 1 here because the duplication might - # not actually occur if the -n option is being used. - else: - # The source file does not exist. Make sure no old - # copy remains in the build directory. - if Base.exists(self) or self.islink(): - self.fs.unlink(self.path) - # Return None explicitly because the Base.exists() call - # above will have cached its value if the file existed. - return None - return Base.exists(self) + if not src is self: + # At this point, src is meant to be copied in a build directory. + src = src.rfile() + if src.abspath != self.abspath: + if src.exists(): + self.do_duplicate(src) + # Can't return 1 here because the duplication might + # not actually occur if the -n option is being used. + else: + # The source file does not exist. Make sure no old + # copy remains in the build directory. + if Base.exists(self) or self.islink(): + self.fs.unlink(self.path) + # Return None explicitly because the Base.exists() call + # above will have cached its value if the file existed. + self._memo['exists'] = None + return None + result = Base.exists(self) + self._memo['exists'] = result + return result # # SIGNATURE SUBSYSTEM @@ -2063,7 +2238,6 @@ class File(Base): self.binfo = self.gen_binfo(calc) return self._cur2() def _cur2(self): - "__cacheable__" if self.always_build: return None if not self.exists(): @@ -2082,8 +2256,14 @@ class File(Base): else: return self.is_up_to_date() + memoizer_counters.append(SCons.Memoize.CountValue('rfile')) + def rfile(self): - "__cacheable__" + try: + return self._memo['rfile'] + except KeyError: + pass + result = self if not self.exists(): norm_name = _my_normcase(self.name) for dir in self.dir.get_all_rdirs(): @@ -2092,8 +2272,10 @@ class File(Base): if node and node.exists() and \ (isinstance(node, File) or isinstance(node, Entry) \ or not node.is_derived()): - return node - return self + result = node + break + self._memo['rfile'] = result + return result def rstr(self): return str(self.rfile()) @@ -2121,72 +2303,82 @@ class File(Base): default_fs = None -def find_file(filename, paths, verbose=None): +class FileFinder: + """ """ - find_file(str, [Dir()]) -> [nodes] + if SCons.Memoize.use_memoizer: + __metaclass__ = SCons.Memoize.Memoized_Metaclass - filename - a filename to find - 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. + memoizer_counters = [] - returns - the node created from the found file. + def __init__(self): + self._memo = {} - Find a node corresponding to either a derived file or a file - that exists already. + def _find_file_key(self, filename, paths, verbose=None): + return (filename, paths) + + memoizer_counters.append(SCons.Memoize.CountDict('find_file', _find_file_key)) - Only the first file found is returned, and none is returned - if no file is found. - __cacheable__ - """ - if verbose: - if not SCons.Util.is_String(verbose): - verbose = "find_file" - if not callable(verbose): - verbose = ' %s: ' % verbose - verbose = lambda s, v=verbose: sys.stdout.write(v + s) - else: - verbose = lambda x: x + def find_file(self, filename, paths, verbose=None): + """ + find_file(str, [Dir()]) -> [nodes] + + filename - a filename to find + 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. - if callable(paths): - paths = paths() + returns - the node created from the found file. - # Give Entries a chance to morph into Dirs. - paths = map(lambda p: p.must_be_a_Dir(), paths) + Find a node corresponding to either a derived file or a file + that exists already. - filedir, filename = os.path.split(filename) - if filedir: - def filedir_lookup(p, fd=filedir): + Only the first file found is returned, and none is returned + if no file is found. + """ + memo_key = self._find_file_key(filename, paths) + try: + memo_dict = self._memo['find_file'] + except KeyError: + memo_dict = {} + self._memo['find_file'] = memo_dict + else: try: - return p.Dir(fd) - except TypeError: - # We tried to look up a Dir, but it seems there's already - # a File (or something else) there. No big. - return None - paths = filter(None, map(filedir_lookup, paths)) - - for dir in paths: - verbose("looking for '%s' in '%s' ...\n" % (filename, dir)) - node, d = dir.srcdir_find_file(filename) - if node: - verbose("... FOUND '%s' in '%s'\n" % (filename, d)) - return node - return None + return memo_dict[memo_key] + except KeyError: + pass -def find_files(filenames, paths): - """ - find_files([str], [Dir()]) -> [nodes] + if verbose: + if not SCons.Util.is_String(verbose): + verbose = "find_file" + if not callable(verbose): + verbose = ' %s: ' % verbose + verbose = lambda s, v=verbose: sys.stdout.write(v + s) + else: + verbose = lambda x: x - filenames - a list of filenames to find - paths - a list of directory path *nodes* to search in + filedir, filename = os.path.split(filename) + if filedir: + def filedir_lookup(p, fd=filedir): + try: + return p.Dir(fd) + except TypeError: + # We tried to look up a Dir, but it seems there's + # already a File (or something else) there. No big. + return None + paths = filter(None, map(filedir_lookup, paths)) - returns - the nodes created from the found files. + result = None + for dir in paths: + verbose("looking for '%s' in '%s' ...\n" % (filename, dir)) + node, d = dir.srcdir_find_file(filename) + if node: + verbose("... FOUND '%s' in '%s'\n" % (filename, d)) + result = node + break - Finds nodes corresponding to either derived files or files - that exist already. + memo_dict[memo_key] = result - Only the first file found is returned for each filename, - and any files that aren't found are ignored. - """ - nodes = map(lambda x, paths=paths: find_file(x, paths), filenames) - return filter(None, nodes) + return result + +find_file = FileFinder().find_file diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py index 1b38ffe..434709c 100644 --- a/src/engine/SCons/Node/FSTests.py +++ b/src/engine/SCons/Node/FSTests.py @@ -740,14 +740,22 @@ class FileNodeInfoTestCase(_tempdirTestCase): test.write('fff', "fff\n") - assert ni.timestamp != os.path.getmtime('fff'), ni.timestamp - assert ni.size != os.path.getsize('fff'), ni.size + st = os.stat('fff') + + mtime = st[stat.ST_MTIME] + assert ni.timestamp != mtime, (ni.timestamp, mtime) + size = st[stat.ST_SIZE] + assert ni.size != size, (ni.size, size) fff.clear() ni.update(fff) - assert ni.timestamp == os.path.getmtime('fff'), ni.timestamp - assert ni.size == os.path.getsize('fff'), ni.size + st = os.stat('fff') + + mtime = st[stat.ST_MTIME] + assert ni.timestamp == mtime, (ni.timestamp, mtime) + size = st[stat.ST_SIZE] + assert ni.size == size, (ni.size, size) class FileBuildInfoTestCase(_tempdirTestCase): def test___init__(self): @@ -1219,9 +1227,9 @@ class FSTestCase(_tempdirTestCase): exc_caught = 0 try: e.get_contents() - except AttributeError: + except SCons.Errors.UserError: exc_caught = 1 - assert exc_caught, "Should have caught an AttributError" + assert exc_caught, "Should have caught an IOError" test.write("file", "file\n") try: @@ -1266,18 +1274,18 @@ class FSTestCase(_tempdirTestCase): assert t == 0, "expected 0, got %s" % str(t) test.subdir('tdir2') - d = fs.Dir('tdir2') f1 = test.workpath('tdir2', 'file1') f2 = test.workpath('tdir2', 'file2') test.write(f1, 'file1\n') test.write(f2, 'file2\n') - fs.File(f1) - fs.File(f2) current_time = float(int(time.time() / 2) * 2) t1 = current_time - 4.0 t2 = current_time - 2.0 os.utime(f1, (t1 - 2.0, t1)) os.utime(f2, (t2 - 2.0, t2)) + d = fs.Dir('tdir2') + fs.File(f1) + fs.File(f2) t = d.get_timestamp() assert t == t2, "expected %f, got %f" % (t2, t) @@ -1861,9 +1869,9 @@ class EntryTestCase(_tempdirTestCase): exc_caught = None try: e3n.get_contents() - except AttributeError: + except SCons.Errors.UserError: exc_caught = 1 - assert exc_caught, "did not catch expected AttributeError" + assert exc_caught, "did not catch expected SCons.Errors.UserError" test.subdir('e4d') test.write('e4f', "e4f\n") @@ -2133,25 +2141,25 @@ class RepositoryTestCase(_tempdirTestCase): rep2_sub_d1 = fs.Dir(test.workpath('rep2', 'sub', 'd1')) rep3_sub_d1 = fs.Dir(test.workpath('rep3', 'sub', 'd1')) - r = fs.Rfindalldirs(d1, fs.Top) + r = fs.Top.Rfindalldirs((d1,)) assert r == [d1], map(str, r) - r = fs.Rfindalldirs([d1, d2], fs.Top) + r = fs.Top.Rfindalldirs((d1, d2)) assert r == [d1, d2], map(str, r) - r = fs.Rfindalldirs('d1', fs.Top) + r = fs.Top.Rfindalldirs(('d1',)) assert r == [d1, rep1_d1, rep2_d1, rep3_d1], map(str, r) - r = fs.Rfindalldirs('#d1', fs.Top) + r = fs.Top.Rfindalldirs(('#d1',)) assert r == [d1, rep1_d1, rep2_d1, rep3_d1], map(str, r) - r = fs.Rfindalldirs('d1', sub) + r = sub.Rfindalldirs(('d1',)) assert r == [sub_d1, rep1_sub_d1, rep2_sub_d1, rep3_sub_d1], map(str, r) - r = fs.Rfindalldirs('#d1', sub) + r = sub.Rfindalldirs(('#d1',)) assert r == [d1, rep1_d1, rep2_d1, rep3_d1], map(str, r) - r = fs.Rfindalldirs(['d1', d2], fs.Top) + r = fs.Top.Rfindalldirs(('d1', d2)) assert r == [d1, rep1_d1, rep2_d1, rep3_d1, d2], map(str, r) def test_rexists(self): @@ -2223,6 +2231,7 @@ class find_fileTestCase(unittest.TestCase): """Testing find_file function""" test = TestCmd(workdir = '') test.write('./foo', 'Some file\n') + test.write('./foo2', 'Another file\n') test.subdir('same') test.subdir('bar') test.write(['bar', 'on_disk'], 'Another file\n') @@ -2237,7 +2246,7 @@ class find_fileTestCase(unittest.TestCase): node_pseudo = fs.File(test.workpath('pseudo')) node_pseudo.set_src_builder(1) # Any non-zero value. - paths = map(fs.Dir, ['.', 'same', './bar']) + paths = tuple(map(fs.Dir, ['.', 'same', './bar'])) nodes = [SCons.Node.FS.find_file('foo', paths)] nodes.append(SCons.Node.FS.find_file('baz', paths)) nodes.append(SCons.Node.FS.find_file('pseudo', paths)) @@ -2261,19 +2270,18 @@ class find_fileTestCase(unittest.TestCase): try: sio = StringIO.StringIO() sys.stdout = sio - SCons.Node.FS.find_file('foo', paths, verbose="xyz") - expect = " xyz: looking for 'foo' in '.' ...\n" + \ - " xyz: ... FOUND 'foo' in '.'\n" + SCons.Node.FS.find_file('foo2', paths, verbose="xyz") + expect = " xyz: looking for 'foo2' in '.' ...\n" + \ + " xyz: ... FOUND 'foo2' in '.'\n" c = sio.getvalue() assert c == expect, c sio = StringIO.StringIO() sys.stdout = sio - SCons.Node.FS.find_file('baz', paths, verbose=1) - expect = " find_file: looking for 'baz' in '.' ...\n" + \ - " find_file: looking for 'baz' in 'same' ...\n" + \ - " find_file: looking for 'baz' in 'bar' ...\n" + \ - " find_file: ... FOUND 'baz' in 'bar'\n" + SCons.Node.FS.find_file('baz2', paths, verbose=1) + expect = " find_file: looking for 'baz2' in '.' ...\n" + \ + " find_file: looking for 'baz2' in 'same' ...\n" + \ + " find_file: looking for 'baz2' in 'bar' ...\n" c = sio.getvalue() assert c == expect, c @@ -2717,12 +2725,29 @@ class disambiguateTestCase(unittest.TestCase): f = efile.disambiguate() assert f.__class__ is fff.__class__, f.__class__ + test.subdir('build') + test.subdir(['build', 'bdir']) + test.write(['build', 'bfile'], "build/bfile\n") + test.subdir('src') + test.write(['src', 'bdir'], "src/bdir\n") + test.subdir(['src', 'bfile']) + test.subdir(['src', 'edir']) test.write(['src', 'efile'], "src/efile\n") fs.BuildDir(test.workpath('build'), test.workpath('src')) + build_bdir = fs.Entry(test.workpath('build/bdir')) + d = build_bdir.disambiguate() + assert d is build_bdir, d + assert d.__class__ is ddd.__class__, d.__class__ + + build_bfile = fs.Entry(test.workpath('build/bfile')) + f = build_bfile.disambiguate() + assert f is build_bfile, f + assert f.__class__ is fff.__class__, f.__class__ + build_edir = fs.Entry(test.workpath('build/edir')) d = build_edir.disambiguate() assert d.__class__ is ddd.__class__, d.__class__ @@ -2731,6 +2756,10 @@ class disambiguateTestCase(unittest.TestCase): f = build_efile.disambiguate() assert f.__class__ is fff.__class__, f.__class__ + build_nonexistant = fs.Entry(test.workpath('build/nonexistant')) + f = build_nonexistant.disambiguate() + assert f.__class__ is fff.__class__, f.__class__ + class postprocessTestCase(unittest.TestCase): def runTest(self): """Test calling the postprocess() method.""" diff --git a/src/engine/SCons/Node/__init__.py b/src/engine/SCons/Node/__init__.py index 42be5b1..e5d064e 100644 --- a/src/engine/SCons/Node/__init__.py +++ b/src/engine/SCons/Node/__init__.py @@ -166,6 +166,8 @@ class Node: if SCons.Memoize.use_memoizer: __metaclass__ = SCons.Memoize.Memoized_Metaclass + memoizer_counters = [] + class Attrs: pass @@ -210,6 +212,8 @@ class Node: self.post_actions = [] self.linked = 0 # is this node linked to the build directory? + self.clear_memoized_values() + # Let the interface in which the build engine is embedded # annotate this Node with its own info (like a description of # what line in what file created the node, for example). @@ -223,7 +227,7 @@ class Node: def get_build_env(self): """Fetch the appropriate Environment to build this node. - __cacheable__""" + """ return self.get_executor().get_build_env() def get_build_scanner_path(self, scanner): @@ -349,8 +353,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.clear_memoized_values() self.executor_cleanup() self.del_binfo() try: @@ -361,13 +365,15 @@ class Node: self.found_includes = {} self.implicit = None + def clear_memoized_values(self): + self._memo = {} + def visited(self): """Called just after this node has been visited without requiring a build..""" pass def builder_set(self, builder): - "__cache_reset__" self.builder = builder def has_builder(self): @@ -424,7 +430,6 @@ class Node: signatures when they are used as source files to other derived files. For example: source with source builders are not derived in this sense, and hence should not return true. - __cacheable__ """ return self.has_builder() or self.side_effect @@ -474,7 +479,6 @@ class Node: d = filter(lambda x, seen=seen: not seen.has_key(x), n.get_found_includes(env, scanner, path)) if d: - d = map(lambda N: N.disambiguate(), d) deps.extend(d) for n in d: seen[n] = 1 @@ -609,24 +613,34 @@ class Node: env = self.env or SCons.Defaults.DefaultEnvironment() return env.get_calculator() + memoizer_counters.append(SCons.Memoize.CountValue('calc_signature')) + 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._memo['calc_signature'] + except KeyError: + pass if self.is_derived(): import SCons.Defaults env = self.env or SCons.Defaults.DefaultEnvironment() if env.use_build_signature(): - return self.get_bsig(calc) + result = self.get_bsig(calc) + else: + result = self.get_csig(calc) elif not self.rexists(): - return None - return self.get_csig(calc) + result = None + else: + result = self.get_csig(calc) + self._memo['calc_signature'] = result + return result def new_ninfo(self): return self.NodeInfo(self) @@ -661,7 +675,6 @@ class Node: node's children's signatures. We expect that they're already built and updated by someone else, if that's what's wanted. - __cacheable__ """ if calc is None: @@ -676,7 +689,7 @@ class Node: def calc_signature(node, calc=calc): return node.calc_signature(calc) - sources = executor.process_sources(None, self.ignore) + sources = executor.get_unignored_sources(self.ignore) sourcesigs = executor.process_sources(calc_signature, self.ignore) depends = self.depends @@ -767,7 +780,6 @@ class Node: return self.exists() def missing(self): - """__cacheable__""" return not self.is_derived() and \ not self.is_pseudo_derived() and \ not self.linked and \ @@ -850,7 +862,7 @@ class Node: self.wkids.append(wkid) def _children_reset(self): - "__cache_reset__" + self.clear_memoized_values() # We need to let the Executor clear out any calculated # bsig info that it's cached so we can re-calculate it. self.executor_cleanup() @@ -881,11 +893,17 @@ class Node: else: return self.sources + self.depends + self.implicit + memoizer_counters.append(SCons.Memoize.CountValue('_children_get')) + def _children_get(self): - "__cacheable__" + try: + return self._memo['children_get'] + except KeyError: + pass children = self._all_children_get() if self.ignore: children = filter(self.do_not_ignore, children) + self._memo['children_get'] = children return children def all_children(self, scan=1): @@ -1101,14 +1119,6 @@ else: del l del ul -if SCons.Memoize.use_old_memoization(): - _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 |