Repository navigation
Expand file tree
/
Copy pathutils.py
More file actions
231 lines (175 loc) · 6.53 KB
/
Copy pathutils.py
File metadata and controls
231 lines (175 loc) · 6.53 KB
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
# --------------------------------------------------------------------
# SPDX-License-Identifier: AGPL-3.0-or-later
# Licensed under the GNU Affero General Public License v3.0 or later.
# Author: Jose M. Rodriguez-Rosa (a.k.a. Boriel) - https://boriel.com
# See https://www.gnu.org/licenses/agpl-3.0.html for details.
# --------------------------------------------------------------------
import errno
import os
import shelve
import signal
from collections.abc import Callable, Iterable
from contextlib import contextmanager
from functools import wraps
from typing import IO, Any, TypeVar
from src.api import constants, errmsg, global_
__all__ = (
"chdir",
"first",
"flatten_list",
"open_file",
"read_txt_file",
"sanitize_filename",
"timeout",
)
__doc__ = """Utils module contains many helpers for several task,
like reading files or path management"""
SHELVE_PATH = os.path.join(constants.ZXBASIC_ROOT, "parsetab", "tabs.dbm")
SHELVE = shelve.open(SHELVE_PATH)
T = TypeVar("T")
def first(iter_: Iterable[T], default: T | None = None) -> T | None:
"""Return the first element of an Iterable, or None if it's empty or
there are no more elements to return."""
return next(iter(iter_), default)
def sfirst(iter_: Iterable[T]) -> T:
"""Return the first element of an Iterable, or fails if it's empty"""
return next(iter(iter_))
def read_txt_file(fname: str) -> str:
"""Reads a txt file, regardless of its encoding"""
encodings = ["utf-8-sig", "cp1252"]
with open(fname, "rb") as f:
content = bytes(f.read())
for i in encodings:
try:
result = content.decode(i)
return result
except UnicodeDecodeError:
pass
global_.FILENAME = fname
errmsg.error(1, "Invalid file encoding. Use one of: %s" % ", ".join(encodings))
return ""
def open_file(fname: str, mode: str = "rb", encoding: str = "utf-8") -> IO[Any]:
"""An open() wrapper for PY2 and PY3 which allows encoding
:param fname: file name (string)
:param mode: file mode (string) optional
:param encoding: optional encoding (string). Ignored in python2 or if not in text mode
:return: an open file handle
"""
if "t" not in mode or not encoding:
return open(fname, mode)
return open(fname, mode, encoding=encoding)
def sanitize_filename(fname: str) -> str:
"""Given a file name (string) returns it with back-slashes reversed.
This is to make all BASIC programs compatible in all OSes
"""
return fname.replace("\\", "/")
def get_absolute_filename_path(fname: str) -> str:
"""Given a filename, if it does not start with '/' or '\', it
will be returned a given absolute filename path
"""
return os.path.realpath(os.path.expanduser(fname))
def get_relative_filename_path(fname: str, current_dir: str | None = None) -> str:
"""Given an absolute path, returns it relative to the current directory,
that is, if the file is in the same folder or any of it children, only
the path from the current folder onwards is returned. Otherwise, the
absolute path is returned
"""
fname_abs = get_absolute_filename_path(fname)
current_path = get_absolute_filename_path(os.path.curdir if current_dir is None else current_dir)
if not fname_abs.startswith(current_path):
return fname_abs
return fname_abs[len(current_path) :].lstrip(os.path.sep)
def current_data_label() -> str:
"""Returns a data label to which all labels must point to, until
a new DATA line is declared
"""
return f"{global_.DATAS_NAMESPACE}.__DATA__{len(global_.DATAS)}"
def flatten_list(x: Iterable[Any], iterables=(list,)) -> list[Any]:
"""Flattens a nested iterable and returns it as a List.
Nested iterables will be flattened recursively (default only nested lists)
"""
result = []
for elem in x:
if not isinstance(elem, iterables):
result.append(elem)
else:
result.extend(flatten_list(elem))
return result
def parse_int(num: str | None) -> int | None:
"""Given an integer number, return its value,
or None if it could not be parsed.
Allowed formats: DECIMAL, HEXA (0xnnn, $nnnn or nnnnh)
An hexadecimal number is ambiguous if it starts with a letter (i.e. A0h can be a label),
and won't be parsed. Such numbers must be prefixed with 0 digit (i.e. 0A0h)
:param num: (string) the number to be parsed
:return: an integer number or None if it could not be parsed
"""
num = (num or "").strip().upper()
if not num:
return None
base = 10
if num[:2] == "0X":
base = 16
elif num[-1] == "H":
if num[0] not in "0123456789":
return None
base = 16
num = num[:-1]
elif num[0] == "$":
base = 16
num = num[1:]
elif num[0] == "%":
base = 2
num = num[1:]
elif num[-1] == "B":
if num[0] not in "01":
return None
base = 2
num = num[:-1]
try:
return int(num, base)
except ValueError:
pass
return None
def eval_to_num(expr: str) -> int | float | None:
"""Evaluates the expression and returns the result or None
if it was non-numeric."""
try:
result = eval(expr, {}, {})
except (NameError, SyntaxError, ValueError):
return None
if isinstance(result, (int, float)):
return result
return None
def load_object(key: str) -> Any:
return SHELVE[key] if key in SHELVE else None
def save_object(key: str, obj: Any) -> Any:
SHELVE[key] = obj
SHELVE.sync()
return obj
def get_or_create(key: str, fn: Callable[[], Any]) -> Any:
return load_object(key) or save_object(key, fn())
def timeout(seconds: Callable[[], int] | int = 10, error_message=os.strerror(errno.ETIME)):
def decorator(func):
def _handle_timeout(signum, frame):
raise TimeoutError(error_message)
def wrapper(*args, **kwargs):
signal.signal(signal.SIGALRM, _handle_timeout)
signal.alarm(seconds() if isinstance(seconds, Callable) else seconds)
try:
result = func(*args, **kwargs)
finally:
signal.alarm(0)
return result
return wraps(func)(wrapper)
return decorator
@contextmanager
def chdir(path: str):
"""Context manager to temporarily enter a directory, and return back
to the original folder upon exit."""
current_path = os.path.abspath(os.getcwd())
try:
os.chdir(path)
yield
finally:
os.chdir(current_path)