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 # I am **NOT** my ancestor 

94 child_dir = child_dir.parent 

95 while not is_mount(child_dir): 

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

97 config_heir.append(child_dir) 

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

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

100 # project directory 

101 config_heir.append(child_dir) 

102 break 

103 child_dir = child_dir.parent 

104 return config_heir 

105 

106 

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

108 """ 

109 Get XDG_<BASE>_HOME locations. 

110 

111 `specifications 

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

113 

114 Args: 

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

116 

117 Returns: 

118 List of xdg-<base> Paths 

119 First directory is most dominant 

120 Raises: 

121 KeyError: bad variable name 

122 

123 """ 

124 xdgbase = XDG_BASES[base.upper()] 

125 xdg_heir: List[Path] = [] 

126 # environment 

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

128 # windows 

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

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

131 xdg_base_home = Path( 

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

133 xdg_heir.append(xdg_base_home) 

134 xdg_heir.append(root_var) 

135 else: 

136 # assume POSIX 

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

138 xdg_base_home = Path( 

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

140 xdg_heir.append(xdg_base_home) 

141 if xdgbase.unix_vars: 

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

143 xdgbase.unix_root) 

144 else: 

145 xdg_base_dirs = xdgbase.unix_root 

146 if xdg_base_dirs: 

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

148 xdg_heir.append(Path(xdg_dirs)) 

149 return xdg_heir 

150 

151 

152def locate_base(project: str, 

153 custom: os.PathLike = None, 

154 ancestors: bool = False, 

155 base_type: str = 'CONFIG', 

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

157 """ 

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

159 

160 Args: 

161 project: name of project whose base is being fetched 

162 custom: custom location (directory) 

163 ancestors: inherit ancestor directories that contain __init__.py 

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

165 py_bin: namespace.__file__ that imports this function 

166 

167 Returns: 

168 List of all possible base directory paths: 

169 Existing and non-existing 

170 First directory is most dominant 

171 

172 """ 

173 # Preference of base *Most dominant first* 

174 base_heir: List[Path] = [] 

175 

176 # custom 

177 if custom is not None: 

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

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

180 base_heir.append(Path(custom)) 

181 

182 # Current directory 

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

184 base_heir.append(current_dir) 

185 

186 if ancestors: 

187 # ancestral directories 

188 ancestor_parents = walk_ancestors(current_dir) 

189 base_heir.extend(ancestor_parents) 

190 

191 # xdg locations 

192 xdg_heir = xdg_base(base_type) 

193 for heir in xdg_heir: 

194 base_heir.append(heir / project) 

195 

196 # Shipped location 

197 if py_bin: 

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

199 return base_heir