Skip to content

Commit 8e06859

Browse files
committed
Apply changes from bundled appdirs to vendored
* Convert Windows app data directory values to bytes on Python 2, so the output type is consistent across platforms (#4000) * Also look in ~/.config for user config on macOS (#4100) * Remove pywin32 dependency, only use ctypes and winreg for directory lookup on Windows (#2467) * Always use os.path.join() instead of os.sep.join() so cross-platform tests work as expected (#3275)
1 parent 2472a6e commit 8e06859

File tree

4 files changed

+160
-286
lines changed

4 files changed

+160
-286
lines changed

src/pip/_internal/utils/appdirs.py

Lines changed: 15 additions & 250 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
11
"""
2-
This code was taken from https://github.com/ActiveState/appdirs and modified
3-
to suit our purposes.
4-
"""
2+
This code wraps the vendored appdirs module to so the return values are
3+
compatible for the current pip code base.
54
6-
# The following comment should be removed at some point in the future.
7-
# mypy: disallow-untyped-defs=False
5+
The intention is to rewrite current usages guradually, keeping the tests pass,
6+
and eventually drop this after all usages are changed.
7+
"""
88

99
from __future__ import absolute_import
1010

1111
import os
12-
import sys
1312

14-
from pip._vendor.six import PY2, text_type
13+
from pip._vendor import appdirs as _appdirs
1514

16-
from pip._internal.utils.compat import WINDOWS, expanduser
1715
from pip._internal.utils.typing import MYPY_CHECK_RUNNING
1816

1917
if MYPY_CHECK_RUNNING:
@@ -22,255 +20,22 @@
2220

2321
def user_cache_dir(appname):
2422
# type: (str) -> str
25-
r"""
26-
Return full path to the user-specific cache dir for this application.
27-
28-
"appname" is the name of application.
29-
30-
Typical user cache directories are:
31-
macOS: ~/Library/Caches/<AppName>
32-
Unix: ~/.cache/<AppName> (XDG default)
33-
Windows: C:\Users\<username>\AppData\Local\<AppName>\Cache
34-
35-
On Windows the only suggestion in the MSDN docs is that local settings go
36-
in the `CSIDL_LOCAL_APPDATA` directory. This is identical to the
37-
non-roaming app data dir (the default returned by `user_data_dir`). Apps
38-
typically put cache data somewhere *under* the given dir here. Some
39-
examples:
40-
...\Mozilla\Firefox\Profiles\<ProfileName>\Cache
41-
...\Acme\SuperApp\Cache\1.0
42-
43-
OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value.
44-
"""
45-
if WINDOWS:
46-
# Get the base path
47-
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
48-
49-
# When using Python 2, return paths as bytes on Windows like we do on
50-
# other operating systems. See helper function docs for more details.
51-
if PY2 and isinstance(path, text_type):
52-
path = _win_path_to_bytes(path)
53-
54-
# Add our app name and Cache directory to it
55-
path = os.path.join(path, appname, "Cache")
56-
elif sys.platform == "darwin":
57-
# Get the base path
58-
path = expanduser("~/Library/Caches")
59-
60-
# Add our app name to it
61-
path = os.path.join(path, appname)
62-
else:
63-
# Get the base path
64-
path = os.getenv("XDG_CACHE_HOME", expanduser("~/.cache"))
65-
66-
# Add our app name to it
67-
path = os.path.join(path, appname)
68-
69-
return path
70-
71-
72-
def user_data_dir(appname, roaming=False):
73-
# type: (str, bool) -> str
74-
r"""
75-
Return full path to the user-specific data dir for this application.
76-
77-
"appname" is the name of application.
78-
If None, just the system directory is returned.
79-
"roaming" (boolean, default False) can be set True to use the Windows
80-
roaming appdata directory. That means that for users on a Windows
81-
network setup for roaming profiles, this user data will be
82-
sync'd on login. See
83-
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
84-
for a discussion of issues.
85-
86-
Typical user data directories are:
87-
macOS: ~/Library/Application Support/<AppName>
88-
if it exists, else ~/.config/<AppName>
89-
Unix: ~/.local/share/<AppName> # or in
90-
$XDG_DATA_HOME, if defined
91-
Win XP (not roaming): C:\Documents and Settings\<username>\ ...
92-
...Application Data\<AppName>
93-
Win XP (roaming): C:\Documents and Settings\<username>\Local ...
94-
...Settings\Application Data\<AppName>
95-
Win 7 (not roaming): C:\\Users\<username>\AppData\Local\<AppName>
96-
Win 7 (roaming): C:\\Users\<username>\AppData\Roaming\<AppName>
97-
98-
For Unix, we follow the XDG spec and support $XDG_DATA_HOME.
99-
That means, by default "~/.local/share/<AppName>".
100-
"""
101-
if WINDOWS:
102-
const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA"
103-
path = os.path.join(os.path.normpath(_get_win_folder(const)), appname)
104-
elif sys.platform == "darwin":
105-
path = os.path.join(
106-
expanduser('~/Library/Application Support/'),
107-
appname,
108-
) if os.path.isdir(os.path.join(
109-
expanduser('~/Library/Application Support/'),
110-
appname,
111-
)
112-
) else os.path.join(
113-
expanduser('~/.config/'),
114-
appname,
115-
)
116-
else:
117-
path = os.path.join(
118-
os.getenv('XDG_DATA_HOME', expanduser("~/.local/share")),
119-
appname,
120-
)
121-
122-
return path
23+
return _appdirs.user_cache_dir(appname, appauthor=False)
12324

12425

12526
def user_config_dir(appname, roaming=True):
12627
# type: (str, bool) -> str
127-
"""Return full path to the user-specific config dir for this application.
128-
129-
"appname" is the name of application.
130-
If None, just the system directory is returned.
131-
"roaming" (boolean, default True) can be set False to not use the
132-
Windows roaming appdata directory. That means that for users on a
133-
Windows network setup for roaming profiles, this user data will be
134-
sync'd on login. See
135-
<http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx>
136-
for a discussion of issues.
137-
138-
Typical user data directories are:
139-
macOS: same as user_data_dir
140-
Unix: ~/.config/<AppName>
141-
Win *: same as user_data_dir
28+
return _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming)
14229

143-
For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME.
144-
That means, by default "~/.config/<AppName>".
145-
"""
146-
if WINDOWS:
147-
path = user_data_dir(appname, roaming=roaming)
148-
elif sys.platform == "darwin":
149-
path = user_data_dir(appname)
150-
else:
151-
path = os.getenv('XDG_CONFIG_HOME', expanduser("~/.config"))
152-
path = os.path.join(path, appname)
15330

154-
return path
31+
def user_data_dir(appname, roaming=False):
32+
# type: (str, bool) -> str
33+
return _appdirs.user_data_dir(appname, appauthor=False, roaming=roaming)
15534

15635

157-
# for the discussion regarding site_config_dirs locations
158-
# see <https://github.com/pypa/pip/issues/1733>
15936
def site_config_dirs(appname):
16037
# type: (str) -> List[str]
161-
r"""Return a list of potential user-shared config dirs for this application.
162-
163-
"appname" is the name of application.
164-
165-
Typical user config directories are:
166-
macOS: /Library/Application Support/<AppName>/
167-
Unix: /etc or $XDG_CONFIG_DIRS[i]/<AppName>/ for each value in
168-
$XDG_CONFIG_DIRS
169-
Win XP: C:\Documents and Settings\All Users\Application ...
170-
...Data\<AppName>\
171-
Vista: (Fail! "C:\ProgramData" is a hidden *system* directory
172-
on Vista.)
173-
Win 7: Hidden, but writeable on Win 7:
174-
C:\ProgramData\<AppName>\
175-
"""
176-
if WINDOWS:
177-
path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA"))
178-
pathlist = [os.path.join(path, appname)]
179-
elif sys.platform == 'darwin':
180-
pathlist = [os.path.join('/Library/Application Support', appname)]
181-
else:
182-
# try looking in $XDG_CONFIG_DIRS
183-
xdg_config_dirs = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg')
184-
if xdg_config_dirs:
185-
pathlist = [
186-
os.path.join(expanduser(x), appname)
187-
for x in xdg_config_dirs.split(os.pathsep)
188-
]
189-
else:
190-
pathlist = []
191-
192-
# always look in /etc directly as well
193-
pathlist.append('/etc')
194-
195-
return pathlist
196-
197-
198-
# -- Windows support functions --
199-
200-
def _get_win_folder_from_registry(csidl_name):
201-
# type: (str) -> str
202-
"""
203-
This is a fallback technique at best. I'm not sure if using the
204-
registry for this guarantees us the correct answer for all CSIDL_*
205-
names.
206-
"""
207-
import _winreg
208-
209-
shell_folder_name = {
210-
"CSIDL_APPDATA": "AppData",
211-
"CSIDL_COMMON_APPDATA": "Common AppData",
212-
"CSIDL_LOCAL_APPDATA": "Local AppData",
213-
}[csidl_name]
214-
215-
key = _winreg.OpenKey(
216-
_winreg.HKEY_CURRENT_USER,
217-
r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders"
218-
)
219-
directory, _type = _winreg.QueryValueEx(key, shell_folder_name)
220-
return directory
221-
222-
223-
def _get_win_folder_with_ctypes(csidl_name):
224-
# type: (str) -> str
225-
# On Python 2, ctypes.create_unicode_buffer().value returns "unicode",
226-
# which isn't the same as str in the annotation above.
227-
csidl_const = {
228-
"CSIDL_APPDATA": 26,
229-
"CSIDL_COMMON_APPDATA": 35,
230-
"CSIDL_LOCAL_APPDATA": 28,
231-
}[csidl_name]
232-
233-
buf = ctypes.create_unicode_buffer(1024)
234-
windll = ctypes.windll # type: ignore
235-
windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf)
236-
237-
# Downgrade to short path name if have highbit chars. See
238-
# <http://bugs.activestate.com/show_bug.cgi?id=85099>.
239-
has_high_char = False
240-
for c in buf:
241-
if ord(c) > 255:
242-
has_high_char = True
243-
break
244-
if has_high_char:
245-
buf2 = ctypes.create_unicode_buffer(1024)
246-
if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024):
247-
buf = buf2
248-
249-
# The type: ignore is explained under the type annotation for this function
250-
return buf.value # type: ignore
251-
252-
253-
if WINDOWS:
254-
try:
255-
import ctypes
256-
_get_win_folder = _get_win_folder_with_ctypes
257-
except ImportError:
258-
_get_win_folder = _get_win_folder_from_registry
259-
260-
261-
def _win_path_to_bytes(path):
262-
"""Encode Windows paths to bytes. Only used on Python 2.
263-
264-
Motivation is to be consistent with other operating systems where paths
265-
are also returned as bytes. This avoids problems mixing bytes and Unicode
266-
elsewhere in the codebase. For more details and discussion see
267-
<https://github.com/pypa/pip/issues/3463>.
268-
269-
If encoding using ASCII and MBCS fails, return the original Unicode path.
270-
"""
271-
for encoding in ('ASCII', 'MBCS'):
272-
try:
273-
return path.encode(encoding)
274-
except (UnicodeEncodeError, LookupError):
275-
pass
276-
return path
38+
dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True)
39+
if _appdirs.system == "linux2":
40+
return dirval.split(os.pathsep)
41+
return [dirval]

src/pip/_vendor/appdirs.py

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
6464
for a discussion of issues.
6565
6666
Typical user data directories are:
67-
Mac OS X: ~/Library/Application Support/<AppName>
67+
Mac OS X: ~/Library/Application Support/<AppName> # or ~/.config/<AppName>, if the other does not exist
6868
Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined
6969
Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName>
7070
Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName>
@@ -88,6 +88,10 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False):
8888
path = os.path.expanduser('~/Library/Application Support/')
8989
if appname:
9090
path = os.path.join(path, appname)
91+
if not os.path.isdir(path):
92+
path = os.path.expanduser('~/.config/')
93+
if appname:
94+
path = os.path.join(path, appname)
9195
else:
9296
path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share"))
9397
if appname:
@@ -150,7 +154,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False):
150154
if appname:
151155
if version:
152156
appname = os.path.join(appname, version)
153-
pathlist = [os.sep.join([x, appname]) for x in pathlist]
157+
pathlist = [os.path.join(x, appname) for x in pathlist]
154158

155159
if multipath:
156160
path = os.pathsep.join(pathlist)
@@ -203,6 +207,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False):
203207
return path
204208

205209

210+
# for the discussion regarding site_config_dir locations
211+
# see <https://github.com/pypa/pip/issues/1733>
206212
def site_config_dir(appname=None, appauthor=None, version=None, multipath=False):
207213
r"""Return full path to the user-shared data dir for this application.
208214
@@ -245,7 +251,9 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False)
245251
if appname:
246252
if version:
247253
appname = os.path.join(appname, version)
248-
pathlist = [os.sep.join([x, appname]) for x in pathlist]
254+
pathlist = [os.path.join(x, appname) for x in pathlist]
255+
# always look in /etc directly as well
256+
pathlist.append('/etc')
249257

250258
if multipath:
251259
path = os.pathsep.join(pathlist)
@@ -291,6 +299,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True):
291299
if appauthor is None:
292300
appauthor = appname
293301
path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA"))
302+
# When using Python 2, return paths as bytes on Windows like we do on
303+
# other operating systems. See helper function docs for more details.
304+
if not PY3 and isinstance(path, unicode):
305+
path = _win_path_to_bytes(path)
294306
if appname:
295307
if appauthor is not False:
296308
path = os.path.join(path, appauthor, appname)
@@ -567,6 +579,24 @@ def _get_win_folder_with_jna(csidl_name):
567579
_get_win_folder = _get_win_folder_from_registry
568580

569581

582+
def _win_path_to_bytes(path):
583+
"""Encode Windows paths to bytes. Only used on Python 2.
584+
585+
Motivation is to be consistent with other operating systems where paths
586+
are also returned as bytes. This avoids problems mixing bytes and Unicode
587+
elsewhere in the codebase. For more details and discussion see
588+
<https://github.com/pypa/pip/issues/3463>.
589+
590+
If encoding using ASCII and MBCS fails, return the original Unicode path.
591+
"""
592+
for encoding in ('ASCII', 'MBCS'):
593+
try:
594+
return path.encode(encoding)
595+
except (UnicodeEncodeError, LookupError):
596+
pass
597+
return path
598+
599+
570600
#---- self test code
571601

572602
if __name__ == "__main__":

0 commit comments

Comments
 (0)