1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
|
#!/usr/bin/env python
"""
utilities for pyshortcuts
"""
import os
import sys
import io
from pathlib import Path
from datetime import datetime
from string import ascii_letters
from charset_normalizer import from_bytes
try:
from pwd import getpwnam
except ImportError:
getpwnam = None
uname = "unknown"
scut_ext = "lnk"
ico_ext = ("ico",)
if sys.platform.startswith('lin'):
uname = "linux"
scut_ext = "desktop"
ico_ext = ("ico", "png")
elif sys.platform.startswith('darwin'):
uname = 'darwin'
scut_ext = "app"
ico_ext = ("icns",)
elif sys.platform.startswith('win') or os.name.startswith('nt'):
uname = "win"
scut_ext = "lnk"
ico_ext = ("ico",)
def get_pyexe():
"python executable"
return Path(sys.executable).as_posix()
def get_homedir():
"determine home directory"
# for Unixes, allow for sudo case
susername = os.environ.get("SUDO_USER", None)
try:
if susername is not None and getpwnam is not None:
return Path(getpwnam(susername).pw_dir).resolve().as_posix()
except KeyError:
pass
homedir = Path.home()
# For Windows, ask for parent of Roaming 'Application Data' directory
if homedir is None and os.name == 'nt':
try:
from win32com.shell import shellcon, shell
homedir = Path(shell.SHGetFolderPath(0, shellcon.CSIDL_APPDATA, 0, 0))
except ImportError:
pass
# try the HOME environmental variable
if homedir is None:
test = os.path.expandvars('$HOME')
if test not in (None, '$HOME'):
homedir = Path(test)
# finally, use current folder
if homedir is None:
homedir = Path('.')
return homedir.resolve().as_posix()
def get_cwd():
"""get current working directory
Note: os.getcwd() can fail with permission error.
when that happens, this changes to the users `HOME` directory
and returns that directory so that it always returns an existing
and readable directory.
"""
try:
return Path('.').resolve().as_posix()
except Exception:
home = get_homedir()
os.chdir(home)
return home
def mkdir(name, mode=0o775):
"""create directory (and any intermediate subdirectories)
Options:
--------
mode permission mask to use for creating directory (default=0775)
"""
path = Path(name)
if path.exists():
if path.is_dir():
os.chmod(name, mode)
else:
raise FileExistsError(f"'{name}' is an existing file")
else:
os.makedirs(name, mode=mode)
def isotime(dtime=None, timespec='seconds', sep=' '):
"""return ISO format of current timestamp:
2024-04-27 17:31:12
"""
if dtime is None:
dtime = datetime.now()
elif isinstance(dtime, (float, int)):
dtime = datetime.fromtimestamp(dtime)
return datetime.isoformat(dtime, timespec=timespec, sep=sep)
BAD_FILECHARS = ';~,`!%$@$&^?*#:"/|\'\\\t\r\n(){}[]<>'
GOOD_FILECHARS = '_'*len(BAD_FILECHARS)
TRANS_FILE = str.maketrans(BAD_FILECHARS, GOOD_FILECHARS)
def fix_filename(filename, allow_spaces=False):
"""
fix string to be a 'good' filename, with very few special
characters and (optionally) no more than 1 '.'.
More restrictive than most OSes, but avoids hard-to-deal with filenames
argument `allow_spaces` [default is False] allows spaces in filenames.
"""
fname = str(filename).translate(TRANS_FILE)
if not allow_spaces:
fname = fname.replace(' ', '_')
if fname.count('.') > 1:
words = fname.split('.')
ext = words.pop()
fname = f"{'_'.join(words)}.{ext}"
while '__' in fname:
fname = fname.replace('__', '_')
return fname
def fix_varname(varname):
"""fix string to be a 'good' variable name."""
vname = fix_filename(varname, allow_spaces=False)
if vname[0] not in (ascii_letters+'_'):
vname = f'_{vname}'
for c in '=+-.':
vname = vname.replace(c, '_')
while '__' in vname:
vname = vname.replace('__', '_')
if vname.endswith('_'):
vname = vname[:-1]
return vname
def new_filename(filename):
"""
increment filename to be an unused filename
"""
fpath = Path(filename)
if fpath.exists():
fstem = fpath.stem
fsuffix = fpath.suffix
if fsuffix.startswith('.'):
fsuffix = fsuffix[1:]
if len(fsuffix) == 0:
fsuffix = 'txt'
int_suffix = True
fsint = 0
try:
fsint = int(fsuffix)
except (ValueError, TypeError):
int_suffix = False
while fpath.exists():
fsint += 1
if int_suffix:
fpath = Path(f"{fstem}.{fsint:03d}")
else:
if '_' in fstem:
w = fstem.split('_')
try:
fsint = int(w[-1])
fstem = '_'.join(w[:-1])
except (ValueError, TypeError):
pass
fpath = Path(f"{fstem}_{fsint:03d}.{fsuffix}")
return fpath.as_posix()
def bytes2str(s):
'byte to string conversion'
if isinstance(s, str):
return s
if isinstance(s, bytes):
return s.decode(sys.stdout.encoding)
return str(s, sys.stdout.encoding)
def str2bytes(s):
'string to byte conversion'
if isinstance(s, bytes):
return s
return bytes(s, sys.stdout.encoding)
def strict_ascii(sinp, replacement='_'):
"""ensure a string to be truly ASCII with all characters below 128
replacing characters will char(c) >=128 with 'replacement'"""
t = bytes(sinp, 'UTF-8')
return ''.join([chr(a) if a < 128 else replacement for a in t])
def pathname(input):
"""
return a full path name from Path().as_posix() given
a filename (str or byts) or Path object.
"""
if isinstance(input, bytes):
input = str(from_bytes(input).best())
if isinstance(input, str):
input = Path(input)
if not isinstance(input, Path):
raise ValueError(f"cannot get Path name from {input}")
return input.absolute().as_posix()
def read_textfile(filename, size=None):
"""read text from a file as string
Argument
--------
filename (str or file): name of file to read or file-like object
size (int or None): maximum number of bytes to read
Returns
-------
text of file as string.
Notes
------
1. the encoding is detected with charset_normalizer.from_bytes()
which is then used to decode bytes read from file.
2. line endings are normalized to be '\n', so that
splitting on '\n' will give a list of lines.
"""
text = ''
def decode(bytedata):
return str(from_bytes(bytedata).best())
if isinstance(filename, io.IOBase):
text = filename.read(size)
if filename.mode == 'rb':
text = decode(text)
else:
with open(pathname(filename), 'rb') as fh:
text = decode(fh.read(size))
return text.replace('\r\n', '\n').replace('\r', '\n')
|