Index: Doc/lib/libposixpath.tex =================================================================== --- Doc/lib/libposixpath.tex (revision 54211) +++ Doc/lib/libposixpath.tex (working copy) @@ -8,7 +8,8 @@ \index{path!operations} \warning{On Windows, many of these functions do not properly -support UNC pathnames. \function{splitunc()} and \function{ismount()} +support UNC pathnames. \function{splitunc()}, \function{ismount()} +\function{isabs()}, \function{isrelative()} and \function{join()} do handle them correctly.} @@ -118,10 +119,24 @@ \end{funcdesc} \begin{funcdesc}{isabs}{path} -Return \code{True} if \var{path} is an absolute pathname (begins with a -slash). +Return \code{True} if \var{path} is an absolute pathname (i.e. the path +it refers to is not affected by calling \function{os.chdir()}). On POSIX +systems, this checks that the path begins with a slash. On Windows, +this checks that the path either starts \file{d:\textbackslash} (for some +drive letter \file{d:}), or is a UNC path. +\versionchanged[Older versions of Python on Windows would return true for +paths like \samp{\textbackslash{}foo}, which start with a slash but do not +contain a drive letter.]{2.6} \end{funcdesc} +\begin{funcdesc}{isrelative}{path} +Return \code{True} if \var{path} is a relative pathname. On POSIX systems, +this is inverse of \function{isabs()}. On Windows, there are some paths +where both \code{isabs(path)} and \code{isrelative(path)} will return +\code{False}, e.g. \file{c:foo} or \file{\textbackslash{}foo}. +\versionadded{2.6} +\end{funcdesc} + \begin{funcdesc}{isfile}{path} Return \code{True} if \var{path} is an existing regular file. This follows symbolic links, so both \function{islink()} and \function{isfile()} @@ -150,15 +165,14 @@ \begin{funcdesc}{join}{path1\optional{, path2\optional{, ...}}} Join one or more path components intelligently. If any component is -an absolute path, all previous components (on Windows, including the -previous drive letter, if there was one) are thrown away, and joining +an absolute path, all previous components are thrown away, and joining continues. The return value is the concatenation of \var{path1}, and optionally \var{path2}, etc., with exactly one directory separator (\code{os.sep}) inserted between components, unless \var{path2} is empty. Note that on Windows, since there is a current directory for each drive, \function{os.path.join("c:", "foo")} represents a path relative to the current directory on drive \file{C:} (\file{c:foo}), not -\file{c:\textbackslash\textbackslash foo}. +\file{c:\textbackslash{}foo}. \end{funcdesc} \begin{funcdesc}{normcase}{path} Index: Doc/whatsnew/whatsnew26.tex =================================================================== --- Doc/whatsnew/whatsnew26.tex (revision 54211) +++ Doc/whatsnew/whatsnew26.tex (working copy) @@ -140,6 +140,17 @@ automatically restores them to their old values. (Contributed by Brett Cannon.) +\item New function in the \module{os.path} module: \function{isrelative()} +will tell you if a path is relative. On Windows this is not quite the +opposite of \function{isabs()}, because paths like \file{a:bar} +and \file{\textbackslash{}foo} are neither absolute nor relative. + +\item Changes to the behaviour of the \module{os.path} module on Windows: +\function{isabs()} has been fixed to not classify \file{\textbackslash{}foo} +as an absolute path. \function{join()} now supports UNCs, and correctly +handles paths like \file{a:bar} and \file{\textbackslash{}foo}. +(Contributed by Jon Foster.) + \end{itemize} Index: Lib/os2emxpath.py =================================================================== --- Lib/os2emxpath.py (revision 54211) +++ Lib/os2emxpath.py (working copy) @@ -9,14 +9,15 @@ import stat from genericpath import * from ntpath import (expanduser, expandvars, isabs, islink, splitdrive, - splitext, split, walk) + splitext, split, walk, isrelative) __all__ = ["normcase","isabs","join","splitdrive","split","splitext", "basename","dirname","commonprefix","getsize","getmtime", "getatime","getctime", "islink","exists","lexists","isdir","isfile", "ismount","walk","expanduser","expandvars","normpath","abspath", "splitunc","curdir","pardir","sep","pathsep","defpath","altsep", - "extsep","devnull","realpath","supports_unicode_filenames"] + "extsep","devnull","realpath","supports_unicode_filenames", + "isrelative"] # strings representing various path-related bits and pieces curdir = '.' Index: Lib/posixpath.py =================================================================== --- Lib/posixpath.py (revision 54211) +++ Lib/posixpath.py (working copy) @@ -21,7 +21,7 @@ "ismount","walk","expanduser","expandvars","normpath","abspath", "samefile","sameopenfile","samestat", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", - "devnull","realpath","supports_unicode_filenames"] + "devnull","realpath","supports_unicode_filenames", "isrelative"] # strings representing various path-related bits and pieces curdir = '.' @@ -50,6 +50,9 @@ """Test whether a path is absolute""" return s.startswith('/') +def isrelative(s): + """Test whether a path is relative""" + return not isabs(s) # Join pathnames. # Ignore the previous parts if a part is absolute. Index: Lib/ntpath.py =================================================================== --- Lib/ntpath.py (revision 54211) +++ Lib/ntpath.py (working copy) @@ -16,7 +16,8 @@ "getatime","getctime", "islink","exists","lexists","isdir","isfile", "ismount","walk","expanduser","expandvars","normpath","abspath", "splitunc","curdir","pardir","sep","pathsep","defpath","altsep", - "extsep","devnull","realpath","supports_unicode_filenames"] + "extsep","devnull","realpath","supports_unicode_filenames", + "isrelative"] # strings representing various path-related bits and pieces curdir = '.' @@ -46,46 +47,59 @@ # Return whether a path is absolute. # Trivial in Posix, harder on the Mac or MS-DOS. -# For DOS it is absolute if it starts with a slash or backslash (current -# volume), or if a pathname after the volume letter and colon / UNC resource -# starts with a slash or backslash. +# On MS-DOS, paths cannot be divided into just 'relative' and 'absolute'. +# There are actually 4 different types of path: +# +# 1) Completely relative, e.g. foo\bar +# 2) Completely absolute, e.g. c:\foo\bar or \\server\share +# 3) Halfbreeds with no drive, e.g. \foo\bar +# 4) Halfbreeds relative to cwd on a drive, e.g. c:foo\bar def isabs(s): - """Test whether a path is absolute""" - s = splitdrive(s)[1] - return s != '' and s[:1] in '/\\' + """Test whether a path is absolute. + Note that, on Windows, this does not imply that the path is relative.""" + return ((s[1:2] == ':' and s[2:3] in '/\\') or + (s[0:1] in '/\\' and s[1:2] in '/\\')) +def isrelative(s): + """Return True iff the path is completely relative. + + Note that, on Windows, this does not imply that the path is absolute.""" + return s[1:2] != ':' and s[0:1] not in '/\\' + + # Join two (or more) paths. def join(a, *p): - """Join two or more pathname components, inserting "\\" as needed""" + """Join two or more pathname components, inserting "\\" as needed.""" path = a for b in p: - b_wins = 0 # set to 1 iff b makes path irrelevant - if path == "": - b_wins = 1 - - elif isabs(b): - # This probably wipes out path so far. However, it's more - # complicated if path begins with a drive letter: + if len(b) >= 2 and b[0] in '/\\' and b[1] in '/\\': + # Specifying a UNC. This wipes out path so far. E.g: + # join('anything', '//server/share') = '//server/share' + path = '' + elif os.path.splitdrive(b)[0]: + # Specifying a new drive. This wipes out path so far. E.g: + # join('c:', 'd:/') = 'd:/' + # join('c:/', 'd:/') = 'd:/' + # join('c:/', 'd:') = 'd:' + # join('c:', 'd:') = 'd:' + # join('/foo/', 'd:') = 'd:' + # join('foo/', 'd:') = 'd:' + path = '' + elif len(b) >= 1 and b[0] in '/\\': + # Adding a path relative to the root of a drive. + # Throw away everything apart from the drive spec (if present). + # E.g: # 1. join('c:', '/a') == 'c:/a' # 2. join('c:/', '/a') == 'c:/a' - # But # 3. join('c:/a', '/b') == '/b' - # 4. join('c:', 'd:/') = 'd:/' - # 5. join('c:/', 'd:/') = 'd:/' - if path[1:2] != ":" or b[1:2] == ":": - # Path doesn't start with a drive letter, or cases 4 and 5. - b_wins = 1 + # 4. join('/a', '/b') == '/b' + # 5. join('foo', '/b') == '/b' + path = splitdrive(path)[0] - # Else path has a drive letter, and b doesn't but is absolute. - elif len(path) > 3 or (len(path) == 3 and - path[-1] not in "/\\"): - # case 3 - b_wins = 1 - - if b_wins: + if path == '': path = b else: # Join, and ensure there's a separator. Index: Lib/macpath.py =================================================================== --- Lib/macpath.py (revision 54211) +++ Lib/macpath.py (working copy) @@ -10,7 +10,7 @@ "getatime","getctime", "islink","exists","lexists","isdir","isfile", "walk","expanduser","expandvars","normpath","abspath", "curdir","pardir","sep","pathsep","defpath","altsep","extsep", - "devnull","realpath","supports_unicode_filenames"] + "devnull","realpath","supports_unicode_filenames", "isrelative"] # strings representing various path-related bits and pieces curdir = ':' @@ -37,7 +37,11 @@ return ':' in s and s[0] != ':' +def isrelative(s): + """Test whether a path is relative""" + return not isabs(s) + def join(s, *p): path = s for t in p: Index: Lib/test/test_ntpath.py =================================================================== --- Lib/test/test_ntpath.py (revision 54211) +++ Lib/test/test_ntpath.py (working copy) @@ -47,11 +47,43 @@ tester('ntpath.split("c:/")', ('c:/', '')) tester('ntpath.split("//conky/mountpoint/")', ('//conky/mountpoint', '')) +# Tests for isabs and isrelative: +# 0) Old tests tester('ntpath.isabs("c:\\")', 1) tester('ntpath.isabs("\\\\conky\\mountpoint\\")', 1) -tester('ntpath.isabs("\\foo")', 1) -tester('ntpath.isabs("\\foo\\bar")', 1) +tester('ntpath.isabs("\\foo")', 0) # CHANGED: Was 1 +tester('ntpath.isabs("\\foo\\bar")', 0) # CHANGED: Was 1 +#1) Completely relative, e.g. foo\bar +tester('ntpath.isrelative("foo\\bar")', True) +tester('ntpath.isrelative("foo/bar")', True) +tester('ntpath.isabs("foo\\bar")', False) +tester('ntpath.isabs("foo/bar")', False) + +#2) Completely absolute, e.g. c:\foo\bar ... +tester('ntpath.isrelative("c:\\foo\\bar")', False) +tester('ntpath.isrelative("c:/foo/bar")', False) +tester('ntpath.isabs("c:\\foo\\bar")', True) +tester('ntpath.isabs("c:/foo/bar")', True) + +# ... or \\server\share +tester('ntpath.isrelative("\\\\server\\\\share")', False) +tester('ntpath.isrelative("//server/share")', False) +tester('ntpath.isabs("\\\\server\\\\share")', True) +tester('ntpath.isabs("//server/share")', True) + +#3) Halfbreeds with no drive, e.g. \foo\bar +tester('ntpath.isrelative("\\foo\\bar")', False) +tester('ntpath.isrelative("/foo/bar")', False) +tester('ntpath.isabs("\\foo\\bar")', False) +tester('ntpath.isabs("/foo/bar")', False) + +#4) Halfbreeds relative to cwd on a drive, e.g. c:foo\bar +tester('ntpath.isrelative("c:foo\\bar")', False) +tester('ntpath.isrelative("c:foo/bar")', False) +tester('ntpath.isabs("c:foo\\bar")', False) +tester('ntpath.isabs("c:foo/bar")', False) + tester('ntpath.commonprefix(["/home/swenson/spam", "/home/swen/spam"])', "/home/swen") tester('ntpath.commonprefix(["\\home\\swen\\spam", "\\home\\swen\\eggs"])', @@ -59,6 +91,7 @@ tester('ntpath.commonprefix(["/home/swen/spam", "/home/swen/spam"])', "/home/swen/spam") +# Old join() tests tester('ntpath.join("")', '') tester('ntpath.join("", "", "")', '') tester('ntpath.join("a")', 'a') @@ -78,7 +111,7 @@ tester('ntpath.join("d:\\", "a", "b")', 'd:\\a\\b') tester("ntpath.join('c:', '/a')", 'c:/a') tester("ntpath.join('c:/', '/a')", 'c:/a') -tester("ntpath.join('c:/a', '/b')", '/b') +tester("ntpath.join('c:/a', '/b')", 'c:/b') # CHANGED: Was '/b') tester("ntpath.join('c:', 'd:/')", 'd:/') tester("ntpath.join('c:/', 'd:/')", 'd:/') tester("ntpath.join('c:/', 'd:/a/b')", 'd:/a/b') @@ -93,6 +126,64 @@ tester("ntpath.join('a\\', '')", 'a\\') tester("ntpath.join('a\\', '', '', '', '')", 'a\\') +# New join() tests +# A note on test strategy: we take advantage of the fact that join +# works internally on pairs of paths, so we mostly only test pairs. +# (i.e. we expect r=join(a,b,c) to be identical to z=join(a,b) and +# r=join(z,c), so we only have to test join(a,b) and join(z,c) to +# cover this. And there are a lot less interesting combinations +# of 2 paths then there are combinations of 3 paths). +# +# Where possible we run each test twice, once with / and once with \\. +# +# Simple join +tester('ntpath.join("foo", "bar")', "foo\\bar") +tester('ntpath.join("c:", "bar")', "c:bar") +tester('ntpath.join("c:\\", "bar")', "c:\\bar") +tester('ntpath.join("c:/", "bar")', "c:/bar") +# join where 2nd arg has a drive +tester('ntpath.join("c:", "d:\\")', "d:\\") +tester('ntpath.join("c:", "d:/")', "d:/") +tester('ntpath.join("c:\\", "d:\\")', "d:\\") +tester('ntpath.join("c:/", "d:/")', "d:/") +tester('ntpath.join("c:\\", "d:")', "d:") +tester('ntpath.join("c:/", "d:")', "d:") +tester('ntpath.join("c:", "d:")', "d:") +tester('ntpath.join("\\foo\\", "d:")', "d:") +tester('ntpath.join("/foo/", "d:")', "d:") +tester('ntpath.join("foo\\", "d:")', "d:") +tester('ntpath.join("foo/", "d:")', "d:") +# join where 2nd arg has a slash +tester('ntpath.join("c:", "\\a")', "c:\\a") +tester('ntpath.join("c:", "/a")', "c:/a") +tester('ntpath.join("c:\\", "\\a")', "c:\\a") +tester('ntpath.join("c:/", "/a")', "c:/a") +tester('ntpath.join("c:\\a", "\\b")', "c:\\b") +tester('ntpath.join("c:/a", "/b")', "c:/b") +tester('ntpath.join("\\a", "\\b")', "\\b") +tester('ntpath.join("/a", "/b")', "/b") +tester('ntpath.join("foo", "\\b")', "\\b") +tester('ntpath.join("foo", "/b")', "/b") +# Join with a UNC +tester('ntpath.join("anything", "\\\\server\\share")', "\\\\server\\share") +tester('ntpath.join("anything", "//server/share")', "//server/share") +tester('ntpath.join("\\\\server\\share", "filename")', "\\\\server\\share\\filename") +tester('ntpath.join("//server/share", "filename")', "//server/share\\filename") +# Join to make a UNC +tester('ntpath.join("anything", "\\\\")', "\\\\") +tester('ntpath.join("anything", "//")', "//") +tester('ntpath.join("\\\\", "server")', "\\\\server") +tester('ntpath.join("//", "server")', "//server") +tester('ntpath.join("\\\\server", "share")', "\\\\server\\share") +tester('ntpath.join("//server", "share")', "//server\\share") +# Special case where 2nd arg is empty +tester('ntpath.join("a\\", "")', "a\\") +tester('ntpath.join("a", "")', "a\\") +# Join more than 2 path components: +tester('ntpath.join("foo", "bar", "baz")', "foo\\bar\\baz") +tester('ntpath.join("1", "2", "3", "4", "5", "6", "7", "8", "9", "10")', + "1\\2\\3\\4\\5\\6\\7\\8\\9\\10") + tester("ntpath.normpath('A//////././//.//B')", r'A\B') tester("ntpath.normpath('A/./B')", r'A\B') tester("ntpath.normpath('A/foo/../B')", r'A\B')