codekingpro/portable-devtools
115k
1from __future__ import annotations2 3import contextlib4import os5import sys6from typing import TYPE_CHECKING, TypeVar, Union7 8from more_itertools import unique_everseen9 10if TYPE_CHECKING:11 from typing_extensions import TypeAlias12 13StrPath: TypeAlias = Union[str, os.PathLike[str]] # Same as _typeshed.StrPath14StrPathT = TypeVar("StrPathT", bound=Union[str, os.PathLike[str]])15 16 17def ensure_directory(path):18 """Ensure that the parent directory of `path` exists"""19 dirname = os.path.dirname(path)20 os.makedirs(dirname, exist_ok=True)21 22 23def same_path(p1: StrPath, p2: StrPath) -> bool:24 """Differs from os.path.samefile because it does not require paths to exist.25 Purely string based (no comparison between i-nodes).26 >>> same_path("a/b", "./a/b")27 True28 >>> same_path("a/b", "a/./b")29 True30 >>> same_path("a/b", "././a/b")31 True32 >>> same_path("a/b", "./a/b/c/..")33 True34 >>> same_path("a/b", "../a/b/c")35 False36 >>> same_path("a", "a/b")37 False38 """39 return normpath(p1) == normpath(p2)40 41 42def _cygwin_patch(filename: StrPath): # pragma: nocover43 """44 Contrary to POSIX 2008, on Cygwin, getcwd (3) contains45 symlink components. Using46 os.path.abspath() works around this limitation. A fix in os.getcwd()47 would probably better, in Cygwin even more so, except48 that this seems to be by design...49 """50 return os.path.abspath(filename) if sys.platform == 'cygwin' else filename51 52 53def normpath(filename: StrPath) -> str:54 """Normalize a file/dir name for comparison purposes."""55 return os.path.normcase(os.path.realpath(os.path.normpath(_cygwin_patch(filename))))56 57 58@contextlib.contextmanager59def paths_on_pythonpath(paths):60 """61 Add the indicated paths to the head of the PYTHONPATH environment62 variable so that subprocesses will also see the packages at63 these paths.64 65 Do this in a context that restores the value on exit.66 67 >>> getfixture('monkeypatch').setenv('PYTHONPATH', 'anything')68 >>> with paths_on_pythonpath(['foo', 'bar']):69 ... assert 'foo' in os.environ['PYTHONPATH']70 ... assert 'anything' in os.environ['PYTHONPATH']71 >>> os.environ['PYTHONPATH']72 'anything'73 74 >>> getfixture('monkeypatch').delenv('PYTHONPATH')75 >>> with paths_on_pythonpath(['foo', 'bar']):76 ... assert 'foo' in os.environ['PYTHONPATH']77 >>> os.environ.get('PYTHONPATH')78 """79 nothing = object()80 orig_pythonpath = os.environ.get('PYTHONPATH', nothing)81 current_pythonpath = os.environ.get('PYTHONPATH', '')82 try:83 prefix = os.pathsep.join(unique_everseen(paths))84 to_join = filter(None, [prefix, current_pythonpath])85 new_path = os.pathsep.join(to_join)86 if new_path:87 os.environ['PYTHONPATH'] = new_path88 yield89 finally:90 if orig_pythonpath is nothing:91 os.environ.pop('PYTHONPATH', None)92 else:93 os.environ['PYTHONPATH'] = orig_pythonpath94 