Repository: sdss/sdss_access

Issue:
force_modules or other flag should retain user-defined envvars
The Path() object currently defines the environmental variables within a Python according to a specific tree version.

For example, for release='sdss5', if I define ROBOSTRATEGY_DATA in my environment, and then enter Python and instantiate a Path object, it will alter my environmental variables and ROBOSTRATEGY_DATA will have whatever value is in the config file for sdss5. 

This behavior is quite useful for data users, who should be accessing a very specific set of released data. For development and pipelines, it is not clearly as useful, because sometimes we need fine-grained control of the envvars; requiring a new tree version for every individual test is burdensome.

The documentation for force_modules=True makes it sound as if it will force sdss_access to respect existing envvars, but it does not seem to do so (i.e. it changes ROBOSTRATEGY_DATA even if it is set). Brian tells me it works only with a subset of the envvars, though I don't see in the code how that happens (in fact, I was unable to figure out WHY it fails, so I don't understand the code!). 

It would be useful to have an option which makes sdss_access respect all existing environmental variables (or some subset of them). 

Discussion:

Patch:
diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 2d7fc88..3be1621 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -9,6 +9,7 @@ This document records the main changes to the sdss_access code.
 1.1.1 (unreleasd)
 -----------------
 - Feature - Adds new functions for mwm `healpixgrp`, `apginst`, and `apgprefix` to determine other keywords from existing `telescope` kwarg
+- Support :issue:`25` - Adds ``preserve_envvars`` option to preserve original os environment variables
 
 1.1.0 (2020-07-07)
 ------------------
diff --git a/docs/sphinx/api.rst b/docs/sphinx/api.rst
index 8f189c4..74f74e3 100644
--- a/docs/sphinx/api.rst
+++ b/docs/sphinx/api.rst
@@ -28,6 +28,13 @@ Auth
    :undoc-members:
    :show-inheritance:
 
+BaseAccess
+^^^^^^^^^^
+.. automodule:: sdss_access.sync.baseaccess
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 Client
 ^^^^^^
 .. automodule:: sdss_access.sync.cli
diff --git a/docs/sphinx/conf.py b/docs/sphinx/conf.py
index 6ac4464..8fe4df6 100644
--- a/docs/sphinx/conf.py
+++ b/docs/sphinx/conf.py
@@ -39,7 +39,8 @@ except ModuleNotFoundError:
 # ones.
 extensions = ['sphinx.ext.autodoc', 'sphinx.ext.napoleon', 'sphinx.ext.autosummary',
               'sphinx.ext.todo', 'sphinx.ext.viewcode', 'sphinx.ext.mathjax',
-              'sphinx.ext.intersphinx', 'sdss_access.misc.docupaths', 'recommonmark']
+              'sphinx.ext.intersphinx', 'sdss_access.misc.docupaths', 'recommonmark',
+              'sphinx_issues']
 
 # Add any paths that contain templates here, relative to this directory.
 templates_path = ['_templates']
@@ -84,6 +85,8 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
 # documents.
 default_role = 'py:obj'
 
+issues_github_path = "sdss/sdss_access"
+
 # If true, '()' will be appended to :func: etc. cross-reference text.
 # add_function_parentheses = True
 
diff --git a/docs/sphinx/index.rst b/docs/sphinx/index.rst
index 52d0d2f..50f2cc1 100644
--- a/docs/sphinx/index.rst
+++ b/docs/sphinx/index.rst
@@ -9,13 +9,15 @@
 
 This is the Sphinx documentation for the SDSS Python product sdss_access
 
-Introduction
-------------
+Contents
+--------
 
-* :ref:`What's new in sdss_access? <changelog>`
-* :ref:`Installation <install>`
-* :ref:`Introduction to sdss_access <intro>`
-* :ref:`sdss-access path definitions <paths>`
+.. toctree::
+  :maxdepth: 1
+
+  Installation <installation>
+  Introduction to sdss_access <intro>
+  Available sdss-access paths <paths>
 
 Reference
 ---------
@@ -24,6 +26,9 @@ Reference
    :maxdepth: 1
 
    api
+   CHANGELOG
+   path_evolution
+   path_defs
 
 Indices and tables
 ==================
diff --git a/docs/sphinx/intro.rst b/docs/sphinx/intro.rst
index 463e79c..b92b5d3 100644
--- a/docs/sphinx/intro.rst
+++ b/docs/sphinx/intro.rst
@@ -5,12 +5,13 @@ Introduction to sdss_access
 ===============================
 
 SDSS Access provides a convenient way of navigating local and remote filesystem paths from the Science Archive Server (SAS).
-`sdss_access` uses the SDSS Tree product for all path look-ups.
+``sdss_access`` uses the SDSS Tree product for all path look-ups.
 
 Path Generation
 ---------------
 
-You can generate full paths to local files easily with `Path.full`::
+You can generate full paths to local files easily with `Path.full <.BasePath.full>`.
+::
 
     # import the path
     from sdss_access import SDSSPath
@@ -21,7 +22,8 @@ You can generate full paths to local files easily with `Path.full`::
     '/Users/Brian/Work/sdss/sas/mangawork/manga/spectro/redux/v2_3_1/8485/stack/manga-8485-1902-LOGCUBE.fits.gz'
 
 Note that this only generates a path. The file may not actually exist locally.  If you want to generate a URL path to
-the file on the SAS at Utah, you can use `Path.url`::
+the file on the SAS at Utah, you can use `Path.url <.BasePath.url>`.
+::
 
     # generate a http path to the file
     path.url('mangacube', drpver='v2_3_1', plate='8485', ifu='1901', wave='LOG')
@@ -39,12 +41,12 @@ be an empty string.
 Path Names
 ----------
 
-The syntax for all paths defined in `sdss_access`, for most methods, is ``(name, **kwargs)``.  Each path is defined by
+The syntax for all paths defined in ``sdss_access``, for most methods, is ``(name, **kwargs)``.  Each path is defined by
 a **name** and several **keyword arguments**, indicated in the template filepath by **{keyword_name}**.  For example,
 the path to a MaNGA data cube has **name** ``mangacube`` and path keywords, **plate**, **drpver**, **ifu**, and **wave**,
 defined in the path ``$MANGA_SPECTRO_REDUX/{drpver}/{plate}/stack/manga-{plate}-{ifu}-{wave}CUBE.fits.gz``.  All paths
-are defined inside the SDSS `tree` product, within a `[PATHS]` section in the environment configuration files, e.g. `data/sdsswork.cfg`
-or `data/dr15.cfg`.  Within `sdss_access`, all paths are available as a dictionary, ``path.templates``::
+are defined inside the SDSS ``tree`` product, within a `[PATHS]` section in the environment configuration files, e.g. `data/sdsswork.cfg`
+or `data/dr15.cfg`.  Within ``sdss_access``, all paths are available as a dictionary, ``path.templates``::
 
     from sdss_access.path import SDSSPath
     path = SDSSPath()
@@ -52,13 +54,15 @@ or `data/dr15.cfg`.  Within `sdss_access`, all paths are available as a dictiona
     # show the dictionary of available paths
     path.templates
 
-To look up what path names are available, you can use ``path.lookup_names``::
+To look up what path names are available, you can use `Path.lookup_names <.BasePath.lookup_names>`.
+::
 
     # look up the available path names
     path.lookup_names()
     ['BOSSLyaDR_cat', ..., 'mangacube', ..., 'xdqso_index']
 
-To look up what keywords are needed for a given path, you can use ``path.lookup_keys``::
+To look up what keywords are needed for a given path, you can use `Path.lookup_keys <.BasePath.lookup_keys>`.
+::
 
     # look up the keyword arguments needed to define a MaNGA cube path
     path.lookup_keys('mangacube')
@@ -80,24 +84,83 @@ the ``remote`` keyword argument
     path.exists('mangacube', drpver='v2_3_1', plate='8485', ifu='1901', wave='LOG', remote=True)
     True
 
+Environment Paths
+-----------------
+
+By default, when instantiating a new `.Path`, it will automatically load the ``tree`` environment from any currently loaded
+module file, identified with any `TREE_VER` environment variable.  Otherwise it loads the ``sdsswork`` environment, and all
+paths relevant to that environment.
+::
+
+    >>> # load the default environment / paths
+    >>> from sdss_access.path import Path
+    >>> path = Path()
+    >>> path
+    <Path(release="sdsswork", public=False, n_paths=358)
+
+To access paths from a different environment, you can change environments by passing in the ``release`` keyword argument.  The
+``release`` acts as an indicator for both a valid data release, e.g. "DR15", and a valid environment configuration,
+e.g. "sdsswork".
+::
+
+    >>> # load the sdss5 environment and paths
+    >>> from sdss_access.path import Path
+    >>> path = Path(release='sdss5')
+    >>> path
+    <Path(release="sdss5", public=False, n_paths=97)
+
+    >>> # switch to the environment for public data release DR15
+    >>> path = Path(release='DR15')
+    >>> path
+    <Path(release="dr15", public=True, n_paths=325)
+
+When reloading a new ``tree`` environment configuration, ``sdss_access`` automatically updates the Python session
+``os.environ`` with the relevant environment variables for the given release/configuration.  You can preserve your original
+``os.environ`` by setting the ``preserve_envvars`` keyword to True. This will preserve your original environment in its
+entirety.
+::
+
+    >>> # load the sdss5 environment but preserve your original os.environ
+    >>> path = Path(release='sdss5', preserve_envvars=True)
+
+Alternatively, you can preserve a subset of enviroment variables from your original ``os.environ`` by passing in a list of
+environment variables.
+::
+
+    >>> # preserve only a single environment variable
+    >>> path = Path(release='sdss5', preserve_envvars=['ROBOSTRATEGY_DATA'])
+
+If you wish to permanently preserve your locally set environment variables, you can set the ``preserve_envvars`` parameter to
+``true`` in a custom tree YAML configuration file located at ``~/.config/sdss/sdss_access.yml``.  For example
+::
+
+    preserve_envvars: true
+
+Alternatively, you can permanently set a subset of environment variables to preserve by defining a list.
+::
+
+    preserve_envvars:
+      - ROBOSTRATEGY_DATA
+      - ALLWISE_DIR
+
 Downloading Files
 -----------------
 
-You can download files from the SAS and place them in your local SAS.  `sdss_access` expects a local SAS filesystem
+You can download files from the SAS and place them in your local SAS.  ``sdss_access`` expects a local SAS filesystem
 that mimics the real SAS at Utah.  If you do not already have a `SAS_BASE_DIR` set, one will be defined in your
-home directory, as a new `sas` directory.
+home directory, as a new ``sas`` directory.
 
 sdss_access has four classes designed to facilitate access to SAS data.
 
-- **Access** - class that automatically decides between `RsyncAccess` and `CurlAccess` based on the operating system.
+- **Access** - class that automatically decides between `.RsyncAccess` and `.CurlAccess` based on the operating system.
 - **HttpAccess** - uses the `urllib` package to download data using a direct http request
 - **RsyncAccess** - uses `rsync` to download data.  Available for Linux and MacOS.
 - **CurlAccess** - uses `curl` to download data.  This is the only available method for use on Windows machines.
 
-Note that all remote access classes, after instantiation, must call the ``remote()`` method before adding paths to ensure
-successful downloading of data.
+Note that all remote access classes, after instantiation, must call the `Access.remote <.BaseAccess.remote>` method before
+adding paths to ensure successful downloading of data.
 
-Using the `HttpAccess` class.
+Using the `.HttpAccess` class.
 
 ::
 
@@ -110,7 +173,7 @@ Using the `HttpAccess` class.
     # get the file
     http_access.get('mangacube', drpver='v2_3_1', plate='8485', ifu='1901', wave='LOG')
 
-Using the `RsyncAccess` class.  `RsyncAccess` is generally much faster then `HttpAccess` as it spreads multiple
+Using the `.RsyncAccess` class.  `.RsyncAccess` is generally much faster then `.HttpAccess` as it spreads multiple
 file downloads across multiple continuous rsync download streams.
 
 ::
@@ -132,25 +195,25 @@ file downloads across multiple continuous rsync download streams.
     # start the download(s)
     rsync.commit()
 
-The default mode of `RsyncAccess` is for collaboration access.  You can also access data from publicly available
-SDSS data releases, by specifying the `public` and `release` keyword arguments on init.
+The default mode of `.RsyncAccess` is for collaboration access.  You can also access data from publicly available
+SDSS data releases, by specifying the ``public`` and ``release`` keyword arguments on init.
 
 ::
 
     # setup rsync access to download public data from DR14
     rsync = RsyncAccess(public=True, release='dr14')
 
-Using the `CurlAccess` class.  `CurlAccess` behaves exactly the same way as `RsyncAccess`.  After importing and
-instantiating a `CurlAccess` object, all methods and behavior are the same as in the `RsyncAccess` class.
+Using the `.CurlAccess` class.  `.CurlAccess` behaves exactly the same way as `.RsyncAccess`.  After importing and
+instantiating a `.CurlAccess` object, all methods and behavior are the same as in the `.RsyncAccess` class.
 ::
 
     # import the curl class
     from sdss_access import CurlAccess
     curl = CurlAccess()
 
-Using the `Access` class.  Depending on your operating system, `posix` or not, Access will either create itself using
-`RsyncAccess` or `CurlAccess`, and behave as either object.  Via `Acccess`, Windows machines will always use `CurlAccess`,
-while Linux or Macs will automatically utilize `RsyncAccess`.
+Using the `.Access` class.  Depending on your operating system, ``posix`` or not, Access will either create itself using
+`.RsyncAccess` or `.CurlAccess`, and behave as either object.  Via `.Acccess`, Windows machines will always use `.CurlAccess`,
+while Linux or Macs will automatically utilize `.RsyncAccess`.
 ::
 
     # import the access class
@@ -168,8 +231,8 @@ while Linux or Macs will automatically utilize `RsyncAccess`.
     access.set_stream()
     access.commit()
 
-In all all cases, successful `sdss_access` downloads will return a code of 0. Any other number indicates that a problem
-occurred.  If no verbose message is displayed, you may need to check the `sdss_access_XX.log` and `sdss_access_XX.err`
+In all all cases, successful ``sdss_access`` downloads will return a code of 0. Any other number indicates that a problem
+occurred.  If no verbose message is displayed, you may need to check the ``sdss_access_XX.log`` and ``sdss_access_XX.err``
 files within the temporary directory.
 
 Accessing Public Data Products
@@ -262,7 +325,7 @@ instantiation.
     path.full('mangapreimg', designid=8405, designgrp='D0084XX', mangaid='1-42007')
     '/Users/Brian/Work/sdss/data/manga/mangapreim/trunk/data/D0084XX/8405/preimage-1-42007_irg.jpg'
 
-You can also set the ``force_modules`` parameter in your custom config file, ``~/.config/sdss_access/sdss_access.yml`` to
+You can also set the ``force_modules`` parameter in your custom config file, ``~/.config/sdss/sdss_access.yml`` to
 set it once permanently.
 
 .. _sdss-access-windows:
@@ -270,9 +333,9 @@ set it once permanently.
 Notes for Windows Users
 -----------------------
 
-`sdss_access` downloads files into a directory defined by the `SAS_BASE_DIR` enviroment variable.  If this path points
+``sdss_access`` downloads files into a directory defined by the `SAS_BASE_DIR` enviroment variable.  If this path points
 to another drive other than the C drive, make sure that the new drive and paths have full write permissions available
-to `curl`.  `CurlAccess` may not work properly until correct permissions are set up in your folder system.
+to `curl`.  `.CurlAccess` may not work properly until correct permissions are set up in your folder system.
 
 .. _sdss-access-api:
 
@@ -281,29 +344,29 @@ Reference/API
 
 .. rubric:: Class
 
-.. autosummary:: sdss_access.path.Path
-.. autosummary:: sdss_access.Access
-.. autosummary:: sdss_access.HttpAccess
-.. autosummary:: sdss_access.RsyncAccess
-.. autosummary:: sdss_access.CurlAccess
+.. autosummary:: sdss_access.path.path.Path
+.. autosummary:: sdss_access.sync.access.Access
+.. autosummary:: sdss_access.sync.http.HttpAccess
+.. autosummary:: sdss_access.sync.rsync.RsyncAccess
+.. autosummary:: sdss_access.sync.curl.CurlAccess
 
 .. rubric:: Methods
 
 .. autosummary::
 
-    sdss_access.SDSSPath.full
-    sdss_access.SDSSPath.url
-    sdss_access.SDSSPath.lookup_names
-    sdss_access.SDSSPath.lookup_keys
-    sdss_access.SDSSPath.extract
-    sdss_access.SDSSPath.location
-    sdss_access.SDSSPath.name
-    sdss_access.SDSSPath.dir
-    sdss_access.SDSSPath.any
-    sdss_access.SDSSPath.expand
-    sdss_access.SDSSPath.random
-    sdss_access.SDSSPath.one
-    sdss_access.Access.remote
-    sdss_access.Access.add
-    sdss_access.Access.set_stream
-    sdss_access.Access.commit
+    sdss_access.path.path.BasePath.full
+    sdss_access.path.path.BasePath.url
+    sdss_access.path.path.BasePath.lookup_names
+    sdss_access.path.path.BasePath.lookup_keys
+    sdss_access.path.path.BasePath.extract
+    sdss_access.path.path.BasePath.location
+    sdss_access.path.path.BasePath.name
+    sdss_access.path.path.BasePath.dir
+    sdss_access.path.path.BasePath.any
+    sdss_access.path.path.BasePath.expand
+    sdss_access.path.path.BasePath.random
+    sdss_access.path.path.BasePath.one
+    sdss_access.sync.baseaccess.BaseAccess.remote
+    sdss_access.sync.baseaccess.BaseAccess.add
+    sdss_access.sync.baseaccess.BaseAccess.set_stream
+    sdss_access.sync.baseaccess.BaseAccess.commit
diff --git a/docs/sphinx/path_defs.rst b/docs/sphinx/path_defs.rst
index 7515142..645dba7 100644
--- a/docs/sphinx/path_defs.rst
+++ b/docs/sphinx/path_defs.rst
@@ -1,5 +1,12 @@
 
-:tocdepth: 1
+:tocdepth: 2
+
+Path Template Definitions
+=========================
+
+Here we describe, for each release, all the available path template definitions.  Each row indicates the
+shorthand path name, the template path for the filepath, and the list of input keyword arguments needed
+as input when generating dynamic filepaths.
 
 .. _sdsswork:
 
diff --git a/docs/sphinx/path_evolution.rst b/docs/sphinx/path_evolution.rst
index cf6fdbc..44532d3 100644
--- a/docs/sphinx/path_evolution.rst
+++ b/docs/sphinx/path_evolution.rst
@@ -4,7 +4,7 @@
 Path Evolution
 ==============
 
-This page highlights how `sdss_access`` path definitions have evolved between releases, broken up into public data
+This page highlights how ``sdss_access`` path definitions have evolved between releases, broken up into public data
 releases (DRs) and internal SDSS data releases, e.g. MaNGA Product Launches (MPLs) for the MaNGA survey.
 
 Public Data Releases
@@ -71,3 +71,4 @@ MaNGA Product Launches (MPLs)
 .. changelog:: sdss_access.path.changelog:compute_changelog
    :prog: changes
    :drs: mpl4, mpl3
+
diff --git a/docs/sphinx/paths.rst b/docs/sphinx/paths.rst
index 5f688ac..2636bd8 100644
--- a/docs/sphinx/paths.rst
+++ b/docs/sphinx/paths.rst
@@ -36,7 +36,7 @@ Internal SDSS Release Paths
 * :ref:`MPL-3<mpl3>`
 
 
-Lastest Path Evolution
+Latest Path Evolution
 ----------------------
 
 ``sdss_access`` paths sometimes change between various SDSS data releases.  Here we show changes for the latest public data release.  See
diff --git a/python/sdss_access/path/path.py b/python/sdss_access/path/path.py
index d434689..9a60a94 100644
--- a/python/sdss_access/path/path.py
+++ b/python/sdss_access/path/path.py
@@ -45,13 +45,17 @@ class BasePath(object):
     release : str
         The release name, e.g. 'DR15', 'MPL-9'.
     public : bool
-        If True, uses public urls.  Only needed for public data releases. Automatically set to True when release contains "DR".
+        If True, uses public urls.  Only needed for public data releases. Automatically set to True
+        when release contains "DR".
     mirror : bool
         If True, uses the mirror data domain url.  Default is False.
-    verbose: bool
+    verbose : bool
         If True, turns on verbosity.  Default is False.
-    force_modules: bool
-        If True, forces software products to use any existing Module environment paths
+    force_modules : bool
+        If True, forces svn or github software products to use any existing local Module
+        environment paths, e.g. PLATEDESIGN_DIR
+    preserve_envvars : bool | list
+        Flag(s) to indicate some or all original environment variables to preserve
 
     Attributes
     ----------
@@ -62,12 +66,14 @@ class BasePath(object):
     _netloc = {"dtn": "sdss@dtn01.sdss.org", "sdss": "data.sdss.org",
                "mirror": "data.mirror.sdss.org", 'svn': 'svn.sdss.org'}
 
-    def __init__(self, release=None, public=False, mirror=False, verbose=False, force_modules=None):
+    def __init__(self, release=None, public=False, mirror=False, verbose=False,
+                 force_modules=None, preserve_envvars=None):
         self.release = release or os.getenv('TREE_VER', 'sdsswork')
         self.public = 'dr' in self.release.lower() or public
         self.mirror = mirror
         self.verbose = verbose
         self.force_modules = force_modules or config.get('force_modules')
+        self.preserve_envvars = preserve_envvars or config.get('preserve_envvars')
         self.set_netloc()
         self.set_remote_base()
 
@@ -80,19 +86,23 @@ class BasePath(object):
         if self.release:
             self.replant_tree(release=self.release)
 
+    def __repr__(self):
+        return '<BasePath(release="{0}", public={1}, n_paths={2})'.format(self.release.lower(), self.public, len(self.templates))
+
     def replant_tree(self, release=None):
-        ''' replants the tree based on release
+        ''' Replants the tree based on release
 
         Resets the path definitions given a specified release
 
-        Parameters:
-            release (str):
+        Parameters
+        ----------
+            release : str
                 A release to use when replanting the tree
         '''
         release = release or self.release
         if release:
             release = release.lower().replace('-', '')
-        tree.replant_tree(release)
+        tree.replant_tree(release, preserve_envvars=self.preserve_envvars)
         self.templates = tree.paths
         self.release = release
 
@@ -999,8 +1009,10 @@ class Path(BasePath):
         If True, uses the mirror data domain url.  Default is False.
     verbose: bool
         If True, turns on verbosity.  Default is False.
-    force_modules: bool
-        If True, forces software products to use any existing Module environment paths
+    force_modules : bool
+        If True, forces svn or github software products to use any existing local Module environment paths, e.g. PLATEDESIGN_DIR
+    preserve_envvars : bool | list
+        Flag(s) to indicate some or all original environment variables to preserve
 
     Attributes
     ----------
@@ -1009,9 +1021,14 @@ class Path(BasePath):
 
     """
 
-    def __init__(self, release=None, public=False, mirror=False, verbose=False, force_modules=None):
+    def __init__(self, release=None, public=False, mirror=False, verbose=False, force_modules=None,
+                 preserve_envvars=None):
         super(Path, self).__init__(release=release, public=public, mirror=mirror, verbose=verbose,
-                                   force_modules=force_modules)
+                                   force_modules=force_modules, preserve_envvars=preserve_envvars)
+
+    def __repr__(self):
+        rep = super().__repr__()
+        return rep.replace('BasePath', 'Path')
 
     def plateid6(self, filetype, **kwargs):
         """Print plate ID, accounting for 5-6 digit plate IDs.
diff --git a/python/sdss_access/sync/baseaccess.py b/python/sdss_access/sync/baseaccess.py
index 06a7553..3659264 100644
--- a/python/sdss_access/sync/baseaccess.py
+++ b/python/sdss_access/sync/baseaccess.py
@@ -17,10 +17,11 @@ class BaseAccess(six.with_metaclass(abc.ABCMeta, AuthMixin, SDSSPath)):
     access_mode = 'rsync' if is_posix else 'curl'
 
     def __init__(self, label=None, stream_count=5, mirror=False, public=False, release=None,
-                 verbose=False, force_modules=None):
+                 verbose=False, force_modules=None, preserve_envvars=None):
         super(BaseAccess, self).__init__(release=release, public=public,
                                          mirror=mirror, verbose=verbose,
-                                         force_modules=force_modules)
+                                         force_modules=force_modules,
+                                         preserve_envvars=preserve_envvars)
         self.label = label
         self.auth = None
         self.stream = None
diff --git a/setup.cfg b/setup.cfg
index 781fb05..39d1737 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -35,7 +35,7 @@ install_requires =
     six>=1.11
     requests>=2.10.0
     sdss-tree>=3.0.0
-	sdsstools>=0.1.7
+	sdsstools>=0.4.5
 	tqdm>=4.46.0
 
 scripts =
@@ -64,7 +64,7 @@ dev =
 	coverage[toml]>=5.0
     coveralls>=1.7
 	ipdb>=0.12.3
-	sdsstools[dev]>=0.1.0
+	sdsstools[dev]>=0.4.0
 	# The following are needed because sdsstools[dev] as an extra not always
 	# gets installed. See https://github.com/pypa/pip/issues/4957.
 	invoke>=1.3.0
@@ -75,6 +75,8 @@ docs =
 	Sphinx>=2.1.0
 	sphinx_bootstrap_theme>=0.4.12
 	recommonmark>=0.6
+	sphinx-issues>=1.2.0
+	importlib_metadata>=1.6.0
 
 [isort]
 line_length = 100


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index 30fd0f6..cc94e60 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -11,12 +11,14 @@
 from __future__ import print_function, division, absolute_import
 import glob
 import gzip
+import importlib
 import os
 import pytest
 import yaml
 import contextlib
 import shutil
 
+import tree.tree as treemod
 from sdss_access import RsyncAccess, HttpAccess, CurlAccess
 from sdss_access.path import Path
 
@@ -256,3 +258,11 @@ def monkeyhome(monkeypatch, tmp_path):
     ''' monkeypatch the HOME directory '''
     path = (tmp_path / 'tmp').mkdir()
     monkeypatch.setenv("HOME", str(path))
+
+
+@pytest.fixture()
+def monkeysdss5(monkeypatch):
+    monkeypatch.setenv('ALLWISE_DIR', '/tmp/allwise')
+    monkeypatch.setenv('EROSITA_DIR', '/tmp/erosita')
+    monkeypatch.setenv('ROBOSTRATEGY_DATA', '/tmp/robodata')
+    importlib.reload(treemod)
\ No newline at end of file
diff --git a/tests/path/test_sdss5.py b/tests/path/test_sdss5.py
index 23ed873..0cfa5cc 100644
--- a/tests/path/test_sdss5.py
+++ b/tests/path/test_sdss5.py
@@ -12,8 +12,10 @@
 
 
 from __future__ import print_function, division, absolute_import
+import os
 import pytest
 from sdss_access.path import Path
+from sdss_access import config
 
 
 @pytest.fixture(scope='module')
@@ -35,3 +37,60 @@ class TestSVPaths(object):
         assert special in path.templates[name]
         full = path.full(name, **keys)
         assert exp in full
+
+    def assert_orig_sdss5_envvars(self):
+        assert os.getenv("ROBOSTRATEGY_DATA") == '/tmp/robodata'
+        assert os.getenv("ALLWISE_DIR") == '/tmp/allwise'
+        assert os.getenv("EROSITA_DIR") == '/tmp/erosita'
+
+    def assert_updated_sdss5_envvars(self):
+        assert 'sdsswork/sandbox/robostrategy' in os.getenv("ROBOSTRATEGY_DATA")
+        assert 'sdsswork/target/catalogs/allwise' in os.getenv("ALLWISE_DIR")
+        assert 'sdsswork/target/catalogs/eRosita' in os.getenv("EROSITA_DIR")
+
+    def assert_all_envvars(self):
+        self.assert_orig_sdss5_envvars()
+        assert 'sdsswork/sandbox/robostrategy' not in os.getenv("ROBOSTRATEGY_DATA")
+
+    def assert_subset_envvars(self):
+        assert 'sdsswork/sandbox/robostrategy' in os.getenv("ROBOSTRATEGY_DATA")
+        assert os.getenv("ROBOSTRATEGY_DATA") != '/tmp/robodata'
+        assert os.getenv("ALLWISE_DIR") == '/tmp/allwise'
+        assert os.getenv("EROSITA_DIR") == '/tmp/erosita'
+
+    def test_replant_updated(self, monkeysdss5):
+        self.assert_orig_sdss5_envvars()
+        pp = Path(release='sdss5')
+        self.assert_updated_sdss5_envvars()
+        assert 'sdsswork/sandbox/robostrategy' in pp.full('rsFields', plan='A', observatory='apo')
+
+    def test_replant_preserve_all_envvars(self, monkeysdss5):
+        self.assert_orig_sdss5_envvars()
+        pp = Path(release='sdss5', preserve_envvars=True)
+        self.assert_all_envvars()
+        assert 'tmp/robodata' in pp.full('rsFields', plan='A', observatory='apo')
+        assert 'tmp/allwise' in pp.full('allwisecat', ver='1.0', num=1234)
+
+    def test_replant_preserve_subset_envvars(self, monkeysdss5):
+        self.assert_orig_sdss5_envvars()
+        pp = Path(release='sdss5', preserve_envvars=['ALLWISE_DIR', 'EROSITA_DIR'])
+        self.assert_subset_envvars()
+        assert 'sdsswork/sandbox/robostrategy' in pp.full('rsFields', plan='A', observatory='apo')
+        assert 'tmp/allwise' in pp.full('allwisecat', ver='1.0', num=1234)
+
+    def test_replant_preserve_all_from_config(self, monkeysdss5, monkeypatch):
+        monkeypatch.setitem(config, 'preserve_envvars', True)
+        self.assert_orig_sdss5_envvars()
+        pp = Path(release='sdss5')
+        self.assert_all_envvars()
+        assert 'tmp/robodata' in pp.full('rsFields', plan='A', observatory='apo')
+        assert 'tmp/allwise' in pp.full('allwisecat', ver='1.0', num=1234)
+
+    def test_replant_preserve_subset_from_config(self, monkeysdss5, monkeypatch):
+        monkeypatch.setitem(config, 'preserve_envvars', ['ALLWISE_DIR', 'EROSITA_DIR'])
+        self.assert_orig_sdss5_envvars()
+        pp = Path(release='sdss5')
+        self.assert_subset_envvars()
+        assert 'sdsswork/sandbox/robostrategy' in pp.full(
+            'rsFields', plan='A', observatory='apo')
+        assert 'tmp/allwise' in pp.full('allwisecat', ver='1.0', num=1234)