Skip to content

Commit 1dc894e

Browse files
committed
Fixed tsm script install in executable path
1 parent 69fea21 commit 1dc894e

File tree

8 files changed

+67
-23
lines changed

8 files changed

+67
-23
lines changed

docs/pages/helpers.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,9 +246,13 @@ Tinyscript also provides modified/additional `pathlib`-related classes:
246246
- `choice(*filetypes:str)`: chooses a random file in the current folder among the given extensions (mentioning the dot ; e.g. `.py`)
247247
- `find(name:str, regex:bool)`: finds a file or folder, using `name` as a regex or not
248248
- `generate(prefix:str, suffix:str, length:int, alphabet:str)`: generates a random folder name (guaranteed to be non-existent) using the given prefix, suffix, length and alphabet, and returns the joined path
249+
- `is_executable()`: checks whether the path is executable
249250
- `is_hidden()`: checks whether the current file/folder is hidden
251+
- `is_in_path_env_var()`: checks whether the path (if not a folder, its `.dirname`) is in the `PATH` environment variable
252+
- `is_readable()`: checks whether the path is readable
250253
- `is_samepath(other_path:str|Path)`: checks whether the given path is the same
251254
- `is_under(path:str|Path)`: checks whether the path is under the given parent path
255+
- `is_writable()`: checks whether the path is writable
252256
- `iterfiles()`: iterates over files only
253257
- `iterpubdir()`: iterates over visible directories only
254258
- `listdir(filter_func:lambda, sort:bool)`: list the current path based on a filter function, sorted or not

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ classifiers = [
3737
]
3838
dependencies = [
3939
"argcomplete>=3.0.8",
40-
"asciistuff>=1.2.6",
40+
"asciistuff>=1.3.0",
4141
"bitstring==4.0.2",
4242
"codext>=1.15.0",
4343
"coloredlogs",

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
argcomplete>=3.0.8
2-
asciistuff>=1.2.6
2+
asciistuff>=1.3.0
33
bitstring>3
44
codext>=1.15.0
55
coloredlogs

src/tinyscript/VERSION.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1.30.6
1+
1.30.7

src/tinyscript/__main__.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,18 @@
2525
"""
2626

2727

28-
BIN = Path("~/.bin", expand=True, create=True)
28+
for _bin in ["~/.bin", "~/.local/bin", "~/.opt/bin"]:
29+
BIN = Path(_bin, expand=True, create=True)
30+
if BIN.is_in_path_env_var():
31+
break
32+
BIN = None
33+
if BIN is None:
34+
for p in os.environ['PATH'].split(":"):
35+
p = Path(p, expand=True)
36+
if p.is_writable():
37+
BIN = p
38+
break
39+
2940
CACHE = ts.ConfigPath("tinyscript", create=True).joinpath("cache.json")
3041
DUNDERS = ["__author__", "__email__", "__version__", "__copyright__", "__license__"]
3142
SOURCES = ts.ConfigPath("tinyscript").joinpath("sources.conf")
@@ -49,21 +60,21 @@ def __download(url, **kw):
4960
content = b"".join(chunk for chunk in resp.iter_content(chunk_size=4096)).decode()
5061
return content, _parse_metadata(content)
5162
except requests.exceptions.Timeout:
52-
logger.error("Request for '%s' timed out" % url)
63+
logger.error(f"Request for '{url}' timed out")
5364
except requests.exceptions.TooManyRedirects:
54-
logger.error("Too many redirects for '%s'" % url)
65+
logger.error(f"Too many redirects for '{url}'")
5566
except requests.exceptions.RequestException as e:
5667
resp = e.response
5768
if resp:
58-
logger.error("'%s' cannot be opened (status code: %d - %s)" % (url, resp.status_code, resp.reason))
69+
logger.error(f"'{url}' cannot be opened (status code: {resp.status_code} - {resp.reason})")
5970
else:
6071
logger.error(str(e))
6172
return None, {}
6273

6374

6475
def _fetch_source(target):
6576
target = target.strip(" \r\n")
66-
logger.info("Fetching source '%s'..." % target)
77+
logger.info(f"Fetching source '{target}'...")
6778
CACHE.touch()
6879
with CACHE.open() as f:
6980
try:
@@ -97,7 +108,7 @@ def _get_sources_list(fetch=False):
97108
if source == "" or source.startswith("#"):
98109
continue
99110
if fetch:
100-
logger.info("Fetching source '%s'..." % source)
111+
logger.info(f"Fetching source '{source}'...")
101112
__download(source)
102113
r.append(source)
103114
return r
@@ -111,12 +122,12 @@ def _iter_sources(source=None):
111122
try:
112123
yield cache, source, cache[source]
113124
except KeyError:
114-
logger.error("Source '%s' does not exist" % source)
125+
logger.error(f"Source '{source}' does not exist")
115126
else:
116127
for source, scripts in cache.items():
117128
yield cache, source, scripts
118129
else:
119-
logger.warning("No cache available ; please use 'tinyscript update' to get script links from sources")
130+
logger.warning("No cache available ; please use 'tsm update' to get script links from sources")
120131

121132

122133
def _parse_metadata(content):
@@ -160,17 +171,21 @@ def main():
160171
s, sources = args.url, _get_sources_list()
161172
if not args.fetch or args.fetch and _fetch_source(s):
162173
if s in sources:
163-
logger.warning("Source '%s' already exists" % s)
174+
logger.warning(f"Source '{s}' already exists")
164175
else:
165176
with SOURCES.open('a') as f:
166177
f.write(s)
167-
logger.info("Added source '%s'" % s)
178+
logger.info(f"Added source '{s}'")
168179
elif args.command == "install":
169-
script = BIN.joinpath(args.name)
180+
if BIN is None:
181+
script = Path(args.name)
182+
logger.warning("Could not find a suitable path from the PATH environment variable to put the script in")
183+
else:
184+
script = BIN.joinpath(args.name)
170185
if script.exists() and not args.force:
171186
meta = _parse_metadata(script.read_text())
172187
v = meta.get('version')
173-
logger.warning(("Script '%s' already exists" % args.name) + [" (version: %s)" % v, ""][v is None])
188+
logger.warning((f"Script '{args.name}' already exists") + [f" (version: {v})", ""][v is None])
174189
else:
175190
local_v = __version(_parse_metadata(script.read_text()).get('version') if script.exists() else "0.0.0")
176191
for cache, source, scripts in _iter_sources(args.source):
@@ -185,11 +200,11 @@ def main():
185200
script.write_text(content)
186201
cache[source][args.name].update(meta)
187202
script.chmod(args.mode)
188-
logger.info("Script '%s' %s" % (args.name, status))
203+
logger.info(f"Script '{args.name}' {status}")
189204
else:
190205
logger.warning("Remote script has a lower version, hence not updated")
206+
_update_cache(cache)
191207
break
192-
_update_cache(cache)
193208
elif args.command == "new":
194209
new_script(args.name, args.target)
195210
elif args.command == "remove-source":
@@ -199,18 +214,18 @@ def main():
199214
l = len(sources)
200215
new = [_ for _ in sources if _.strip(" #\r\n") != s]
201216
if l == len(new):
202-
logger.warning("'%s' not found" % s)
217+
logger.warning(f"'{s}' not found")
203218
else:
204219
f.seek(0)
205220
f.truncate()
206221
f.writelines(new)
207-
logger.info("Removed source '%s'" % s)
222+
logger.info(f"Removed source '{s}'")
208223
elif args.command == "search":
209224
for cache, source, scripts in _iter_sources(args.source):
210225
for name, data in scripts.items():
211226
link = data['link']
212227
if re.search(args.pattern, name) or args.extended and re.search(args.pattern, link):
213-
print("%s\n URL : %s\n Source: %s" % (name, link, source))
228+
print(f"{name}\n URL : {link}\n Source: {source}")
214229
if args.fetch:
215230
_, meta = __download(link, headers={'Range': "bytes=32-1024"})
216231
cache[source][name] = {'link': link}
@@ -228,7 +243,7 @@ def main():
228243
["\n", ""]["email" in dunders]
229244
elif k == "email":
230245
if "author" in dunders:
231-
s += " (%s)\n" % v
246+
s += f" ({v})\n"
232247
else:
233248
func = globals().get(k, lambda s: s)
234249
try:
@@ -247,5 +262,5 @@ def main():
247262
continue
248263
_fetch_source(l)
249264
else:
250-
logger.warning("'%s' does not exist" % SOURCES)
265+
logger.warning(f"'{SOURCES}' does not exist")
251266

src/tinyscript/argreparse.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ def add_mutually_exclusive_group(self, **kwargs):
288288
class _NewArgumentGroup(_ArgumentGroup, _NewActionsContainer):
289289
""" Alternative argparse._ArgumentGroup for modifying argument groups handling in the modified ActionsContainer. """
290290
pass
291-
291+
292292

293293
class _NewMutuallyExclusiveGroup(_MutuallyExclusiveGroup, _NewArgumentGroup):
294294
""" Alternative argparse._MutuallyExclusiveGroup for modifying arguments mutually exclusive groups handling in the

src/tinyscript/helpers/path.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,15 @@ def generate(self, prefix="", suffix="", length=8, alphabet="0123456789abcdef"):
210210
return new
211211
rand_folder_name = generate
212212

213+
def is_executable(self):
214+
""" Check if the path can be executed. """
215+
return os.access(str(self.expanduser().absolute()), os.X_OK)
216+
217+
def is_in_path_env_var(self):
218+
""" Check if the current path is in the PATH environment variable. """
219+
return (self.dirname if self.is_file() else self).expanduser().absolute() in \
220+
[Path(p, expand=True) for p in os.environ['PATH'].split(":")]
221+
213222
def is_hidden(self):
214223
""" Check if the current path is hidden. """
215224
if DARWIN:
@@ -234,6 +243,14 @@ def is_under(self, parentpath):
234243
p = Path(p.dirname)
235244
return p in self.absolute().parents
236245

246+
def is_readable(self):
247+
""" Check if the path can be read. """
248+
return os.access(str(self.expanduser().absolute()), os.R_OK)
249+
250+
def is_writable(self):
251+
""" Check if the path can be written. """
252+
return os.access(str(self.expanduser().absolute()), os.W_OK)
253+
237254
def iterfiles(self, filetype=None, filename_only=False, relative=False):
238255
""" List all files from the current directory. """
239256
for i in self.iterdir():

tests/test_helpers_path.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,10 @@ def test_file_extensions(self):
6969
self.assertEqual(FILE.generate(), FILE)
7070
self.assertRaises(TypeError, FILE.append_text, 0)
7171
self.assertTrue(FILE.is_under(FILE))
72+
self.assertIsNotNone(FILE.is_executable())
73+
self.assertIsNotNone(FILE.is_readable())
74+
self.assertIsNotNone(FILE.is_writable())
75+
self.assertIsNotNone(FILE.is_in_path_env_var())
7276
self.assertTrue(FILE.copy(FILE2).is_file())
7377
FILE2.remove()
7478
self.assertFalse(FILE2.copy(FILE).is_file())
@@ -89,6 +93,10 @@ def test_folder_extensions(self):
8993
self.assertNotEqual(len(list(PATH.find("test"))), 0)
9094
self.assertNotEqual(len(list(PATH.find("te*"))), 0)
9195
self.assertEqual(len(list(PATH.find("test2.+", True))), 0)
96+
self.assertIsNotNone(PATH.is_executable())
97+
self.assertIsNotNone(PATH.is_readable())
98+
self.assertIsNotNone(PATH.is_writable())
99+
self.assertIsNotNone(PATH.is_in_path_env_var())
92100

93101
def test_config_path(self):
94102
PATH = ConfigPath("test-app", file=True)

0 commit comments

Comments
 (0)