_path.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. from __future__ import annotations
  2. import contextlib
  3. import os
  4. import sys
  5. from typing import TYPE_CHECKING, TypeVar, Union
  6. from more_itertools import unique_everseen
  7. if TYPE_CHECKING:
  8. from typing_extensions import TypeAlias
  9. StrPath: TypeAlias = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath
  10. StrPathT = TypeVar("StrPathT", bound=Union[str, os.PathLike[str]])
  11. def ensure_directory(path):
  12. """Ensure that the parent directory of `path` exists"""
  13. dirname = os.path.dirname(path)
  14. os.makedirs(dirname, exist_ok=True)
  15. def same_path(p1: StrPath, p2: StrPath) -> bool:
  16. """Differs from os.path.samefile because it does not require paths to exist.
  17. Purely string based (no comparison between i-nodes).
  18. >>> same_path("a/b", "./a/b")
  19. True
  20. >>> same_path("a/b", "a/./b")
  21. True
  22. >>> same_path("a/b", "././a/b")
  23. True
  24. >>> same_path("a/b", "./a/b/c/..")
  25. True
  26. >>> same_path("a/b", "../a/b/c")
  27. False
  28. >>> same_path("a", "a/b")
  29. False
  30. """
  31. return normpath(p1) == normpath(p2)
  32. def _cygwin_patch(filename: StrPath): # pragma: nocover
  33. """
  34. Contrary to POSIX 2008, on Cygwin, getcwd (3) contains
  35. symlink components. Using
  36. os.path.abspath() works around this limitation. A fix in os.getcwd()
  37. would probably better, in Cygwin even more so, except
  38. that this seems to be by design...
  39. """
  40. return os.path.abspath(filename) if sys.platform == 'cygwin' else filename
  41. def normpath(filename: StrPath) -> str:
  42. """Normalize a file/dir name for comparison purposes."""
  43. return os.path.normcase(os.path.realpath(os.path.normpath(_cygwin_patch(filename))))
  44. @contextlib.contextmanager
  45. def paths_on_pythonpath(paths):
  46. """
  47. Add the indicated paths to the head of the PYTHONPATH environment
  48. variable so that subprocesses will also see the packages at
  49. these paths.
  50. Do this in a context that restores the value on exit.
  51. >>> getfixture('monkeypatch').setenv('PYTHONPATH', 'anything')
  52. >>> with paths_on_pythonpath(['foo', 'bar']):
  53. ... assert 'foo' in os.environ['PYTHONPATH']
  54. ... assert 'anything' in os.environ['PYTHONPATH']
  55. >>> os.environ['PYTHONPATH']
  56. 'anything'
  57. >>> getfixture('monkeypatch').delenv('PYTHONPATH')
  58. >>> with paths_on_pythonpath(['foo', 'bar']):
  59. ... assert 'foo' in os.environ['PYTHONPATH']
  60. >>> os.environ.get('PYTHONPATH')
  61. """
  62. nothing = object()
  63. orig_pythonpath = os.environ.get('PYTHONPATH', nothing)
  64. current_pythonpath = os.environ.get('PYTHONPATH', '')
  65. try:
  66. prefix = os.pathsep.join(unique_everseen(paths))
  67. to_join = filter(None, [prefix, current_pythonpath])
  68. new_path = os.pathsep.join(to_join)
  69. if new_path:
  70. os.environ['PYTHONPATH'] = new_path
  71. yield
  72. finally:
  73. if orig_pythonpath is nothing:
  74. os.environ.pop('PYTHONPATH', None)
  75. else:
  76. os.environ['PYTHONPATH'] = orig_pythonpath