[path] Remove a lot of magic from the path module.

This makes all the available methods show up in the documentation and
removes some magic double-underscore methods.

This also removes refines expanduser a bit; We've only use the "~"
formulation to get the current user's homedir, not any other users'
homedir.

Will remove `expanduser` in favor of api.path['home'] in downstream
repos as a followup.

R=yiwzhang@google.com

Change-Id: I6edd0ae056ce1ee5c90de25f65c7af07e1a98f26
Reviewed-on: https://chromium-review.googlesource.com/c/infra/luci/recipes-py/+/2406432
Commit-Queue: Robbie Iannucci <iannucci@chromium.org>
Auto-Submit: Robbie Iannucci <iannucci@chromium.org>
Reviewed-by: Yiwei Zhang <yiwzhang@google.com>
diff --git a/README.recipes.md b/README.recipes.md
index 31837da..5ec0e3e 100644
--- a/README.recipes.md
+++ b/README.recipes.md
@@ -2027,14 +2027,14 @@
 `depot_tools/infra_paths` module). Refer to those modules for additional
 documentation.
 
-#### **class [PathApi](/recipe_modules/path/api.py#218)([RecipeApi](/recipe_engine/recipe_api.py#878)):**
+#### **class [PathApi](/recipe_modules/path/api.py#206)([RecipeApi](/recipe_engine/recipe_api.py#878)):**
 
-&mdash; **def [\_\_getitem\_\_](/recipe_modules/path/api.py#475)(self, name):**
+&mdash; **def [\_\_getitem\_\_](/recipe_modules/path/api.py#438)(self, name):**
 
 Gets the base path named `name`. See module docstring for more
 information.
 
-&mdash; **def [abs\_to\_path](/recipe_modules/path/api.py#405)(self, abs_string_path):**
+&mdash; **def [abs\_to\_path](/recipe_modules/path/api.py#368)(self, abs_string_path):**
 
 Converts an absolute path string `string_path` to a real Path object,
 using the most appropriate known base path.
@@ -2060,27 +2060,66 @@
 Raises an ValueError if the preconditions are not met, otherwise returns the
 Path object.
 
-&mdash; **def [assert\_absolute](/recipe_modules/path/api.py#345)(self, path):**
+&mdash; **def [abspath](/recipe_modules/path/api.py#461)(self, path):**
+
+Equivalent to os.path.abspath.
+
+&mdash; **def [assert\_absolute](/recipe_modules/path/api.py#307)(self, path):**
 
 Raises AssertionError if the given path is not an absolute path.
 
 Args:
   * path (Path|str) - The path to check.
 
-&mdash; **def [get](/recipe_modules/path/api.py#468)(self, name, default=None):**
+&mdash; **def [basename](/recipe_modules/path/api.py#465)(self, path):**
+
+Equivalent to os.path.basename.
+
+&mdash; **def [dirname](/recipe_modules/path/api.py#469)(self, path):**
+
+Equivalent to os.path.dirname.
+
+&mdash; **def [exists](/recipe_modules/path/api.py#517)(self, path):**
+
+Equivalent to os.path.exists.
+
+The presence or absence of paths can be mocked during the execution of the
+recipe by using the mock_* methods.
+
+&mdash; **def [expanduser](/recipe_modules/path/api.py#508)(self, path):**
+
+Do not use this, use `api.path['home']` instead.
+
+This ONLY handles `path` == "~", and returns `str(api.path['home'])`.
+
+&mdash; **def [get](/recipe_modules/path/api.py#431)(self, name, default=None):**
 
 Gets the base path named `name`. See module docstring for more
 information.
 
-&mdash; **def [get\_config\_defaults](/recipe_modules/path/api.py#230)(self):**
+&mdash; **def [get\_config\_defaults](/recipe_modules/path/api.py#209)(self):**
 
 Internal recipe implementation function.
 
-&mdash; **def [initialize](/recipe_modules/path/api.py#290)(self):**
+&mdash; **def [initialize](/recipe_modules/path/api.py#271)(self):**
 
 Internal recipe implementation function.
 
-&mdash; **def [mkdtemp](/recipe_modules/path/api.py#353)(self, prefix=tempfile.template):**
+&mdash; **def [join](/recipe_modules/path/api.py#473)(self, path, \*paths):**
+
+Equivalent to os.path.join.
+
+Note that Path objects returned from this module (e.g.
+api.path['start_dir']) have a built-in join method (e.g.
+new_path = p.join('some', 'name')). Many recipe modules expect Path objects
+rather than strings. Using this `join` method gives you raw path joining
+functionality and returns a string.
+
+If your path is rooted in one of the path module's root paths (i.e. those
+retrieved with api.path[something]), then you can convert from a string path
+back to a Path with the `abs_to_path` method.
+
+&mdash; **def [mkdtemp](/recipe_modules/path/api.py#316)(self, prefix=tempfile.template):**
 
 Makes a new temporary directory, returns Path to it.
 
@@ -2090,7 +2129,7 @@
 
 Returns a Path to the new directory.
 
-&mdash; **def [mkstemp](/recipe_modules/path/api.py#378)(self, prefix=tempfile.template):**
+&mdash; **def [mkstemp](/recipe_modules/path/api.py#341)(self, prefix=tempfile.template):**
 
 Makes a new temporary file, returns Path to it.
 
@@ -2101,15 +2140,15 @@
 Returns a Path to the new file. Unlike tempfile.mkstemp, the file's file
 descriptor is closed.
 
-&mdash; **def [mock\_add\_paths](/recipe_modules/path/api.py#324)(self, path):**
+&mdash; **def [mock\_add\_paths](/recipe_modules/path/api.py#525)(self, path):**
 
 For testing purposes, mark that |path| exists.
 
-&mdash; **def [mock\_copy\_paths](/recipe_modules/path/api.py#329)(self, source, dest):**
+&mdash; **def [mock\_copy\_paths](/recipe_modules/path/api.py#530)(self, source, dest):**
 
 For testing purposes, copy |source| to |dest|.
 
-&mdash; **def [mock\_remove\_paths](/recipe_modules/path/api.py#334)(self, path, filt=(lambda p: True)):**
+&mdash; **def [mock\_remove\_paths](/recipe_modules/path/api.py#535)(self, path, filt=(lambda p: True)):**
 
 For testing purposes, assert that |path| doesn't exist.
 
@@ -2117,6 +2156,37 @@
   * path (str|Path) - The path to remove.
   * filt (func[str] bool) - Called for every candidate path. Return
     True to remove this path.
+
+&emsp; **@property**<br>&mdash; **def [pardir](/recipe_modules/path/api.py#446)(self):**
+
+Equivalent to os.path.pardir.
+
+&emsp; **@property**<br>&mdash; **def [pathsep](/recipe_modules/path/api.py#456)(self):**
+
+Equivalent to os.path.pathsep.
+
+&mdash; **def [realpath](/recipe_modules/path/api.py#496)(self, path):**
+
+Equivalent to os.path.realpath.
+
+&mdash; **def [relpath](/recipe_modules/path/api.py#500)(self, path, start):**
+
+Roughly equivalent to os.path.relpath.
+
+Unlike os.path.relpath, `start` is _required_. If you want the 'current
+directory', use the `recipe_engine/context` module's `cwd` property.
+
+&emsp; **@property**<br>&mdash; **def [sep](/recipe_modules/path/api.py#451)(self):**
+
+Equivalent to os.path.sep.
+
+&mdash; **def [split](/recipe_modules/path/api.py#488)(self, path):**
+
+Equivalent to os.path.split.
+
+&mdash; **def [splitext](/recipe_modules/path/api.py#492)(self, path):**
+
+Equivalent to os.path.splitext.
 ### *recipe_modules* / [platform](/recipe_modules/platform)
 
 [DEPS](/recipe_modules/platform/__init__.py#5): [version](#recipe_modules-version)
diff --git a/recipe_modules/path/api.py b/recipe_modules/path/api.py
index 5204a1a..8001d59 100644
--- a/recipe_modules/path/api.py
+++ b/recipe_modules/path/api.py
@@ -58,15 +58,6 @@
   return PathToString_inner
 
 
-def string_filter(func):
-
-  @functools.wraps(func)
-  def inner(*args, **kwargs):
-    return func(*map(str, args), **{k: str(v) for k, v in kwargs.iteritems()})
-
-  return inner
-
-
 class path_set(object):
   """ implements a set which contains all the parents folders of added folders.
   """
@@ -78,7 +69,7 @@
     self._initial_paths = set(initial_paths)
     self._paths = set()
 
-  def _initialize(self):
+  def _initialize(self):  # pylint: disable=method-hidden
     self._initialize = lambda: None
     for path in self._initial_paths:
       self.add(path)
@@ -125,7 +116,7 @@
         if self._is_contained_in(p, path, match_root) and filt(p))
     self._paths -= kill_set
 
-  def contains(self, path):
+  def contains(self, path):  # pylint: disable=method-hidden
     self._initialize()
     return self.contains(path)
 
@@ -211,22 +202,10 @@
     """Returns the canonical version of the path."""
     return self.normpath(path)
 
-  def expanduser(self, path):
-    return path.replace('~', '[HOME]')
-
 
 class PathApi(recipe_api.RecipeApi):
   _paths_client = recipe_api.RequireClient('paths')
 
-  # Attribute accesses that we pass through to our "_path_mod" module.
-  OK_ATTRS = ('pardir', 'sep', 'pathsep')
-
-  # Because the native 'path' type in python is a str, we filter the *args
-  # of these methods to stringify them first (otherwise they would be getting
-  # recipe_util_types.Path instances).
-  FILTER_METHODS = ('abspath', 'basename', 'dirname', 'exists', 'expanduser',
-                    'join', 'split', 'splitext', 'realpath', 'relpath')
-
   def get_config_defaults(self):
     """Internal recipe implementation function."""
     return {
@@ -236,6 +215,7 @@
         'TEMP_DIR': self._temp_dir,
         'CACHE_DIR': self._cache_dir,
         'CLEANUP_DIR': self._cleanup_dir,
+        'HOME_DIR': self._home_dir,
     }
 
   def __init__(self, path_properties, **kwargs):
@@ -247,10 +227,11 @@
 
     # Assigned at "initialize".
     self._path_mod = None  # NT or POSIX path module, or "os.path" in prod.
-    self._start_dir = None
+    self._startup_cwd = None
     self._temp_dir = None
     self._cache_dir = None
     self._cleanup_dir = None
+    self._home_dir = None
 
     # Used in mkdtemp when generating and checking expectations.
     self._test_counter = 0
@@ -293,6 +274,7 @@
       self._path_mod = os.path
       start_dir = self._paths_client.start_dir
       self._startup_cwd = self._split_path(start_dir)
+      self._home_dir = self._split_path(self._path_mod.expanduser('~'))
 
       tmp_dir = self._read_path('temp_dir', tempfile.gettempdir())
       self._ensure_dir(tmp_dir)
@@ -318,37 +300,18 @@
       self._temp_dir = [root]
       self._cache_dir = [root, 'b', 'c']
       self._cleanup_dir = [root, 'b', 'cleanup']
+      self._home_dir = [root, 'home', 'fake_user']
 
     self.set_config('BASE')
 
-  def mock_add_paths(self, path):
-    """For testing purposes, mark that |path| exists."""
-    if self._test_data.enabled:
-      self._path_mod.mock_add_paths(path)
-
-  def mock_copy_paths(self, source, dest):
-    """For testing purposes, copy |source| to |dest|."""
-    if self._test_data.enabled:
-      self._path_mod.mock_copy_paths(source, dest)
-
-  def mock_remove_paths(self, path, filt=lambda p: True):
-    """For testing purposes, assert that |path| doesn't exist.
-
-    Args:
-      * path (str|Path) - The path to remove.
-      * filt (func[str] bool) - Called for every candidate path. Return
-        True to remove this path.
-    """
-    if self._test_data.enabled:
-      self._path_mod.mock_remove_paths(path, filt)
-
   def assert_absolute(self, path):
     """Raises AssertionError if the given path is not an absolute path.
 
     Args:
       * path (Path|str) - The path to check.
     """
-    assert self.abspath(path) == str(path), '%s is not absolute' % path
+    if self.abspath(path) != str(path):
+      raise AssertionError('%s is not absolute' % path)
 
   def mkdtemp(self, prefix=tempfile.template):
     """Makes a new temporary directory, returns Path to it.
@@ -480,15 +443,102 @@
       raise KeyError('Unknown path: %s' % name)
     return result
 
-  def __getattr__(self, name):
-    # retrieve os.path attributes
-    if name in self.OK_ATTRS:
-      return getattr(self._path_mod, name)
-    if name in self.FILTER_METHODS:
-      return string_filter(getattr(self._path_mod, name))
-    raise AttributeError("'%s' object has no attribute '%s'" %
-                         (self._path_mod, name))  # pragma: no cover
+  @property
+  def pardir(self):
+    """Equivalent to os.path.pardir."""
+    return self._path_mod.pardir
 
-  def __dir__(self):  # pragma: no cover
-    # Used for helping out show_me_the_modules.py
-    return self.__dict__.keys() + list(self.OK_ATTRS + self.FILTER_METHODS)
+  @property
+  def sep(self):
+    """Equivalent to os.path.sep."""
+    return self._path_mod.sep
+
+  @property
+  def pathsep(self):
+    """Equivalent to os.path.pathsep."""
+    return self._path_mod.pathsep
+
+  def abspath(self, path):
+    """Equivalent to os.path.abspath."""
+    return self._path_mod.abspath(str(path))
+
+  def basename(self, path):
+    """Equivalent to os.path.basename."""
+    return self._path_mod.basename(str(path))
+
+  def dirname(self, path):
+    """Equivalent to os.path.dirname."""
+    return self._path_mod.dirname(str(path))
+
+  def join(self, path, *paths):
+    """Equivalent to os.path.join.
+
+    Note that Path objects returned from this module (e.g.
+    api.path['start_dir']) have a built-in join method (e.g.
+    new_path = p.join('some', 'name')). Many recipe modules expect Path objects
+    rather than strings. Using this `join` method gives you raw path joining
+    functionality and returns a string.
+
+    If your path is rooted in one of the path module's root paths (i.e. those
+    retrieved with api.path[something]), then you can convert from a string path
+    back to a Path with the `abs_to_path` method.
+    """
+    return self._path_mod.join(str(path), *map(str, paths))
+
+  def split(self, path):
+    """Equivalent to os.path.split."""
+    return self._path_mod.split(str(path))
+
+  def splitext(self, path):
+    """Equivalent to os.path.splitext."""
+    return self._path_mod.splitext(str(path))
+
+  def realpath(self, path):
+    """Equivalent to os.path.realpath."""
+    return self._path_mod.realpath(str(path))
+
+  def relpath(self, path, start):
+    """Roughly equivalent to os.path.relpath.
+
+    Unlike os.path.relpath, `start` is _required_. If you want the 'current
+    directory', use the `recipe_engine/context` module's `cwd` property.
+    """
+    return self._path_mod.relpath(str(path), str(start))
+
+  def expanduser(self, path):  # pragma: no cover
+    """Do not use this, use `api.path['home']` instead.
+
+    This ONLY handles `path` == "~", and returns `str(api.path['home'])`.
+    """
+    if path == "~":
+      return str(self['home'])
+    raise ValueError("expanduser only supports `~`.")
+
+  def exists(self, path):
+    """Equivalent to os.path.exists.
+
+    The presence or absence of paths can be mocked during the execution of the
+    recipe by using the mock_* methods.
+    """
+    return self._path_mod.exists(str(path))
+
+  def mock_add_paths(self, path):
+    """For testing purposes, mark that |path| exists."""
+    if self._test_data.enabled:
+      self._path_mod.mock_add_paths(path)
+
+  def mock_copy_paths(self, source, dest):
+    """For testing purposes, copy |source| to |dest|."""
+    if self._test_data.enabled:
+      self._path_mod.mock_copy_paths(source, dest)
+
+  def mock_remove_paths(self, path, filt=lambda p: True):
+    """For testing purposes, assert that |path| doesn't exist.
+
+    Args:
+      * path (str|Path) - The path to remove.
+      * filt (func[str] bool) - Called for every candidate path. Return
+        True to remove this path.
+    """
+    if self._test_data.enabled:
+      self._path_mod.mock_remove_paths(path, filt)
diff --git a/recipe_modules/path/config.py b/recipe_modules/path/config.py
index ec8c43c..1122789 100644
--- a/recipe_modules/path/config.py
+++ b/recipe_modules/path/config.py
@@ -6,12 +6,13 @@
 from recipe_engine.config_types import Path
 
 
-def BaseConfig(PLATFORM, START_DIR, TEMP_DIR, CACHE_DIR, CLEANUP_DIR,
+def BaseConfig(PLATFORM, START_DIR, TEMP_DIR, CACHE_DIR, CLEANUP_DIR, HOME_DIR,
                **_kwargs):
   assert START_DIR[0].endswith(('\\', '/')), START_DIR
   assert TEMP_DIR[0].endswith(('\\', '/')), TEMP_DIR
   assert CACHE_DIR[0].endswith(('\\', '/')), CACHE_DIR
   assert CLEANUP_DIR[0].endswith(('\\', '/')), CLEANUP_DIR
+  assert HOME_DIR[0].endswith(('\\', '/')), HOME_DIR
   return ConfigGroup(
       # base path name -> [tokenized absolute path]
       base_paths=Dict(value_type=tuple),
@@ -23,6 +24,7 @@
       TEMP_DIR=Static(tuple(TEMP_DIR)),
       CACHE_DIR=Static(tuple(CACHE_DIR)),
       CLEANUP_DIR=Static(tuple(CLEANUP_DIR)),
+      HOME_DIR=Static(tuple(HOME_DIR)),
   )
 
 
@@ -35,4 +37,5 @@
   c.base_paths['tmp_base'] = c.TEMP_DIR
   c.base_paths['cache'] = c.CACHE_DIR
   c.base_paths['cleanup'] = c.CLEANUP_DIR
+  c.base_paths['home'] = c.HOME_DIR
   c.dynamic_paths['checkout'] = None
diff --git a/recipe_modules/path/examples/full.expected/linux.json b/recipe_modules/path/examples/full.expected/linux.json
index b13f069..41fafeb 100644
--- a/recipe_modules/path/examples/full.expected/linux.json
+++ b/recipe_modules/path/examples/full.expected/linux.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]/file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]/copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',), 'home': ('/', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/linux_kitchen.json b/recipe_modules/path/examples/full.expected/linux_kitchen.json
index b13f069..41fafeb 100644
--- a/recipe_modules/path/examples/full.expected/linux_kitchen.json
+++ b/recipe_modules/path/examples/full.expected/linux_kitchen.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]/file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]/copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',), 'home': ('/', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/linux_luci.json b/recipe_modules/path/examples/full.expected/linux_luci.json
index b13f069..41fafeb 100644
--- a/recipe_modules/path/examples/full.expected/linux_luci.json
+++ b/recipe_modules/path/examples/full.expected/linux_luci.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]/file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]/copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',), 'home': ('/', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/linux_swarming.json b/recipe_modules/path/examples/full.expected/linux_swarming.json
index b13f069..41fafeb 100644
--- a/recipe_modules/path/examples/full.expected/linux_swarming.json
+++ b/recipe_modules/path/examples/full.expected/linux_swarming.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]/file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]/copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',), 'home': ('/', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/mac.json b/recipe_modules/path/examples/full.expected/mac.json
index b13f069..41fafeb 100644
--- a/recipe_modules/path/examples/full.expected/mac.json
+++ b/recipe_modules/path/examples/full.expected/mac.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]/file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]/copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',), 'home': ('/', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/mac_kitchen.json b/recipe_modules/path/examples/full.expected/mac_kitchen.json
index b13f069..41fafeb 100644
--- a/recipe_modules/path/examples/full.expected/mac_kitchen.json
+++ b/recipe_modules/path/examples/full.expected/mac_kitchen.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]/file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]/copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',), 'home': ('/', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/mac_luci.json b/recipe_modules/path/examples/full.expected/mac_luci.json
index b13f069..41fafeb 100644
--- a/recipe_modules/path/examples/full.expected/mac_luci.json
+++ b/recipe_modules/path/examples/full.expected/mac_luci.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]/file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]/copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',), 'home': ('/', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/mac_swarming.json b/recipe_modules/path/examples/full.expected/mac_swarming.json
index b13f069..41fafeb 100644
--- a/recipe_modules/path/examples/full.expected/mac_swarming.json
+++ b/recipe_modules/path/examples/full.expected/mac_swarming.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]/file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]/copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('/', 'b', 'FakeTestingCWD'), 'cleanup': ('/', 'b', 'cleanup'), 'cache': ('/', 'b', 'c'), 'tmp_base': ('/',), 'home': ('/', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/win.json b/recipe_modules/path/examples/full.expected/win.json
index 2b304bf..b9fdb3b 100644
--- a/recipe_modules/path/examples/full.expected/win.json
+++ b/recipe_modules/path/examples/full.expected/win.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]\\file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]\\copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('C:\\\\', 'b', 'FakeTestingCWD'), 'cleanup': ('C:\\\\', 'b', 'cleanup'), 'cache': ('C:\\\\', 'b', 'c'), 'tmp_base': ('C:\\\\',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('C:\\\\', 'b', 'FakeTestingCWD'), 'cleanup': ('C:\\\\', 'b', 'cleanup'), 'cache': ('C:\\\\', 'b', 'c'), 'tmp_base': ('C:\\\\',), 'home': ('C:\\\\', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/win_kitchen.json b/recipe_modules/path/examples/full.expected/win_kitchen.json
index 2b304bf..b9fdb3b 100644
--- a/recipe_modules/path/examples/full.expected/win_kitchen.json
+++ b/recipe_modules/path/examples/full.expected/win_kitchen.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]\\file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]\\copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('C:\\\\', 'b', 'FakeTestingCWD'), 'cleanup': ('C:\\\\', 'b', 'cleanup'), 'cache': ('C:\\\\', 'b', 'c'), 'tmp_base': ('C:\\\\',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('C:\\\\', 'b', 'FakeTestingCWD'), 'cleanup': ('C:\\\\', 'b', 'cleanup'), 'cache': ('C:\\\\', 'b', 'c'), 'tmp_base': ('C:\\\\',), 'home': ('C:\\\\', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/win_luci.json b/recipe_modules/path/examples/full.expected/win_luci.json
index 2b304bf..b9fdb3b 100644
--- a/recipe_modules/path/examples/full.expected/win_luci.json
+++ b/recipe_modules/path/examples/full.expected/win_luci.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]\\file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]\\copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('C:\\\\', 'b', 'FakeTestingCWD'), 'cleanup': ('C:\\\\', 'b', 'cleanup'), 'cache': ('C:\\\\', 'b', 'c'), 'tmp_base': ('C:\\\\',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('C:\\\\', 'b', 'FakeTestingCWD'), 'cleanup': ('C:\\\\', 'b', 'cleanup'), 'cache': ('C:\\\\', 'b', 'c'), 'tmp_base': ('C:\\\\',), 'home': ('C:\\\\', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.expected/win_swarming.json b/recipe_modules/path/examples/full.expected/win_swarming.json
index 2b304bf..b9fdb3b 100644
--- a/recipe_modules/path/examples/full.expected/win_swarming.json
+++ b/recipe_modules/path/examples/full.expected/win_swarming.json
@@ -36,13 +36,6 @@
   },
   {
     "cmd": [
-      "touch",
-      "[HOME]\\file"
-    ],
-    "name": "touch my home"
-  },
-  {
-    "cmd": [
       "rm",
       "-rf",
       "[START_DIR]\\copy2"
@@ -109,12 +102,13 @@
       "echo",
       "[CACHE]",
       "[CLEANUP]",
+      "[HOME]",
       "[START_DIR]",
       "[TMP_BASE]"
     ],
     "name": "base paths",
     "~followup_annotations": [
-      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('C:\\\\', 'b', 'FakeTestingCWD'), 'cleanup': ('C:\\\\', 'b', 'cleanup'), 'cache': ('C:\\\\', 'b', 'c'), 'tmp_base': ('C:\\\\',)}@@@",
+      "@@@STEP_LOG_LINE@result@base_paths: {'start_dir': ('C:\\\\', 'b', 'FakeTestingCWD'), 'cleanup': ('C:\\\\', 'b', 'cleanup'), 'cache': ('C:\\\\', 'b', 'c'), 'tmp_base': ('C:\\\\',), 'home': ('C:\\\\', 'home', 'fake_user')}@@@",
       "@@@STEP_LOG_END@result@@@"
     ]
   },
diff --git a/recipe_modules/path/examples/full.py b/recipe_modules/path/examples/full.py
index c01b120..3f663c5 100644
--- a/recipe_modules/path/examples/full.py
+++ b/recipe_modules/path/examples/full.py
@@ -53,6 +53,27 @@
   file_path = api.path['tmp_base'].join('new_file')
   abspath = api.path.abspath(file_path)
   api.path.assert_absolute(abspath)
+  try:
+    api.path.assert_absolute("not/abs")
+    assert False, "assert_absolute failed to catch relative path"
+  except AssertionError:
+    pass
+
+  assert api.path.pardir == '..'
+  if api.platform.is_win:
+    assert api.path.sep == '\\'
+    assert api.path.pathsep == ';'
+  else:
+    assert api.path.sep == '/'
+    assert api.path.pathsep == ':'
+
+  assert api.path.basename(file_path) == 'new_file'
+  assert api.path.dirname(file_path) == str(api.path['tmp_base'])
+  assert api.path.split(file_path) == (str(api.path['tmp_base']), 'new_file')
+  assert api.path.splitext('thing.bat.mkv') == ('thing.bat', '.mkv')
+
+  assert api.path.relpath(file_path, api.path['tmp_base']) == 'new_file'
+
 
   api.step('touch me', ['touch', api.path.abspath(file_path)])
   # Assert for testing that a file exists.
@@ -67,11 +88,6 @@
   realpath = api.path.realpath(file_path)
   assert api.path.exists(realpath)
 
-  home_path = api.path.join(api.path.expanduser('~'), 'file')
-  api.step('touch my home', ['touch', home_path])
-  api.path.mock_add_paths(home_path)
-  assert api.path.exists(home_path)
-
   # can mock copy paths. See the file module to do this for real.
   copy1 = api.path['start_dir'].join('copy1')
   copy10 = api.path['start_dir'].join('copy10')