|
11 | 11 | import sys
|
12 | 12 | import warnings
|
13 | 13 | from contextlib import contextmanager
|
| 14 | +from pathlib import Path |
14 | 15 | from typing import Any, Generator, NewType, Sequence
|
15 | 16 | from urllib.parse import (
|
16 | 17 | SplitResult,
|
@@ -338,81 +339,65 @@ def is_namespace_package(namespace: str) -> bool | None:
|
338 | 339 | return isinstance(spec.submodule_search_locations, _NamespacePath)
|
339 | 340 |
|
340 | 341 |
|
341 |
| -def filefind(filename: str, path_dirs: Sequence[str] | str | None = None) -> str: |
| 342 | +def filefind(filename: str, path_dirs: Sequence[str]) -> str: |
342 | 343 | """Find a file by looking through a sequence of paths.
|
343 |
| - This iterates through a sequence of paths looking for a file and returns |
344 |
| - the full, absolute path of the first occurrence of the file. If no set of |
345 |
| - path dirs is given, the filename is tested as is, after running through |
346 |
| - :func:`expandvars` and :func:`expanduser`. Thus a simple call:: |
347 | 344 |
|
348 |
| - filefind("myfile.txt") |
| 345 | + For use in FileFindHandler. |
349 | 346 |
|
350 |
| - will find the file in the current working dir, but:: |
| 347 | + Iterates through a sequence of paths looking for a file and returns |
| 348 | + the full, absolute path of the first occurrence of the file. |
351 | 349 |
|
352 |
| - filefind("~/myfile.txt") |
| 350 | + Absolute paths are not accepted for inputs. |
353 | 351 |
|
354 |
| - Will find the file in the users home directory. This function does not |
355 |
| - automatically try any paths, such as the cwd or the user's home directory. |
| 352 | + This function does not automatically try any paths, |
| 353 | + such as the cwd or the user's home directory. |
356 | 354 |
|
357 | 355 | Parameters
|
358 | 356 | ----------
|
359 | 357 | filename : str
|
360 |
| - The filename to look for. |
361 |
| - path_dirs : str, None or sequence of str |
362 |
| - The sequence of paths to look for the file in. If None, the filename |
363 |
| - need to be absolute or be in the cwd. If a string, the string is |
364 |
| - put into a sequence and the searched. If a sequence, walk through |
365 |
| - each element and join with ``filename``, calling :func:`expandvars` |
366 |
| - and :func:`expanduser` before testing for existence. |
| 358 | + The filename to look for. Must be a relative path. |
| 359 | + path_dirs : sequence of str |
| 360 | + The sequence of paths to look in for the file. |
| 361 | + Walk through each element and join with ``filename``. |
| 362 | + Only after ensuring the path resolves within the directory is it checked for existence. |
367 | 363 |
|
368 | 364 | Returns
|
369 | 365 | -------
|
370 |
| - Raises :exc:`IOError` or returns absolute path to file. |
| 366 | + Raises :exc:`OSError` or returns absolute path to file. |
371 | 367 | """
|
372 |
| - |
373 |
| - # If paths are quoted, abspath gets confused, strip them... |
374 |
| - filename = filename.strip('"').strip("'") |
375 |
| - # If the input is an absolute path, just check it exists |
376 |
| - if os.path.isabs(filename) and os.path.isfile(filename): |
377 |
| - return filename |
378 |
| - |
379 |
| - if path_dirs is None: |
380 |
| - path_dirs = ("",) |
381 |
| - elif isinstance(path_dirs, str): |
382 |
| - path_dirs = (path_dirs,) |
383 |
| - |
384 |
| - for path in path_dirs: |
385 |
| - if path == ".": |
386 |
| - path = os.getcwd() # noqa: PLW2901 |
387 |
| - testname = expand_path(os.path.join(path, filename)) |
388 |
| - if os.path.isfile(testname): |
389 |
| - return os.path.abspath(testname) |
| 368 | + file_path = Path(filename) |
| 369 | + |
| 370 | + # If the input is an absolute path, reject it |
| 371 | + if file_path.is_absolute(): |
| 372 | + msg = f"{filename} is absolute, filefind only accepts relative paths." |
| 373 | + raise OSError(msg) |
| 374 | + |
| 375 | + for path_str in path_dirs: |
| 376 | + path = Path(path_str).absolute() |
| 377 | + test_path = path / file_path |
| 378 | + # os.path.abspath resolves '..', but Path.absolute() doesn't |
| 379 | + # Path.resolve() does, but traverses symlinks, which we don't want |
| 380 | + test_path = Path(os.path.abspath(test_path)) |
| 381 | + if sys.version_info >= (3, 9): |
| 382 | + if not test_path.is_relative_to(path): |
| 383 | + # points outside root, e.g. via `filename='../foo'` |
| 384 | + continue |
| 385 | + else: |
| 386 | + # is_relative_to is new in 3.9 |
| 387 | + try: |
| 388 | + test_path.relative_to(path) |
| 389 | + except ValueError: |
| 390 | + # points outside root, e.g. via `filename='../foo'` |
| 391 | + continue |
| 392 | + # make sure we don't call is_file before we know it's a file within a prefix |
| 393 | + # GHSA-hrw6-wg82-cm62 - can leak password hash on windows. |
| 394 | + if test_path.is_file(): |
| 395 | + return os.path.abspath(test_path) |
390 | 396 |
|
391 | 397 | msg = f"File {filename!r} does not exist in any of the search paths: {path_dirs!r}"
|
392 | 398 | raise OSError(msg)
|
393 | 399 |
|
394 | 400 |
|
395 |
| -def expand_path(s: str) -> str: |
396 |
| - """Expand $VARS and ~names in a string, like a shell |
397 |
| -
|
398 |
| - :Examples: |
399 |
| - In [2]: os.environ['FOO']='test' |
400 |
| - In [3]: expand_path('variable FOO is $FOO') |
401 |
| - Out[3]: 'variable FOO is test' |
402 |
| - """ |
403 |
| - # This is a pretty subtle hack. When expand user is given a UNC path |
404 |
| - # on Windows (\\server\share$\%username%), os.path.expandvars, removes |
405 |
| - # the $ to get (\\server\share\%username%). I think it considered $ |
406 |
| - # alone an empty var. But, we need the $ to remains there (it indicates |
407 |
| - # a hidden share). |
408 |
| - if os.name == "nt": |
409 |
| - s = s.replace("$\\", "IPYTHON_TEMP") |
410 |
| - s = os.path.expandvars(os.path.expanduser(s)) |
411 |
| - if os.name == "nt": |
412 |
| - s = s.replace("IPYTHON_TEMP", "$\\") |
413 |
| - return s |
414 |
| - |
415 |
| - |
416 | 401 | def import_item(name: str) -> Any:
|
417 | 402 | """Import and return ``bar`` given the string ``foo.bar``.
|
418 | 403 | Calling ``bar = import_item("foo.bar")`` is the functional equivalent of
|
|
0 commit comments