diff --git a/Doc/library/pathlib.rst b/Doc/library/pathlib.rst --- a/Doc/library/pathlib.rst +++ b/Doc/library/pathlib.rst @@ -420,16 +420,33 @@ Pure paths provide the following methods >>> p = PurePosixPath('/etc/passwd') >>> p.as_uri() 'file:///etc/passwd' >>> p = PureWindowsPath('c:/Windows') >>> p.as_uri() 'file:///c:/Windows' +.. method:: PurePath.expandvars([*, env]) + + Return a version of the path with environment variables expanded. + Equivalent to :meth:`os.path.expandvars`. + + To override environment variables, use the *env* keyword-only argument. + If *env* is not a :term:`mapping` object, :exc:`TypeError` is raised:: + + >>> p.PurePosixPath('$HOME/life-of-brian') + >>> p.expandvars() + >>> PurePosixPath('/home/ericidle/life-of-brian')' + >>> p.expandvars(env={'HOME': '/home/grahamchapman'}) + >>> PurePosixPath('/home/grahamchapman/life-of-brian') + + .. versionadded:: 3.5 + + .. method:: PurePath.is_absolute() Return whether the path is absolute or not. A path is considered absolute if it has both a root and (if the flavour allows) a drive:: >>> PurePosixPath('/a/b').is_absolute() True >>> PurePosixPath('a/b').is_absolute() diff --git a/Lib/ntpath.py b/Lib/ntpath.py --- a/Lib/ntpath.py +++ b/Lib/ntpath.py @@ -359,17 +359,17 @@ def expanduser(path): # - ${varname} is accepted. # - $varname is accepted. # - %varname% is accepted. # - varnames can be made out of letters, digits and the characters '_-' # (though is not verified in the ${varname} and %varname% cases) # XXX With COMMAND.COM you can use any characters in a variable name, # XXX except '^|<>='. -def expandvars(path): +def expandvars(path, _env=None): """Expand shell variables of the forms $var, ${var} and %var%. Unknown variables are left unchanged.""" if isinstance(path, bytes): if ord('$') not in path and ord('%') not in path: return path import string varchars = bytes(string.ascii_letters + string.digits + '_-', 'ascii') @@ -383,16 +383,18 @@ def expandvars(path): return path import string varchars = string.ascii_letters + string.digits + '_-' quote = '\'' percent = '%' brace = '{' dollar = '$' environ = os.environ + if _env is not None and isinstance(_env, dict): + environ = _env res = path[:0] index = 0 pathlen = len(path) while index < pathlen: c = path[index:index+1] if c == quote: # no expansion within single quotes path = path[index + 1:] pathlen = len(path) diff --git a/Lib/pathlib.py b/Lib/pathlib.py --- a/Lib/pathlib.py +++ b/Lib/pathlib.py @@ -799,16 +799,24 @@ class PurePath(object): cf = self._flavour.casefold_parts if (root or drv) if n == 0 else cf(abs_parts[:n]) != cf(to_abs_parts): formatted = self._format_parsed_parts(to_drv, to_root, to_parts) raise ValueError("{!r} does not start with {!r}" .format(str(self), str(formatted))) return self._from_parsed_parts('', root if n == 1 else '', abs_parts[n:]) + def expandvars(self, *, env=None): + if env is not None: + if not isinstance(env, dict): + msg = "'env' should be a dict, not '%s'" + raise TypeError(msg % type(env).__name__) + return self.__class__(os.path.expandvars(str(self), _env=env)) + return self.__class__(os.path.expandvars(str(self))) + @property def parts(self): """An object providing sequence-like access to the components in the filesystem path.""" # We cache the tuple to avoid building a new one each time .parts # is accessed. XXX is this necessary? try: return self._pparts diff --git a/Lib/posixpath.py b/Lib/posixpath.py --- a/Lib/posixpath.py +++ b/Lib/posixpath.py @@ -261,17 +261,17 @@ def expanduser(path): # Expand paths containing shell variable substitutions. # This expands the forms $variable and ${variable} only. # Non-existent variables are left unchanged. _varprog = None _varprogb = None -def expandvars(path): +def expandvars(path, _env=None): """Expand shell variables of form $var and ${var}. Unknown variables are left unchanged.""" global _varprog, _varprogb if isinstance(path, bytes): if b'$' not in path: return path if not _varprogb: import re @@ -285,16 +285,18 @@ def expandvars(path): return path if not _varprog: import re _varprog = re.compile(r'\$(\w+|\{[^}]*\})', re.ASCII) search = _varprog.search start = '{' end = '}' environ = os.environ + if _env is not None and isinstance(_env, dict): + environ = _env i = 0 while True: m = search(path, i) if not m: break i, j = m.span(0) name = m.group(1) if name.startswith(start) and name.endswith(end): diff --git a/Lib/test/test_pathlib.py b/Lib/test/test_pathlib.py --- a/Lib/test/test_pathlib.py +++ b/Lib/test/test_pathlib.py @@ -675,16 +675,39 @@ class PurePosixPathTest(_BasePurePathTes p = P('//a') pp = p / 'b' self.assertEqual(pp, P('//a/b')) pp = P('/a') / '//c' self.assertEqual(pp, P('//c')) pp = P('//a') / '/c' self.assertEqual(pp, P('/c')) + def test_expandvars(self): + P = self.cls + p = P('$HOME/life-of-brian') + with support.EnvironmentVarGuard() as env: + env['HOME'] = '/home/ericidle' + self.assertEqual(str(p), '$HOME/life-of-brian') + self.assertEqual(str(p.expandvars()), '/home/ericidle/life-of-brian') + + def test_expandvars_override_env(self): + P = self.cls + p = P('$HOME/life-of-brian') + self.assertEqual(str(p), '$HOME/life-of-brian') + self.assertEqual(str(p.expandvars(env={'HOME': '/home/grahamchapman'})), + '/home/grahamchapman/life-of-brian') + + def test_expandvars_override_env_typerror(self): + P = self.cls + p = P('$HOME/life-of-brian') + with self.assertRaises(TypeError) as cm: + p.expandvars(env=['invalid', 'env']) + self.assertEqual(str(cm.exception), + "'env' should be a dict, not 'list'") + class PureWindowsPathTest(_BasePurePathTest, unittest.TestCase): cls = pathlib.PureWindowsPath equivalences = _BasePurePathTest.equivalences.copy() equivalences.update({ 'c:a': [ ('c:', 'a'), ('c:', 'a/'), ('/', 'c:', 'a') ], 'c:/a': [