This issue tracker has been migrated to GitHub, and is currently read-only.
For more information, see the GitHub FAQs in the Python's Developer Guide.

classification
Title: Windows os.path.isabs UNC path bug
Type: behavior Stage: needs patch
Components: Library (Lib), Windows Versions: Python 3.10, Python 3.9
process
Status: open Resolution:
Dependencies: Superseder:
Assigned To: Nosy List: akima, eryksun, pitrou, serhiy.storchaka, steve.dower
Priority: normal Keywords:

Created on 2014-08-29 21:58 by akima, last changed 2022-04-11 14:58 by admin.

Messages (18)
msg226091 - (view) Author: Akima (akima) Date: 2014-08-29 21:58
A UNC path pointing to the root of a share is not being recognised as an absolute path when it should be.

See this interpreter session.

PythonWin 3.3.5 (v3.3.5:62cf4e77f785, Mar  9 2014, 10:35:05) [MSC v.1600 64 bit (AMD64)] on win32.
Portions Copyright 1994-2008 Mark Hammond - see 'Help/About PythonWin' for further copyright information.
>>> import os.path
>>> os.path.isabs(r"\\server")
True
>>> os.path.isabs(r"\\server\share")
False
>>> os.path.isabs(r"\\server\share\folder")
True
>>> os.path.isabs(r"\\server\share\folder\folder")
True
>>>
msg226108 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2014-08-29 23:52
On second thought, while the parallels between drives and shares are nice, beyond that this makes no sense to me.

http://hg.python.org/cpython/file/c0e311e010fc/Lib/ntpath.py#l91

Windows uses hidden environment variables (e.g. "=C:") for the working directory on each drive, but not for network shares.
msg226122 - (view) Author: Akima (akima) Date: 2014-08-30 06:39
FYI: I've only tested this bug on Python 3.3.5 on Windows 7.  I expect the bug exists in other versions of Python.
msg226243 - (view) Author: Akima (akima) Date: 2014-09-01 21:18
I checked for the existence of this bug in 2 other python versions today.  It's present in CPython 3.4.1, but CPython 2.7.5 doesn't exhibit the issue.


Python 2.7.5 (default, May 15 2013, 22:43:36) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os.path
>>> os.path.isabs(r"\\server")
True
>>> os.path.isabs(r"\\server\share")
True
>>> os.path.isabs(r"\\server\share\folder")
True
>>> os.path.isabs(r"\\server\share\folder\folder")
True
>>>


Python 3.4.1 (v3.4.1:c0e311e010fc, May 18 2014, 10:38:22) [MSC v.1600 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os.path
>>> os.path.isabs(r"\\server")
True
>>> os.path.isabs(r"\\server\share")
False
>>> os.path.isabs(r"\\server\share\folder")
True
>>> os.path.isabs(r"\\server\share\folder\folder")
True
>>>
msg226427 - (view) Author: Steve Dower (steve.dower) * (Python committer) Date: 2014-09-05 16:32
Antoine almost certainly thought about this with pathlib and may know about the change, or at least have some decent context on it.

I'm more inclined to think that os.path.isabs(r"\\server") should also return False, since it's not a path that can be opened directly unless you add more path before or after it.

Indeed, pathlib seems to support my understanding:

>>> Path('//server').is_absolute()
False
>>> Path('//server/share').is_absolute()
True
>>> Path('//server/share/').is_absolute()
True
>>> Path('//server/share/file').is_absolute()
True

If the regression appeared in 3.4, it should be easy enough to look at what changed.
msg226429 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2014-09-05 16:52
Under Windows, pathlib's "absolute" means a fully qualified path as defined in http://msdn.microsoft.com/en-us/library/aa365247%28v=vs.85%29.aspx

>>> PureWindowsPath("c:").is_absolute()
False
>>> PureWindowsPath("/").is_absolute()
False
>>> PureWindowsPath("c:/").is_absolute()
True

The fact that "//server" isn't considered absolute is a bug in pathlib, since "//server/" is:

>>> PureWindowsPath("//foo").is_absolute()
False
>>> PureWindowsPath("//foo/").is_absolute()
True

I agree that it's not really important, since both aren't actual paths (in the sense that they may not point to anything, AFAIK).
msg226430 - (view) Author: Steve Dower (steve.dower) * (Python committer) Date: 2014-09-05 17:16
My experience says the main reason people want to know whether the path is absolute is to figure out whether to do `Path.cwd() / path` or not. According to that MSDN page, they shouldn't because the path starts with "//" (that is, the last character and the existence of the share name are irrelevant here).

Both pathlib and ntpath are reasonably consistent in determining the "drive" of the path:
>>> splitdrive('\\\\server\\')[0]
'\\\\server\\'
>>> splitdrive('\\\\server')[0]
''
>>> Path('\\\\server\\').parts[0]
'\\\\server\\\\'
>>> Path('\\\\server').parts[0]
'\\'

I assume the bug in the last statement is what Antoine is referring to.

The difference in results then comes from how they determine roots.
>>> splitdrive('\\\\server\\')[1]
''
>>> splitdrive('\\\\server')[1]
'\\\\server'
>>> Path('//server/').root
'\\'
>>> Path('//server').root
'\\'

Pathlib always has a root, but splitdrive doesn't.

I'm not sure exactly which one to fix where, but hopefully that helps someone else figure it out.
msg226432 - (view) Author: Steve Dower (steve.dower) * (Python committer) Date: 2014-09-05 17:18
Just did a double-take, but that output for Path('\\\\server\\').parts[0] is actually correct...

>>> Path('\\\\server\\').parts[0]
'\\\\server\\\\'
>>> print(Path('\\\\server\\').parts[0])
\\server\\
msg226434 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2014-09-05 17:30
It looks to me that rather handling of "//server/" is buggy than "//server". "//server" is just the same as "/server" or "///server". But "//server/" looks as an UNC path with empty ("") mount point. This is invalid path and we can't create valid path from this path by adding components. Perhaps PureWindowsPath("//server/") should raise an exception.
msg226436 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2014-09-05 18:06
Isn't this bug about the "root of a share" case with ntpath.isabs in 3.x and 2.7 (splitdrive was backported)? For example:

    >>> os.path.isabs("//server/share")
    False
    >>> os.path.splitdrive('//server/share') 
    ('//server/share', '')

vs.

    >>> os.path.isabs('//server/share/')             
    True
    >>> os.path.splitdrive('//server/share/')
    ('//server/share', '/')

I think '//server/share' is an absolute path, and pathlib agrees. Network shares do not maintain a current directory the way drives do (i.e. the hidden environment variable trick). There's no such thing as a share-relative path, nor would there be any way to even write such a path, as compared to "C:drive/relative/path".
msg226440 - (view) Author: Antoine Pitrou (pitrou) * (Python committer) Date: 2014-09-05 18:47
Serhiy, you may be right. I'd suggest opening a python-dev discussion to gather more feedback.
msg226444 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2014-09-05 19:09
> "//server" is just the same as "/server" or "///server".

Repeated slashes aren't collapsed at the start of a Windows path. Here's what I get in Windows 7:

    >>> os.listdir('/server')   
    []
    >>> os.listdir('//server')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    FileNotFoundError: [WinError 53] The network path was not found: '//server'
    >>> os.listdir('///server')
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    FileNotFoundError: [WinError 161] The specified path is invalid: '///server'
msg226455 - (view) Author: Akima (akima) Date: 2014-09-05 20:50
As eryksun pointed out, I created this bug report to report on one issue; that \\server\share isn't being consider absolute by os.path.isabs when it should be considered absolute.

I found this bug when I was writing a basic config file parsing module.  One of the options in the config (named log_dir) required an absolute path to a directory where my program could dump log files.  It has to be absolute because the working directory of the program changes.  A user entered a path \\pollux\logs as the log_dir value in their config file and my program rejected it informing the user that it wasn't absolute (because I checked the path using isabs).  I've worked around this bug in my program, but it is clearly a problem that needs fixing.

Steve: with regard to what isabs should return when given the string r"\\server": does it really matter whether it returns True or False?  As you said; \\server isn't a real path.  It's invalid.  If you ask python if it's absolute (using os.path.isabs) and you expect a boolean response, no matter whether the response is True or False, the response will be meaningless.  Consider this:

Python 3.2.3 (default, Feb 27 2014, 21:31:18) 
[GCC 4.6.3] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import os.path
>>> os.path.isabs("&monkeyfart!££")
False
>>> 

I just asked isabs if "&monkeyfart!££" was absolute and it told me it's not an absolute path, it's relative!  It's infact, not even a path.  There is no "os.path.isrelative" function.  If we want to know if a path is relative we use the isabs function and decide the path is relative if the result is False.  If you give the function a junk-string which isn't even a path (eg \\server  or  &monkeyfart!££ ) then you can only expect a junk response.  If we decide that all invalid paths provide to isabs should return False then what we are saying is all invalid paths provided to isabs should be considered relative paths.  What use is that to anyone?  Really, the programmer should ensure that the path is valid before trying to find out if it's relative or absolute.

To summarize: I think it's important that isabs works correctly when given a *valid* path (like \\server\share).  When it's given a string which is an invalid path (like \\server), its behaviour should be undefined.
msg226473 - (view) Author: Akima (akima) Date: 2014-09-06 07:24
I just realised that "&monkeyfart!££" is actually a valid relative path name.  What I said still holds, just replace "&monkeyfart!££" with ">elephant*dung?" (which contains the invalid characters: >*? ).
msg226475 - (view) Author: Akima (akima) Date: 2014-09-06 07:31
eryksun: You have marked this bug as effecting Python 2.7.  When I tested for the bug on 2.7.5 the problem didn't show up:

Python 2.7.5 (default, May 15 2013, 22:43:36) [MSC v.1500 32 bit (Intel)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import os.path
>>> os.path.isabs(r"\\server")
True
>>> os.path.isabs(r"\\server\share")
True
>>> os.path.isabs(r"\\server\share\folder")
True
>>> os.path.isabs(r"\\server\share\folder\folder")
True
>>>

Did you independently test a later release of version 2.7 and find the issue?  If not, could you remove Python 2.7 from the "Versions" list.
msg226477 - (view) Author: Serhiy Storchaka (serhiy.storchaka) * (Python committer) Date: 2014-09-06 08:02
This is the consequence of recent backporting splitdrive() implementation from 
3.x to 2.7.
msg285510 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2017-01-15 14:31
isabs also fails for device paths such as r"\\.\C:", which is an absolute path for opening the C: volume. UNC and device paths (i.e. \\server, \\?, and \\.) should always be considered absolute. Only logical drives (i.e. C:, D:, etc) support drive-relative paths.

Also, join() needs to be smarter in this case:

    >>> os.path.join(r'\\.\C:', 'spam')
    '\\\\.\\C:spam'

It doesn't insert a backslash because the 'drive' ends in a colon. It needs to always insert a backslash for UNC paths, which cannot be reliably identified by checking whether the last character of the drive is a colon.
msg387681 - (view) Author: Eryk Sun (eryksun) * (Python triager) Date: 2021-02-25 17:05
> figure out whether to do `Path.cwd() / path`

Thus a UNC path is absolute, i.e. any path that starts with 2 or more slashes, and all other paths are relative unless they have both a drive and a root. For example:

    def isabs(s):
        """Test whether a path is absolute"""
        s = os.fspath(s)
        seps = _get_bothseps(s)
        if len(s) > 1 and s[0] in seps and s[1] in seps:
            return True
        drive, rest = splitdrive(s)
        if drive and rest:
            return rest[0] in seps
        return False

This also fixes the mistaken result that a rooted path such as "/spam" is absolute. When opened directly, a rooted path is relative to the drive of the current working directory. When accessed indirectly as the target of a symlink, a rooted path is relative to the volume device of the opened path. 

An example of the latter is a symlink named "link" that targets "\". If it's accessed as E:\link, it resolves to E:\. If it's accessed as C:\Mount\VolumeE\link, it resolves instead to C:\. The relative link resolves differently depending on the path traversed to access it. (Note that when resolving the target of a relative symlink, mountpoints such as "VolumeE" that are traversed in the opened path do not get replaced by their target path, unlike directory symlinks, which do get replaced by their target path. This behavior is basically the same as the way mountpoints and symlinks are handled in Unix.)
History
Date User Action Args
2022-04-11 14:58:07adminsetgithub: 66498
2021-02-25 17:05:14eryksunsetmessages: + msg387681
versions: + Python 3.9, Python 3.10, - Python 2.7, Python 3.5, Python 3.6, Python 3.7
2017-01-15 14:31:24eryksunsetmessages: + msg285510
2017-01-15 03:13:00eryksunsetmessages: - msg226094
2017-01-15 03:12:29eryksunsetstage: needs patch
versions: + Python 3.6, Python 3.7, - Python 3.3, Python 3.4
2014-09-06 08:02:47serhiy.storchakasetmessages: + msg226477
2014-09-06 07:31:12akimasetmessages: + msg226475
2014-09-06 07:24:07akimasetmessages: + msg226473
2014-09-05 20:50:53akimasetmessages: + msg226455
2014-09-05 19:09:11eryksunsetmessages: + msg226444
2014-09-05 18:47:31pitrousetmessages: + msg226440
2014-09-05 18:06:25eryksunsetmessages: + msg226436
versions: + Python 2.7, Python 3.5
2014-09-05 17:30:24serhiy.storchakasetnosy: + serhiy.storchaka
messages: + msg226434
2014-09-05 17:18:25steve.dowersetmessages: + msg226432
2014-09-05 17:16:57steve.dowersetmessages: + msg226430
2014-09-05 16:52:14pitrousetmessages: + msg226429
2014-09-05 16:32:59steve.dowersetnosy: + pitrou, steve.dower
messages: + msg226427
2014-09-01 21:18:11akimasetmessages: + msg226243
versions: + Python 3.4
2014-08-30 06:39:50akimasettype: behavior
messages: + msg226122
2014-08-29 23:52:12eryksunsetmessages: + msg226108
2014-08-29 22:29:32eryksunsetnosy: + eryksun
messages: + msg226094
2014-08-29 21:58:22akimacreate