Coverage for /home/pradyumna/Languages/python/packages/xdgpspconf/xdgpspconf/common.py : 97%

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"""
23import os
24import sys
25from dataclasses import dataclass
26from pathlib import Path
27from typing import List, Union
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
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}
57def is_mount(path: Path):
58 """
59 Check across platform if path is mountpoint or drive.
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
74def walk_ancestors(child_dir: Path) -> List[Path]:
75 """
76 Walk up to nearest mountpoint or project root.
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
84 Args:
85 child_dir: walk ancestry of `this` directory
87 Returns:
88 List of Paths to parents of ancestral configurations:
89 First directory is most dominant
90 """
91 config_heir: List[Path] = []
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
107def xdg_base(base: str = 'CONFIG') -> List[Path]:
108 """
109 Get XDG_<BASE>_HOME locations.
111 `specifications
112 <https://specifications.freedesktop.org/basedir-spec/latest/ar01s03.html>`__
114 Args:
115 base: xdg base to fetch {CACHE,CONFIG,DATA,STATE}
117 Returns:
118 List of xdg-<base> Paths
119 First directory is most dominant
120 Raises:
121 KeyError: bad variable name
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
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.
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
167 Returns:
168 List of all possible base directory paths:
169 Existing and non-existing
170 First directory is most dominant
172 """
173 # Preference of base *Most dominant first*
174 base_heir: List[Path] = []
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))
182 # Current directory
183 current_dir = Path('.').resolve()
184 base_heir.append(current_dir)
186 if ancestors:
187 # ancestral directories
188 ancestor_parents = walk_ancestors(current_dir)
189 base_heir.extend(ancestor_parents)
191 # xdg locations
192 xdg_heir = xdg_base(base_type)
193 for heir in xdg_heir:
194 base_heir.append(heir / project)
196 # Shipped location
197 if py_bin:
198 base_heir.append(Path(py_bin).parent)
199 return base_heir