Skip to content

Commit 4110471

Browse files
authored
2.2.0rc2 (#74)
1 parent 0b1b89b commit 4110471

File tree

8 files changed

+229
-96
lines changed

8 files changed

+229
-96
lines changed

docs/source/dirs.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ Directory validation
3333
validate_filename
3434
get_file_pathnames
3535
check_files_exist
36+
check_relative_pathname
3637

3738
Directory/file management
3839
-------------------------

pyhelpers/_cache.py

Lines changed: 56 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -194,59 +194,6 @@ def _confirmed(prompt=None, confirmation_required=True, resp=False):
194194
return True
195195

196196

197-
def _check_relative_pathname(pathname):
198-
"""
199-
Checks if the pathname is relative to the current working directory.
200-
201-
This function returns a relative pathname of the input ``pathname`` to the current working
202-
directory if ``pathname`` is within the current working directory;
203-
otherwise, it returns a copy of the input.
204-
205-
:param pathname: Pathname (of a file or directory).
206-
:type pathname: str | bytes | pathlib.Path
207-
:return: A location relative to the current working directory
208-
if ``pathname`` is within the current working directory; otherwise, a copy of ``pathname``.
209-
:rtype: str
210-
211-
**Tests**::
212-
213-
>>> from pyhelpers._cache import _check_relative_pathname
214-
>>> from pyhelpers.dirs import cd
215-
>>> _check_relative_pathname(".")
216-
'.'
217-
>>> _check_relative_pathname(cd())
218-
'.'
219-
>>> _check_relative_pathname("C:\\Program Files")
220-
'C:\\Program Files'
221-
>>> _check_relative_pathname(pathname="C:/Windows")
222-
'C:\\Windows'
223-
"""
224-
225-
if isinstance(pathname, (bytes, bytearray)):
226-
pathname_ = str(pathname, encoding='utf-8')
227-
else:
228-
pathname_ = str(pathname)
229-
230-
abs_pathname = os.path.abspath(pathname_)
231-
abs_cwd = os.getcwd()
232-
233-
if os.name == "nt": # Handle different drive letters on Windows
234-
if os.path.splitdrive(abs_pathname)[0] != os.path.splitdrive(abs_cwd)[0]:
235-
return abs_pathname # Return absolute path if drives differ
236-
237-
# Check if the pathname is inside the current working directory
238-
if os.path.commonpath([abs_pathname, abs_cwd]) == abs_cwd:
239-
try:
240-
rel_path = os.path.relpath(pathname_)
241-
except ValueError:
242-
rel_path = copy.copy(pathname_)
243-
244-
else:
245-
rel_path = abs_pathname # Return original absolute path if outside CWD
246-
247-
return rel_path
248-
249-
250197
def _normalize_pathname(pathname, sep="/", add_slash=False, **kwargs):
251198
# noinspection PyShadowingNames
252199
"""
@@ -339,6 +286,62 @@ def _add_slashes(pathname, normalized=True, surrounded_by='"'):
339286
return f'{s}{path}{s}'
340287

341288

289+
def _check_relative_pathname(pathname, normalized=True):
290+
"""
291+
Checks if the pathname is relative to the current working directory.
292+
293+
This function returns a relative pathname of the input ``pathname`` to the current working
294+
directory if ``pathname`` is within the current working directory;
295+
otherwise, it returns a copy of the input.
296+
297+
:param pathname: Pathname (of a file or directory).
298+
:type pathname: str | bytes | pathlib.Path
299+
:param normalized: Whether to normalize the returned pathname; defaults to ``True``.
300+
:type normalized: bool
301+
:return: A location relative to the current working directory
302+
if ``pathname`` is within the current working directory; otherwise, a copy of ``pathname``.
303+
:rtype: str
304+
305+
**Tests**::
306+
307+
>>> from pyhelpers._cache import _check_relative_pathname
308+
>>> from pyhelpers.dirs import cd
309+
>>> _check_relative_pathname(pathname=".")
310+
'.'
311+
>>> _check_relative_pathname(pathname=cd())
312+
'.'
313+
>>> _check_relative_pathname(pathname="C:/Windows")
314+
'C:/Windows'
315+
>>> _check_relative_pathname(pathname="C:\\Program Files", normalized=False)
316+
'C:\\Program Files'
317+
"""
318+
319+
if isinstance(pathname, (bytes, bytearray)):
320+
pathname_ = str(pathname, encoding='utf-8')
321+
else:
322+
pathname_ = str(pathname)
323+
324+
abs_pathname = os.path.abspath(pathname_)
325+
abs_cwd = os.getcwd()
326+
327+
if os.name == "nt": # Handle different drive letters on Windows
328+
if os.path.splitdrive(abs_pathname)[0] != os.path.splitdrive(abs_cwd)[0]:
329+
# Return absolute path if drives differ
330+
return _normalize_pathname(abs_pathname) if normalized else abs_pathname
331+
332+
# Check if the pathname is inside the current working directory
333+
if os.path.commonpath([abs_pathname, abs_cwd]) == abs_cwd:
334+
try:
335+
rel_path = os.path.relpath(pathname_)
336+
except ValueError:
337+
rel_path = copy.copy(pathname_)
338+
339+
else:
340+
rel_path = abs_pathname # Return original absolute path if outside CWD
341+
342+
return _normalize_pathname(rel_path) if normalized else rel_path
343+
344+
342345
def _check_file_pathname(name, options=None, target=None):
343346
# noinspection PyShadowingNames
344347
"""

pyhelpers/dirs/validation.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ def is_dir(path_to_dir):
8888

8989
for i in range(len(dir_names)):
9090
try:
91-
os.lstat(os.path.join(root_dirname, *dir_names[:i+1]))
91+
os.lstat(os.path.join(root_dirname, *dir_names[:i + 1]))
9292
except OSError as exc:
9393
if hasattr(exc, 'winerror'):
9494
if exc.winerror == 123: # ERROR_INVALID_NAME
@@ -365,3 +365,36 @@ def check_files_exist(filenames, path_to_dir, verbose=False, **kwargs):
365365
rslt = False
366366

367367
return rslt
368+
369+
370+
def check_relative_pathname(pathname, normalized=True):
371+
"""
372+
Checks if the pathname is relative to the current working directory.
373+
374+
This function returns a relative pathname of the input ``pathname`` to the current working
375+
directory if ``pathname`` is within the current working directory;
376+
otherwise, it returns a copy of the input.
377+
378+
:param pathname: Pathname (of a file or directory).
379+
:type pathname: str | bytes | pathlib.Path
380+
:param normalized: Whether to normalize the returned pathname; defaults to ``True``.
381+
:type normalized: bool
382+
:return: A location relative to the current working directory
383+
if ``pathname`` is within the current working directory; otherwise, a copy of ``pathname``.
384+
:rtype: str
385+
386+
**Tests**::
387+
388+
>>> from pyhelpers._cache import _check_relative_pathname
389+
>>> from pyhelpers.dirs import cd
390+
>>> _check_relative_pathname(pathname=".")
391+
'.'
392+
>>> _check_relative_pathname(pathname=cd())
393+
'.'
394+
>>> _check_relative_pathname(pathname="C:/Windows")
395+
'C:/Windows'
396+
>>> _check_relative_pathname(pathname="C:\\Program Files", normalized=False)
397+
'C:\\Program Files'
398+
"""
399+
400+
return _check_relative_pathname(pathname=pathname, normalized=normalized)

pyhelpers/ops/downloads.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def is_downloadable(url, request_field='content-type', **kwargs):
6060

6161

6262
def _download_file_from_url(response, path_to_file, chunk_multiplier=1, desc=None, bar_format=None,
63-
colour=None, validate=True, **kwargs):
63+
colour=None, validate=True, print_wrap_limit=None, **kwargs):
6464
# noinspection PyShadowingNames
6565
"""
6666
Downloads a file from a given HTTP response and saves it to the specified location.
@@ -90,6 +90,11 @@ def _download_file_from_url(response, path_to_file, chunk_multiplier=1, desc=Non
9090
:param validate: Whether to validate if the downloaded file size matches the expected content
9191
length; defaults to ``True``.
9292
:type validate: bool
93+
:param print_wrap_limit: Maximum length of the string before splitting into two lines;
94+
defaults to ``None``, which disables splitting. If the string exceeds this value,
95+
e.g. ``100``, it will be split at (before) ``state_prep`` to improve readability
96+
when printed.
97+
:type print_wrap_limit: int | None
9398
:param kwargs: [Optional] Additional parameters passed to `tqdm.tqdm()`_, allowing customisation
9499
of the progress bar (e.g. ``disable=True`` to hide the progress bar).
95100
@@ -150,16 +155,20 @@ def _download_file_from_url(response, path_to_file, chunk_multiplier=1, desc=Non
150155
except (IOError, TypeError) as e:
151156
_print_failure_message(e, prefix="Download failed:", verbose=True, raise_error=True)
152157

153-
_check_saving_path(path_to_file, verbose=True, print_prefix="\t", belated=belated)
158+
_check_saving_path(
159+
path_to_file, verbose=True, print_prefix="\t", print_wrap_limit=print_wrap_limit,
160+
belated=belated)
161+
154162
if validate and (written != file_size) and (file_size > 0):
155163
raise ValueError(f"Download failed: expected {file_size} bytes, got {written} bytes.")
156164
else:
157165
print("Done.")
158166

159167

160168
def download_file_from_url(url, path_to_file, if_exists='replace', max_retries=5,
161-
requests_session_args=None, verbose=False, chunk_multiplier=1, desc=None,
162-
bar_format=None, colour=None, validate=True, **kwargs):
169+
requests_session_args=None, verbose=False, print_wrap_limit=None,
170+
chunk_multiplier=1, desc=None, bar_format=None, colour=None,
171+
validate=True, **kwargs):
163172
# noinspection PyShadowingNames
164173
"""
165174
Downloads a file from a valid URL.
@@ -185,6 +194,11 @@ def download_file_from_url(url, path_to_file, if_exists='replace', max_retries=5
185194
the requests session; defaults to ``None``.
186195
:type requests_session_args: dict | None
187196
:type verbose: bool | int
197+
:param print_wrap_limit: Maximum length of the string before splitting into two lines;
198+
defaults to ``None``, which disables splitting. If the string exceeds this value,
199+
e.g. ``100``, it will be split at (before) ``state_prep`` to improve readability
200+
when printed.
201+
:type print_wrap_limit: int | None
188202
:param chunk_multiplier: A factor by which the default chunk size (1MB) is multiplied;
189203
this can be adjusted to optimise download performance based on file size; defaults to ``1``.
190204
:type chunk_multiplier: int | float
@@ -243,7 +257,7 @@ def download_file_from_url(url, path_to_file, if_exists='replace', max_retries=5
243257

244258
if os.path.isfile(path_to_file_) and if_exists != 'replace':
245259
if verbose:
246-
print(f'File "{os.path.basename(path_to_file)}" already exists. Aborting download. '
260+
print(f'File "{os.path.basename(path_to_file)}" already exists. Aborting download.\n'
247261
f'Set `if_exists="replace"` to update the existing file.')
248262

249263
else:
@@ -272,6 +286,7 @@ def download_file_from_url(url, path_to_file, if_exists='replace', max_retries=5
272286
bar_format=bar_format,
273287
colour=colour,
274288
validate=validate,
289+
print_wrap_limit=print_wrap_limit,
275290
**kwargs
276291
)
277292

pyhelpers/store/utils.py

Lines changed: 58 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,49 @@
1010
from .._cache import _add_slashes, _check_relative_pathname
1111

1212

13+
def _print_wrapped_string(message, filename, end, print_wrap_limit=100):
14+
"""
15+
Prints a string, splitting it into two lines if it exceeds the threshold, with the second half
16+
being indented based on the initial indentation level.
17+
18+
:param message: The string to print.
19+
:type message: str
20+
:param filename: The filename to be included in the string.
21+
:type filename: str
22+
:param end:
23+
:param print_wrap_limit: The maximum length before splitting.
24+
:type print_wrap_limit: int | None
25+
"""
26+
27+
if print_wrap_limit:
28+
stripped_text = message.lstrip() # Remove leading spaces for accurate tab count
29+
leading_tabs = len(message) - len(stripped_text)
30+
31+
if len(stripped_text) <= print_wrap_limit:
32+
print(message, end=end)
33+
34+
else:
35+
# Find a suitable split point (preferably at a space)
36+
split_index = message.find(f'"{filename}"') + len(f'"{filename}"')
37+
if message[split_index] == " ":
38+
split_index += 1 # Move to space if it exists
39+
40+
first_part = message[:split_index].rstrip() # Trim trailing spaces
41+
second_part = message[split_index:].lstrip() # Trim leading spaces
42+
43+
# Print first part as is
44+
print(first_part + " ... ")
45+
46+
# Print second part with increased indentation (original + 1 extra tab)
47+
print("\t" * (leading_tabs + 1) + second_part, end=end)
48+
49+
else:
50+
print(message, end=end)
51+
52+
1353
def _check_saving_path(path_to_file, verbose=False, print_prefix="", state_verb="Saving",
14-
state_prep="to", print_suffix="", print_end=" ... ", belated=False,
15-
ret_info=False):
54+
state_prep="to", print_suffix="", print_end=" ... ", print_wrap_limit=None,
55+
belated=False, ret_info=False):
1656
# noinspection PyShadowingNames
1757
"""
1858
Verifies a specified file path before saving.
@@ -32,6 +72,11 @@ def _check_saving_path(path_to_file, verbose=False, print_prefix="", state_verb=
3272
:type print_suffix: str
3373
:param print_end: String passed to the ``end`` parameter of ``print``; defaults to ``" ... "``.
3474
:type print_end: str
75+
:param print_wrap_limit: Maximum length of the string before splitting into two lines;
76+
defaults to ``None``, which disables splitting. If the string exceeds this value,
77+
e.g. ``100``, it will be split at (before) ``state_prep`` to improve readability
78+
when printed.
79+
:type print_wrap_limit: int | None
3580
:param ret_info: Whether to return file path information; defaults to ``False``.
3681
:type ret_info: bool
3782
:return: A tuple containing the relative path and, if ``ret_info=True``, the filename.
@@ -52,6 +97,9 @@ def _check_saving_path(path_to_file, verbose=False, print_prefix="", state_verb=
5297
>>> path_to_file = cd("tests", "documents", "pyhelpers.pdf")
5398
>>> _check_saving_path(path_to_file, verbose=True); print("Passed.")
5499
Saving "pyhelpers.pdf" in "./tests/documents/" ... Passed.
100+
>>> _check_saving_path(path_to_file, verbose=True, print_wrap_limit=10); print("Passed.")
101+
Updating "pyhelpers.pdf" ...
102+
in "./tests/documents/" ... Passed.
55103
>>> path_to_file = "C:\\Windows\\pyhelpers.pdf"
56104
>>> _check_saving_path(path_to_file, verbose=True); print("Passed.")
57105
Saving "pyhelpers.pdf" to "C:/Windows/" ... Passed.
@@ -90,12 +138,15 @@ def _check_saving_path(path_to_file, verbose=False, print_prefix="", state_verb=
90138

91139
if (rel_dir_path == rel_dir_path.parent or rel_dir_path == abs_path_to_file.parent) and (
92140
rel_dir_path.absolute().drive == pathlib.Path.cwd().drive):
93-
msg = f'{print_prefix}{state_verb} "{filename}"{print_suffix}'
94-
print(msg, end=end)
141+
message = f'{print_prefix}{state_verb} "{filename}"{print_suffix}'
142+
print(message, end=end)
143+
95144
else:
96-
msg = (f'{print_prefix}{state_verb} "{filename}" '
97-
f'{state_prep} {_add_slashes(rel_dir_path)}{print_suffix}')
98-
print(msg, end=end)
145+
message = (f'{print_prefix}{state_verb} "{filename}" '
146+
f'{state_prep} {_add_slashes(rel_dir_path)}{print_suffix}')
147+
148+
_print_wrapped_string(
149+
message=message, filename=filename, end=end, print_wrap_limit=print_wrap_limit)
99150

100151
if ret_info:
101152
return rel_dir_path, filename

0 commit comments

Comments
 (0)