[Jython-checkins] jython: Improve os.chdir for Windows, partially addressing #2117

jeff.allen jython-checkins at python.org
Sat Mar 15 10:00:31 CET 2014


http://hg.python.org/jython/rev/f553f6a07fb5
changeset:   7192:f553f6a07fb5
user:        Jeff Allen <ja.py at farowl.co.uk>
date:        Sat Mar 08 17:49:48 2014 +0000
summary:
  Improve os.chdir for Windows, partially addressing #2117
Corrects ntpath.abspath based on CPython 2.7.6 but using PySystemState.getPath;
adds logic to maximise use of native wit behind java.io.File for Windows.

files:
  Lib/ntpath.py                          |   26 +-
  Lib/test/test_chdir.py                 |   58 ++++-
  src/org/python/core/PySystemState.java |  144 ++++++++++++-
  3 files changed, 194 insertions(+), 34 deletions(-)


diff --git a/Lib/ntpath.py b/Lib/ntpath.py
--- a/Lib/ntpath.py
+++ b/Lib/ntpath.py
@@ -452,23 +452,25 @@
     from nt import _getfullpathname
 
 except ImportError: # not running on Windows - mock up something sensible
-    import java.io.File
-    from org.python.core.Py import newString
 
     def abspath(path):
         """Return the absolute version of a path."""
-        if not isabs(path):
+        try:
             if isinstance(path, unicode):
-                cwd = os.getcwdu()
+                if path:
+                    path = sys.getPath(path)
+                else:
+                    # Empty path must return current working directory
+                    path = os.getcwdu()
             else:
-                cwd = os.getcwd()
-            path = join(cwd, path)
-        if not splitunc(path)[0] and not splitdrive(path)[0]:
-            # cwd lacks a UNC mount point, so it should have a drive
-            # letter (but lacks one): determine it
-            canon_path = newString(java.io.File(path).getCanonicalPath())
-            drive = splitdrive(canon_path)[0]
-            path = join(drive, path)
+                if path:
+                    path = sys.getPath(path).encode('latin-1')
+                else:
+                    # Empty path must return current working directory
+                    path = os.getcwd()
+        except EnvironmentError:
+             pass # Bad path - return unchanged.
+
         return normpath(path)
 
 else:  # use native Windows method on Windows
diff --git a/Lib/test/test_chdir.py b/Lib/test/test_chdir.py
--- a/Lib/test/test_chdir.py
+++ b/Lib/test/test_chdir.py
@@ -194,11 +194,39 @@
         self.assertEqual(os.getcwd(), os.path.realpath(dos_name))
 
     def test_windows_getcwd_ensures_drive_letter(self):
-        drive = os.path.splitdrive(self.subdir)[0]
-        os.chdir('\\')
+        # subdir is in the TEMP directory, usually on C:, while the
+        # current working directory could be (for the sake of comments)
+        # D:\HOME . TEMP and HOME stand for arbitrarily long relative paths.
+
+        # Check chdir to \ occurs without change of drive letter.
+        drive0, sub0 = os.path.splitdrive(os.getcwd()) # d:, HOME
+        os.chdir('\\') # d:\
+        self.assertEqual(os.path.normcase(os.getcwd()),
+                         os.path.normcase(os.path.join(drive0, '\\')))
+
+        # Check chdir to HOME occurs without change of drive letter.
+        os.chdir(sub0) # d:\HOME
+        self.assertEqual(os.path.normcase(os.getcwd()),
+                         os.path.normcase(os.path.join(drive0, sub0)))
+
+        # Check chdir to path with drive letter, changes drive in cwd.
+        drive, sub = os.path.splitdrive(self.subdir) # c:, TEMP\Program Files
+        os.chdir(self.subdir) # c:\TEMP\Program Files
+        self.assertEqual(os.path.normcase(os.getcwd()),
+                         os.path.normcase(os.path.join(drive, sub)))
+
+        # Check chdir to \ occurs without change of drive letter (again).
+        os.chdir('\\') # c:\
         self.assertEqual(os.path.normcase(os.getcwd()),
                          os.path.normcase(os.path.join(drive, '\\')))
 
+        if drive.upper() != drive0.upper():
+            # Check chdir to (different) original drive takes us to previous directory too.
+            # You only get this test if the temp directory and cwd are on different drives.
+            os.chdir(drive0) # d:\HOME
+            self.assertEqual(os.path.normcase(os.getcwd()),
+                             os.path.normcase(os.path.join(drive0, sub0)))
+
     def test_windows_chdir_slash_isabs(self):
         drive = os.path.splitdrive(os.getcwd())[0]
         os.chdir('/')
@@ -702,17 +730,19 @@
 
 
 def test_main():
-    tests = [ChdirTestCase,
-             ImportTestCase,
-             ImportPackageTestCase,
-             ZipimportTestCase,
-             PyCompileTestCase,
-             ExecfileTestCase,
-             ExecfileTracebackTestCase,
-             ListdirTestCase,
-             DirsTestCase,
-             FilesTestCase,
-             SymlinkTestCase]
+    tests = [
+                ChdirTestCase,
+                ImportTestCase,
+                ImportPackageTestCase,
+                ZipimportTestCase,
+                PyCompileTestCase,
+                ExecfileTestCase,
+                ExecfileTracebackTestCase,
+                ListdirTestCase,
+                DirsTestCase,
+                FilesTestCase,
+                SymlinkTestCase
+            ]
     if WINDOWS:
         tests.append(WindowsChdirTestCase)
         tests.remove(SymlinkTestCase)       #  os.symlink ... Availability: Unix.
@@ -720,8 +750,10 @@
     if test_support.is_jython:
         tests.extend((ImportJavaClassTestCase,
                       ImportJarTestCase))
+ 
     if test_support.is_resource_enabled('subprocess'):
         tests.append(SubprocessTestCase)
+
     test_support.run_unittest(*tests)
 
 
diff --git a/src/org/python/core/PySystemState.java b/src/org/python/core/PySystemState.java
--- a/src/org/python/core/PySystemState.java
+++ b/src/org/python/core/PySystemState.java
@@ -546,22 +546,148 @@
     }
 
     private static String getPath(PySystemState sys, String path) {
-        if (path == null) {
-            return path;
+        if (path != null) {
+            path = getFile(sys, path).getAbsolutePath();
         }
+        return path;
+    }
 
+    /**
+     * Resolve a path, returning a {@link File}, taking the current working directory into account.
+     *
+     * @param path a path <code>String</code>
+     * @return a resolved <code>File</code>
+     */
+    public File getFile(String path) {
+        return getFile(this, path);
+    }
+
+    /**
+     * Resolve a path, returning a {@link File}, taking the current working directory of the
+     * specified <code>PySystemState</code> into account. Use of a <code>static</code> here is a
+     * trick to avoid getting the current state if the path is absolute. (Noted that this may be
+     * needless optimisation.)
+     *
+     * @param sys a <code>PySystemState</code> or null meaning the current one
+     * @param path a path <code>String</code>
+     * @return a resolved <code>File</code>
+     */
+    private static File getFile(PySystemState sys, String path) {
         File file = new File(path);
-        // Python considers r'\Jython25' and '/Jython25' abspaths on Windows, unlike
-        // java.io.File
-        if (!file.isAbsolute()
-                && (!Platform.IS_WINDOWS || !(path.startsWith("\\") || path.startsWith("/")))) {
+        if (!file.isAbsolute()) {
+            // path meaning depends on the current working directory
             if (sys == null) {
                 sys = Py.getSystemState();
             }
-            file = new File(sys.getCurrentWorkingDir(), path);
+            String cwd = sys.getCurrentWorkingDir();
+            if (Platform.IS_WINDOWS) {
+                // Form absolute reference (with mysterious Windows semantics)
+                file = getWindowsFile(cwd, path);
+            } else {
+                // Form absolute reference (with single slash)
+                file = new File(cwd, path);
+            }
         }
-        // This needs to be performed always to trim trailing backslashes on Windows
-        return file.getPath();
+        return file;
+    }
+
+    /**
+     * Resolve a relative path against the supplied current working directory or Windows environment
+     * working directory for any drive specified in the path. and return a file object. Essentially
+     * equivalent to os.path.join, but the work is done by {@link File}. The intention is that
+     * calling {@link File#getAbsolutePath()} should return the corresponding absolute path.
+     * <p>
+     * Note: in the context where we use this method, <code>path</code> is already known not to be
+     * absolute, and <code>cwd</code> is assumed to be absolute.
+     *
+     * @param cwd current working directory (of some {@link PySystemState})
+     * @param path to resolve
+     * @return specifier of the intended file
+     */
+    private static File getWindowsFile(String cwd, String path) {
+        // Assumptions: cwd is absolute and path is not absolute
+
+        // Start by getting the slashes the right (wrong) way round.
+        if (path.indexOf('/') >= 0) {
+            path = path.replace('/', '\\');
+        }
+
+        // Does path start with a drive letter?
+        char d = driveLetter(path);
+        if (d != 0) {
+            if (d == driveLetter(cwd)) {
+                /*
+                 * path specifies the same drive letter as in the cwd of this PySystemState. Let
+                 * File interpret the rest of the path relative to cwd as parent.
+                 */
+                return new File(cwd, path.substring(2));
+            } else {
+                // Let File resolve the specified drive against the process environment.
+                return new File(path);
+            }
+
+        } else if (path.startsWith("\\")) {
+            // path replaces the file part of the cwd. (Method assumes path is not UNC.)
+            if (driveLetter(cwd) != 0) {
+                // cwd has a drive letter
+                return new File(cwd.substring(0, 2), path);
+            } else {
+                // cwd has no drive letter, so should be a UNC path \\host\share\directory\etc
+                return new File(uncShare(cwd), path);
+            }
+
+        } else {
+            // path is relative to the cwd of this PySystemState.
+            return new File(cwd, path);
+        }
+    }
+
+    /**
+     * Return the Windows drive letter from the start of the path, upper case, or 0 if
+     * the path does not start X: where X is a letter.
+     *
+     * @param path to examine
+     * @return drive letter or char 0 if no drive letter
+     */
+    private static char driveLetter(String path) {
+        if (path.length() >= 2 && path.charAt(1) == ':') {
+            // Looks like both strings start with a drive letter
+            char pathDrive = path.charAt(0);
+            if (Character.isLetter(pathDrive)) {
+                return Character.toUpperCase(pathDrive);
+            }
+        }
+        return (char)0;
+    }
+
+    /**
+     * Return the Windows UNC share name from the start of the path, or <code>null</code> if the
+     * path is not of Windows UNC type. The path has to be formed with Windows-backslashes:
+     * slashes '/' are not accepted as a substitute here.
+     *
+     * @param path to examine
+     * @return share name or null
+     */
+    private static String uncShare(String path) {
+        int n = path.length();
+        // Has to accommodate at least \\A (3 chars)
+        if (n >= 5 && path.startsWith("\\\\")) {
+            // Look for the separator backslash A\B
+            int p = path.indexOf('\\', 2);
+            // Has to be at least index 3 (path begins \\A) and 2 more characters left \B
+            if (p >= 3 && n > p + 2) {
+                // Look for directory backslash that ends the share name
+                int dir = path.indexOf('\\', p + 1);
+                if (dir < 0) {
+                    // path has the form \\A\B (is just the share name)
+                    return path;
+                } else if (dir > p + 1) {
+                    // path has the form \\A\B\C
+                    return path.substring(0, dir);
+                }
+            }
+        }
+        return null;
     }
 
     public void callExitFunc() throws PyIgnoreMethodTag {

-- 
Repository URL: http://hg.python.org/jython


More information about the Jython-checkins mailing list