From 1e7d98c00155fc66b2a81b127fe4297d6fcf0f79 Mon Sep 17 00:00:00 2001 From: stevenknight Date: Wed, 19 Sep 2001 12:16:13 +0000 Subject: [PATCH] Clean up the Node.FS class. git-svn-id: http://scons.tigris.org/svn/scons/trunk@55 fdb21ef1-2011-0410-befe-b5e4ea1792b1 --- src/engine/SCons/Builder.py | 6 +- src/engine/SCons/BuilderTests.py | 10 +- src/engine/SCons/Node/FS.py | 283 ++++++++++++++++++++++--------- src/engine/SCons/Node/FSTests.py | 175 ++++++++++--------- src/script/scons.py | 5 +- 5 files changed, 300 insertions(+), 179 deletions(-) diff --git a/src/engine/SCons/Builder.py b/src/engine/SCons/Builder.py index 6dda645d..53e4bcc5 100644 --- a/src/engine/SCons/Builder.py +++ b/src/engine/SCons/Builder.py @@ -23,12 +23,12 @@ class Builder: action = None, input_suffix = None, output_suffix = None, - node_class = SCons.Node.FS.File): + node_factory = SCons.Node.FS.default_fs.File): self.name = name self.action = Action(action) self.insuffix = input_suffix self.outsuffix = output_suffix - self.node_class = node_class + self.node_factory = node_factory if not self.insuffix is None and self.insuffix[0] != '.': self.insuffix = '.' + self.insuffix if not self.outsuffix is None and self.outsuffix[0] != '.': @@ -38,7 +38,7 @@ class Builder: return cmp(self.__dict__, other.__dict__) def __call__(self, env, target = None, source = None): - node = SCons.Node.FS.lookup(self.node_class, target) + node = self.node_factory(target) node.builder_set(self) node.env_set(self) node.sources = source # XXX REACHING INTO ANOTHER OBJECT diff --git a/src/engine/SCons/BuilderTests.py b/src/engine/SCons/BuilderTests.py index e19b389b..ab7e1227 100644 --- a/src/engine/SCons/BuilderTests.py +++ b/src/engine/SCons/BuilderTests.py @@ -86,13 +86,15 @@ class BuilderTestCase(unittest.TestCase): builder = SCons.Builder.Builder(name = 'foo') assert builder.name == 'foo' - def test_node_class(self): + def test_node_factory(self): """Test a Builder that creates nodes of a specified class """ class Foo: - pass - builder = SCons.Builder.Builder(node_class = Foo) - assert builder.node_class is Foo + pass + def FooFactory(target): + return Foo(target) + builder = SCons.Builder.Builder(node_factory = FooFactory) + assert builder.node_factory is FooFactory def test_outsuffix(self): """Test Builder creation with a specified output suffix diff --git a/src/engine/SCons/Node/FS.py b/src/engine/SCons/Node/FS.py index 2d1c8ea7..9cd0607d 100644 --- a/src/engine/SCons/Node/FS.py +++ b/src/engine/SCons/Node/FS.py @@ -1,89 +1,188 @@ -"""SCons.Node.FS +"""scons.Node.FS File system nodes. +This initializes a "default_fs" Node with an FS at the current directory +for its own purposes, and for use by scripts or modules looking for the +canonical default. + """ __revision__ = "__FILE__ __REVISION__ __DATE__ __DEVELOPER__" - - import os import os.path -import SCons.Node +from SCons.Node import Node +from UserDict import UserDict +import sys + +class PathName: + """This is a string like object with limited capabilities (i.e., + cannot always be used interchangeably with strings). This class + is used by PathDict to manage case-insensitive path names. It preserves + the case of the string with which it was created, but on OS's with + case insensitive paths, it will hash equal to any case of the same + path when placed in a dictionary.""" + try: + convert_path = unicode + except NameError: + convert_path = str + def __init__(self, path_name=''): + self.data = PathName.convert_path(path_name) + self.norm_path = os.path.normcase(self.data) -Top = None -Root = {} + def __hash__(self): + return hash(self.norm_path) + def __cmp__(self, other): + return cmp(self.norm_path, + os.path.normcase(PathName.convert_path(other))) + def __rcmp__(self, other): + return cmp(os.path.normcase(PathName.convert_path(other)), + self.norm_path) + def __str__(self): + return str(self.data) + def __repr__(self): + return repr(self.data) +class PathDict(UserDict): + """This is a dictionary-like class meant to hold items keyed + by path name. The difference between this class and a normal + dictionary is that string or unicode keys will act differently + on OS's that have case-insensitive path names. Specifically + string or unicode keys of different case will be treated as + equal on the OS's. + All keys are implicitly converted to PathName objects before + insertion into the dictionary.""" -def init(path = None): - """Initialize the Node.FS subsystem. + def __init__(self, initdict = {}): + UserDict.__init__(self, initdict) + old_dict = self.data + self.data = {} + for key, val in old_dict.items(): + self.data[PathName(key)] = val - The supplied path is the top of the source tree, where we - expect to find the top-level build file. If no path is - supplied, the current directory is the default. - """ - global Top - if path == None: - path = os.getcwd() - Top = lookup(Dir, path, directory = None) - Top.path = '.' - -def lookup(fsclass, name, directory = Top): - """Look up a file system node for a path name. If the path - name is relative, it will be looked up relative to the - specified directory node, or to the top-level directory - if no node was specified. An initial '#' specifies that - the name will be looked up relative to the top-level directory, - regardless of the specified directory argument. Returns the - existing or newly-created node for the specified path name. - The node returned will be of the specified fsclass (Dir or - File). - """ - global Top - head, tail = os.path.split(name) - if not tail: - drive, path = os.path.splitdrive(head) - if not Root.has_key(drive): - Root[drive] = Dir(head, None) - Root[drive].abspath = head - Root[drive].path = head - return Root[drive] - if tail[0] == '#': - directory = Top - tail = tail[1:] - elif directory is None: - directory = Top - if head: - directory = lookup(Dir, head, directory) - try: - self = directory.entries[tail] - except AttributeError: - # There was no "entries" attribute on the directory, - # which essentially implies that it was a file. - # Return it as a more descriptive exception. - raise TypeError, directory - except KeyError: - # There was to entry for "tail," so create the new - # node and link it in to the existing structure. - self = fsclass(tail, directory) - self.name = tail - if self.path[0:2] == "./": - self.path = self.path[2:] - directory.entries[tail] = self - except: - raise - if self.__class__.__name__ != fsclass.__name__: - # Here, we found an existing node for this path, - # but it was the wrong type (a File when we were - # looking for a Dir, or vice versa). - raise TypeError, self - return self + def __setitem__(self, key, val): + self.data[PathName(key)] = val + + def __getitem__(self, key): + return self.data[PathName(key)] + + def __delitem__(self, key): + del(self.data[PathName(key)]) + + if not hasattr(UserDict, 'setdefault'): + def setdefault(self, key, value): + try: + return self.data[PathName(key)] + except KeyError: + self.data[PathName(key)] = value + return value + +class FS: + def __init__(self, path = None): + """Initialize the Node.FS subsystem. + + The supplied path is the top of the source tree, where we + expect to find the top-level build file. If no path is + supplied, the current directory is the default. + + The path argument must be a valid absolute path. + """ + if path == None: + path = os.getcwd() + self.Root = PathDict() + self.Top = self.__doLookup(Dir, path) + self.Top.path = '.' + + def __doLookup(self, fsclass, name, directory=None): + """This method differs from the File and Dir factory methods in + one important way: the meaning of the directory parameter. + In this method, if directory is None or not supplied, the supplied + 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.""" + head, tail = os.path.split(os.path.normpath(name)) + if not tail: + # We have reached something that looks like a root + # of an absolute path. What we do here is a little + # weird. If we are on a UNIX system, everything is + # well and good, just return the root node. + # + # On DOS/Win32 things are strange, since a path + # starting with a slash is not technically an + # absolute path, but a path relative to the + # current drive. Therefore if we get a path like + # that, we will return the root Node of the + # directory parameter. If the directory parameter is + # None, raise an exception. + drive, tail = os.path.splitdrive(head) + if sys.platform is 'win32' and not drive: + if not directory: + raise OSError, 'No drive letter supplied for absolute path.' + return directory.root() + return self.Root.setdefault(drive, Dir(tail)) + if head: + # Recursively look up our parent directories. + directory = self.__doLookup(Dir, head, directory) + else: + # This path looks like a relative path. No leading slash or drive + # letter. Therefore, we will look up this path relative to the + # supplied top-level directory. + assert directory, "Tried to lookup a node by relative path with no top-level directory supplied." + ret = directory.entries.setdefault(tail, fsclass(tail, directory)) + if not isinstance(ret, fsclass): + raise TypeError, ret + return ret + + def __transformPath(self, name, directory): + """Take care of setting up the correct top-level directory, + usually in preparation for a call to doLookup(). + + If the path name is prepended with a '#', then it is unconditionally + interpreted as replative to the top-level directory of this FS. + + If directory is None, and name is a relative path, + then the same applies. + """ + if name[0] == '#': + directory = self.Top + name = os.path.join(os.path.normpath('./'), name[1:]) + elif not directory: + directory = self.Top + return (name, directory) + + def File(self, name, directory = None): + """Lookup or create a File node with the specified name. If + the name is a relative path (begins with ./, ../, or a file name), + then it is looked up relative to the supplied directory node, + or to the top level directory of the FS (supplied at construction + time) if no directory is supplied. + + This method will raise TypeError if a directory is found at the + specified path. + """ + name, directory = self.__transformPath(name, directory) + return self.__doLookup(File, name, directory) + + def Dir(self, name, directory = None): + """Lookup or create a Dir node with the specified name. If + the name is a relative path (begins with ./, ../, or a file name), + then it is looked up relative to the supplied directory node, + or to the top level directory of the FS (supplied at construction + time) if no directory is supplied. + + This method will raise TypeError if a normal file is found at the + specified path. + """ + name, directory = self.__transformPath(name, directory) + return self.__doLookup(Dir, name, directory) + + # XXX TODO? # Annotate with the creator @@ -94,20 +193,33 @@ def lookup(fsclass, name, directory = Top): # linked_targets # is_accessible -class Dir(SCons.Node.Node): +class Dir(Node): """A class for directories in a file system. """ - def __init__(self, name, directory): - self.entries = {} - self.entries['.'] = self - self.entries['..'] = directory - if not directory is None: - self.abspath = os.path.join(directory.abspath, name, '') - self.path = os.path.join(directory.path, name, '') + def __init__(self, name, directory = None): + self.entries = PathDict() + self.entries['.'] = self + + if directory: + self.entries['..'] = directory + self.abspath = os.path.join(directory.abspath, name, '') + if str(directory.path) == '.': + self.path = os.path.join(name, '') + else: + self.path = os.path.join(directory.path, name, '') + else: + self.abspath = self.path = name + self.entries['..'] = None def up(self): - return self.entries['..'] + return self.entries['..'] + + def root(self): + if not self.entries['..']: + return self + else: + return self.entries['..'].root() # XXX TODO? @@ -130,10 +242,21 @@ class Dir(SCons.Node.Node): # is_under # relpath -class File(SCons.Node.Node): +class File(Node): """A class for files in a file system. """ def __init__(self, name, directory): - self.abspath = os.path.join(directory.abspath, name) - self.path = os.path.join(directory.path, name) + self.abspath = os.path.join(directory.abspath, name) + if str(directory.path) == '.': + self.path = name + else: + self.path = os.path.join(directory.path, name) + self.parent = directory + + def root(self): + return self.parent.root() + + + +default_fs = FS() diff --git a/src/engine/SCons/Node/FSTests.py b/src/engine/SCons/Node/FSTests.py index 24785dac..f95837fb 100644 --- a/src/engine/SCons/Node/FSTests.py +++ b/src/engine/SCons/Node/FSTests.py @@ -6,104 +6,103 @@ import unittest import SCons.Node.FS - - built_it = None class Builder: def execute(self, target = None, source = None): - global built_it - built_it = 1 - - + global built_it + built_it = 1 class FSTestCase(unittest.TestCase): def runTest(self): - """Test FS (file system) Node operations - - This test case handles all of the file system node - tests in one environment, so we don't have to set up a - complicated directory structure for each test individually. - """ - from TestCmd import TestCmd - - test = TestCmd(workdir = '') - test.subdir('sub', ['sub', 'dir']) - - wp = test.workpath('') - sub = test.workpath('sub', '') - sub_dir = test.workpath('sub', 'dir', '') - sub_dir_foo = test.workpath('sub', 'dir', 'foo', '') - sub_dir_foo_bar = test.workpath('sub', 'dir', 'foo', 'bar', '') - sub_foo = test.workpath('sub', 'foo', '') - - os.chdir(sub_dir) - - SCons.Node.FS.init() - - def Dir_test(lpath, path, abspath, up_path): - dir = SCons.Node.FS.lookup(SCons.Node.FS.Dir, lpath) - assert(dir.path == path) - assert(dir.abspath == abspath) - assert(dir.up().path == up_path) - - Dir_test('foo', 'foo/', sub_dir_foo, '.') - Dir_test('foo/bar', 'foo/bar/', sub_dir_foo_bar, 'foo/') - Dir_test('/foo', '/foo/', '/foo/', '/') - Dir_test('/foo/bar', '/foo/bar/', '/foo/bar/', '/foo/') - Dir_test('..', sub, sub, wp) - Dir_test('foo/..', '.', sub_dir, sub) - Dir_test('../foo', sub_foo, sub_foo, sub) - Dir_test('.', '.', sub_dir, sub) - Dir_test('./.', '.', sub_dir, sub) - Dir_test('foo/./bar', 'foo/bar/', sub_dir_foo_bar, 'foo/') - - d1 = SCons.Node.FS.lookup(SCons.Node.FS.Dir, 'd1') - - f1 = SCons.Node.FS.lookup(SCons.Node.FS.File, 'f1', directory = d1) - - assert(f1.path == 'd1/f1') - - try: - f2 = SCons.Node.FS.lookup(SCons.Node.FS.File, 'f1/f2', directory = d1) - except TypeError, x: - node = x.args[0] - assert(node.path == 'd1/f1') - assert(node.__class__.__name__ == 'File') - except: - raise - - try: - dir = SCons.Node.FS.lookup(SCons.Node.FS.Dir, 'd1/f1') - except TypeError, x: - node = x.args[0] - assert(node.path == 'd1/f1') - assert(node.__class__.__name__ == 'File') - except: - raise - - # Test for sub-classing of node building. - global built_it - - built_it = None - assert not built_it - d1.path = "d" # XXX FAKE SUBCLASS ATTRIBUTE - d1.sources = "d" # XXX FAKE SUBCLASS ATTRIBUTE - d1.builder_set(Builder()) - d1.build() - assert built_it - - built_it = None - assert not built_it - f1.path = "f" # XXX FAKE SUBCLASS ATTRIBUTE - f1.sources = "f" # XXX FAKE SUBCLASS ATTRIBUTE - f1.builder_set(Builder()) - f1.build() - assert built_it + """Test FS (file system) Node operations + + This test case handles all of the file system node + tests in one environment, so we don't have to set up a + complicated directory structure for each test individually. + """ + from TestCmd import TestCmd + + test = TestCmd(workdir = '') + test.subdir('sub', ['sub', 'dir']) + + wp = test.workpath('') + sub = test.workpath('sub', '') + sub_dir = test.workpath('sub', 'dir', '') + sub_dir_foo = test.workpath('sub', 'dir', 'foo', '') + sub_dir_foo_bar = test.workpath('sub', 'dir', 'foo', 'bar', '') + sub_foo = test.workpath('sub', 'foo', '') + + os.chdir(sub_dir) + + fs = SCons.Node.FS.FS() + + def Dir_test(lpath, path, abspath, up_path, fileSys=fs): + dir = fileSys.Dir(lpath) + assert dir.path == path, "Dir.path %s != expected path %s" % \ + (dir.path, path) + assert dir.abspath == abspath, "Dir.abspath %s != expected abs. path %s" % \ + (dir.abspath, path) + assert dir.up().path == up_path, "Dir.up().path %s != expected parent path %s" % \ + (dir.up().path, up_path) + + Dir_test('foo', 'foo/', sub_dir_foo, '.') + Dir_test('foo/bar', 'foo/bar/', sub_dir_foo_bar, 'foo/') + Dir_test('/foo', '/foo/', '/foo/', '/') + Dir_test('/foo/bar', '/foo/bar/', '/foo/bar/', '/foo/') + Dir_test('..', sub, sub, wp) + Dir_test('foo/..', '.', sub_dir, sub) + Dir_test('../foo', sub_foo, sub_foo, sub) + Dir_test('.', '.', sub_dir, sub) + Dir_test('./.', '.', sub_dir, sub) + Dir_test('foo/./bar', 'foo/bar/', sub_dir_foo_bar, 'foo/') + + d1 = fs.Dir('d1') + + f1 = fs.File('f1', directory = d1) + + assert f1.path == 'd1/f1', "f1.path %s != d1/f1" % f1.path + + try: + f2 = fs.File('f1/f2', directory = d1) + except TypeError, x: + node = x.args[0] + assert node.path == 'd1/f1', "node.path %s != d1/f1" % node.path + assert node.__class__.__name__ == 'File' + except: + raise + + try: + dir = fs.Dir('d1/f1') + except TypeError, x: + node = x.args[0] + assert node.path == 'd1/f1', "node.path %s != d1/f1" % node.path + assert node.__class__.__name__ == 'File' + except: + raise + + # Test for sub-classing of node building. + global built_it + + built_it = None + assert not built_it + d1.path = "d" # XXX FAKE SUBCLASS ATTRIBUTE + d1.sources = "d" # XXX FAKE SUBCLASS ATTRIBUTE + d1.builder_set(Builder()) + d1.build() + assert built_it + + built_it = None + assert not built_it + f1.path = "f" # XXX FAKE SUBCLASS ATTRIBUTE + f1.sources = "f" # XXX FAKE SUBCLASS ATTRIBUTE + f1.builder_set(Builder()) + f1.build() + assert built_it if __name__ == "__main__": suite = unittest.TestSuite() suite.addTest(FSTestCase()) if not unittest.TextTestRunner().run(suite).wasSuccessful(): - sys.exit(1) + sys.exit(1) diff --git a/src/script/scons.py b/src/script/scons.py index c981737e..8736787c 100644 --- a/src/script/scons.py +++ b/src/script/scons.py @@ -516,9 +516,6 @@ def main(): sys.path = include_dirs + sys.path - # initialize node factory - SCons.Node.FS.init() - while Scripts: file, Scripts = Scripts[0], Scripts[1:] if file == "-": @@ -538,7 +535,7 @@ def main(): sys.exit(0) taskmaster = Taskmaster(map( - lambda x: SCons.Node.FS.lookup(SCons.Node.FS.File, x), + lambda x: SCons.Node.FS.default_fs.File(x), targets)) jobs = SCons.Job.Jobs(num_jobs, taskmaster) -- 2.26.2