Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1#!/usr/bin/env python3 

2# -*- coding: utf-8; mode: python; -*- 

3# Copyright © 2020-2021 Pradyumna Paranjape 

4# 

5# This file is part of xdgpspconf. 

6# 

7# xdgpspconf is free software: you can redistribute it and/or modify 

8# it under the terms of the GNU Lesser General Public License as published by 

9# the Free Software Foundation, either version 3 of the License, or 

10# (at your option) any later version. 

11# 

12# xdgpspconf is distributed in the hope that it will be useful, 

13# but WITHOUT ANY WARRANTY; without even the implied warranty of 

14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

15# GNU Lesser General Public License for more details. 

16# 

17# You should have received a copy of the GNU Lesser General Public License 

18# along with xdgpspconf. If not, see <https://www.gnu.org/licenses/>. # 

19""" 

20Common filesystem discovery functions. 

21""" 

22 

23import os 

24import sys 

25from dataclasses import dataclass 

26from pathlib import Path 

27from typing import List, Union 

28 

29 

30@dataclass 

31class XdgVar(): 

32 win_root: str 

33 win_var: str 

34 win_default: str 

35 unix_root: Union[str, None] 

36 unix_var: str 

37 unix_vars: Union[str, None] 

38 unix_default: str 

39 

40 

41XDG_BASES = { 

42 'CACHE': 

43 XdgVar('TEMP', 'TEMP', 'AppData/Local/Temp', None, 'XDG_CACHE_HOME', None, 

44 '.cache'), 

45 'CONFIG': 

46 XdgVar('APPDATA', 'LOCALAPPDATA', 'AppData/Local', '/etc/xdg', 

47 'XDG_CONFIG_HOME', 'XDG_CONFIG_DIRS', '.config'), 

48 'DATA': 

49 XdgVar('APPDATA', 'LOCALAPPDATA', 'AppData/Local', '/local/share', 

50 'XDG_DATA_HOME', 'XDG_DATA_DIRS', '.local/share'), 

51 'STATE': 

52 XdgVar('APPDATA', 'LOCALAPPDATA', 'AppData/Local', '/local/share', 

53 'XDG_STATE_HOME', 'XDG_STATE_DIRS', '.local/state') 

54} 

55 

56 

57def is_mount(path: Path): 

58 """ 

59 Check across platform if path is mountpoint or drive. 

60 

61 Args: 

62 path: path to be checked 

63 """ 

64 try: 

65 if path.is_mount(): 

66 return True 

67 return False 

68 except NotImplementedError: # pragma: no cover 

69 if path.resolve().drive + '\\' == str(path): 

70 return True 

71 return False 

72 

73 

74def walk_ancestors(child_dir: Path) -> List[Path]: 

75 """ 

76 Walk up to nearest mountpoint or project root. 

77 

78 - collect all directories containing __init__.py 

79 (assumed to be source directories) 

80 - project root is directory that contains ``setup.cfg`` or ``setup.py`` 

81 - mountpoint is a unix mountpoint or windows drive root 

82 - I am **NOT** my ancestor 

83 

84 Args: 

85 child_dir: walk ancestry of `this` directory 

86 

87 Returns: 

88 List of Paths to parents of ancestral configurations: 

89 First directory is most dominant 

90 """ 

91 config_heir: List[Path] = [] 

92 

93 while not is_mount(child_dir): 

94 if (child_dir / '__init__.py').is_file(): 

95 config_heir.append(child_dir) 

96 if any((child_dir / setup).is_file() 

97 for setup in ('setup.cfg', 'setup.py')): 

98 # project directory 

99 config_heir.append(child_dir) 

100 break 

101 child_dir = child_dir.parent 

102 return config_heir 

103 

104 

105def xdg_base(base: str = 'CONFIG') -> List[Path]: 

106 """ 

107 Get XDG_<BASE>_HOME locations. 

108 

109 `specifications 

110 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__ 

111 

112 Args: 

113 base: xdg base to fetch {CACHE,CONFIG,DATA,STATE} 

114 

115 Returns: 

116 List of xdg-<base> Paths 

117 First directory is most dominant 

118 Raises: 

119 KeyError: bad variable name 

120 

121 """ 

122 xdgbase = XDG_BASES[base.upper()] 

123 xdg_heir: List[Path] = [] 

124 # environment 

125 if sys.platform.startswith('win'): # pragma: no cover 

126 # windows 

127 user_home = Path(os.environ['USERPROFILE']) 

128 root_var = Path(os.environ[xdgbase.win_root]) 

129 xdg_base_home = Path( 

130 os.environ.get(xdgbase.win_var, user_home / xdgbase.win_default)) 

131 xdg_heir.append(xdg_base_home) 

132 xdg_heir.append(root_var) 

133 else: 

134 # assume POSIX 

135 user_home = Path(os.environ['HOME']) 

136 xdg_base_home = Path( 

137 os.environ.get(xdgbase.unix_var, user_home / xdgbase.unix_default)) 

138 xdg_heir.append(xdg_base_home) 

139 if xdgbase.unix_vars: 

140 xdg_base_dirs = os.environ.get(xdgbase.unix_vars, 

141 xdgbase.unix_root) 

142 else: 

143 xdg_base_dirs = xdgbase.unix_root 

144 if xdg_base_dirs: 

145 for xdg_dirs in xdg_base_dirs.split(':'): 

146 xdg_heir.append(Path(xdg_dirs)) 

147 return xdg_heir 

148 

149 

150def locate_base(project: str, 

151 custom: os.PathLike = None, 

152 ancestors: bool = False, 

153 base_type: str = 'CONFIG', 

154 py_bin: os.PathLike = None) -> List[Path]: 

155 """ 

156 Locate base (data/base) directories at standard locations. 

157 

158 Args: 

159 project: name of project whose base is being fetched 

160 custom: custom location (directory) 

161 ancestors: inherit ancestor directories that contain __init__.py 

162 base_type: type of xdg base {CACHE,CONFIG,DATA,STATE} 

163 py_bin: namespace.__file__ that imports this function 

164 

165 Returns: 

166 List of all possible base directory paths: 

167 Existing and non-existing 

168 First directory is most dominant 

169 

170 """ 

171 # Preference of base *Most dominant first* 

172 base_heir: List[Path] = [] 

173 

174 # custom 

175 if custom is not None: 

176 if not Path(custom).is_dir(): 

177 raise FileNotFoundError(f'Custom base: {custom} not found') 

178 base_heir.append(Path(custom)) 

179 

180 # Current directory 

181 current_dir = Path('.').resolve() 

182 base_heir.append(current_dir) 

183 

184 if ancestors: 

185 # ancestral directories 

186 ancestor_parents = walk_ancestors(current_dir) 

187 base_heir.extend(ancestor_parents) 

188 

189 # xdg locations 

190 xdg_heir = xdg_base(base_type) 

191 for heir in xdg_heir: 

192 base_heir.append(heir / project) 

193 

194 # Shipped location 

195 if py_bin: 

196 base_heir.append(Path(py_bin).parent) 

197 return base_heir