Repository: cropsinsilico/yggdrasil (language: python)

Issue:
Conda recipe generation script
Add a tool for 

1. Updating the version (and possibly requirements) in the local conda recipe during a release
2. Updating the conda recipe in the yggdrasil-feedstock repo from the local copy


Discussion / hints:

Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 6fd734ff5..c2f8ebf21 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,3 +1,4 @@
+repos:
 -   repo: https://github.com/pycqa/flake8
     rev: '4.0.1'
     hooks:
diff --git a/HISTORY.rst b/HISTORY.rst
index ec64d1b6b..e20ded9b0 100644
--- a/HISTORY.rst
+++ b/HISTORY.rst
@@ -2,7 +2,31 @@
 History
 =======
 
-1.8.2 (2021-03-18) Migrate tests out of package into pytest fixtures & various bug fixes
+1.8.4 (2023-01-27) Update CI to use mamba, improve dev utilities, & various bug fixes
+------------------
+* Update utility scripts so that requirements can be maintained in a single file with pip requirements.txt file and conda recipe generated via utils/manage_requirements.py
+* Consolidate CLI utilities for creating environments
+* Add cron job to build docker images periodically to ensure that there is always one available
+* Migrate CI to use mamba instead of conda for improved performance
+* Update documents to encourage use of mamba and reflect updates to development utilities
+* Manage optional dependencies through pip extras and additional conda builds
+* Disable flaky tests by default on CI so that tests that fail in teardown are re-run in the second attempt. They can be enabled via the pytest plugin flag "--rerun-flaky"
+* Add utility for tracking memory usage of multitasking processes
+* Update remote integration tests to use render.io instance and only run on one CI job to prevent overtaxing it
+* Handle failure on CI where MPI was not being installed
+* Various updates to comply with updated PEP8 standards
+* Add option to track memory usage to integration services manager
+* Expand output of ygginfo to include python configuration variables, common executables, and environment variables as well as providing more detailed information when a language is marked as not configured
+* Limit SBML testing to single CI job
+* Use sysconfig options for compiled languages if they match selected compilation tools
+* Fix bug where loading a yaml file with "model_only=True" yielded a result with default inputs and outputs
+
+1.8.3 (2022-07-18) Minor bug fixes for the R interface and CI
+------------------
+* Fix bug in R interface resulting from calling "is.na" on vectors
+* Stop using specific installations of R from apt on linux CI
+
+1.8.2 (2022-03-18) Migrate tests out of package into pytest fixtures & various bug fixes
 ------------------
 
 * Move tests out of package to take advantage of pytest conftest.py structure and reduce the size of the package
diff --git a/MANIFEST.in b/MANIFEST.in
index 4c79a4992..dd4cdca78 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -46,3 +46,7 @@ include versioneer.py
 include yggdrasil/_version.py
 include create_coveragerc.py
 include utils/*.py
+include utils/requirements/*.txt
+include utils/requirements/*.yaml
+include utils/requirements/*.json
+include utils/requirements/*.ini
diff --git a/docs/source/development/general.rst b/docs/source/development/general.rst
index f10c94fb4..698f6e12d 100644
--- a/docs/source/development/general.rst
+++ b/docs/source/development/general.rst
@@ -10,8 +10,7 @@ Development of |yggdrasil| should be done on branches and/or forks that
 are then merged in via pull request after passing linting (via
 `flake8 <http://flake8.pycqa.org/en/latest/>`_), testing (via
 continuous integration on
-`travis <https://travis-ci.org/cropsinsilico/yggdrasil>`_ and
-`appveyor <https://ci.appveyor.com/project/langmm/yggdrasil>`_),
+`Github Actions <https://github.com/cropsinsilico/yggdrasil/actions>`_),
 and code review. Prior to beginning new development,
 developers should open a Github issue on the repository that describes
 the proposed changes and/or features so that discussion can help identify
@@ -36,36 +35,40 @@ The following is only one method for setting up a development environment. You a
 
      $ conda config --add channels conda-forge
 
+#. [OPTIONAL] Install mamba. Mamba is a drop in replacement for conda that is often much faster. If you don't install mamba, replace mamba with conda in the following commands.::
+
+     $ conda install mamba
+
 #. Create a fork of the |yggdrasil| Github repository for you Github account (``Fork`` button located in the upper right corner of the |yggdrasil| `Github repository <https://github.com/cropsinsilico/yggdrasil>`_).
-#. Clone your fork of the |yggdrasil| repository using git and then change directory into the cloned repository. NOTE: If you do not have ``git`` you can either `install it yourself <https://git-scm.com/book/en/v2/Getting-Started-Installing-Git>`_ or using conda (``conda install git``).::
+#. Clone your fork of the |yggdrasil| repository using git and then change directory into the cloned repository. NOTE: If you do not have ``git`` you can either `install it yourself <https://git-scm.com/book/en/v2/Getting-Started-Installing-Git>`_ or using mamba/conda (``mamba install git``).::
 
      $ git clone --recurse-submodules https://github.com/<your Github username>/yggdrasil.git
      $ cd yggdrasil
 
 #. [OPTIONAL] Create a conda environment for development and activate that environment. You can create an environment using any of the supported versions of Python and may need more than one for testing.::
 
-     $ conda create -n ygg python=3.6
+     $ mamba create -n ygg python=3.7
      $ conda activate ygg
      
-   |yggdrasil| provides a python script, ``utils/create_envs.py`` for creating development environments and installing yggdrasil in develoment mode for a combination of dependency installation methods (pip vs. conda) and versions of Python. For example, to create a conda environment with Python 3.6 (as above) and install |yggdrasil|'s dependencies using conda, the following commands can
+   |yggdrasil| provides a python script, ``utils/setup_test_env.py`` for creating development environments and installing yggdrasil in develoment mode for a combination of dependency installation methods (pip vs. conda) and versions of Python. For example, to create a conda environment with Python 3.7 (as above) and install |yggdrasil|'s dependencies using mamba, the following commands can
    be executed from the root directory of your local |yggdrasil| repository.::
 
-     $ python utils/create_envs.py --name=ygg
+     $ python utils/setup_test_env.py devenv mamba 3.7 --name=ygg
      $ conda activate ygg
 
    .. note::
       **If you use ``utils/create_envs.py`` to create your dev environment, you can skip to the last step.**
-#. Install the requirements using conda via the helper script ``utils/install_from_requirements.py``::
+#. Install the requirements using mamba/conda via the helper script ``utils/install_from_requirements.py``::
 
-     $ python utils/install_from_requirements.py conda requirements.txt requirements_condaonly.txt requirements_testing.txt
+     $ python utils/manage_requirements.py install mamba --for-development
 
-   Alternatively, if you did not install conda, you can install the Python dependencies using ``pip`` via the same script.::
+   Alternatively, if you did not install mamba or conda, you can install the Python dependencies using ``pip`` via the same script. This script will attempt to install non-python dependencies via a package manager like ``apt``, ``brew``, or ``vcpkg`` unless ``--python-only`` is passed.::
 
-     $ python utils/install_from_requirements.py pip requirements.txt requirements_testing.txt
+     $ python utils/manage_requirements.py install pip --for-development
 
-   However, if you use this method, you will need to manually install the non-Python dependencies as described :ref:`here <manual_install_rst>`. This should be done BEFORE installing |yggdrasil| in the next step.
+   If you use this method with the ``--python-only`` flag, you will need to manually install the non-Python dependencies as described :ref:`here <manual_install_rst>`. This should be done BEFORE installing |yggdrasil| in the next step.
 
-   There are also two additional requirements files, ``requirements_documentation.txt`` and ``requirements_optional.txt``, that can optionally be added to the end of either of these commands. ``requirements_documentation.txt`` includes packages required for building the documentation and ``requirements_optional.txt`` includes packages required for optional |yggdrasil| features (e.g. using ``astropy`` table parsing or ``pika`` for RabbitMQ communication).
+   There are additional optional requirements that can be enabled via various command line flags. To see what these options are, take a look at the help information for the ``utils/manage_requirements.py install`` script by passing ``-h`` or ``--help``.
 #. Run the |yggdrasil| installation script in development mode.::
 
      $ python setup.py develop
@@ -98,4 +101,4 @@ the module directory containing the class being tested. In some cases
 attributes and/or methods are defined (e.g. serialization, communication,
 and connection driver classes).
 
-Tests can be run using the ``yggtest`` command. If no arguments are provided, ``yggtest`` will run all of the tests (excluding the examples). If you only want to run some tests, you can provide the path to file or directory containing the tests you would like to run; these paths can be absolute, relative to the current directory, or relative to the top level directory of the |yggdrasil| source tree. To also run example tests, include the ``--with-examples`` flag.
+Tests can be run by passing the tests directory to the ``pytest`` command. If no other arguments are provided, ``pytest`` will run all of the tests (excluding the examples). If you only want to run some tests, you can provide the path to file or directory containing the tests you would like to run; these paths can be absolute, relative to the current directory, or relative to the top level directory of the |yggdrasil| source tree. To also run example tests, include the ``--with-examples`` flag.
diff --git a/docs/source/install.rst b/docs/source/install.rst
index 45d7b9ea9..a38d0d7ab 100644
--- a/docs/source/install.rst
+++ b/docs/source/install.rst
@@ -14,7 +14,7 @@ Installation
     * "Desktop development with C++" - Workload under "Windows" section
     * "MSVC v140 - VS 2015 C++ build tools (v14.00)" - Individual component under "Compilers, build tools, and runtimes" section.
 
-   If you *do not use conda* to install |yggdrasil|, you will also need to initialize the command line build tools in any prompt you will be calling |yggdrasil| from. This can be done by calling |yggdrasil| from a "VS2015 x64 Native Tools Developer Command Prompt", or by locating the ``vsvarsall.bat`` script that comes with Visual Studio. Information on the developer prompt and how to locate the ``vsvarsall.bat`` script can be found `here <https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=vs-2019>`_. The prompt/script used must enable the Visual Studio 2015 build tools (the specific year) to be compatible with the Python C library (see the discussion `here <https://wiki.python.org/moin/WindowsCompilers>`_). The VS 2015 tools prompt/script will be installed from within Visual Studio 2019 by selecting the components indicated above. On a 64bit Windows machine (assuming you will be using 64 bit Python), the command to initialize these tools within a regular command prompt will look something like this, but the exact path will vary with your particular installation::
+   If you *do not use conda/mamba* to install |yggdrasil|, you will also need to initialize the command line build tools in any prompt you will be calling |yggdrasil| from. This can be done by calling |yggdrasil| from a "VS2015 x64 Native Tools Developer Command Prompt", or by locating the ``vsvarsall.bat`` script that comes with Visual Studio. Information on the developer prompt and how to locate the ``vsvarsall.bat`` script can be found `here <https://docs.microsoft.com/en-us/cpp/build/building-on-the-command-line?view=vs-2019>`_. The prompt/script used must enable the Visual Studio 2015 build tools (the specific year) to be compatible with the Python C library (see the discussion `here <https://wiki.python.org/moin/WindowsCompilers>`_). The VS 2015 tools prompt/script will be installed from within Visual Studio 2019 by selecting the components indicated above. On a 64bit Windows machine (assuming you will be using 64 bit Python), the command to initialize these tools within a regular command prompt will look something like this, but the exact path will vary with your particular installation::
 
      $ call "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\vcvarsall.bat" amd64
 
@@ -22,11 +22,17 @@ Installation
 
    **Mac Users**
 
-   If you will be running C/C++/Fortran models, you will need to install the command line developer tools on Mac which provides several utilities for compilation and code management (e.g. clang, gcc, make, git). `This <https://cloudtechpoint.medium.com/how-to-install-command-line-tools-homebrew-in-macos-without-xcode-1b910742d923>`_ article outlines how to check if they are installed and install them if they are not. It also describes how to install the Homebrew package manager, which will be useful for installing non-Python dependencies if you do not plan on using conda to install |yggdrasil|.
+   If you will be running C/C++/Fortran models, you will need to install the command line developer tools on Mac which provides several utilities for compilation and code management (e.g. clang, gcc, make, git). `This <https://cloudtechpoint.medium.com/how-to-install-command-line-tools-homebrew-in-macos-without-xcode-1b910742d923>`_ article outlines how to check if they are installed and install them if they are not. It also describes how to install the Homebrew package manager, which will be useful for installing non-Python dependencies if you do not plan on using conda/mamba to install |yggdrasil|.
 
 
-Conda Installation (recommended)
---------------------------------
+Conda/Mamba Installation (recommended)
+--------------------------------------
+
+.. note::
+
+   **Mamba Installation**
+
+   Mamba is a drop in replacement for conda that is often faster and provides clearer error messages when there are conflicts. You can install mamba by following the instructions `here <https://mamba.readthedocs.io/en/latest/installation.html>`_ (including by using conda). Then you may just use ``mamba`` in place of any of the commands below.
 
 Download and install Miniconda from `here <https://docs.conda.io/en/latest/miniconda.html>`_ (or Anaconda from `here <https://www.anaconda.com/download/>`_ if you would like additional Python libraries installed by default). There are conda distributions available for |yggdrasil| from `conda-forge <https://github.com/conda-forge/yggdrasil-feedstock>`_. You can install |yggdrasil| from conda-forge by calling::
 
@@ -54,7 +60,7 @@ the terminal (or Anaconda Prompt on Windows).::
    Then run ``yggconfig`` to finish the installation process for C and C++.
 
 .. warning::
-   If conda takes a very long time to install |yggdrasil| (>5 min spent solving the environment) or fails with an error about conflicts and you are using a version of conda older than 4.7.2, try either updating conda and/or adding the conda-forge channel (if you havn't already)::
+   If conda takes a very long time to install |yggdrasil| (>5 min spent solving the environment) or fails with an error about conflicts and you are using a version of conda older than 4.7.2, try either updating conda, using mamba (see note above), and/or adding the conda-forge channel (if you havn't already)::
 
      conda config --add channels conda-forge
 
diff --git a/recipe/meta.yaml b/recipe/meta.yaml
index 4b32e01df..5753562db 100644
--- a/recipe/meta.yaml
+++ b/recipe/meta.yaml
@@ -1,5 +1,7 @@
 {% set name = "yggdrasil" %}
-{% set version = "1.8.0" %}
+{% set version = "1.8.4" %}
+{% set PY_VER_MAJOR = PY_VER.split('.')[0] %}
+{% set PY_VER_MINOR = PY_VER.split('.')[1] %}
 
 package:
   name: {{ name|lower }}
@@ -10,6 +12,9 @@ source:
 
 build:
   number: 0
+  string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+  run_exports:
+    - {{ pin_subpackage('yggdrasil', max_pin='x.x.x') }}
   script: '{{ PYTHON }} -m pip install . --no-deps --ignore-installed --no-cache-dir -vvv '
   skip: true  # [(win and vc<14) or py<35]
   entry_points:
@@ -34,64 +39,36 @@ build:
     - yggtime_paper = yggdrasil.command_line:yggtime_paper
     - yggvalidate = yggdrasil.command_line:validate_yaml
     - ygginstall = yggdrasil.command_line:ygginstall
+    - yggclean = yggdrasil.command_line:yggclean
+    - yggmodelform = yggdrasil.command_line:yggmodelform
     - yggdevup = yggdrasil.command_line:yggdevup
     - ygggha = yggdrasil.command_line:generate_gha_workflow
 
 requirements:
   host:
-    - czmq
-    - lz4
+    - numpy>=1.13.0
     - pip
     - python
-    - r-base
-    - r-bit64
-    - r-r6
-    - r-reticulate
-    - r-units
-    - r-zeallot
-    - sysv_ipc  # [unix]
-    - zeromq
-    - numpy >=1.17.0
+    - sysv_ipc  # [not win]
   run:
+    - GitPython
     - chevron
-    - cmake  # [not win]
-    - czmq
-    - lz4
-    - flaky
     - flask
-    - git
-    - curl==7.85.0  # [osx]
-    - GitPython <=3.1.20  # [py==36]
-    - GitPython  # [py>36]
     - jsonschema==3.2.0
     - matplotlib-base
-    - numpy >=1.17.0
+    - numpy>=1.13.0
     - pandas
-    - pyperf
-    - pint  # [py2k]
     - psutil
+    - pyperf
     - python
-    - python-rapidjson  # [not py2k]
+    - python-rapidjson
     - pyyaml
     - pyzmq
-    - r-base
-    - r-bit64
-    - r-r6
-    - r-reticulate
-    - r-units
-    - r-zeallot
     - requests
     - scipy
     - six
-    - sysv_ipc  # [unix]
-    - unyt  # [not py2k]
-    - zeromq
-    - {{ compiler('c') }}  # [win]
-    - {{ compiler('cxx') }}  # [win]
-    # - {{ compiler('fortran') }}  # [not win]
-    # - fortran-compiler  # [not win]
-    - m2w64-gcc-fortran  # [win]
-    - m2w64-toolchain_win-64  # [win]
+    - sysv_ipc  # [not win]
+    - unyt
 
 test:
   imports:
@@ -99,6 +76,185 @@ test:
   commands:
     - yggrun -h
 
+outputs:
+  - name: yggdrasil
+  - name: yggdrasil.c
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.c') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - cmake  # [not win]
+        - make  # [not win]
+        - {{ compiler('c') }}
+        - {{ compiler('cxx') }}
+  - name: yggdrasil.fortran
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.fortran') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil.c', exact=True) }}
+        - m2w64-gcc-fortran  # [win]
+        - m2w64-toolchain_win-64  # [win]
+        - {{ compiler('fortran') }}  # [not win]
+  - name: yggdrasil.r
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.r') }}
+    requirements:
+      host:
+        - python
+        - r-base
+        - r-bit64
+        - r-r6
+        - r-reticulate
+        - r-units
+        - r-zeallot
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - r-base
+        - r-bit64
+        - r-r6
+        - r-reticulate
+        - r-units
+        - r-zeallot
+  - name: yggdrasil.zmq
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.zmq') }}
+    requirements:
+      host:
+        - czmq
+        - lz4
+        - python
+        - zeromq
+      run:
+        - {{ pin_subpackage('yggdrasil.c', exact=True) }}
+        - czmq
+        - lz4
+        - zeromq
+  - name: yggdrasil.mpi
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.mpi') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - mpi4py
+        - mpich  # [linux]
+        - msmpi  # [win]
+        - openmpi  # [osx]
+  - name: yggdrasil.rmq
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.rmq') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - pika>=1.0.0
+  - name: yggdrasil.testing
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.testing') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - codecov
+        - conda-tree
+        - coverage
+        - flake8
+        - flaky
+        - pytest
+        - pytest-cov
+        - pytest-mpi
+        - pytest-repeat
+        - pytest-rerunfailures
+        - pytest-timeout
+  - name: yggdrasil.docs
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.docs') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - breathe
+        - doxygen
+        - pypandoc
+        - r-roxygen2
+        - sphinx
+        - sphinx-argparse
+        - sphinx_rtd_theme
+  - name: yggdrasil.dev
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.dev') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - PyGithub
+        - black
+        - git
+        - ipython
+  - name: yggdrasil.astropy
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.astropy') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - astropy
+  - name: yggdrasil.pygments
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.pygments') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - pygments
+  - name: yggdrasil.trimesh
+    build:
+      string: py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h{{ PKG_HASH }}_{{ PKG_BUILDNUM }}
+      run_exports:
+        - {{ pin_subpackage('yggdrasil.trimesh') }}
+    requirements:
+      host:
+        - python
+      run:
+        - {{ pin_subpackage('yggdrasil', exact=True) }}
+        - trimesh
+
+
 about:
   home: https://github.com/cropsinsilico/yggdrasil
   license: BSD-3-Clause
diff --git a/requirements.txt b/requirements.txt
index a18ca2512..cbfb790b9 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,11 +1,12 @@
+GitPython
 chevron
 flask
 jsonschema==3.2.0
-matplotlib
+matplotlib       # [pip]
 numpy>=1.13.0
 pandas
-pyperf
 psutil
+pyperf
 python-rapidjson
 pyyaml
 pyzmq
@@ -14,4 +15,3 @@ scipy
 six
 sysv_ipc; platform_system != 'Windows'
 unyt
-GitPython
diff --git a/requirements_condaonly.txt b/requirements_condaonly.txt
deleted file mode 100644
index e8efdf76f..000000000
--- a/requirements_condaonly.txt
+++ /dev/null
@@ -1,17 +0,0 @@
-cmake; platform_system != 'Windows'  # [conda,c]
-czmq             # [conda,zmq,c]
-lz4              # [conda,zmq,c]
-matplotlib       # [conda]
-r-rcpp<=1.0.3; platform_system == 'Darwin'  # [conda,R]
-r-base           # [conda,R]
-r-bit64          # [conda,R]
-r-r6             # [conda,R]
-r-reticulate     # [conda,R]
-r-zeallot        # [conda,R]
-r-units          # [conda,R]
-zeromq           # [conda,zmq,c]
-# c-compiler; platform_system == 'Windows'    # [conda,c]
-# cxx-compiler; platform_system == 'Windows'  # [conda,c]
-m2w64-gcc-fortran; platform_system == 'Windows'       # [conda,fortran]
-m2w64-toolchain_win-64; platform_system == 'Windows'  # [conda,fortran]
-fortran-compiler; platform_system != 'Windows'  # [conda,fortran]
diff --git a/requirements_development.txt b/requirements_development.txt
deleted file mode 100644
index f66d8af4a..000000000
--- a/requirements_development.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-black
-git  # [conda]
-PyGithub
diff --git a/requirements_extras.ini b/requirements_extras.ini
new file mode 100644
index 000000000..c897f762a
--- /dev/null
+++ b/requirements_extras.ini
@@ -0,0 +1,18 @@
+[mpi]
+mpi4py
+
+[rmq]
+pika>=1.0.0
+
+[sbml]
+libroadrunner<2.0.7
+
+[astropy]
+astropy
+
+[pygments]
+pygments
+
+[trimesh]
+trimesh
+
diff --git a/requirements_optional.txt b/requirements_optional.txt
deleted file mode 100644
index adb9491d1..000000000
--- a/requirements_optional.txt
+++ /dev/null
@@ -1,7 +0,0 @@
-astropy       # [astropy]
-pika>=1.0.0   # [rmq]
-pygments      # [pygments]
-trimesh       # [trimesh]
-libroadrunner<2.0.7  # [pip,sbml]
-ccache; platform_system != 'Windows'  # [conda]
-mpi4py        # [mpi]
diff --git a/setup.py b/setup.py
index c9882abba..2f50a9055 100644
--- a/setup.py
+++ b/setup.py
@@ -6,6 +6,7 @@
 from distutils.sysconfig import get_python_lib
 import versioneer
 import create_coveragerc
+import configparser
 ygg_ver = versioneer.get_version()
 ROOT_PATH = os.path.abspath(os.path.dirname(__file__))
 LANG_PATH = os.path.join(ROOT_PATH, 'yggdrasil', 'languages')
@@ -49,11 +50,16 @@
 
 
 # Create requirements list based on platform
+req_dir = os.path.join("utils", "requirements")
 with open("requirements.txt", 'r') as fd:
     requirements = fd.read().splitlines()
-with open("requirements_testing.txt", 'r') as fd:
+with open(os.path.join(req_dir, "requirements_testing.txt"), 'r') as fd:
     test_requirements = fd.read().splitlines()
-# with open("requirements_optional.txt", 'r') as fd:
+extras_config = configparser.ConfigParser(allow_no_value=True)
+extras_config.read(os.path.join(req_dir, "requirements_extras.ini"))
+extras_requirements = {s: list(extras_config.options(s))
+                       for s in extras_config.sections()}
+# with open(os.path.join(req_dir, "requirements_optional.txt"), 'r') as fd:
 #     optional_requirements = fd.read().splitlines()
 with open("console_scripts.txt", 'r') as fd:
     console_scripts = fd.read().splitlines()
@@ -87,6 +93,7 @@
     keywords=["plants", "simulation", "models", "framework"],
     install_requires=requirements,
     tests_require=test_requirements,
+    extras_require=extras_requirements,
     classifiers=[
         "Programming Language :: C",
         "Programming Language :: C++",
diff --git a/utils/create_envs.py b/utils/create_envs.py
deleted file mode 100644
index 83ed3554a..000000000
--- a/utils/create_envs.py
+++ /dev/null
@@ -1,77 +0,0 @@
-import os
-import argparse
-from setup_test_env import create_env, install_pkg
-_utils_dir = os.path.dirname(os.path.abspath(__file__))
-_pkg_dir = os.path.dirname(_utils_dir)
-
-
-def create_devenv(env_type, python='3.6', name=None, **kwargs):
-    r"""Create a new conda environment with the specified parameters with
-    yggdrasil installed in development mode.
-
-    Args:
-        env_type (str): Type of environment that should be created, either
-            'pip' or 'conda'.
-        python (str, optional): Version of python that should be used. Defaults
-            to '3.6'.
-        name (str, optional): Name that should be given to the environment.
-            Defaults to None and a name will be created from the provided
-            values for env_type and python.
-        **kwargs: Additional keyword arguments are passed to create_env.
-
-    """
-    assert(env_type in ['pip', 'conda'])
-    if name is None:
-        name = '%s%s' % (env_type, python.replace('.', ''))
-    create_env('conda', python=python, name=name, **kwargs)
-    install_pkg('%s-dev' % env_type, python=python,
-                conda_env=name, always_yes=True, only_python=True,
-                fallback_to_conda=True)
-    # python_cmd = locate_conda_exe(name, 'python')
-    # req_suffixes = ['', '_testing', '_development']
-    # if env_type == 'conda':
-    #     req_suffixes.append('_condaonly')
-    #     # TODO: Find a way to install pip-only packages into conda
-    #     # environment
-    # elif env_type == 'pip':
-    #     req_suffixes.append('_piponly')
-    #     print(call_conda_command(['conda', 'install', '-y', '--name', name,
-    #                               'czmq', 'zeromq']))
-    # req = [os.path.join(_pkg_dir, 'requirements%s.txt' % x)
-    #        for x in req_suffixes]
-    # args = ([python_cmd, os.path.join(_utils_dir,
-    #                                   'install_from_requirements.py'),
-    #          '--conda-env', name, env_type] + req)
-    # print(call_conda_command(args))
-    # # This is not called directly because it needs to be invoked with the
-    # # version of Python installed in the target environment
-    # # install_from_requirements(env_type, req, conda_env=name)
-    # print(call_conda_command([python_cmd, 'setup.py', 'develop'],
-    #                          cwd=_pkg_dir))
-    # print("Created environment %s" % name)
-    
-
-if __name__ == "__main__":
-    parser = argparse.ArgumentParser(
-        "Create dev environments for matrix of installation methods "
-        "and Python versions.")
-    parser.add_argument('--method', '--methods',
-                        nargs='+', default=['conda', 'pip'],
-                        choices=['conda', 'pip'],
-                        help=("Method(s) that should be used to install the "
-                              "dependencies."))
-    parser.add_argument('--version', '--versions', '--python', '--pythons',
-                        nargs='+', default=['3.6'],
-                        choices=['3.6', '3.7', '3.8', '3.9'],
-                        help=("Python version(s) to create environments for."))
-    parser.add_argument('--name', default=None,
-                        help=("Name of the conda env that should be created or "
-                              "updated. If provided, 'method' will default to "
-                              "'conda' and 'version' will default to '3.6'."))
-    args = parser.parse_args()
-    if args.name:
-        args.method = ['conda']
-        args.version = ['3.6']
-    for env_type in args.method:
-        for python in args.version:
-            create_devenv(env_type, python=python, name=args.name)
diff --git a/utils/install_from_requirements.py b/utils/install_from_requirements.py
deleted file mode 100644
index 29641e440..000000000
--- a/utils/install_from_requirements.py
+++ /dev/null
@@ -1,376 +0,0 @@
-# https://www.python.org/dev/peps/pep-0508/
-from pip._vendor.packaging.requirements import Requirement, InvalidRequirement
-import os
-import re
-import copy
-import uuid
-import pprint
-import argparse
-from setup_test_env import (
-    call_conda_command, locate_conda_exe, get_install_opts,
-    PYTHON_CMD, CONDA_CMD, _is_win)
-
-
-class DependencyNotFound(BaseException):
-    r"""Exception raised when a dependency cannot be located."""
-    pass
-
-
-def isolate_package_name(entry):
-    r"""Get the package name without any constraints or conditions.
-
-    Args:
-        entry (str): Requirements entry.
-
-    Returns:
-        str: Package name.
-
-    """
-    keys = ['<', '>', '=', ';', '#']
-    out = entry
-    for k in keys:
-        out = out.split(k)[0].strip()
-    return out.strip()
-
-
-def get_pip_dependencies(pkg):
-    r"""Get the dependencies required by a package via pip.
-
-    Args:
-        pkg (str): The name of a pip-installable package.
-
-    Returns:
-        list: The package's dependencies.
-
-    """
-    import requests
-    url = 'https://pypi.org/pypi/{}/json'
-    json = requests.get(url.format(pkg)).json()
-    return json['info']['requires_dist']
-
-
-def get_pip_dependency_version(pkg, dep):
-    r"""Get the version of a dependency required by a pip-installable
-    package.
-
-    Args:
-        pkg (str): The name of a pip-installable package.
-        dep (str): The name of a dependency of pkg.
-
-    Returns:
-        str: The version of the dependency required by the package.
-
-    """
-    reqs = get_pip_dependencies(pkg)
-    dep_regex = r'%s(?:\s*\((?P<ver>[^\)]+)\))?' % dep
-    for x in reqs:
-        m = re.fullmatch(dep_regex, x)
-        if m:
-            ver = ''
-            if m.group('ver'):
-                ver = m.group('ver').strip()
-            return dep + ver
-    raise DependencyNotFound(
-        ("Could not locate the dependency '%s' "
-         "in the list of requirements for package '%s': %s")
-        % (dep, pkg, reqs))
-
-
-def prune(fname_in, fname_out=None, excl_method=None, incl_method=None,
-          install_opts=None, additional_packages=[], skip_packages=[],
-          verbose=False, return_list=False, dont_evaluate_markers=False,
-          environment=None):
-    r"""Prune a requirements.txt file to remove/select dependencies that are
-    dependent on the current environment.
-
-    Args:
-        fname_in (str, list): Full path to one or more requirements files that
-            should be read.
-        fname_out (str, optional): Full path to requirements file that should be
-            created. Defaults to None and is set to <fname_in[0]>_pruned.txt.
-        excl_method (str, optional): Installation method (pip or conda) that
-            should be ignored. Defaults to None and is ignored.
-        incl_method (str, optional): Installation method (pip or conda) that
-            should be installed (requirements with without an installation method
-            or with a different method will be ignored). Defaults to None and is
-            ignored.
-        install_opts (dict, optional): Mapping from language/package to bool
-            specifying whether or not the language/package should be installed.
-            If not provided, get_install_opts is used to create it.
-        additional_packages (list, optional): Additional packages that should
-            be installed. Defaults to empty list. Versions specified here take
-            precedence over versions in the provided files.
-        skip_packages (list, optional): A list of packages that should not
-            be added to the pruned list. Defaults to an empty list.
-        verbose (bool, optional): If True, setup steps are run with verbosity
-            turned up. Defaults to False.
-        return_list (bool, optional): If True, return the list of requirements
-            rather than writing it to a file. Defaults to False.
-        dont_evaluate_markers (bool, optional): If True, don't check the pip-
-            style markers when pruning (only those based on install_opts).
-            Defaults to False.
-        environment (dict, optional): Environment properties that should be
-            used to evaluate pip-style markers. Defaults to None and the
-            current environment will be used.
-
-    Returns:
-        str: Full path to created file.
-
-    """
-    regex_constrain = r'(?:(?:pip)|(?:conda)|(?:[a-zA-Z][a-zA-Z0-9]*))'
-    regex_comment = r'\s*\[\s*(?P<vals>%s(?:\s*\,\s*%s)*)\s*\]\s*' % (
-        regex_constrain, regex_constrain)
-    # regex_elem = r'(?P<val>%s)\s*(?:(?:\,)|(?:\]))' % regex_constrain
-    install_opts = get_install_opts(install_opts)
-    if not isinstance(fname_in, (list, tuple)):
-        fname_in = [fname_in]
-    packages = []
-    new_lines = []
-    orig_lines = copy.copy(additional_packages)
-    for line in additional_packages:
-        pkg = isolate_package_name(line)
-        if pkg in packages:
-            continue
-        if pkg in skip_packages:
-            continue
-        new_lines.append(line)
-        packages.append(pkg)
-    for ifname_in in fname_in:
-        with open(ifname_in, 'r') as fd:
-            old_lines = fd.readlines()
-            orig_lines += old_lines
-        for line in old_lines:
-            line = line.strip()
-            if line.startswith('#'):
-                continue
-            pkg = isolate_package_name(line)
-            if pkg in packages:
-                continue
-            if pkg in skip_packages:
-                continue
-            packages.append(pkg)
-            skip_line = False
-            req_name = line
-            if '#' in line:
-                req_name, comment = line.split('#')
-                m = re.fullmatch(regex_comment, comment)
-                if m:
-                    values = [x.strip() for x in m.group('vals').split(',')]
-                    if verbose:
-                        print('line: %s, values = %s, excl = %s, incl = %s'
-                              % (line, values, excl_method, incl_method))
-                    if excl_method and (excl_method in values):
-                        continue
-                    if incl_method and (incl_method not in values):
-                        continue
-                    for v in values:
-                        if v not in ['pip', 'conda']:
-                            if v not in install_opts:
-                                raise RuntimeError("Unsupported install opt: '%s'" % v)
-                            if not install_opts[v]:
-                                skip_line = True
-                                break
-                elif incl_method:
-                    skip_line = True
-            elif incl_method:
-                skip_line = True
-            if skip_line:
-                continue
-            if dont_evaluate_markers:
-                new_lines.append(req_name.strip())
-                continue
-            try:
-                req = Requirement(req_name.strip())
-                if ((req.marker
-                     and (not req.marker.evaluate(environment=environment)))):
-                    continue
-                new_lines.append(req.name + str(req.specifier))
-            except InvalidRequirement as e:
-                print(e)
-                continue
-    if return_list:
-        return new_lines
-    # Write file
-    if fname_out is None:
-        if fname_in:
-            fname_out = ('_pruned%s' % str(uuid.uuid4())).join(
-                os.path.splitext(fname_in[0]))
-        else:
-            fname_out = ('pruned%s.txt' % str(uuid.uuid4()))
-    if new_lines:
-        with open(fname_out, 'w') as fd:
-            fd.write('\n'.join(new_lines))
-    if verbose:
-        print('INSTALL OPTS:\n%s' % pprint.pformat(install_opts))
-        print('ORIGINAL DEP LIST:\n\t%s\nPRUNED DEP LIST:\n\t%s'
-              % ('\n\t'.join([x.strip() for x in orig_lines]),
-                 '\n\t'.join(new_lines)))
-    return fname_out
-
-
-def install_from_requirements(method, fname_in, conda_env=None,
-                              user=False, unique_to_method=False,
-                              python_cmd=None, install_opts=None,
-                              verbose=False, verbose_prune=False,
-                              additional_packages=[], skip_packages=[],
-                              return_cmds=False, append_cmds=None,
-                              temp_file=None):
-    r"""Install packages via pip or conda from one or more pip-style
-    requirements file(s).
-
-    Args:
-        method (str): Installation method; either 'pip' or 'conda'.
-        fname_in (str, list): Full path to one or more requirements files that
-            should be read.
-        conda_env (str, optional): Name of conda environment that requirements
-            should be installed into. Defaults to None and is ignored.
-        user (bool, optional): If True, install in user mode. Defaults to
-            False.
-        unique_to_method (bool, optional): If True, only those packages that
-            can only be installed via the specified method will be installed.
-        install_opts (dict, optional): Mapping from language/package to bool
-            specifying whether or not the language/package should be installed.
-            If not provided, get_install_opts is used to create it.
-        python_cmd (str, optional): Python executable that should be used to
-            call pip. Defaults to None and will be determined from conda_env if
-            provided. Otherwise the current executable will be used.
-        verbose (bool, optional): If True, setup steps are run with verbosity
-            turned up. Defaults to False.
-        verbose_prune (bool, optional): If True, additional information will
-            be printed when determine the list of requirements that should be
-            installed. Defaults to False. If verbose is True, verbose_prune
-            will be set to True as well.
-        additional_packages (list, optional): Additional packages that should
-            be installed. Defaults to empty list.
-        skip_packages (list, optional): A list of packages that should not
-            be added to the pruned list. Defaults to an empty list.
-        return_cmds (bool, optional): If True, the necessary commands will be
-            returned. Defaults to False.
-        append_cmds (list, optional): List that commands should be appended to.
-            Defaults to None and is ignored. If provided, the temporary file
-            is returned. This keyword will be ignored if return_cmds is True.
-        temp_file (str, optional): File where pruned requirements list should
-            be stored. Defaults to None and one will be created.
-
-    """
-    if verbose:
-        verbose_prune = True
-    return_temp = (return_cmds or isinstance(append_cmds, list))
-    install_opts = get_install_opts(install_opts)
-    if python_cmd is None:
-        python_cmd = PYTHON_CMD
-    if conda_env:
-        python_cmd = locate_conda_exe(conda_env, 'python')
-    if method == 'pip':
-        excl_method = 'conda'
-    elif method == 'conda':
-        excl_method = 'pip'
-    else:
-        raise ValueError("Invalid method: '%s'" % method)
-    if unique_to_method:
-        incl_method = method
-    else:
-        incl_method = None
-    temp_file = prune(fname_in, fname_out=temp_file,
-                      excl_method=excl_method, incl_method=incl_method,
-                      install_opts=install_opts, verbose=verbose_prune,
-                      additional_packages=additional_packages,
-                      skip_packages=skip_packages)
-    try:
-        if method == 'conda':
-            assert(CONDA_CMD)
-            args = [CONDA_CMD, 'install', '-y']
-            if verbose:
-                args.append('-vvv')
-            else:
-                args.append('-q')
-            if conda_env:
-                args += ['--name', conda_env]
-            args += ['--file', temp_file]
-            if user:
-                args.append('--user')
-            args.append('--update-all')
-        elif method == 'pip':
-            assert(python_cmd)
-            args = [python_cmd, '-m', 'pip', 'install']
-            if verbose:
-                args.append('--verbose')
-            args += ['-r', temp_file]
-            if user:
-                args.append('--user')
-        if return_temp:
-            if return_cmds:
-                cmd_list = []
-            else:
-                cmd_list = append_cmds
-            if os.path.isfile(temp_file):
-                cmd_list += [' '.join(args)]
-                if _is_win:
-                    cmd_list.append(
-                        ('%s -c \'exec(\"import os;if os.path.isfile'
-                         '(\\"%s\\"): os.remove(\\"%s\\")\")\'')
-                        % (python_cmd, temp_file, temp_file))
-                else:
-                    cmd_list.append(
-                        ('%s -c \'import os\nif os.path.isfile'
-                         '(\"%s\"): os.remove(\"%s\")\'')
-                        % (python_cmd, temp_file, temp_file))
-        if return_cmds:
-            return cmd_list
-        if isinstance(append_cmds, list):
-            return temp_file
-        if os.path.isfile(temp_file):
-            print(call_conda_command(args))
-    except BaseException:
-        if os.path.isfile(temp_file):
-            with open(temp_file, 'r') as fd:
-                print(fd.read())
-        raise
-    finally:
-        if os.path.isfile(temp_file) and (not return_temp):
-            os.remove(temp_file)
-
-
-if __name__ == "__main__":
-    install_opts = get_install_opts()
-    parser = argparse.ArgumentParser(
-        "Install dependencies via pip or conda from one or more "
-        "pip-style requirements files.")
-    parser.add_argument('method', choices=['conda', 'pip'],
-                        help=("Method that should be used to install the "
-                              "dependencies."))
-    parser.add_argument('files', nargs='+',
-                        help='One or more pip-style requirements files.')
-    parser.add_argument('--conda-env', default=None,
-                        help=('Conda environment that requirements should be '
-                              'installed into.'))
-    parser.add_argument('--user', action='store_true',
-                        help=('Install in user mode.'))
-    parser.add_argument('--unique-to-method', action='store_true',
-                        help=('Only install packages that are unique to the specified '
-                              'installation method.'))
-    parser.add_argument('--verbose', action='store_true',
-                        help="Turn up verbosity of output.")
-    parser.add_argument('--additional-packages', nargs='+',
-                        help="Additional packages that should be installed.")
-    for k, v in install_opts.items():
-        if v:
-            parser.add_argument(
-                '--dont-install-%s' % k, action='store_true',
-                help=("Don't install %s" % k))
-        else:
-            parser.add_argument(
-                '--install-%s' % k, action='store_true',
-                help=("Install %s" % k))
-    args = parser.parse_args()
-    new_opts = {}
-    for k, v in install_opts.items():
-        if v and getattr(args, 'dont_install_%s' % k, False):
-            new_opts[k] = False
-        elif (not v) and getattr(args, 'install_%s' % k, False):
-            new_opts[k] = True
-    install_opts.update(new_opts)
-    install_from_requirements(args.method, args.files, conda_env=args.conda_env,
-                              user=args.user, unique_to_method=args.unique_to_method,
-                              install_opts=install_opts, verbose=args.verbose,
-                              additional_packages=args.additional_packages)
diff --git a/utils/manage_requirements.py b/utils/manage_requirements.py
new file mode 100644
index 000000000..f0963b1c9
--- /dev/null
+++ b/utils/manage_requirements.py
@@ -0,0 +1,1857 @@
+# https://www.python.org/dev/peps/pep-0508/
+import os
+import re
+import pprint
+import argparse
+import shutil
+import uuid
+import json
+import copy
+from collections import UserList, UserDict, OrderedDict
+from setup_test_env import (
+    SetupParam, get_install_opts, _on_travis, call_script,
+    get_summary_commands)
+try:
+    import yaml
+except ImportError:
+    yaml = None
+_pip_os_map = {'osx': 'Darwin',
+               'win': 'Windows',
+               'linux': 'Linux'}
+_rev_pip_os_map = {v.lower(): k for k, v in _pip_os_map.items()}
+_req_dir = os.path.join(os.path.dirname(__file__), 'requirements')
+
+
+class NoValidRequirementOptions(Exception):
+    r"""Indicates that none of the requirement's options are valid."""
+    pass
+
+
+class DependencyNotFound(BaseException):
+    r"""Exception raised when a dependency cannot be located."""
+    pass
+
+
+def isolate_package_name(entry):
+    r"""Get the package name without any constraints or conditions.
+
+    Args:
+        entry (str): Requirements entry.
+
+    Returns:
+        str: Package name.
+
+    """
+    keys = ['<', '>', '=', ';', '#']
+    out = entry
+    for k in keys:
+        out = out.split(k)[0].strip()
+    return out.strip()
+
+
+def get_pip_dependencies(pkg):
+    r"""Get the dependencies required by a package via pip.
+
+    Args:
+        pkg (str): The name of a pip-installable package.
+
+    Returns:
+        list: The package's dependencies.
+
+    """
+    import requests
+    url = 'https://pypi.org/pypi/{}/json'
+    json = requests.get(url.format(pkg)).json()
+    return json['info']['requires_dist']
+
+
+def get_pip_dependency_version(pkg, dep):
+    r"""Get the version of a dependency required by a pip-installable
+    package.
+
+    Args:
+        pkg (str): The name of a pip-installable package.
+        dep (str): The name of a dependency of pkg.
+
+    Returns:
+        str: The version of the dependency required by the package.
+
+    """
+    reqs = get_pip_dependencies(pkg)
+    dep_regex = r'%s(?:\s*\((?P<ver>[^\)]+)\))?' % dep
+    for x in reqs:
+        m = re.fullmatch(dep_regex, x)
+        if m:
+            ver = ''
+            if m.group('ver'):
+                ver = m.group('ver').strip()
+            return dep + ver
+    raise DependencyNotFound(
+        ("Could not locate the dependency '%s' "
+         "in the list of requirements for package '%s': %s")
+        % (dep, pkg, reqs))
+
+
+class YggRequirements(UserDict):
+    r"""Structure outlining requirements for yggdrasil under different
+    circumstances."""
+
+    def __init__(self, *args, **kwargs):
+        add = kwargs.pop('add', None)
+        raw_data = copy.deepcopy(dict(*args, **kwargs))
+        super(YggRequirements, self).__init__(*args, **kwargs)
+        self.raw_data = raw_data
+        if add:
+            self.data['general'] += add
+        self.data['general'] = YggRequirementsList(
+            self.data['general'])
+        for k in self.data['extras'].keys():
+            self.data['extras'][k] = YggRequirementsList(
+                self.data['extras'][k], extras=[k])
+        for k in self.data.pop('bespoke_extras', []):
+            self.data['extras'][k] = YggRequirementsList(
+                [k], extras=[k])
+    
+    @classmethod
+    def from_file(cls, fname=None, force_yaml=False, add=None):
+        r"""Load requirements from a file.
+
+        Args:
+            fname (str, optional): Path to YAML/JSON file containing
+                requirements. Defaults to 'yggdrasil/requirements.yaml'
+                unless pyyaml is not installed and then json will be
+                used (unless force_yaml is True).
+            force_yaml (bool, optional): If True and fname is None,
+                the YAML file will be used even if pyyaml is not
+                installed (causing an ImportError). Defaults to False.
+            add (list, optional): Additional packages that should be
+                added to the general list. Defaults to None and is
+                ignored.
+
+        Returns:
+            YggRequirements: Requirements instance.
+
+        """
+        if fname is None:
+            ext = 'yaml'
+            if yaml is None and not force_yaml:
+                ext = 'json'
+            fname = os.path.join(_req_dir, f'requirements.{ext}')
+        if fname.endswith(('.yml', '.yaml')):
+            out = yaml.load(open(fname, 'r').read(), yaml.SafeLoader)
+        else:
+            out = json.load(open(fname, 'r'))
+        return cls(out, add=add)
+
+    def extras(self, methods=None):
+        r"""Get the list of extras.
+
+        Args:
+            method (list, optional): List of methods that should
+               be present in an extra's requirements for it to be
+               returned. Defaults to None and is ignored.
+
+        Returns:
+            str: Yggdrasil build varients.
+
+        """
+        out = list(self.data['extras'].keys())
+        if methods:
+            for k, v in self.data['extras'].items():
+                if not v.has_method(methods):
+                    out.remove(k)
+        return out
+
+    def select_extra(self, extra):
+        r"""Select the requirements for an extra.
+
+        Args:
+            extra (str): Extra to select.
+
+        Returns:
+            YggRequirementsList: Extra requirements.
+
+        """
+        if extra is None or extra == 'general':
+            return self.data['general']
+        return self.data['extras'][extra]
+
+    def select(self, param, **kwargs):
+        r"""Select requirements that are valid for the provided
+        installation options.
+
+        Args:
+            param (SetupParam): Setup parameters instance.
+            **kwargs: Additional keyword arguments are passed to
+                select methods for YggRequirementList members.
+
+        """
+        out = YggRequirementsList()
+        if param.for_development:
+            out += self.data['general'].select(param, **kwargs)
+        for extra in self.data['extras']:
+            if not param.install_opts[extra]:
+                continue
+            out += self.data['extras'][extra].select(param, **kwargs)
+        return out
+
+    def create_requirements_file_varient(self, varient=None,
+                                         save=False, fname=None,
+                                         param=None, format_kws=None):
+        r"""Create a requirements file for a varient.
+
+        Args:
+            varient (str, optional): Build varient that requirements
+                should be taken from. Defaults to None and the general
+                requirements will be used.
+            save (bool, optional): If True and fname is not provided,
+                the requirements will be saved to a file with a name
+                based on the varient. Defaults to False.
+            fname (str, optional): File where requirements should be
+                saved. Defaults to None and is ignored.
+            param (SetupParam, optional): Parameters that should be
+                used to select requirements. Defaults to all Python
+                requirements by default.
+            format_kws (dict, optional): Keyword arguments to pass to
+                the format operation for each requirement.
+
+        """
+        if format_kws is None:
+            format_kws = {}
+        if param is None:
+            install_opts = get_install_opts(empty=True)
+            kwargs = {}
+            if varient is not None:
+                for x in self.data['extras'][varient].extras:
+                    install_opts[x] = True
+            elif 'conda' not in format_kws.get('included_methods', []):
+                kwargs['fallback_to_conda'] = False
+            param = SetupParam(install_opts=install_opts, **kwargs)
+        if save and fname is None:
+            if varient is None:
+                fname = os.path.join(
+                    os.path.dirname(os.path.dirname(__file__)),
+                    'requirements.txt')
+            else:
+                fname = os.path.join(
+                    _req_dir, f'requirements_{varient}.txt')
+        reqs = self.select_extra(varient).select(
+            param, allow_multiple=True, allow_missing=True,
+            ignore_existing=True,
+            deselect_flags={'not_in_txt': True})
+        lines = reqs.format(standard='pip', fname=fname, **format_kws)
+        if fname is not None:
+            lines_str = '\n\t'.join(lines)
+            print(f"Created '{fname}':\n\t{lines_str}")
+        return lines
+
+    def create_requirements_files(self):
+        r"""Create requirements files for all available varients."""
+        self.create_requirements_file_varient(save=True,
+                                              format_kws={'excluded_methods': ['conda']})
+        extras_sep = ['dev', 'testing', 'docs']
+        format_kws = {'included_methods': ['conda']}
+        for k in extras_sep:
+            self.create_requirements_file_varient(
+                k, save=True, format_kws=format_kws)
+        # Optional requirements
+        format_kws = {'include_extra': True,
+                      'excluded_methods': ['conda', 'pip']}
+        lines = []
+        for k in self.extras():
+            if k in extras_sep:
+                continue
+            lines += self.create_requirements_file_varient(
+                k, format_kws=format_kws)
+        lines = sorted(lines)
+        fname_opt = os.path.join(
+            _req_dir, 'requirements_optional.txt')
+        with open(fname_opt, 'w') as fd:
+            fd.write('\n'.join(lines) + '\n')
+        lines_str = '\n\t'.join(lines)
+        print(f"Created '{fname_opt}':\n\t{lines_str}")
+        # Piponly/condaonly
+        for method in ['pip', 'conda']:
+            if method == 'pip':
+                excluded = 'conda'
+            else:
+                excluded = 'pip'
+            format_kws = {'include_extra': True,
+                          'included_methods': [method],
+                          'excluded_methods': [excluded, 'python']}
+            fname = os.path.join(
+                _req_dir,
+                f'requirements_{method}only.txt')
+            lines = []
+            if method == 'conda':
+                lines += self.create_requirements_file_varient(
+                    format_kws=format_kws)
+            for k in self.extras():
+                if k in extras_sep:
+                    continue
+                lines += self.create_requirements_file_varient(
+                    k, format_kws=format_kws)
+            lines = sorted(lines)
+            with open(fname, 'w') as fd:
+                fd.write('\n'.join(lines) + '\n')
+            lines_str = '\n\t'.join(lines)
+            print(f"Created '{fname}':\n\t{lines_str}")
+
+    def create_conda_recipe_varient(self, varient=None,
+                                    param=None, format_kws=None):
+        r"""Create a conda entry for a varient.
+
+        Args:
+            varient (str, optional): Build varient that requirements
+                should be taken from. Defaults to None and the general
+                requirements will be used.
+            param (SetupParam, optional): Parameters that should be
+                used to select requirements. Defaults to all Python
+                requirements by default.
+            format_kws (dict, optional): Keyword arguments to pass to
+                the format operation for each requirement.
+
+        Returns:
+            dict: Entry for the varient.
+
+        """
+        if param is None:
+            install_opts = get_install_opts(empty=True)
+            if varient is not None:
+                for x in self.data['extras'][varient].extras:
+                    install_opts[x] = True
+            param = SetupParam('conda', install_opts=install_opts,
+                               deps_method='conda_recipe')
+        if format_kws is None:
+            format_kws = {}
+        format_kws.setdefault('included_methods', ['conda_recipe'])
+        reqs = self.select_extra(varient).select(
+            param, ignore_existing=True, allow_multiple=True,
+            deselect_flags={'not_in_recipe': True})
+        if not reqs:
+            return {}
+        deps = reqs.format(standard='conda', **format_kws)
+        return deps
+
+    def create_conda_recipe(self):
+        r"""Create a conda recipe file."""
+        fname = os.path.join(
+            os.path.dirname(os.path.dirname(__file__)),
+            'recipe', 'meta.yaml')
+        lines = open(fname, 'r').read()
+        bound_defs = {
+            'version': ('{% set version = "', '"'),
+            'entry_points': ('\n  entry_points:', '\nrequirements:'),
+            'requirements': ('\nrequirements:', '\ntest:'),
+            'extras': ('\noutputs:', '\nabout:')}
+        bounds = {}
+        for k, v in bound_defs.items():
+            idx_beg = lines.find(v[0])
+            if k in ['version', 'entry_points']:
+                idx_beg += len(v[0])
+            idx_end = lines.find(v[1], idx_beg)
+            bounds[k] = (idx_beg, idx_end)
+        if any(x[0] == -1 or x[1] == -1 for x in bounds.values()):
+            raise ValueError(f"Could not find indices to replace: "
+                             f"{bounds}")
+        
+        class OrderedDumper(yaml.SafeDumper):
+
+            def increase_indent(self, flow=False, indentless=False):
+                return super(OrderedDumper, self).increase_indent(
+                    flow, False)
+
+            def choose_scalar_style(self, *args, **kwargs):
+                out = super(OrderedDumper, self).choose_scalar_style(
+                    *args, **kwargs)
+                if out in ["\"", "\'"]:
+                    return None
+                return out
+
+        def _dict_representer(dumper, data):
+            return dumper.represent_mapping(
+                yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
+                data.items())
+
+        OrderedDumper.add_representer(OrderedDict, _dict_representer)
+        yaml_kws = {"Dumper": OrderedDumper,
+                    'default_flow_style': False,
+                    'width': 1000}
+        # Base package
+        fname_entry_points = os.path.join(
+            os.path.dirname(os.path.dirname(__file__)),
+            'console_scripts.txt')
+        entry_points = open(fname_entry_points, 'r').read()
+        entry_points = entry_points.replace('=', ' = ').splitlines()
+        deps = self.create_conda_recipe_varient()
+        new_lines = lines[:bounds['entry_points'][0]]
+        new_lines += '\n    - ' + '\n    - '.join(entry_points) + '\n'
+        new_lines += lines[bounds['entry_points'][1]:bounds['requirements'][0]]
+        new_lines += '\n' + yaml.dump(deps, **yaml_kws)
+        new_lines += lines[bounds['requirements'][1]:
+                           bounds['extras'][0]]
+        # Varients
+        extra_deps = {'outputs': [{'name': 'yggdrasil'}]}
+        for k in self.extras():
+            k_out = self.create_conda_recipe_varient(k)
+            if k_out:
+                extra_deps['outputs'].append(k_out)
+        new_lines += '\n' + yaml.dump(extra_deps, **yaml_kws)
+        new_lines += '\n' + lines[bounds['extras'][1]:]
+        with open(fname, 'w') as fd:
+            fd.write(new_lines)
+        print(f"Created '{fname}':\n{new_lines}")
+
+    def create_extras_require(self):
+        r"""Create a config file containing extras_require."""
+        import configparser
+        fname = os.path.join(_req_dir, 'requirements_extras.ini')
+        extras = {}
+        format_kws = {'include_method': False, 'include_extra': False}
+        for k in self.extras():
+            extras[k] = self.create_requirements_file_varient(
+                k, format_kws=format_kws)
+        config = configparser.ConfigParser(allow_no_value=True)
+        for k, v in extras.items():
+            if not v:
+                continue
+            config.add_section(k)
+            for x in v:
+                config.set(k, x, None)
+        with open(fname, 'w') as fd:
+            config.write(fd)
+        print(f"Created '{fname}':\n{open(fname, 'r').read()}")
+
+
+class YggRequirementsList(UserList):
+    r"""Set of requirements."""
+
+    def __init__(self, *args, **kwargs):
+        if len(args) == 1 and isinstance(args[0], dict):
+            kwargs.update(args[0])
+            args = (kwargs.pop('requirements'), )
+        requires_extras = kwargs.pop('requires_extras', None)
+        extras = kwargs.pop('extras', None)
+        if requires_extras:
+            if extras is None:
+                extras = []
+            for k in requires_extras:
+                if k not in extras:
+                    extras.append(k)
+        super(YggRequirementsList, self).__init__(*args, **kwargs)
+        self.extras = extras
+        self.data = [YggRequirement.from_file(x, extras=extras)
+                     for x in self.data]
+
+    @classmethod
+    def from_files(cls, fname, skip=None, add=None):
+        r"""Read a list of requirements from one or more pip-style
+        files.
+
+        Args:
+            fname (str, list): One or more file names.
+            skip (list, optional): Names of packages to skip. Defaults
+                to None and is ignored.
+            add (list, optional): Additional packages that should be
+                added to the list. Defaults to None and is ignored.
+
+        Returns:
+            YggRequirementsList: Requirements list.
+
+        """
+        if not isinstance(fname, list):
+            fname = [fname]
+        out = YggRequirementsList()
+        lines = []
+        packages = []
+        for ifname in fname:
+            with open(ifname, 'r') as fd:
+                lines += fd.readlines()
+        if add:
+            lines += add
+        for x in lines:
+            x = x.strip()
+            if x.startswith('#'):
+                continue
+            pkg = YggRequirement.from_pip_requirement(x)
+            pkg_name = pkg.base_name
+            if (skip and pkg_name in skip) or pkg_name in packages:
+                continue
+            out.append(pkg)
+            packages.append(pkg_name)
+        return out
+
+    def flatten(self):
+        r"""Return a list of requirements that is flattened so that
+        members do not have any 'add' members.
+
+        Returns:
+            list: Complete list of requirements covered by this set.
+
+        """
+        if not any(x.add for x in self.data):
+            return self
+        out = YggRequirementsList(extras=self.extras)
+        for x in self.data:
+            out += x.flatten()
+        return out
+
+    def has_method(self, methods):
+        r"""Determine if one or more methods is present in any of the
+        member requirements.
+
+        Args:
+            methods (list): Methods to check for.
+
+        Returns:
+            bool: True if any of the specified methods are present,
+                False otherwise.
+
+        """
+        for x in self.data:
+            if x.has_method(methods):
+                return True
+        return False
+
+    def select_method(self, included_methods=None,
+                      excluded_methods=None):
+        r"""Select requirements based on methods constraints.
+
+        Args:
+            included_methods (list, optional): Installation methods
+                that should be included. By default, python and the
+                format standard will be added to this list. If a
+                requirement has a method that is not in this list, it
+                will be skipped.
+            excluded_methods (list, optional): Installation methods
+                that should be excluded. Defaults to empty list.
+
+        Returns:
+            YggRequirementsList: List of selected requirements.
+
+        """
+        if included_methods is None:
+            included_methods = []
+        if excluded_methods is None:
+            excluded_methods = []
+        out = YggRequirementsList(extras=self.extras)
+        for x in self.data:
+            if ((x.method in included_methods
+                 and x.method not in excluded_methods)):
+                out.append(x)
+        return out
+
+    def select(self, param, **kwargs):
+        r"""Select requirements that are valid for the provided
+        installation options.
+
+        Args:
+            param (SetupParam): Setup parameters instance.
+            **kwargs: Additional keyword arguments are passed to
+                the add_option method of member YggRequirement
+                instances.
+
+        Returns:
+            YggRequirementsList: List of selected requirements.
+
+        """
+        out = YggRequirementsList(extras=self.extras)
+        for x in self.data:
+            x.add_option(out, param, **kwargs)
+        return out
+
+    def sorted_by_method(self, format_names=False):
+        r"""
+
+        Args:
+            format_names (bool, optional): If True, the returned
+                dictionary will be a formatted list of requirements.
+                Defaults to False.
+
+        Returns:
+            dict: Requirements sorted by method.
+
+        """
+        out = {}
+        for x in self.data:
+            if x.method not in out:
+                out[x.method] = YggRequirementsList()
+            out[x.method].append(x)
+        if format_names:
+            for k, v in out.items():
+                out[k] = v.format(included_methods=[k], name_only=True)
+        else:
+            for k, v in out.items():
+                out[k] = out[k].flatten()
+        return out
+
+    def format(self, fname=None, included_methods=None,
+               excluded_methods=None, standard='pip', **kwargs):
+        r"""Format this set of requirements according to a certain
+        standard.
+
+        Args:
+            fname (str, optional): File where lines should be written.
+            standard (str, optional): Standard that should be used.
+            included_methods (list, optional): Installation methods
+                that should be included. By default, python and the
+                format standard will be added to this list. If a
+                requirement has a method that is not in this list, it
+                will be skipped.
+            excluded_methods (list, optional): Installation methods
+                that should be excluded. Defaults to empty list.
+            **kwargs: Additional keyword arguments are passed to the
+                format method for each requirement.
+
+        Returns:
+            list: Formated requirement lines.
+
+        """
+        if included_methods is None:
+            included_methods = []
+        included_methods += ['python', standard]
+        selected = self.select_method(
+            included_methods=included_methods,
+            excluded_methods=excluded_methods).flatten()
+        kwargs['standard'] = standard
+        out = [
+            x.format(**kwargs) for x in selected if not x.host_only]
+        out = sorted(list(set(out)))
+        if standard == 'conda':
+            req = out
+            out = OrderedDict()
+            host_req = [
+                x.format(**kwargs) for x in selected if x.host]
+            if self.extras:
+                host_req.append('python')
+                name = self.extras[0]
+                out['name'] = f"yggdrasil.{name}"
+                out['build'] = OrderedDict([
+                    ('string', (
+                        "py{{ PY_VER_MAJOR }}{{ PY_VER_MINOR }}h"
+                        "{{ PKG_HASH }}_{{ PKG_BUILDNUM }}")),
+                    ('run_exports', [
+                        f"{{{{ pin_subpackage('yggdrasil.{name}') }}}}"
+                    ])
+                ])
+                if len(self.extras) > 1:
+                    req = [
+                        f"{{{{ pin_subpackage('yggdrasil.{x}', exact=True) }}}}"
+                        for x in self.extras[1:]] + req
+                else:
+                    req.insert(
+                        0, "{{ pin_subpackage('yggdrasil', exact=True) }}")
+            out['requirements'] = OrderedDict()
+            if host_req:
+                out['requirements']['host'] = sorted(host_req)
+            out['requirements']['run'] = req
+            if fname is not None:
+                with open(fname, 'w') as fd:
+                    yaml.write(fd, out)
+        else:
+            if fname is not None:
+                with open(fname, 'w') as fd:
+                    fd.write('\n'.join(out) + '\n')
+        return out
+
+    def install(self, param, return_commands=False, **kwargs):
+        r"""Install requirements in this list.
+
+        Args:
+            param (SetupParam) Parameters defining the setup.
+            return_commands (bool, optional): If True, the script
+                is assembled (including any temporary files containing
+                requirements), but not executed. Defaults to False.
+            **kwargs: Additional keyword arguments are passed to
+                the install_command_prefix method for each member
+                requirement.
+
+        Returns:
+            list: Comands resulting in installation.
+
+        """
+        cmds = []
+        reqs = self.sorted_by_method()
+        if 'python' in reqs:
+            if param.method_base == 'pip':
+                reqs['pip'] += reqs.pop('python')
+            else:
+                reqs['conda'] += reqs.pop('python')
+        # Do non-python first
+        order = [k for k in reqs.keys() if k not in
+                 ['python', 'pip', 'conda']]
+        order += [k for k in reqs.keys() if k in
+                  ['python', 'pip', 'conda']]
+        temp_files = []
+        for k in order:
+            v = reqs[k]
+            mapping = {}
+            for x in v:
+                kargs, kparam = x.install_command_prefix(
+                    param, **kwargs)
+                if not kargs:
+                    continue
+                if kargs not in mapping:
+                    mapping[kargs] = kparam
+                else:
+                    mapping[kargs]['requirements'] += kparam['requirements']
+            for k, v in mapping.items():
+                cmds += YggRequirement.format_install_commands(
+                    k, v, param, temp_files=temp_files)
+        if temp_files:
+            if param.install_opts['os'] == 'win':
+                cmds += [
+                    f"{param.python_cmd} -c \'exec(\"import os;"
+                    f"if os.path.isfile(\\\"{x}\\\"): "
+                    f"os.remove(\\\"{x}\\\")\")\'"
+                    for x in temp_files]
+            else:
+                cmds += [
+                    f"{param.python_cmd} -c \'import os\n"
+                    f"if os.path.isfile(\"{x}\"): "
+                    f"os.remove(\"{x}\")\'"
+                    for x in temp_files]
+        if not return_commands:
+            summary_cmds = get_summary_commands(param=param)
+            cmds = summary_cmds + cmds + summary_cmds
+        if not (param.dry_run or return_commands):
+            try:
+                call_script(cmds, verbose=param.verbose)
+            finally:
+                for x in temp_files:
+                    if os.path.isfile(x):
+                        os.remove(x)
+        return cmds
+
+
+class YggRequirement(object):
+    r"""Yggdrasil requirement.
+
+    Args:
+        name (str): Name of the requirement.
+        os (str, optional): Required operating system.
+        method (str, optional): Required installation method.
+        add (list, optional): Additional packages that should be
+            installed with the requirement.
+        executable (str, optional): Executable that should be checked
+            for before installing the requirement.
+        extras (list, optional): yggdrasil build varient/extra(s) that
+            the requirement belongs to.
+        options (list, optional): Different installation options for
+            the requirement under different conditions.
+        host (bool, optional): If True, the requirement must be
+            included in the host section in a conda recipe. Defaults
+            to False.
+        host_only (bool, optional): If True, the requirment should
+            only be included in the host section of a conda recipe.
+            Defaults to False.
+        flags (dict, optional): Additional flags that can be used
+            during installation. Defaults to empty dict.
+
+    """
+
+    def __init__(self, name, os=None, method='python', add=None,
+                 executable=None, extras=None, options=None,
+                 host=False, host_only=False, flags=None):
+        self.name = name
+        self.os = os
+        self.method = method
+        if add is not None:
+            if not isinstance(add, list):
+                add = [add]
+            new_add = []
+            for x in add:
+                x_dict = {'os': os,
+                          'method': method,
+                          'extras': extras,
+                          'host': host,
+                          'host_only': host_only,
+                          'flags': flags}
+                if isinstance(x, str):
+                    x_dict['name'] = x
+                else:
+                    x_dict.update(x)
+                new_add.append(
+                    YggRequirement(x_dict.pop('name'), **x_dict))
+            add = new_add
+        self.add = add
+        self.executable = executable
+        self.extras = extras
+        self.options = options
+        self.host = host
+        self.host_only = host_only
+        if flags is None:
+            flags = {}
+        self.flags = flags
+
+    @property
+    def kwargs(self):
+        r"""dict: Keyword arguments used to create this requirement."""
+        out = {}
+        for k in ["name", "os", "method", "add", "executable",
+                  "extras", "options", "host", "host_only", "flags"]:
+            out[k] = getattr(self, k)
+        return out
+
+    def install(self, param, return_commands=False, **kwargs):
+        r"""Install this requirement.
+
+        Args:
+            param (SetupParam) Parameters defining the setup.
+            return_commands (bool, optional): If True, the script
+                is assembled (including any temporary files containing
+                requirements), but not executed. Defaults to False.
+            **kwargs: Additional keyword arguments are passed to
+                the install_command_prefix method.
+
+        Returns:
+            list: Comands resulting in installation.
+
+        """
+        cmds = []
+        k, v = self.install_command_prefix(param, **kwargs)
+        if not k:
+            return cmds
+        cmds += YggRequirement.format_install_commands(k, v, param)
+        if not return_commands:
+            summary_cmds = get_summary_commands(param=param)
+            cmds = summary_cmds + cmds + summary_cmds
+        if not (param.dry_run or return_commands):
+            call_script(cmds, verbose=param.verbose)
+        return cmds
+
+    @staticmethod
+    def format_install_commands(args, kwargs, param, temp_files=None):
+        r"""Format installation commands.
+
+        Args:
+            args (str): Installation arguments.
+            kwargs (dict): Additional parameters controlling
+                the installation.
+            param (SetupParam) Parameters defining the setup.
+            temp_files (list, optional): Existing list that generated
+                files should be added to so that they can be removed
+                after installation is complete.
+
+        Returns:
+            list: Commands resulting in installation.
+
+        """
+        cmds = []
+        req = sorted(list(set(kwargs['requirements'])))
+        if len(req) == 0:
+            return cmds
+        elif len(req) == 1:
+            if kwargs.get('quote_constraints', False):
+                quoted_req = []
+                for x in req:
+                    if any(y in x for y in '!=<>'):
+                        quoted_req.append(f'"{x}"')
+                    else:
+                        quoted_req.append(x)
+                req = quoted_req
+        else:
+            if kwargs.get('individually', False):
+                for x in req:
+                    ikwargs = dict(kwargs, requirements=[x])
+                    cmds += YggRequirement.format_install_commands(
+                        args, ikwargs, param, temp_files=temp_files)
+                return cmds
+            if kwargs.get('file_flag', False):
+                ifile = f"requirements_{uuid.uuid4()}.txt"
+                if param.dry_run:
+                    req_str = '\n\t'.join(req)
+                    print(f"Temporary file: {ifile}\n\t{req_str}")
+                else:
+                    with open(ifile, 'w') as fd:
+                        fd.write('\n'.join(req))
+                temp_files.append(ifile)
+                args += f" {kwargs['file_flag']} "
+                req = [ifile]
+        cmds.append(f"{args} {' '.join(req)} "
+                    f"{' '.join(kwargs.get('suffix', ''))}")
+        return cmds
+
+    def install_command_prefix(self, param):
+        r"""Get the arguments in the installation command for a given
+        method.
+
+        Args:
+            param (SetupParam) Parameters defining the setup.
+
+        Returns:
+            list, dict: Installation commands and some parameters for
+                the installation command.
+
+        """
+        method = self.method
+        if method == 'python':
+            method = param.method_base
+        args = []
+        install_param = {}
+        if method == 'pip':
+            install_param['file_flag'] = '-r'
+            install_param['quote_constraints'] = True
+            args += [param.python_cmd, '-m', 'pip', 'install']
+            if param.verbose:
+                args.append('--verbose')
+            if param.user:
+                args.append('--user')
+        elif method == 'conda':
+            install_param['file_flag'] = '--file'
+            args = [param.conda_exe, 'install']
+            if param.always_yes:
+                args.append('-y')
+            if param.verbose:
+                args.append('-vvv')
+            else:
+                args.append('-v')
+                # args.append('-q')
+            if param.conda_env:
+                args += ['--name', param.conda_env]
+            if param.user:
+                args.append('--user')
+        elif param.only_python or method.endswith('skip'):
+            pass
+        elif method == 'cran':
+            pass  # TODO?
+        elif method == 'brew':
+            args = ['brew']
+            if self.flags.get('reinstall', False):
+                args.append('reinstall')
+            else:
+                args.append('install')
+            if _on_travis and self.flags.get('from_src_on_travis', False):
+                args.append('--build-from-source')
+        elif method == 'apt':
+            if not param.install_opts['no_sudo']:
+                args += ['sudo']
+            args += ['apt']
+            if param.always_yes and not param.install_opts['no_sudo']:
+                args += ['-y']
+            args += ['update;']
+            if not param.install_opts['no_sudo']:
+                args += ['sudo']
+            args += ['apt-get']
+            if param.always_yes and not param.install_opts['no_sudo']:
+                args += ['-y']
+            args += ['install']
+            install_param['requires_shell'] = True
+        elif method == 'choco':
+            args += ['choco', 'install']
+            install_param.update(
+                individually=True,
+                suffix=['--force'])
+        elif method == 'vcpkg':
+            args += ['vcpkg.exe', 'install']
+            install_param['suffix'] = ['--triplet', 'x64-windows']
+        else:
+            raise NotImplementedError(method)
+        if 'install_flags' in self.flags:
+            args += self.flags['install_flags']
+        if args:
+            install_param['requirements'] = [
+                self.format(name_only=True)]
+        return ' '.join(args), install_param
+
+    @property
+    def base_name(self):
+        r"""str: Base name of the package."""
+        return isolate_package_name(self.full_name)
+
+    @property
+    def full_name(self):
+        r"""str: Full name of the package."""
+        if self.flags and self.flags.get('from_env', False):
+            env = self.name.upper()
+            if isinstance(self.flags['from_env'], str):
+                env = self.flags['from_env']
+            if env in os.environ and os.environ[env] != self.name:
+                return os.environ[env]
+        return self.name
+
+    def pip_requirement(self, include_os=True, include_extra=False,
+                        include_method=True, padding=17,
+                        name_only=False):
+        r"""Get the formatted pip requirement string.
+
+        Args:
+            include_os (bool, optional): If True, include any OS
+               constraints in the requirement using pip's format.
+               Defaults to True.
+            include_extra (bool, optional): If True, include the
+               build extra associated with the requirement (if one
+               exists) in the varients tag next to the requirement.
+               Defaults to False.
+            include_method (bool, optional): If True, include the
+               method associated with the requirement (if one exists)
+               in the varients tag next to the requirement. Defaults
+               to True.
+            padding (int, optional): Number of spaces to pad with
+               before varients.
+            name_only (bool, optional): If True, no additional
+               information will be added to the requirement. Implies
+               include_os==True, include_extra==True, and
+               include_method==True. Defaults to False.
+
+        Returns:
+            str: Pip requirement.
+
+        """
+        out = self.full_name
+        if name_only:
+            include_os = False
+            include_extra = False
+            include_method = False
+        if include_os and self.os is not None:
+            out += "; platform_system"
+            if self.os == 'unix':
+                out += " != 'Windows'"
+            else:
+                assert self.os in _pip_os_map
+                out += f" == '{_pip_os_map[self.os]}'"
+            if 'pip_markers' in self.flags:
+                out += ' ' + '; '.join(self.flags['pip_markers'])
+        varients = []
+        if include_method and self.method != 'python':
+            varients.append(self.method)
+        if include_extra and self.extras is not None:
+            varients += self.extras
+        if varients:
+            suffix = self.format_varients(varients)
+            out = self.add_padded_suffix(out, suffix, padding=padding)
+        if self.add:
+            out = [out]
+            for x in self.add:
+                out.append(
+                    x.pip_requirement(include_os=include_os,
+                                      include_extra=include_extra,
+                                      include_method=include_method,
+                                      padding=padding,
+                                      name_only=name_only))
+            out = '\n'.join(out)
+        return out
+
+    def conda_requirement(self):
+        r"""Get the formatted conda requirement string.
+
+        Returns:
+            str: Conda requirement.
+
+        """
+        suffix = ''
+        varients = []
+        if self.os is not None:
+            if self.os == 'unix':
+                varients.append('not win')
+            else:
+                varients.append(self.os)
+        if varients:
+            suffix += '  ' + self.format_varients(varients)
+        out = self.full_name + suffix
+        if self.add:
+            out = [out]
+            for x in self.add:
+                out.append(x.conda_requirement())
+            out = '\n'.join(out)
+        return out
+
+    def format_varients(self, varients):
+        r"""Format varients into the comment form that should be used
+        at the end of a requirement line.
+
+        Args:
+            varients (list): Varient tags indicating under what
+                conditions a requirement should be used.
+
+        Returns:
+            str: Formatted varients string for end of requirement.
+
+        """
+        return f"# [{','.join(varients)}]"
+
+    def has_method(self, methods):
+        r"""Determine if one or more methods is present in any of the
+        requirement's options.
+
+        Args:
+            methods (list): Methods to check for.
+
+        Returns:
+            bool: True if any of the specified methods are present,
+                False otherwise.
+
+        """
+        if self.options:
+            for x in self.options:
+                if x.has_method(methods):
+                    return True
+            return False
+        if not isinstance(methods, (list, tuple)):
+            methods = [methods]
+        return self.method in methods
+        
+    def add_padded_suffix(self, name, suffix, padding=0):
+        r"""Add a suffix with padding so that first character of
+        suffix lands at padding or greater.
+
+        Args:
+            name (str): Name that proceeds suffix.
+            suffix (str): Suffix to add after padding.
+            padding (int, optional): Minimum column that suffix should
+                be padded to. Defaults to 0. A mininum of 2 spaces
+                will be added between name and suffix.
+
+        Returns:
+            str: Formatted name with suffix.
+
+        """
+        if not suffix:
+            return name
+        spaces = padding - len(name)
+        if spaces < 2:
+            spaces = 2
+        return f"{name}{' ' * spaces}{suffix}"
+
+    def format(self, standard='pip', **kwargs):
+        r"""Format this requirement according to a certain standard.
+
+        Args:
+            standard (str, optional): Standard that should be used.
+            **kwargs: Additional keyword arguments are passed to
+                either pip_requirement or conda_requirement as
+                dictanted by the provided standard.
+
+        Returns:
+            str: Formated requirement line.
+
+        """
+        if standard == 'pip':
+            return self.pip_requirement(**kwargs)
+        elif standard == 'conda':
+            return self.conda_requirement(**kwargs)
+        else:  # pragma: debug
+            raise ValueError(
+                f"Standard must be one of ['pip', 'conda'], not"
+                f" {standard}.")
+
+    def executable_exists(self):
+        r"""Returns true if the executable associated with this
+        requirement already exists."""
+        if self.executable is None:
+            return False
+        return shutil.which(self.executable)
+
+    def os_matches(self, param):
+        r"""Returns true if the OS requirement is met.
+
+        Args:
+            param (SetupParam) Parameters defining the setup.
+
+        Returns:
+            bool: True if the OS requirement is met.
+
+        """
+        return ((self.os is None
+                 or self.os == param.install_opts['os']
+                 or param.install_opts['os'] == 'any'
+                 or (self.os == 'unix' and (param.install_opts['os']
+                                            in ['osx', 'linux']))))
+
+    def method_selected(self, param):
+        r"""Returns True if the method requirement is met.
+
+        Args:
+            param (SetupParam) Parameters defining the setup.
+
+        Returns:
+            bool: True if the method requirement is met.
+
+        """
+        if ((param.only_python
+             and self.method not in ['python', 'pip', 'conda'])):
+            return False
+        methods = [self.method]
+        if 'method_validate' in self.flags:
+            methods.append(self.flags['method_validate'])
+        return any(x in param.valid_methods for x in methods)
+
+    def flags_selected(self, select_flags=None, deselect_flags=None,
+                       required_flags=None):
+        r"""Determine if requirement flags are selected/deselected.
+
+        Args:
+            select_flags (dict, optional): Dictionary of flags for
+                which the requirement will be selected if it has
+                matching flags. The requirement will also match if a
+                flag is missing. Defaults to None and is ignored.
+            deselect_flags (dict, optional): Dictionary of flags for
+                which the requirement will NOT be selected if it has
+                matching flags. Defaults to None and is ignored.
+            required_flags (list, optional): Flags that are required.
+                Defaults to None and is ignored.
+
+        Returns:
+            bool: True if the requirement is selected, False otherwise
+
+        """
+        if select_flags:
+            if not self.flags:
+                return False
+            for k, v in select_flags.items():
+                if k not in self.flags:
+                    continue
+                if self.flags[k] != v:
+                    return False
+        if deselect_flags and self.flags:
+            for k, v in deselect_flags.items():
+                if k not in self.flags:
+                    continue
+                if self.flags[k] == v:
+                    return False
+        if required_flags:
+            if not self.flags:
+                return False
+            for k in required_flags:
+                if k not in self.flags:
+                    return False
+        return True
+
+    def is_selected(self, param, allow_multiple=False,
+                    ignore_existing=False, os_covered=None,
+                    select_flags=None, deselect_flags=None,
+                    required_flags=None):
+        r"""Determine if the provided parameters select this
+        requirement.
+
+        Args:
+            param (SetupParam) Parameters defining the setup.
+            allow_multiple (bool, optional): If True, multiple options
+                may be selected for the same requirement. Defaults to
+                False.
+            ignore_existing (bool, optional): If True, any executable
+                associated with the requirement will not be checked.
+                Defaults to False.
+            os_covered (dict, optional): Mapping of os to booleans to
+                indicate which OSes have a valid requirement option.
+            select_flags (dict, optional): Dictionary of flags for
+                which the requirement will be selected if it has
+                matching flags. Defaults to None and is ignored.
+            deselect_flags (dict, optional): Dictionary of flags for
+                which the requirement will NOT be selected if it has
+                matching flags. Defaults to None and is ignored.
+            required_flags (list, optional): Flags that are required.
+                Defaults to None and is ignored.
+
+        Returns:
+            bool: True if the requirement is selected, False otherwise
+
+        """
+        # print(self.name, not self.os_matches(param),
+        #       not self.method_selected(param)
+        #       not self.flags_selected(select_flags=select_flags,
+        #                               deselect_flags=deselect_flags,
+        #                               required_flags=required_flags),
+        #       ((not ignore_existing) and self.executable_exists()))
+        if not self.os_matches(param):
+            return False
+        if not self.method_selected(param):
+            return False
+        if not self.flags_selected(select_flags=select_flags,
+                                   deselect_flags=deselect_flags,
+                                   required_flags=required_flags):
+            return False
+        if os_covered is not None:
+            os_mark = []
+            if self.os is None:
+                os_mark = list(os_covered.keys())
+            elif self.os == 'unix':
+                os_mark = ['osx', 'linux']
+            else:
+                os_mark = [self.os]
+            
+            # if (not allow_multiple) and
+            if all(os_covered[k] for k in os_mark):
+                return False
+            for k in os_mark:
+                os_covered[k] = True
+        if (not ignore_existing) and self.executable_exists():
+            return False
+        return True
+
+    def add_option(self, out, param, allow_missing=False,
+                   allow_multiple=False, os_covered=None,
+                   for_setup=False, **kwargs):
+        r"""Add the first valid option for this requirement.
+
+        Args:
+            out (list): List that requirement option should be added to.
+            param (SetupParam) Parameters defining the setup.
+            allow_missing (bool, optional): If True, requirements
+                without valid options will be ignored. Defaults to
+                False.
+            allow_multiple (bool, optional): If True, multiple options
+                may be selected for the same requirement. Defaults to
+                False.
+            os_covered (dict, optional): Mapping of os to booleans to
+                indicate which OSes have a valid requirement option.
+            **kwargs: Additional keyword arguments are passed to
+                is_selected and add_option calls for child options.
+
+        """
+        if param.deps_method == 'supplemental':
+            allow_missing = True
+        kwargs.update(os_covered=os_covered,
+                      allow_multiple=allow_multiple)
+        if self.extras and not all([param.install_opts[x]
+                                    for x in self.extras]):
+            return False
+        if self.options is None:
+            solf = self
+            if for_setup and 'method_in_setup' in self.flags:
+                solf_kws = self.kwargs
+                solf_kws['method'] = self.flags['method_in_setup']
+                solf = YggRequirement(**solf_kws)
+            if not solf.is_selected(param, **kwargs):
+                return False
+            if not solf.method.endswith('skip'):
+                out.append(solf)
+            return True
+        else:
+            os_covered = {'win': False, 'linux': False, 'osx': False}
+            kwargs.update(os_covered=os_covered,
+                          allow_missing=allow_missing)
+            kwargs['os_covered'] = os_covered
+            added = []
+            match = False
+            for x in self.options:
+                if x.add_option(out, param, **kwargs):
+                    added.append(x.name)
+                    if all(os_covered.values()) or (not allow_multiple):
+                        match = True
+                        break
+            if all(os_covered.values()):
+                match = True
+            if not (match or allow_missing):
+                raise NoValidRequirementOptions(
+                    f"Failed to find valid option for: {self}"
+                    f" (os_covered = {os_covered})")
+        return False
+
+    def __str__(self):
+        if self.options is None:
+            out = f'Requirement({self.name}'
+            if self.os is not None:
+                out += f', os={self.os}'
+            if self.method is not None:
+                out += f', method={self.method}'
+            if self.flags:
+                out += f', flags={self.flags}'
+            out += ')'
+            return out
+        return str({self.name: self.options})
+    
+    def __repr__(self):
+        return self.__str__()
+
+    def flatten(self):
+        r"""Return a list of requirements covered by this one
+        including any specified by the 'add' keyword.
+
+        Returns:
+            list: Requirements covered by this one.
+
+        """
+        assert not self.options
+        out = YggRequirementsList()
+        if not self.add:
+            out.append(self)
+            return out
+        cpy_kws = self.kwargs
+        cpy_kws.pop('add')
+        out.append(YggRequirement(cpy_kws.pop('name'), **cpy_kws))
+        for x in self.add:
+            out += x.flatten()
+        return out
+
+    @classmethod
+    def from_pip_requirement(cls, src, verbose=False):
+        r"""Create a requirement from a pip-style string.
+
+        Args:
+            src (str): Pip-style requirement string.
+            verbose (bool, optional): If True, setup steps are run
+                with verbosity turned up. Defaults to False.
+
+        Returns:
+            YggRequirement: Pip requirement.
+
+        """
+        regex_constrain = r'(?:(?:pip)|(?:conda)|(?:[a-zA-Z][a-zA-Z0-9]*))'
+        regex_comment = r'\s*\[\s*(?P<vals>%s(?:\s*\,\s*%s)*)\s*\]\s*' % (
+            regex_constrain, regex_constrain)
+        regex_marker = (r'(?P<name>[a-zA-Z][a-zA-Z0-9_]*)\s*'
+                        r'(?P<op>[=!<>]+)\s*'
+                        r'(?:\'|\")(?P<val>[a-zA-Z0-9_.]+)(?:\'|\")')
+        name = src
+        kwargs = {}
+        if '#' in name:
+            name, comment = name.split('#')
+            m = re.fullmatch(regex_comment, comment)
+            if m:
+                values = []
+                for x in m.group('vals').split(','):
+                    v = x.strip()
+                    values.append(v)
+                    if v in ['pip', 'conda']:
+                        assert 'method' not in kwargs
+                        kwargs['method'] = v
+                    elif v in ['win', 'osx', 'linux', 'unix']:
+                        assert 'os' not in kwargs
+                        kwargs['os'] = v
+                    else:
+                        kwargs.setdefault('extras', [])
+                        kwargs['extras'].append(v)
+                if verbose:
+                    print(f'src = {src}, values = {values}')
+        if ';' in name:
+            markers = [x.strip() for x in name.split(';')]
+            name = markers[0]
+            del markers[0]
+            for x in markers:
+                m = re.fullmatch(regex_marker, x)
+                assert m
+                if m.group('name') == 'sys_platform':
+                    assert 'os' not in kwargs
+                    kwargs['os'] = _rev_pip_os_map[m.group('val')]
+                    if m.group('op') == '==':
+                        pass
+                    elif m.group('op') == '!=':
+                        assert kwargs['os'] == 'win'
+                        kwargs['os'] = 'unix'
+                    else:
+                        raise NotImplementedError(m.group('op'))
+                else:
+                    kwargs.setdefault('flags', {})
+                    kwargs['flags'].setdefault('pip_markers', [])
+                    kwargs['flags']['pip_markers'].append(x)
+        return cls(name.strip(), **kwargs)
+
+    @classmethod
+    def from_file(cls, src, parent=None, extras=None):
+        r"""Create a requirement from information loaded from the
+        requirements.yaml or requirements.json file.
+
+        Args:
+            src (str, dict): Requirement information.
+            parent (str, optional): Name of parent requirement if this
+                is a requirement option. Defaults to None and is
+                ignored.
+            extras (list, optional): Build varients that are required
+                for this requirement to be installed. Defaults to None
+                and is ignored.
+
+        Returns:
+            YggRequirement: New requirement instance.
+
+        """
+        name = None
+        kwargs = {}
+        if isinstance(src, str):
+            name = src
+        elif isinstance(src, dict):
+            if 'name' in src:
+                name = src.pop('name')
+                kwargs.update(src)
+            else:
+                assert len(src) == 1
+                for k, v in src.items():
+                    name = k
+                    if isinstance(v, list):
+                        kwargs['options'] = v
+                    elif isinstance(v, dict):
+                        kwargs.update(v)
+                    else:
+                        raise RuntimeError(
+                            f"Unexpected requirement structure: {src}")
+        else:
+            raise RuntimeError(
+                f"Unexpected requirement of type '{type(src)}': {src}")
+        if name is None:
+            if parent is None:
+                raise RuntimeError(f"Could not determine name: {src}")
+            name = parent
+        if 'requires_extras' in kwargs:
+            if extras is None:
+                extras = []
+            else:
+                extras = list(extras)
+            for k in kwargs.pop('requires_extras'):
+                if k not in extras:
+                    extras.append(k)
+        if extras is not None:
+            kwargs['extras'] = extras
+        if 'options' in kwargs:
+            opts = []
+            inherit_kwargs = {'name': name}
+            for k, v in kwargs.items():
+                if k not in ['options', 'requires_extras']:
+                    inherit_kwargs[k] = v
+            for x in kwargs['options']:
+                for k, v in inherit_kwargs.items():
+                    x.setdefault(k, v)
+                opts.append(YggRequirement.from_file(x, parent=name,
+                                                     extras=extras))
+            kwargs['options'] = opts
+        return cls(name, **kwargs)
+
+
+def select_requirements(param, fname=None, req=None, **kwargs):
+    r"""Select requirements that are valid for the provided
+    installation options.
+
+    Args:
+        param (SetupParam): Setup parameters instance.
+        fname (str, optional): Path to YAML/JSON file containing
+            requirements. Defaults to 'yggdrasil/requirements.yaml'
+            unless pyyaml is not installed and then json will be used.
+        req (list, optional): Pre-loaded list of requirements. If not
+            provided, requirements will be loaded from fname.
+        **kwargs: Additional keyword arguments are passed to
+            YggRequirements.select.
+
+    Returns:
+        dict: Requirements sorted by method.
+
+    """
+    if req is None:
+        req = YggRequirements.from_file(fname)
+    return req.select(param, **kwargs).sorted_by_method(
+        format_names=True)
+
+
+def create_requirements(standard, fname=None, req=None):
+    r"""Create requirements files for all available varients.
+
+    Args:
+        standard (str): Standard that should be used to format lines
+            in the file.
+        fname (str, optional): Path to YAML/JSON file containing
+            requirements. Defaults to 'yggdrasil/requirements.yaml'
+            unless pyyaml is not installed and then json will be used.
+        req (YggRequirements, optional): Existing set of requirements
+            to use. If not provided, one will be created from fname.
+
+    """
+    if req is None:
+        req = YggRequirements.from_file(fname, force_yaml=True)
+    if standard == 'all':
+        for k in ['pip', 'conda', 'extras_require', 'json']:
+            create_requirements(k, req=req)
+    elif standard == 'pip':
+        req.create_requirements_files()
+    elif standard == 'conda':
+        req.create_conda_recipe()
+    elif standard == 'extras_require':
+        req.create_extras_require()
+    elif standard == 'json':
+        fname = os.path.join(_req_dir, 'requirements.json')
+        with open(fname, 'w') as fd:
+            json.dump(req.raw_data, fd, indent=2)
+    else:
+        raise NotImplementedError(standard)
+
+
+def create_environment_file(param, fname=None, req=None, add=None,
+                            channels=None,
+                            fname_out='environment.yml', **kwargs):
+    r"""Create an yaml file describing a conda environment.
+
+    Args:
+        param (SetupParam): Setup parameters instance.
+        fname (str, optional): Path to YAML/JSON file containing
+            requirements. Defaults to 'yggdrasil/requirements.yaml'
+            unless pyyaml is not installed and then json will be used.
+        req (YggRequirements, optional): Existing set of requirements
+            to use. If not provided, one will be created from fname
+            and add.
+        add (list, optional): Additional packages that should be
+            installed. Defaults to None and is ignored.
+        channels (list, optional): Conda channels that should be used
+            in the environment file. Defaults to []. 'conda-forge'
+            will be added if it is not already present.
+        fname_out (str, optional): Path where the generate file should
+            be saved. Defaults to 'environment.yml'.
+        **kwargs: Additional keyword arguments are passed to
+            YggRequirements.select.
+
+    """
+    if req is None:
+        file_kwargs = {'add': add}
+        if ((isinstance(fname, list)
+             or (isinstance(fname, str) and fname.endswith('.txt')))):
+            req = YggRequirements.from_files(fname, **file_kwargs)
+        else:
+            req = YggRequirements.from_file(fname, **file_kwargs)
+    kwargs.setdefault('for_setup', True)
+    deps = req.select(param, **kwargs).format(name_only=True)
+    if channels is None:
+        channels = []
+    if 'conda-forge' not in channels:
+        channels.append('conda-forge')
+    out = {'name': param.conda_env,
+           'channels': channels,
+           'dependencies': deps}
+    with open(fname_out, 'w') as fd:
+        yaml.dump(out, fd, Dumper=yaml.SafeDumper)
+    return out
+
+
+def install_requirements(param, fname=None, req=None, add=None,
+                         return_commands=False, **kwargs):
+    r"""Install selected requirements on the current machine.
+
+    Args:
+        param (SetupParam): Setup parameters instance.
+        fname (str, optional): Path to YAML/JSON file containing
+            requirements. Defaults to 'yggdrasil/requirements.yaml'
+            unless pyyaml is not installed and then json will be used.
+        req (YggRequirements, optional): Existing set of requirements
+            to use. If not provided, one will be created from fname
+            and add.
+        add (list, optional): Additional packages that should be
+            installed. Defaults to None and is ignored.
+        return_commands (bool, optional): If True, the commands
+            necessary to install the dependencies are returned instead
+            of running them. Defaults to False.
+        **kwargs: Additional keyword arguments are passed to
+            YggRequirements.select.
+
+    Returns:
+        list: Command resulting installation.
+
+    """
+    if req is None:
+        file_kwargs = {'add': add}
+        if ((isinstance(fname, list)
+             or (isinstance(fname, str) and fname.endswith('.txt')))):
+            req = YggRequirements.from_files(fname, **file_kwargs)
+        else:
+            req = YggRequirements.from_file(fname, **file_kwargs)
+    kwargs.setdefault('for_setup', True)
+    return req.select(param, **kwargs).install(
+        param, return_commands=return_commands)
+
+
+def prune(fname_in, fname_out=None, excl_method=None, incl_method=None,
+          install_opts=None, additional_packages=[], skip_packages=[],
+          verbose=False, return_list=False, dont_evaluate_markers=False,
+          environment=None, skipped_mpi=None, param=None, **kwargs):
+    r"""Prune a requirements.txt file to remove/select dependencies
+    that are dependent on the current environment.
+
+    Args:
+        fname_in (str, list): Full path to one or more requirements
+            files that should be read.
+        fname_out (str, optional): Full path to requirements file that
+            should be created. Defaults to None and is set to
+            <fname_in[0]>_pruned.txt.
+        excl_method (str, list, optional): Installation method(s) (pip
+            or conda) that should be ignored. Defaults to None and is
+            ignored.
+        incl_method (str, list, optional): Installation method(s) (pip
+            or conda) that should be installed (requirements without
+            an installation method or with a different method will be
+            ignored). Defaults to None and is ignored.
+        additional_packages (list, optional): Additional packages that
+            should be installed. Defaults to empty list. Versions
+            specified here take precedence over versions in the
+            provided files.
+        skip_packages (list, optional): A list of packages that should
+            not be added to the pruned list. Defaults to an empty
+            list.
+        verbose (bool, optional): If True, setup steps are run with
+            verbosity turned up. Defaults to False.
+        return_list (bool, optional): If True, return the list of
+            requirements rather than writing it to a file. Defaults to
+            False.
+        dont_evaluate_markers (bool, optional): If True, don't check
+            the pip-style markers when pruning (only those based on
+            install_opts). Defaults to False.
+        environment (dict, optional): Environment properties that
+            should be used to evaluate pip-style markers. Defaults to
+            None and the current environment will be used.
+        skipped_mpi (list, optional): Existing list that skipped mpi
+            packages should be added to. Defaults to False and is
+            ignored.
+        **kwargs: Additional keyword arguments are passed to
+            SetupParam if param not provided.
+
+    Returns:
+        str: Full path to created file.
+
+    """
+    if param is None:
+        if incl_method:
+            kwargs.setdefault('method', incl_method)
+        param = SetupParam(**kwargs)
+    if not isinstance(fname_in, (list, tuple)):
+        fname_in = [fname_in]
+    skip_mpi = ('mpi4py' in skip_packages)
+    mpi_pkgs = ['mpi4py', 'openmpi', 'mpich', 'msmpi', 'pytest-mpi']
+    if isinstance(excl_method, str):
+        excl_method = [excl_method]
+    if skip_mpi:
+        if not isinstance(skipped_mpi, list):
+            skipped_mpi = []
+        for x in mpi_pkgs:
+            if x in skip_packages:
+                skip_packages.remove(x)
+    reqs = YggRequirementsList.from_files(
+        fname_in, skip=skip_packages, add=additional_packages).select(
+            param)
+    if excl_method and not isinstance(excl_method, list, tuple):
+        excl_method = [excl_method]
+    if incl_method:
+        if not isinstance(incl_method, (list, tuple)):
+            incl_method = [incl_method]
+        if not any(x == 'python' for x in incl_method):
+            if not excl_method:
+                excl_method = []
+            excl_method.append('python')
+    format_kws = dict(included_methods=incl_method,
+                      excluded_methods=excl_method,
+                      include_os=True, include_extra=True,
+                      include_method=True)
+    if skip_mpi:
+        cpy = YggRequirementsList()
+        mpi = YggRequirementsList()
+        for x in reqs:
+            if x.base_name in mpi_pkgs:
+                mpi.append(x)
+            else:
+                cpy.append(x)
+        skipped_mpi += mpi.format(**format_kws)
+        reqs = cpy
+    new_lines = reqs.format(**format_kws)
+    if return_list:
+        return new_lines
+    # Write file
+    if fname_out is None:
+        if fname_in:
+            fname_out = ('_pruned%s' % str(uuid.uuid4())).join(
+                os.path.splitext(fname_in[0]))
+        else:
+            fname_out = ('pruned%s.txt' % str(uuid.uuid4()))
+    if new_lines:
+        with open(fname_out, 'w') as fd:
+            fd.write('\n'.join(new_lines))
+    if verbose:
+        orig_lines = copy.copy(additional_packages)
+        for x in fname_in:
+            orig_lines += open(x, 'r').readlines()
+        print(f'INSTALL OPTS:\n{pprint.pformat(install_opts)}')
+        print('ORIGINAL DEP LIST:\n\t%s\nPRUNED DEP LIST:\n\t%s'
+              % ('\n\t'.join([x.strip() for x in orig_lines]),
+                 '\n\t'.join(new_lines)))
+    return fname_out
+
+
+if __name__ == "__main__":
+    install_opts = get_install_opts(empty=True)
+    parser = argparse.ArgumentParser(
+        "Perform actions to manage yggdrasil's requirements.")
+    subparsers = parser.add_subparsers(
+        dest='operation',
+        help="Requirements management operation to performed.")
+    # List requirements
+    parser_req = subparsers.add_parser(
+        'select', help="Determine what dependencies are required.")
+    SetupParam.add_parser_args(
+        parser_req, install_opts=install_opts,
+        skip=['conda-env', 'python'],
+        skip_types=['install'],
+        method_choices=['conda', 'pip', 'mamba',
+                        'conda-dev', 'pip-dev', 'mamba-dev'],
+        target_os_choices=['any', 'win', 'osx', 'linux'],
+        target_os_default='any',
+        additional_args=[
+            (('--allow-missing', ),
+             {'action': 'store_true',
+              'help': "Ignore requirements with no valid options"}),
+        ])
+    # Create requirements
+    parser_cre = subparsers.add_parser(
+        'create', help="Create requirements files.")
+    parser_cre.add_argument(
+        'standard',
+        choices=['conda', 'pip', 'extras_require', 'json',
+                 'env', 'all'],
+        help="Type of requirements file to create.")
+    # Create a conda environment file
+    parser_env = subparsers.add_parser(
+        'env', help="Create a conda environment file.")
+    SetupParam.add_parser_args(
+        parser_env, install_opts=install_opts,
+        env_name_default='env',
+        skip_types=['run'],
+        skip=['method', 'windows_package_manager', 'only_python',
+              'use_mamba', 'fallback_to_conda', 'deps_method'],
+        additional_args=[
+            (('--filename', ),
+             {'default': "environment.yml",
+              'help': ("File where the environment yaml should be "
+                       "saved.")}),
+            (('--channels', '--channel', '-c'),
+             {'nargs': '*',
+              'help': "Name of conda channels that should be used."}),
+            (('--additional-packages', ),
+             {'nargs': '+',
+              'help': "Additional packages that should be installed."}),
+        ])
+    # Install requirements
+    parser_ins = subparsers.add_parser(
+        'install', help="Install required dependencies.")
+    SetupParam.add_parser_args(
+        parser_ins, install_opts=install_opts,
+        method_choices=['conda', 'pip', 'mamba',
+                        'conda-dev', 'pip-dev', 'mamba-dev'],
+        deps_method_default="supplemental",
+        additional_args=[
+            (('--files', ),
+             {'nargs': '+',
+              'help': 'One or more pip-style requirements files'}),
+            (('--additional-packages', ),
+             {'nargs': '+',
+              'help': "Additional packages that should be installed."}),
+            (('--allow-missing', ),
+             {'action': 'store_true',
+              'help': "Ignore requirements with no valid options"}),
+        ])
+    # Call methods
+    args = parser.parse_args()
+    if args.operation == 'select':
+        param = SetupParam.from_args(args, install_opts)
+        x = select_requirements(param,
+                                allow_missing=args.allow_missing)
+        pprint.pprint(x)
+    elif args.operation == 'create':
+        create_requirements(args.standard)
+    elif args.operation == 'env':
+        args.method = 'conda'
+        args.deps_method = 'env'
+        param = SetupParam.from_args(args, install_opts,
+                                     env_created=True)
+        create_environment_file(param, add=args.additional_packages,
+                                channels=args.channels,
+                                fname_out=args.filename)
+    elif args.operation == 'install':
+        # Use the version that takes the environment into account
+        install_opts = get_install_opts()
+        param = SetupParam.from_args(args, install_opts)
+        x = install_requirements(
+            param, fname=args.files, add=args.additional_packages,
+            allow_missing=args.allow_missing)
+        pprint.pprint(x)
+    else:
+        raise NotImplementedError(args.operation)
diff --git a/utils/requirements/requirements.json b/utils/requirements/requirements.json
new file mode 100644
index 000000000..96f12a22d
--- /dev/null
+++ b/utils/requirements/requirements.json
@@ -0,0 +1,512 @@
+{
+  "general": [
+    "chevron",
+    "flask",
+    "jsonschema==3.2.0",
+    {
+      "matplotlib": {
+        "flags": {
+          "from_env": "MATPLOTLIB"
+        },
+        "options": [
+          {
+            "name": "matplotlib-base",
+            "method": "conda"
+          },
+          {
+            "name": "matplotlib",
+            "method": "pip"
+          }
+        ]
+      }
+    },
+    {
+      "numpy>=1.13.0": {
+        "host": true,
+        "flags": {
+          "from_env": "NUMPY",
+          "method_in_setup": "conda"
+        }
+      }
+    },
+    "pandas",
+    {
+      "pip": {
+        "method": "conda_recipe",
+        "host": true,
+        "host_only": true
+      }
+    },
+    "psutil",
+    "pyperf",
+    {
+      "python": {
+        "method": "conda_recipe",
+        "host": true
+      }
+    },
+    "python-rapidjson",
+    "pyyaml",
+    "pyzmq",
+    "requests",
+    {
+      "scipy": {
+        "flags": {
+          "method_in_setup": "conda"
+        }
+      }
+    },
+    "six",
+    "unyt",
+    "GitPython",
+    {
+      "sysv_ipc": {
+        "os": "unix",
+        "host": true
+      }
+    }
+  ],
+  "extras": {
+    "c": [
+      {
+        "cmake": {
+          "method": "conda",
+          "os": "unix"
+        }
+      },
+      {
+        "make": {
+          "method": "conda",
+          "os": "unix"
+        }
+      },
+      {
+        "{{ compiler('c') }}": {
+          "method": "conda_recipe"
+        }
+      },
+      {
+        "{{ compiler('cxx') }}": {
+          "method": "conda_recipe"
+        }
+      }
+    ],
+    "fortran": {
+      "requires_extras": [
+        "c"
+      ],
+      "requirements": [
+        {
+          "gfortran": [
+            {
+              "name": "{{ compiler('fortran') }}",
+              "method": "conda_recipe",
+              "os": "unix"
+            },
+            {
+              "name": "fortran-compiler",
+              "os": "unix",
+              "method": "conda"
+            },
+            {
+              "name": "m2w64-gcc-fortran",
+              "os": "win",
+              "method": "conda",
+              "add": [
+                "m2w64-toolchain_win-64"
+              ]
+            },
+            {
+              "name": "gfortran",
+              "method": "apt"
+            },
+            {
+              "name": "gfortran",
+              "method": "brew",
+              "add": [
+                {
+                  "name": "gcc",
+                  "flags": {
+                    "reinstall": true
+                  }
+                }
+              ]
+            },
+            {
+              "name": "mingw",
+              "method": "choco"
+            }
+          ]
+        }
+      ]
+    },
+    "r": [
+      {
+        "r-base": {
+          "executable": "R",
+          "options": [
+            {
+              "method": "conda",
+              "host": true
+            },
+            {
+              "method": "apt",
+              "add": [
+                "r-base-dev"
+              ]
+            },
+            {
+              "name": "r",
+              "method": "brew"
+            }
+          ]
+        }
+      },
+      {
+        "r-bit64": [
+          {
+            "method": "conda",
+            "host": true
+          },
+          {
+            "method": "cran"
+          }
+        ]
+      },
+      {
+        "r-r6": [
+          {
+            "method": "conda",
+            "host": true
+          },
+          {
+            "method": "cran"
+          }
+        ]
+      },
+      {
+        "r-reticulate": [
+          {
+            "method": "conda",
+            "host": true
+          },
+          {
+            "method": "cran"
+          }
+        ]
+      },
+      {
+        "r-zeallot": [
+          {
+            "method": "conda",
+            "host": true
+          },
+          {
+            "method": "cran"
+          }
+        ]
+      },
+      {
+        "r-units": [
+          {
+            "method": "conda",
+            "host": true
+          },
+          {
+            "method": "cran"
+          }
+        ]
+      },
+      {
+        "udunits": [
+          {
+            "name": "libudunits2-dev",
+            "method": "apt"
+          },
+          {
+            "name": "udunits",
+            "method": "brew"
+          },
+          {
+            "method": "skip",
+            "os": "win"
+          },
+          {
+            "method": "conda_skip"
+          }
+        ]
+      }
+    ],
+    "zmq": {
+      "requires_extras": [
+        "c"
+      ],
+      "requirements": [
+        {
+          "czmq": [
+            {
+              "method": "conda",
+              "host": true
+            },
+            {
+              "name": "libczmq-dev",
+              "method": "apt"
+            },
+            {
+              "method": "brew",
+              "flags": {
+                "from_src_on_travis": true
+              }
+            },
+            {
+              "method": "vcpkg"
+            }
+          ]
+        },
+        {
+          "lz4": [
+            {
+              "name": "lz4",
+              "method": "conda",
+              "host": true
+            },
+            {
+              "name": "lz4",
+              "method": "pip_skip"
+            }
+          ]
+        },
+        {
+          "zeromq": [
+            {
+              "method": "conda",
+              "host": true
+            },
+            {
+              "name": "libzmq3-dev",
+              "method": "apt"
+            },
+            {
+              "name": "zmq",
+              "method": "brew",
+              "flags": {
+                "from_src_on_travis": true
+              }
+            },
+            {
+              "method": "vcpkg"
+            }
+          ]
+        }
+      ]
+    },
+    "mpi": [
+      "mpi4py",
+      {
+        "mpi": [
+          {
+            "name": "mpich",
+            "os": "linux",
+            "method": "conda"
+          },
+          {
+            "name": "openmpi",
+            "os": "osx",
+            "method": "conda"
+          },
+          {
+            "name": "msmpi",
+            "os": "win",
+            "method": "conda"
+          },
+          {
+            "name": "openmpi-bin",
+            "method": "apt",
+            "add": [
+              "libopenmpi-dev"
+            ]
+          },
+          {
+            "name": "openmpi",
+            "method": "brew",
+            "flags": {
+              "method_validate": "conda_supp"
+            },
+            "os": "osx"
+          }
+        ]
+      }
+    ],
+    "omp": [
+      {
+        "libomp": [
+          {
+            "name": "libomp-dev",
+            "method": "apt"
+          },
+          {
+            "method": "brew",
+            "add": [
+              "llvm"
+            ]
+          },
+          {
+            "method": "skip",
+            "os": "win"
+          },
+          {
+            "method": "conda_skip"
+          }
+        ]
+      }
+    ],
+    "rmq": [
+      "pika>=1.0.0"
+    ],
+    "sbml": [
+      {
+        "name": "libroadrunner<2.0.7",
+        "method": "pip",
+        "flags": {
+          "install_flags": [
+            "--no-dependencies"
+          ]
+        }
+      }
+    ],
+    "lpy": [
+      {
+        "name": "lpy",
+        "method": "conda",
+        "add": [
+          "boost==1.66.0"
+        ],
+        "flags": {
+          "install_flags": [
+            "-c",
+            "openalea"
+          ],
+          "not_in_recipe": true,
+          "not_in_txt": true
+        }
+      }
+    ],
+    "testing": [
+      "codecov",
+      {
+        "conda-tree": {
+          "method": "conda"
+        }
+      },
+      "coverage",
+      "flake8",
+      "flaky",
+      "pytest",
+      "pytest-cov",
+      "pytest-mpi",
+      {
+        "pytest-drop-dup-tests": {
+          "method": "pip"
+        }
+      },
+      "pytest-rerunfailures",
+      "pytest-repeat",
+      "pytest-timeout",
+      {
+        "ccache": {
+          "method": "conda",
+          "os": "unix",
+          "flags": {
+            "method_validate": "conda_supp",
+            "not_in_recipe": true
+          }
+        }
+      },
+      {
+        "strace": {
+          "method": "apt",
+          "os": "linux",
+          "flags": {
+            "method_validate": "conda_supp"
+          }
+        }
+      },
+      {
+        "valgrind": {
+          "method": "apt",
+          "os": "linux",
+          "flags": {
+            "method_validate": "conda_supp"
+          }
+        }
+      }
+    ],
+    "docs": [
+      "breathe",
+      {
+        "doxygen": [
+          {
+            "method": "conda"
+          },
+          {
+            "method": "apt"
+          },
+          {
+            "method": "brew"
+          },
+          {
+            "method": "choco"
+          },
+          {
+            "method": "vcpkg"
+          }
+        ]
+      },
+      {
+        "pandoc": [
+          {
+            "method": "apt"
+          },
+          {
+            "name": "pandoc-citeproc",
+            "method": "brew",
+            "add": [
+              "Caskroom/cask/mactex"
+            ]
+          },
+          {
+            "method": "skip",
+            "os": "win"
+          },
+          {
+            "method": "conda_skip"
+          }
+        ]
+      },
+      "pypandoc",
+      {
+        "r-roxygen2": {
+          "method": "conda"
+        }
+      },
+      "sphinx",
+      "sphinx_rtd_theme",
+      "sphinx-argparse"
+    ],
+    "dev": [
+      "black",
+      {
+        "git": {
+          "method": "conda"
+        }
+      },
+      "ipython",
+      "PyGithub"
+    ]
+  },
+  "bespoke_extras": [
+    "astropy",
+    "pygments",
+    "trimesh"
+  ]
+}
\ No newline at end of file
diff --git a/utils/requirements/requirements.yaml b/utils/requirements/requirements.yaml
new file mode 100644
index 000000000..6e04e5f36
--- /dev/null
+++ b/utils/requirements/requirements.yaml
@@ -0,0 +1,257 @@
+general:
+  - chevron
+  - flask
+  # - git:  # This may need to be removed from conda recipe
+  #     method: conda
+  - jsonschema==3.2.0
+  - matplotlib:
+      flags:
+        from_env: MATPLOTLIB
+      options:
+      - name: matplotlib-base
+        method: conda
+      - name: matplotlib
+        method: pip
+  - numpy>=1.13.0:
+      host: True
+      flags:
+        from_env: NUMPY
+        method_in_setup: conda
+  - pandas
+  - pip:
+      method: conda_recipe
+      host: True
+      host_only: True
+  - psutil
+  - pyperf
+  - python:
+      method: conda_recipe
+      host: True
+  - python-rapidjson
+  - pyyaml
+  - pyzmq
+  - requests
+  - scipy:
+      flags:
+        method_in_setup: conda
+  - six
+  - unyt
+  - GitPython
+  - sysv_ipc:
+      os: unix
+      host: True
+  
+extras:
+  c:
+    - cmake:
+        method: conda
+        os: unix
+    - make:
+        method: conda
+        os: unix
+    - "{{ compiler('c') }}":
+        method: conda_recipe
+    - "{{ compiler('cxx') }}":
+        method: conda_recipe
+  fortran:
+    requires_extras: [c]
+    requirements:
+    - gfortran:
+        - name: "{{ compiler('fortran') }}"
+          method: conda_recipe
+          os: unix
+        - name: fortran-compiler
+          os: unix
+          method: conda
+        - name: m2w64-gcc-fortran
+          os: win
+          method: conda
+          add: [m2w64-toolchain_win-64]
+        - name: gfortran
+          method: apt
+        - name: gfortran
+          method: brew
+          add:
+            - name: gcc
+              flags:
+                reinstall: true
+        - name: mingw
+          method: choco
+        # Alternate source for windows
+        # - name: vcpkg-gfortran
+        #   method: vcpkg
+  r:
+    - r-base:
+        executable: R
+        options:
+        - method: conda
+          host: True
+        - method: apt
+          add: [r-base-dev]
+        - name: r
+          method: brew
+    - r-bit64:
+        - method: conda
+          host: True
+        - method: cran
+    - r-r6:
+        - method: conda
+          host: True
+        - method: cran
+    - r-reticulate:
+        - method: conda
+          host: True
+        - method: cran
+    - r-zeallot:
+        - method: conda
+          host: True
+        - method: cran
+    - r-units:
+        - method: conda
+          host: True
+        - method: cran
+    - udunits:
+        - name: libudunits2-dev
+          method: apt
+        - name: udunits
+          method: brew
+        - method: skip
+          os: win
+        - method: conda_skip
+  zmq:
+    requires_extras: [c]
+    requirements:
+    - czmq:
+        - method: conda
+          host: True
+        - name: libczmq-dev
+          method: apt
+        - method: brew
+          flags:
+            from_src_on_travis: true
+        - method: vcpkg
+    - lz4:
+        - name: lz4
+          method: conda
+          host: True
+        - name: lz4
+          method: pip_skip
+    - zeromq:
+        - method: conda
+          host: True
+        - name: libzmq3-dev
+          method: apt
+        - name: zmq
+          method: brew
+          flags:
+            from_src_on_travis: true
+        - method: vcpkg
+  mpi:
+    - mpi4py
+    - mpi:
+        - name: mpich
+          os: linux
+          method: conda
+        - name: openmpi
+          os: osx
+          method: conda
+        - name: msmpi
+          os: win
+          method: conda
+        - name: openmpi-bin
+          method: apt
+          add: [libopenmpi-dev]
+        - name: openmpi
+          method: brew
+          flags:
+            method_validate: conda_supp
+          os: osx
+  omp:
+    - libomp:
+        - name: libomp-dev
+          method: apt
+        - method: brew
+          add: [llvm]  # LLVM clang required to work with omp
+        - method: skip
+          os: win
+        - method: conda_skip
+  rmq:
+    - pika>=1.0.0
+  sbml:
+    - name: libroadrunner<2.0.7
+      method: pip
+      flags:
+        install_flags: [--no-dependencies]
+  lpy:
+    - name: lpy
+      method: conda
+      add:
+        - boost==1.66.0
+      flags:
+        install_flags: [-c, openalea]
+        not_in_recipe: true
+        not_in_txt: true
+  testing:
+    - codecov
+    - conda-tree:
+        method: conda
+    - coverage
+    - flake8
+    - flaky
+    - pytest
+    - pytest-cov
+    - pytest-mpi
+    - pytest-drop-dup-tests:
+        method: pip
+    - pytest-rerunfailures
+    - pytest-repeat
+    - pytest-timeout
+    # Split these into another section?
+    - ccache:
+        method: conda
+        os: unix
+        flags:
+          method_validate: conda_supp
+          not_in_recipe: true
+    - strace:
+        method: apt
+        os: linux
+        flags:
+          method_validate: conda_supp
+    - valgrind:
+        method: apt
+        os: linux
+        flags:
+          method_validate: conda_supp
+  docs:
+    - breathe
+    - doxygen:
+        - method: conda
+        - method: apt
+        - method: brew
+        - method: choco
+        - method: vcpkg
+    - pandoc:
+        - method: apt
+        - name: pandoc-citeproc
+          method: brew
+          add: [Caskroom/cask/mactex]
+        - method: skip
+          os: win
+        - method: conda_skip
+    - pypandoc
+    - r-roxygen2:
+        method: conda
+    - sphinx
+    - sphinx_rtd_theme
+    - sphinx-argparse
+  dev:
+    - black
+    - git:
+        method: conda
+    - ipython
+    - PyGithub
+bespoke_extras:
+- astropy
+- pygments
+- trimesh
diff --git a/utils/requirements/requirements_condaonly.txt b/utils/requirements/requirements_condaonly.txt
new file mode 100644
index 000000000..12cd61f51
--- /dev/null
+++ b/utils/requirements/requirements_condaonly.txt
@@ -0,0 +1,18 @@
+cmake; platform_system != 'Windows'  # [conda,c]
+czmq             # [conda,zmq,c]
+fortran-compiler; platform_system != 'Windows'  # [conda,fortran,c]
+lz4              # [conda,zmq,c]
+m2w64-gcc-fortran; platform_system == 'Windows'  # [conda,fortran,c]
+m2w64-toolchain_win-64; platform_system == 'Windows'  # [conda,fortran,c]
+make; platform_system != 'Windows'  # [conda,c]
+matplotlib-base  # [conda]
+mpich; platform_system == 'Linux'  # [conda,mpi]
+msmpi; platform_system == 'Windows'  # [conda,mpi]
+openmpi; platform_system == 'Darwin'  # [conda,mpi]
+r-base           # [conda,r]
+r-bit64          # [conda,r]
+r-r6             # [conda,r]
+r-reticulate     # [conda,r]
+r-units          # [conda,r]
+r-zeallot        # [conda,r]
+zeromq           # [conda,zmq,c]
diff --git a/utils/requirements/requirements_dev.txt b/utils/requirements/requirements_dev.txt
new file mode 100644
index 000000000..441e32b73
--- /dev/null
+++ b/utils/requirements/requirements_dev.txt
@@ -0,0 +1,4 @@
+PyGithub
+black
+git              # [conda]
+ipython
diff --git a/requirements_distribution.txt b/utils/requirements/requirements_distribution.txt
similarity index 100%
rename from requirements_distribution.txt
rename to utils/requirements/requirements_distribution.txt
diff --git a/requirements_documentation.txt b/utils/requirements/requirements_docs.txt
similarity index 51%
rename from requirements_documentation.txt
rename to utils/requirements/requirements_docs.txt
index 9416ce830..68fc3d026 100644
--- a/requirements_documentation.txt
+++ b/utils/requirements/requirements_docs.txt
@@ -1,7 +1,7 @@
-doxygen  # [conda]
 breathe
+doxygen          # [conda]
 pypandoc
-r-roxygen2  # [conda]
+r-roxygen2       # [conda]
 sphinx
-sphinx_rtd_theme
 sphinx-argparse
+sphinx_rtd_theme
diff --git a/utils/requirements/requirements_extras.ini b/utils/requirements/requirements_extras.ini
new file mode 100644
index 000000000..7b6ce2de8
--- /dev/null
+++ b/utils/requirements/requirements_extras.ini
@@ -0,0 +1,43 @@
+[mpi]
+mpi4py
+
+[rmq]
+pika>=1.0.0
+
+[sbml]
+libroadrunner<2.0.7
+
+[testing]
+codecov
+coverage
+flake8
+flaky
+pytest
+pytest-cov
+pytest-drop-dup-tests
+pytest-mpi
+pytest-repeat
+pytest-rerunfailures
+pytest-timeout
+
+[docs]
+breathe
+pypandoc
+sphinx
+sphinx-argparse
+sphinx_rtd_theme
+
+[dev]
+pygithub
+black
+ipython
+
+[astropy]
+astropy
+
+[pygments]
+pygments
+
+[trimesh]
+trimesh
+
diff --git a/utils/requirements/requirements_optional.txt b/utils/requirements/requirements_optional.txt
new file mode 100644
index 000000000..046b80c9e
--- /dev/null
+++ b/utils/requirements/requirements_optional.txt
@@ -0,0 +1,5 @@
+astropy          # [astropy]
+mpi4py           # [mpi]
+pika>=1.0.0      # [rmq]
+pygments         # [pygments]
+trimesh          # [trimesh]
diff --git a/requirements_piponly.txt b/utils/requirements/requirements_piponly.txt
similarity index 100%
rename from requirements_piponly.txt
rename to utils/requirements/requirements_piponly.txt
diff --git a/yggdrasil/command_line.py b/yggdrasil/command_line.py
index 226d1ee48..a018a2d8c 100755
--- a/yggdrasil/command_line.py
+++ b/yggdrasil/command_line.py
@@ -7,6 +7,7 @@
 import argparse
 import pprint
 import shutil
+import sysconfig
 from yggdrasil import constants
 LANGUAGES = getattr(constants, 'LANGUAGES', {})
 LANGUAGES_WITH_ALIASES = getattr(constants, 'LANGUAGES_WITH_ALIASES', {})
@@ -341,6 +342,10 @@ class integration_service_manager(SubCommand):
                           'help': ('URL for a directory in a Git repository '
                                    'containing models that should be loaded '
                                    'into the service manager registry.')}),
+                        (('--track-memory', ),
+                         {'action': 'store_true',
+                          'help': ('Track the memory used by the '
+                                   'service manager.')}),
                         (('--log-level', ),
                          {'type': int,
                           'help': ('Level of logging that should be '
@@ -425,7 +430,8 @@ def func(cls, args):
                         with_coverage=getattr(args, 'with_coverage', False),
                         model_repository=getattr(args, 'model_repository',
                                                  None),
-                        log_level=getattr(args, 'log_level', None))
+                        log_level=getattr(args, 'log_level', None),
+                        track_memory=getattr(args, 'track_memory', False))
             else:
                 x.send_request(integration_name,
                                yamls=integration_yamls,
@@ -624,8 +630,11 @@ def func(cls, args, return_str=False):
                                     drv.is_comm_installed()))
                     vardict.append((curr_prefix + "Configured",
                                     drv.is_configured()))
-                    vardict.append((curr_prefix + "Disabled",
-                                    drv.is_disabled()))
+                    if not vardict[-1][1]:
+                        curr_prefix += prefix
+                        for k, v in drv.configuration_steps().items():
+                            vardict.append((curr_prefix + k, v))
+                        curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
                     curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
                 curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
             # Add comm information
@@ -684,7 +693,8 @@ def func(cls, args, return_str=False):
                 # Environment variabless
                 env_vars = ['CONDA_PREFIX', 'CONDA', 'SDKROOT', 'CC',
                             'CXX', 'FC', 'GFORTRAN', 'DISPLAY', 'CL',
-                            '_CL_']
+                            '_CL_', 'LD', 'CFLAGS', 'CXXFLAGS',
+                            'LDFLAGS']
                 if platform._is_win:  # pragma: windows
                     env_vars += ['VCPKG_ROOT']
                 vardict.append(('Environment variables:', ''))
@@ -693,8 +703,9 @@ def func(cls, args, return_str=False):
                     vardict.append(
                         (curr_prefix + k, os.environ.get(k, None)))
                 curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
-                # Git location
-                vardict.append(('Git Location:', shutil.which('git')))
+                # Locations of executables
+                for x in ['git', 'mpiexec', 'mpicc']:
+                    vardict.append((f'{x} Location:', shutil.which(x)))
                 # Conda info
                 if os.environ.get('CONDA_PREFIX', ''):
                     if platform._is_win:  # pragma: windows
@@ -826,6 +837,12 @@ def func(cls, args, return_str=False):
                              curr_prefix + prefix,
                              ("\n" + curr_prefix + prefix).join(
                                  out.splitlines(False)))))
+                # System config vars
+                vardict.append(('Sysconfig Vars:', ''))
+                curr_prefix += prefix
+                for k, v in sysconfig.get_config_vars().items():
+                    vardict.append((curr_prefix + k, v))
+                curr_prefix = curr_prefix.rsplit(prefix, 1)[0]
         finally:
             # Print things
             max_len = max(len(x[0]) for x in vardict)
@@ -1076,17 +1093,29 @@ class ygginstall(SubCommand):
 
     name = "install"
     help = "Complete yggdrasil installation for one or more languages."
+    arguments = (
+        [(('languages', ),
+          {'nargs': '*',
+           # 'choices': ['all'] + LANGUAGES_WITH_ALIASES.get('all', []),
+           'default': [],
+           'help': 'One or more languages that should be configured.'}),
+         (('--no-import', ),
+          {'action': 'store_true',
+           'help': ('Don\'t import the yggdrasil package in '
+                    'calling the installation script.')}),
+         ]
+    )
 
     @classmethod
-    def get_parser(cls, **kwargs):
+    def add_arguments(cls, parser, **kwargs):
         from yggdrasil.languages import install_languages
-        # TODO: Migrate
-        return install_languages.update_argparser()
+        super(ygginstall, cls).add_arguments(parser, **kwargs)
+        install_languages.update_argparser(parser=parser)
 
     @classmethod
     def func(cls, args):
         from yggdrasil.languages import install_languages
-        languages = args.language
+        languages = args.languages
         if (((isinstance(languages, str) and (languages == 'all'))
              or (isinstance(languages, list) and ('all' in languages)))):
             languages = [x.lower() for x in
diff --git a/yggdrasil/communication/CommBase.py b/yggdrasil/communication/CommBase.py
index 75fae24ff..94885b70b 100755
--- a/yggdrasil/communication/CommBase.py
+++ b/yggdrasil/communication/CommBase.py
@@ -2474,11 +2474,10 @@ def recv_dict(self, *args, **kwargs):
         return_message_object = kwargs.pop('return_message_object', False)
         kwargs['return_message_object'] = True
         msg = self.recv(*args, **kwargs)
+        msg_dict = msg.args
         if msg.flag == FLAG_SUCCESS:
             msg_dict = self.coerce_to_dict(msg.args, key_order,
                                            copy.deepcopy(msg.header))
-        else:
-            msg_dict = msg.args
         out = copy.deepcopy(msg)
         out.args = msg_dict
         if not return_message_object:
diff --git a/yggdrasil/communication/ZMQComm.py b/yggdrasil/communication/ZMQComm.py
index 4ad40205d..6e2cf9f05 100755
--- a/yggdrasil/communication/ZMQComm.py
+++ b/yggdrasil/communication/ZMQComm.py
@@ -478,7 +478,7 @@ def _init_before_open(self, context=None, socket_type=None,
                 socket_action = 'bind'
             else:
                 socket_action = 'connect'
-        if new_process:
+        if new_process:  # pragma: sbml
             self.context = zmq.Context()
             set_context_opts(self.context)
         else:
@@ -1232,7 +1232,7 @@ def _recv(self, **kwargs):
                     self.special_debug(("Socket could not receive. "
                                         "(errno=%d)"), e.errno)  # pragma: debug
                     self.info("zmq error: %s", e)  # pragma: debug
-                    raise
+                    raise  # pragma: debug
             # Check for server sign-on
             if total_msg.startswith(ZMQProxy.server_signon_msg):
                 if self.cli_address is None:
diff --git a/yggdrasil/drivers/CMakeModelDriver.py b/yggdrasil/drivers/CMakeModelDriver.py
index 430013787..9e833394a 100644
--- a/yggdrasil/drivers/CMakeModelDriver.py
+++ b/yggdrasil/drivers/CMakeModelDriver.py
@@ -960,12 +960,6 @@ def get_target_language_info(cls, target_compiler_flags=None,
                 linker_flag_kwargs['use_library_path_internal'], [])
             internal_library_flags = linker_flag_kwargs[
                 linker_flag_kwargs['use_library_path_internal']]
-        # Add python flags
-        python_flags = sysconfig.get_config_var('LIBS')
-        if python_flags:
-            for x in python_flags.split():
-                if x.startswith(('-L', '-l')) and (x not in target_linker_flags):
-                    target_linker_flags.append(x)
         # Link local lib on MacOS because on Mac >=10.14 setting sysroot
         # clobbers the default paths.
         # https://stackoverflow.com/questions/54068035/linking-not-working-in
@@ -985,6 +979,14 @@ def get_target_language_info(cls, target_compiler_flags=None,
                                + internal_library_flags),
                 external_library_flags=external_library_flags,
                 internal_library_flags=internal_library_flags)
+        # Add python flags
+        if out['compiler'].env_matches_tool(use_sysconfig=True):
+            python_flags = sysconfig.get_config_var('LIBS')
+            if python_flags:
+                for x in python_flags.split():
+                    if ((x.startswith(('-L', '-l'))
+                         and (x not in target_linker_flags))):
+                        target_linker_flags.append(x)
         for k in constants.LANGUAGES['compiled']:
             if k == out['driver'].language:
                 continue
diff --git a/yggdrasil/drivers/CModelDriver.py b/yggdrasil/drivers/CModelDriver.py
index 04a7dbcb6..d2fdc4a52 100755
--- a/yggdrasil/drivers/CModelDriver.py
+++ b/yggdrasil/drivers/CModelDriver.py
@@ -158,7 +158,7 @@ class GCCCompiler(CCompilerBase):
             + list(CCompilerBase.linker_attributes.get('flag_options', {}).items())
             + [('library_rpath', '-Wl,-rpath')]))
     toolset = 'gnu'
-    aliases = ['gnu-cc']
+    aliases = ['gnu-cc', 'gnu-gcc']
 
     @classmethod
     def is_installed(cls):
@@ -236,8 +236,10 @@ def get_flags(cls, *args, **kwargs):
         out = super(ClangCompiler, cls).get_flags(*args, **kwargs)
         if '-fopenmp' in out:
             idx = out.index('-fopenmp')
-            if (idx > 0) and (out[idx - 1] != '-Xpreprocessor'):
-                out.insert(idx, '-Xpreprocessor')
+            # new_flag = '-Xpreprocessor'
+            new_flag = '-Xclang'
+            if (idx > 0) and (out[idx - 1] != new_flag):
+                out.insert(idx, new_flag)
         return out
         
 
@@ -410,6 +412,15 @@ def get_flags(cls, *args, **kwargs):
             out.append('-lstdc++')
         if '-fopenmp' in out:
             out[out.index('-fopenmp')] = '-lomp'
+            if 'conda' not in cls.get_executable(full_path=True):
+                result = subprocess.check_output(
+                    "find /usr/local -xdev -name '*libomp*'",
+                    shell=True).splitlines()
+                for x in result:
+                    x_dir, x_file = os.path.split(x.decode("utf-8"))
+                    if x_file.endswith(('libomp.dylib', 'libomp.a')):
+                        out.append(f'-L{x_dir}')
+                        break
         return out
 
 
diff --git a/yggdrasil/drivers/CPPModelDriver.py b/yggdrasil/drivers/CPPModelDriver.py
index 4c06fcdae..0959589d8 100644
--- a/yggdrasil/drivers/CPPModelDriver.py
+++ b/yggdrasil/drivers/CPPModelDriver.py
@@ -18,19 +18,35 @@ class CPPCompilerBase(CCompilerBase):
     default_executable = None
 
     @classmethod
-    def add_standard_flag(cls, flags):
-        r"""Add a standard flag to the list of flags.
+    def find_standard_flag(cls, flags):
+        r"""Locate the standard flag in a list of flags.
 
         Args:
             flags (list): Compilation flags.
 
+        Returns:
+            int: Index of the standard flag. -1 if not present.
+
         """
-        std_flag = None
         for i, a in enumerate(flags):
             if a.startswith('-std='):
-                std_flag = i
-                break
-        if std_flag is None:
+                return i
+        return -1
+
+    @classmethod
+    def handle_standard_flag(cls, flags, skip_standard_flag=False):
+        r"""Add or remove standard flag from a list of flags.
+
+        Args:
+            flags (list): Compilation flags.
+            skip_standard_flag (bool, optional): If True, the C++
+                standard flag will not be added. Defaults to False.
+
+        """
+        std_flag_idx = cls.find_standard_flag(flags)
+        if skip_standard_flag and (std_flag_idx != -1):
+            del flags[std_flag_idx]
+        elif (not skip_standard_flag) and (std_flag_idx == -1):
             flags.append('-std=%s' % cls.cpp_std)
         return flags
 
@@ -55,9 +71,8 @@ def get_flags(cls, skip_standard_flag=False, **kwargs):
 
         """
         out = super(GPPCompiler, cls).get_flags(**kwargs)
-        # Add standard library flag
-        if not skip_standard_flag:
-            out = cls.add_standard_flag(out)
+        # Add/remove standard library flag
+        out = cls.handle_standard_flag(out, skip_standard_flag)
         return out
 
 
@@ -94,9 +109,8 @@ def get_flags(cls, skip_standard_flag=False, **kwargs):
 
         """
         out = super(ClangPPCompiler, cls).get_flags(**kwargs)
-        # Add standard library flag
-        if not skip_standard_flag:
-            out = cls.add_standard_flag(out)
+        # Add/remove standard library flag
+        out = cls.handle_standard_flag(out, skip_standard_flag)
         return out
         
     @classmethod
@@ -116,7 +130,7 @@ def get_executable_command(cls, args, skip_flags=False, unused_kwargs=None,
             str: Output to stdout from the command execution.
 
         """
-        if platform._is_win:  # pragma: windows
+        if platform._is_win or platform._is_mac:
             for a in args:
                 if a.endswith('.c'):
                     kwargs['skip_standard_flag'] = True
@@ -539,6 +553,6 @@ def get_testing_options(cls, **kwargs):
             else:
                 out['deps'].append('doxygen')
         out['write_try_except_kwargs'] = {'error_type': '...'}
-        out['kwargs'].setdefault('compiler_flags', [])
-        out['kwargs']['compiler_flags'].append('-std=c++11')
+        # out['kwargs'].setdefault('compiler_flags', [])
+        # out['kwargs']['compiler_flags'].append('-std=c++11')
         return out
diff --git a/yggdrasil/drivers/CompiledModelDriver.py b/yggdrasil/drivers/CompiledModelDriver.py
index 2d5339a90..b47bc99d4 100644
--- a/yggdrasil/drivers/CompiledModelDriver.py
+++ b/yggdrasil/drivers/CompiledModelDriver.py
@@ -8,6 +8,7 @@
 import shutil
 import contextlib
 import threading
+import sysconfig
 from collections import OrderedDict
 from yggdrasil import platform, tools, scanf
 from yggdrasil.drivers.ModelDriver import ModelDriver, remove_products
@@ -398,10 +399,12 @@ def before_registration(cls):
         for k in attr_list:
             # Copy so that list modification is not propagated to subclasses
             setattr(cls, k, copy.deepcopy(getattr(cls, k, [])))
-        # Set attributes based on environment variables
-        if cls.env_matches_tool():
-            cls.default_executable = os.environ[cls.default_executable_env].split(
-                'ccache ')[-1]
+        # Set attributes based on environment variables or sysconfig
+        if cls.default_executable is None:
+            cls.default_executable = cls.env_matches_tool()
+        if cls.default_executable is None:
+            cls.default_executable = cls.env_matches_tool(
+                use_sysconfig=True)
         # Set default_executable to name
         if cls.default_executable is None:
             cls.default_executable = cls.toolname
@@ -488,12 +491,16 @@ def set_env(cls, existing=None, **kwargs):
             existing = {}
             existing.update(os.environ)
         if not cls.env_matches_tool():
+            config_vars = {}
+            cls.env_matches_tool(use_sysconfig=True, env=config_vars)
             env = getattr(cls, 'default_flags_env', None)
             if env is not None:
                 if not isinstance(env, list):
                     env = [env]
                 for ienv in env:
                     existing.pop(ienv, [])
+                    if ienv in config_vars:
+                        existing[ienv] = config_vars[ienv]
         return existing
 
     @classmethod
@@ -673,40 +680,63 @@ def is_installed(cls):
         return (exec_path is not None)
 
     @classmethod
-    def env_matches_tool(cls):
+    def env_matches_tool(cls, use_sysconfig=False, env=None,
+                         with_flags=False):
         r"""Determine if the executable pointed to by any environment
         variable matches this compilation tool.
 
+        Args:
+            use_sysconfig (bool, optional): If True, check the
+                sysconfig variables, otherwise check os.environ.
+                Defaults to False.
+            env (dict, optional): Existing dictionary that should be
+                updated with variables. Defaults to None and is ignored.
+            with_flags (bool, optional): If True, preserve any flags
+                included in the environment variable. Defaults to False.
+
         Returns:
             bool: True if the environment variable matches, False otherwise.
 
         """
+        if env is None:
+            env = {}
+        if use_sysconfig:
+            env.update(sysconfig.get_config_vars())
+        else:
+            env.update(os.environ)
         tool_base = cls.aliases.copy()
         envi_base = ''
+        envi_full = ''
         if isinstance(cls.toolname, str):
             tool_base.append(cls.toolname)
         if isinstance(cls.default_executable, str):
             tool_base.append(cls.default_executable)
         if isinstance(cls.default_executable_env, str):
-            envi_base = os.path.basename(
-                os.environ.get(cls.default_executable_env, '').split('ccache ')[-1])
+            envi_full = env.get(cls.default_executable_env, '').split(
+                'ccache ')[-1]
+        if envi_full:
+            envi_base = os.path.basename(envi_full.split(maxsplit=1)[0])
         if os.environ.get('PATHEXT', ''):
             tool_base = [x.split(os.environ['PATHEXT'])[0]
                          for x in tool_base]
             envi_base = envi_base.split(os.environ['PATHEXT'])[0]
-        out = False
+        out = None
         regex_literal = '-+*$%#@!^&(){}[]<>,.;:'
         regex_pathsep = r'(?:[\-\_\.0-9])'
         if tool_base and envi_base:
             for x in tool_base:
                 for k in regex_literal:
                     x = x.replace(k, '\\' + k)
-                regex = r'(?:(?:^)|%s)%s(?:(?:$)|%s)' % (regex_pathsep, x,
-                                                         regex_pathsep)
+                regex = r'(?:(?:^)|%s)%s(?:(?:$)|%s)' % (
+                    regex_pathsep, x, regex_pathsep)
                 if re.search(regex, envi_base):
                     out = True
                     break
             # out = envi_base.endswith(tuple(tool_base))
+        if out:
+            if not with_flags:
+                envi_full = envi_full.split(maxsplit=1)[0]
+            return envi_full
         return out
 
     @classmethod
@@ -718,13 +748,19 @@ def get_env_flags(cls):
 
         """
         out = []
-        if cls.env_matches_tool():
+        env_dict = {}
+        exe = cls.env_matches_tool(env=env_dict, with_flags=True)
+        if not exe:
+            exe = cls.env_matches_tool(env=env_dict, with_flags=True,
+                                       use_sysconfig=True)
+        if exe and env_dict:
+            out += exe.split()[1:]
             env = getattr(cls, 'default_flags_env', None)
             if env is not None:
                 if not isinstance(env, list):
                     env = [env]
                 for ienv in env:
-                    new_val = os.environ.get(ienv, '').split()
+                    new_val = env_dict.get(ienv, '').split()
                     out += [v for v in new_val if v not in out]
         return out
 
@@ -818,11 +854,12 @@ def get_executable(cls, full_path=False):
             out = cls.default_executable
             if cls.languages:
                 out = ygg_cfg.get(cls.languages[0],
-                                  '%s_executable' % cls.toolname,
+                                  f'{cls.toolname}_executable',
                                   out)
         if out is None:
-            raise NotImplementedError("Executable not set for %s '%s'."
-                                      % (cls.tooltype, cls.toolname))
+            raise NotImplementedError(f"Executable not set for "
+                                      f"{cls.tooltype} "
+                                      f"'{cls.tooltype}'.")
         if full_path:
             out = shutil.which(out)
         return out
@@ -3394,26 +3431,26 @@ def is_library_installed(cls, lib, cfg=None):
                 continue
             if not out:  # pragma: no cover
                 break
-            lib_opt = '%s_%s' % (lib, lib_typ)
+            lib_opt = f'{lib}_{lib_typ}'
             out = (cfg.get(dep_lang, lib_opt, None) is not None)
         return out
         
     @classmethod
-    def is_configured(cls):
-        r"""Determine if the appropriate configuration has been performed (e.g.
-        installation of supporting libraries etc.)
+    def configuration_steps(cls):
+        r"""Get a list of configuration steps with tuples of flags and
+        boolean values.
 
         Returns:
-            bool: True if the language has been configured.
+            OrderedDict: Pairs of descriptions and states for
+                different steps in the configuration all steps must be
+                True for the language to be configured.
 
         """
-        out = super(CompiledModelDriver, cls).is_configured()
+        out = super(CompiledModelDriver, cls).configuration_steps()
         for k in cls.get_external_libraries():
-            if not out:  # pragma: no cover
-                break
-            out = cls.is_library_installed(k)
+            out[str(k)] = cls.is_library_installed(k)
         return out
-
+        
     @classmethod
     def is_tool_installed(cls, tooltype):
         r"""Determine if a compilation tool of a certain is installed for
diff --git a/yggdrasil/drivers/DummyModelDriver.py b/yggdrasil/drivers/DummyModelDriver.py
index 41653ac3c..692363d07 100644
--- a/yggdrasil/drivers/DummyModelDriver.py
+++ b/yggdrasil/drivers/DummyModelDriver.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
 from yggdrasil.drivers.InterpretedModelDriver import InterpretedModelDriver
 
 
@@ -31,16 +32,17 @@ def is_language_installed(self):
         return True
 
     @classmethod
-    def is_configured(cls):
-        r"""Determine if the appropriate configuration has been performed (e.g.
-        installation of supporting libraries etc.)
+    def configuration_steps(cls):
+        r"""Get a list of configuration steps with tuples of flags and
+        boolean values.
 
         Returns:
-            bool: True if the language has been configured.
+            OrderedDict: Pairs of descriptions and states for
+                different steps in the configuration all steps must be
+                True for the language to be configured.
 
         """
-        # There are not any config options
-        return True
+        return OrderedDict()
 
     @classmethod
     def language_version(cls, **kwargs):
diff --git a/yggdrasil/drivers/ExecutableModelDriver.py b/yggdrasil/drivers/ExecutableModelDriver.py
index 045ca3692..974972296 100644
--- a/yggdrasil/drivers/ExecutableModelDriver.py
+++ b/yggdrasil/drivers/ExecutableModelDriver.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
 import platform as sys_platform
 from yggdrasil import platform
 from yggdrasil.drivers.ModelDriver import ModelDriver
@@ -80,12 +81,14 @@ def is_language_installed(cls):
         return True
 
     @classmethod
-    def is_configured(cls):
-        r"""Determine if the appropriate configuration has been performed (e.g.
-        installation of supporting libraries etc.)
+    def configuration_steps(cls):
+        r"""Get a list of configuration steps with tuples of flags and
+        boolean values.
 
         Returns:
-            bool: True if the language has been configured.
+            OrderedDict: Pairs of descriptions and states for
+                different steps in the configuration all steps must be
+                True for the language to be configured.
 
         """
-        return True
+        return OrderedDict()
diff --git a/yggdrasil/drivers/MPIPartnerModel.py b/yggdrasil/drivers/MPIPartnerModel.py
index 1c7f4a387..8f0891248 100644
--- a/yggdrasil/drivers/MPIPartnerModel.py
+++ b/yggdrasil/drivers/MPIPartnerModel.py
@@ -1,3 +1,4 @@
+from collections import OrderedDict
 import shutil
 from yggdrasil.components import import_component
 from yggdrasil.drivers.ModelDriver import ModelDriver
@@ -44,15 +45,17 @@ def is_language_installed(self):
         return (MPI is not None)
 
     @classmethod
-    def is_configured(cls):
-        r"""Determine if the appropriate configuration has been performed (e.g.
-        installation of supporting libraries etc.)
+    def configuration_steps(cls):
+        r"""Get a list of configuration steps with tuples of flags and
+        boolean values.
 
         Returns:
-            bool: True if the language has been configured.
+            OrderedDict: Pairs of descriptions and states for
+                different steps in the configuration all steps must be
+                True for the language to be configured.
 
         """
-        return True
+        return OrderedDict()
 
     @classmethod
     def is_comm_installed(cls, **kwargs):
diff --git a/yggdrasil/drivers/ModelDriver.py b/yggdrasil/drivers/ModelDriver.py
index 92007492e..621c80bd0 100755
--- a/yggdrasil/drivers/ModelDriver.py
+++ b/yggdrasil/drivers/ModelDriver.py
@@ -885,6 +885,8 @@ def install_dependency(cls, package=None, package_manager=None,
         if package_manager is None:
             if tools.get_conda_prefix():
                 package_manager = 'conda'
+                if shutil.which('mamba'):
+                    package_manager = 'mamba'
             elif platform._is_mac:
                 package_manager = 'brew'
             elif platform._is_linux:
@@ -895,11 +897,11 @@ def install_dependency(cls, package=None, package_manager=None,
         cmd_kwargs = {}
         if command:
             cmd = copy.copy(command)
-        elif package_manager == 'conda':
-            cmd = ['conda', 'install'] + package
+        elif package_manager in ('conda', 'mamba'):
+            cmd = [package_manager, 'install'] + package
             if platform._is_win:  # pragma: windows
-                # Conda commands must be run on the shell on windows as it
-                # is implemented as a batch script
+                # Conda/mamba commands must be run on the shell on
+                # windows as it is implemented as a batch script
                 cmd.insert(0, 'call')
                 cmd_kwargs['shell'] = True
             yes_cmd = ['-y']
@@ -1268,6 +1270,27 @@ def is_library_installed(cls, lib, **kwargs):
     def is_disabled(cls):
         return (cls.cfg.get(cls.language, 'disable', 'false').lower() == 'true')
 
+    @classmethod
+    def configuration_steps(cls):
+        r"""Get a list of configuration steps with tuples of flags and
+        boolean values.
+
+        Returns:
+            OrderedDict: Pairs of descriptions and states for
+                different steps in the configuration all steps must be
+                True for the language to be configured.
+
+        """
+        out = OrderedDict([
+            ('Not Disabled', not cls.is_disabled()),
+            ('Config Section', cls.cfg.has_section(cls.language))])
+        config_keys = list(cls._config_keys)
+        if out['Config Section'] and len(cls.base_languages) == 0:
+            config_keys.insert(0, 'commtypes')
+        for k in config_keys:
+            out[k] = (cls.cfg.get(cls.language, k, None) is not None)
+        return out
+
     @classmethod
     def is_configured(cls):
         r"""Determine if the appropriate configuration has been performed (e.g.
@@ -1277,18 +1300,7 @@ def is_configured(cls):
             bool: True if the language has been configured.
 
         """
-        # Check for section & diable
-        disable_flag = cls.is_disabled()
-        out = (cls.cfg.has_section(cls.language) and (not disable_flag))
-        # Check for commtypes
-        if out and (len(cls.base_languages) == 0):
-            out = (cls.cfg.get(cls.language, 'commtypes', None) is not None)
-        # Check for config keys
-        for k in cls._config_keys:
-            if not out:  # pragma: no cover
-                break
-            out = (cls.cfg.get(cls.language, k, None) is not None)
-        return out
+        return all(v for v in cls.configuration_steps().values())
 
     @classmethod
     def is_comm_installed(cls, commtype=None, skip_config=False, **kwargs):
diff --git a/yggdrasil/languages/install_languages.py b/yggdrasil/languages/install_languages.py
index 42b1dd2e7..3b29b1fc6 100644
--- a/yggdrasil/languages/install_languages.py
+++ b/yggdrasil/languages/install_languages.py
@@ -160,6 +160,8 @@ def update_argparser(parser=None, language=None, no_import=None,
             language = get_language_directories()
     else:
         args = parser.parse_known_args(args=arglist)[0]
+        args.language = getattr(
+            args, 'languages', getattr(args, 'language', None))
         if no_import is None:
             no_import = args.no_import
         if language is None:
diff --git a/yggdrasil/multitasking.py b/yggdrasil/multitasking.py
index 69f8d03c6..899ce9a06 100644
--- a/yggdrasil/multitasking.py
+++ b/yggdrasil/multitasking.py
@@ -1809,3 +1809,38 @@ def terminate(self, *args, **kwargs):
         self.debug("terminate")
         self.set_break_flag()
         super(YggTaskLoop, self).terminate(*args, **kwargs)
+
+
+class MemoryTracker(YggTaskLoop):
+    r"""Class to track the memory used by another process.
+
+    Args:
+        pid (int): Process ID for the process that should be tracked.
+        interval (float, optional): Time (in seconds) that should be
+            waiting between memory usage checks. Defaults to 1 second.
+
+    """
+
+    def __init__(self, pid, interval=1.0):
+        self.track_pid = pid
+        self.track_interval = interval
+        self.record = []
+        super(MemoryTracker, self).__init__(target=self.record_memory)
+
+    def record_memory(self):
+        import psutil
+        if not psutil.pid_exists(self.track_pid):  # pragma: debug
+            self.set_break_flag()
+            return
+        process = psutil.Process(self.track_pid)
+        children = process.children(recursive=True)
+        total_mem = process.memory_info().rss / 1024 ** 2
+        child_mem = 0
+        for x in children:
+            child_mem += x.memory_info().rss / 1024 ** 2
+        self.record.append(total_mem + child_mem)
+        sleep(self.track_interval)
+
+    @property
+    def max_memory(self):
+        return max(self.record)
diff --git a/yggdrasil/services.py b/yggdrasil/services.py
index 7768cf2fa..e0ee1d790 100644
--- a/yggdrasil/services.py
+++ b/yggdrasil/services.py
@@ -12,7 +12,8 @@
 import logging
 from yggdrasil import runner
 from yggdrasil import platform
-from yggdrasil.multitasking import wait_on_function, ValueEvent
+from yggdrasil.multitasking import (
+    wait_on_function, ValueEvent, MemoryTracker)
 from yggdrasil.tools import YggClass, kill
 from yggdrasil.config import ygg_cfg
 
@@ -129,7 +130,8 @@ def set_log_level(self, log_level):
         logging.basicConfig(level=log_level)
 
     def start_server(self, remote_url=None, with_coverage=False,
-                     log_level=None, model_repository=None):
+                     log_level=None, model_repository=None,
+                     track_memory=False):
         r"""Start the server.
 
         Args:
@@ -145,6 +147,9 @@ def start_server(self, remote_url=None, with_coverage=False,
             model_repository (str, optional): URL of directory in a Git
                 repository containing YAMLs that should be added to the model
                 registry. Defaults to None and is ignored.
+            track_memory (boolean, optional): If True, the memory used
+                by the server will be reported at shutdown. Defaults
+                to False.
 
         """
         if remote_url is None:
@@ -159,6 +164,7 @@ def start_server(self, remote_url=None, with_coverage=False,
             self.set_log_level(log_level)
         if with_coverage:  # pragma: testing
             def handle_shutdown(sig, frame):
+                self.cleanup_server(track_memory=track_memory)
                 sys.exit()
             signal.signal(_shutdown_signal, handle_shutdown)
             # try:
@@ -167,10 +173,26 @@ def handle_shutdown(sig, frame):
             # except ImportError:  # pragma: debug
             #     pass
         self._is_running = True
+        if track_memory:
+            track_memory = MemoryTracker(os.getpid())
+            track_memory.start()
         try:
             self.run_server()
         finally:
-            self._is_running = False
+            self.cleanup_server(track_memory=track_memory)
+
+    def cleanup_server(self, track_memory=False):
+        r"""Cleanup server process after it finishes.
+
+        Args:
+            track_memory (MemoryTracker, optional): If provided,
+                print information about memory usage.
+
+        """
+        if track_memory and self._is_running:
+            track_memory.terminate()
+            print(f"Max memory usage: {track_memory.max_memory} MB")
+        self._is_running = False
 
     def run_server(self):
         r"""Begin listening for requests."""
diff --git a/yggdrasil/yamlfile.py b/yggdrasil/yamlfile.py
index fae1ef337..f841ccdae 100644
--- a/yggdrasil/yamlfile.py
+++ b/yggdrasil/yamlfile.py
@@ -329,6 +329,11 @@ def parse_yaml(files, complete_partial=False, partial_commtype=None,
             existing = parse_component(yml, k[:-1], existing=existing)
     # Exit early
     if model_only:
+        for x in yml_norm['models']:
+            for io in ['inputs', 'outputs']:
+                x[io] = [z for z in x[io] if not z.get('is_default', False)]
+                if not x[io]:
+                    del x[io]
         return yml_norm
     # Add stand-in model that uses unpaired channels
     if complete_partial:


Test patch:
diff --git a/.github/workflows/test-install.yml b/.github/workflows/test-install.yml
index c3b875b94..197193fe6 100644
--- a/.github/workflows/test-install.yml
+++ b/.github/workflows/test-install.yml
@@ -10,11 +10,9 @@ name: Test of package installation & execution
     - '*'
   schedule:
   - cron: 0 10 * * 1
+  - cron: 0 10 1 * *
 env:
   NUMPY: numpy
-  MATPLOTLIB: matplotlib
-  MPLBACKEND: agg
-  JSONSCHEMA: jsonschema
   INSTALLR: 1
   INSTALLC: 1
   INSTALLFORTRAN: 1
@@ -38,8 +36,8 @@ jobs:
       os-versions: ${{ steps.set-os-versions.outputs.versions }}
     steps:
     - id: set-os-versions
-      run: echo "::set-output name=versions::{\"ubuntu\":\"latest\", \"macos\":\"latest\",
-        \"windows\":\"2019\"}"
+      run: echo "versions={\"ubuntu\":\"latest\", \"macos\":\"latest\", \"windows\":\"2019\"}"
+        >> $GITHUB_OUTPUT
   test_pip:
     needs: params
     name: Test (${{ matrix.python-version }}, ${{ matrix.os-base }}, ${{ matrix.install-method
@@ -69,6 +67,8 @@ jobs:
         - true
         install-mpi:
         - true
+        install-sbml:
+        - false
         include:
         - os-base: windows
           python-version: 3.7
@@ -77,19 +77,25 @@ jobs:
           test-flags2: ''
           install-c: false
           install-mpi: false
+          install-sbml: false
       fail-fast: false
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set C installation based on matrix value
-      if: matrix.install-c != true && matrix.install-method != 'conda'
+      if: matrix.install-c != true && matrix.install-method != 'conda' && matrix.install-method
+        != 'mamba'
       run: echo "INSTALLC=0" >> $GITHUB_ENV
       shell: bash -l {0}
     - name: Set MPI installation based on matrix value
       if: matrix.install-mpi != true
       run: echo "INSTALLMPI=0" >> $GITHUB_ENV
       shell: bash -l {0}
+    - name: Set SBML installation based on matrix value
+      if: matrix.install-sbml == true
+      run: echo "INSTALLSBML=1" >> $GITHUB_ENV
+      shell: bash -l {0}
     - name: Store an ID string for the job
       run: echo "YGG_GHA_JOB_ID=${{ runner.os }}-${{ matrix.install-method }}-py${{
         matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
@@ -114,11 +120,11 @@ jobs:
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLMPI
         == 1
     - name: Install R
-      uses: r-lib/actions/setup-r@v1
+      uses: r-lib/actions/setup-r@v2
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLR
         == 1
     - name: (LINUX,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
       with:
         path: ~/.cache/pip
@@ -127,7 +133,7 @@ jobs:
 
           '
     - name: (MACOS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
       with:
         path: ~/Library/Caches/pip
@@ -136,7 +142,7 @@ jobs:
 
           '
     - name: (WINDOWS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
       with:
         path: ~\AppData\Local\pip\Cache
@@ -145,7 +151,7 @@ jobs:
 
           '
     - name: (PIP) Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       if: matrix.install-method == 'pip'
       with:
         python-version: ${{ matrix.python-version }}
@@ -181,77 +187,66 @@ jobs:
       run: yggconfig --c-compiler=/usr/local/opt/llvm/bin/clang --c++-compiler=/usr/local/opt/llvm/bin/clang++
     - name: (WINDOWS,CONDA) Set up miniconda base environment
       uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda' && runner.os == 'Windows'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba')
       with:
+        auto-update-conda: true
+        python-version: ${{ matrix.python-version }}
         channels: conda-forge
         use-only-tar-bz2: true
-        activate-environment: ''
+        channel-priority: strict
+        miniforge-variant: Mambaforge
+        use-mamba: true
     - name: (WINDOWS,CONDA) Set up MSVC Compiler
       uses: ilammy/msvc-dev-cmd@v1
-      if: matrix.install-method == 'conda' && runner.os == 'Windows' && env.INSTALLC
-        == 1
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'Windows' && env.INSTALLC == 1
       with:
         toolset: 14.0
-    - name: (UNIX,CONDA) Add miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
-
-        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
-
-        '
-      shell: bash -l {0}
-    - name: (MACOS,CONDA) Change ownership of conda on MacOS
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
-      run: sudo chown -R $(whoami) $CONDA
-      shell: bash
     - name: (CONDA) Build package in base environment
       id: conda_build
       continue-on-error: true
-      if: matrix.install-method == 'conda'
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
+
+        '
     - name: (CONDA) Build package in base environment (re-attempt)
-      if: ${{ steps.conda_build.outcome == 'failure' && matrix.install-method == 'conda'}}
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
-    - name: (UNIX,CONDA) Remove miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+      if: ${{ steps.conda_build.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
 
         '
-      shell: bash -l {0}
-    - name: (CONDA) Set up miniconda test environment
-      uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda'
-      with:
-        auto-update-conda: true
-        python-version: ${{ matrix.python-version }}
-        channels: conda-forge
-        use-only-tar-bz2: true
-        channel-priority: strict
     - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
         llvm (7)
-      if: runner.os == 'macOS' && matrix.install-method == 'conda'
+      if: runner.os == 'macOS' && (matrix.install-method == 'conda' || matrix.install-method
+        == 'mamba')
       run: ./utils/setup_old_mac_sdk.sh
     - name: (CONDA) Install dependencies & package from existing build
       id: conda_install
       continue-on-error: true
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (CONDA) Install dependencies & package from existing build (re-attempt)
-      if: ${{ steps.conda_install.outcome == 'failure' && matrix.install-method ==
-        'conda'}}
+      if: ${{ steps.conda_install.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (MACOS,CONDA) Set allow_multiple_omp config
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'macOS'
       run: yggconfig --allow-multiple-omp
     - name: Verify installation
       run: python utils/setup_test_env.py verify
     - name: Compile libraries
       run: yggcompile
     - name: Run cmake test in command prompt
+      timeout-minutes: 120
       if: runner.os == 'Windows'
       shell: cmd /C CALL {0}
       run: 'cmake -h
@@ -260,37 +255,45 @@ jobs:
 
         '
     - name: Run tests (1st)
+      timeout-minutes: 120
       id: test1
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }}
       continue-on-error: true
     - name: Re-run test failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (1st)
+      timeout-minutes: 120
       if: ${{ contains(matrix.test-flags1, '--mpi-script') }}
       id: test1_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Run tests (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' }}
       id: test2
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }}
       continue-on-error: true
     - name: Re-run test failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' && contains(matrix.test-flags2, '--mpi-script')
         }}
       id: test2_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Combine coverage reports
@@ -301,14 +304,14 @@ jobs:
 
         '
     - name: (CONDA) Generate XML version of coverage report
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: '# ls .*
 
         coverage xml
 
         '
     - name: Upload coverage report
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         name: ${{ env.YGG_GHA_JOB_ID }}
         token: ${{ secrets.CODECOV_TOKEN }}
@@ -324,7 +327,7 @@ jobs:
     - name: Create a log of Python package versions
       if: ${{ always() }}
       run: python utils/setup_test_env.py log
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@v3
       if: ${{ success() }}
       with:
         name: environment-log-${{ env.YGG_GHA_JOB_ID }}
@@ -355,6 +358,8 @@ jobs:
         - true
         install-mpi:
         - true
+        install-sbml:
+        - false
         include:
         - os-base: ubuntu
           python-version: 3.6
@@ -363,6 +368,7 @@ jobs:
           test-flags2: ''
           install-c: true
           install-mpi: false
+          install-sbml: false
         - os-base: ubuntu
           python-version: 3.8
           install-method: conda
@@ -370,13 +376,15 @@ jobs:
           test-flags2: ''
           install-c: true
           install-mpi: false
+          install-sbml: false
         - os-base: ubuntu
           python-version: 3.9
           install-method: conda
-          test-flags1: --long-running
-          test-flags2: ''
+          test-flags1: --long-running --remote-service
+          test-flags2: --suites examples --parametrize-language=sbml
           install-c: true
           install-mpi: false
+          install-sbml: true
         - os-base: macos
           python-version: 3.8
           install-method: conda
@@ -384,22 +392,28 @@ jobs:
           test-flags2: tests/test_tools.py
           install-c: true
           install-mpi: false
+          install-sbml: false
       fail-fast: false
     defaults:
       run:
         shell: bash -l {0}
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set C installation based on matrix value
-      if: matrix.install-c != true && matrix.install-method != 'conda'
+      if: matrix.install-c != true && matrix.install-method != 'conda' && matrix.install-method
+        != 'mamba'
       run: echo "INSTALLC=0" >> $GITHUB_ENV
       shell: bash -l {0}
     - name: Set MPI installation based on matrix value
       if: matrix.install-mpi != true
       run: echo "INSTALLMPI=0" >> $GITHUB_ENV
       shell: bash -l {0}
+    - name: Set SBML installation based on matrix value
+      if: matrix.install-sbml == true
+      run: echo "INSTALLSBML=1" >> $GITHUB_ENV
+      shell: bash -l {0}
     - name: Store an ID string for the job
       run: echo "YGG_GHA_JOB_ID=${{ runner.os }}-${{ matrix.install-method }}-py${{
         matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
@@ -424,11 +438,11 @@ jobs:
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLMPI
         == 1
     - name: Install R
-      uses: r-lib/actions/setup-r@v1
+      uses: r-lib/actions/setup-r@v2
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLR
         == 1
     - name: (LINUX,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
       with:
         path: ~/.cache/pip
@@ -437,7 +451,7 @@ jobs:
 
           '
     - name: (MACOS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
       with:
         path: ~/Library/Caches/pip
@@ -446,7 +460,7 @@ jobs:
 
           '
     - name: (WINDOWS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
       with:
         path: ~\AppData\Local\pip\Cache
@@ -455,7 +469,7 @@ jobs:
 
           '
     - name: (PIP) Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       if: matrix.install-method == 'pip'
       with:
         python-version: ${{ matrix.python-version }}
@@ -491,77 +505,66 @@ jobs:
       run: yggconfig --c-compiler=/usr/local/opt/llvm/bin/clang --c++-compiler=/usr/local/opt/llvm/bin/clang++
     - name: (WINDOWS,CONDA) Set up miniconda base environment
       uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda' && runner.os == 'Windows'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba')
       with:
+        auto-update-conda: true
+        python-version: ${{ matrix.python-version }}
         channels: conda-forge
         use-only-tar-bz2: true
-        activate-environment: ''
+        channel-priority: strict
+        miniforge-variant: Mambaforge
+        use-mamba: true
     - name: (WINDOWS,CONDA) Set up MSVC Compiler
       uses: ilammy/msvc-dev-cmd@v1
-      if: matrix.install-method == 'conda' && runner.os == 'Windows' && env.INSTALLC
-        == 1
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'Windows' && env.INSTALLC == 1
       with:
         toolset: 14.0
-    - name: (UNIX,CONDA) Add miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
-
-        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
-
-        '
-      shell: bash -l {0}
-    - name: (MACOS,CONDA) Change ownership of conda on MacOS
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
-      run: sudo chown -R $(whoami) $CONDA
-      shell: bash
     - name: (CONDA) Build package in base environment
       id: conda_build
       continue-on-error: true
-      if: matrix.install-method == 'conda'
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
+
+        '
     - name: (CONDA) Build package in base environment (re-attempt)
-      if: ${{ steps.conda_build.outcome == 'failure' && matrix.install-method == 'conda'}}
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
-    - name: (UNIX,CONDA) Remove miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+      if: ${{ steps.conda_build.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
 
         '
-      shell: bash -l {0}
-    - name: (CONDA) Set up miniconda test environment
-      uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda'
-      with:
-        auto-update-conda: true
-        python-version: ${{ matrix.python-version }}
-        channels: conda-forge
-        use-only-tar-bz2: true
-        channel-priority: strict
     - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
         llvm (7)
-      if: runner.os == 'macOS' && matrix.install-method == 'conda'
+      if: runner.os == 'macOS' && (matrix.install-method == 'conda' || matrix.install-method
+        == 'mamba')
       run: ./utils/setup_old_mac_sdk.sh
     - name: (CONDA) Install dependencies & package from existing build
       id: conda_install
       continue-on-error: true
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (CONDA) Install dependencies & package from existing build (re-attempt)
-      if: ${{ steps.conda_install.outcome == 'failure' && matrix.install-method ==
-        'conda'}}
+      if: ${{ steps.conda_install.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (MACOS,CONDA) Set allow_multiple_omp config
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'macOS'
       run: yggconfig --allow-multiple-omp
     - name: Verify installation
       run: python utils/setup_test_env.py verify
     - name: Compile libraries
       run: yggcompile
     - name: Run cmake test in command prompt
+      timeout-minutes: 120
       if: runner.os == 'Windows'
       shell: cmd /C CALL {0}
       run: 'cmake -h
@@ -570,37 +573,45 @@ jobs:
 
         '
     - name: Run tests (1st)
+      timeout-minutes: 120
       id: test1
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }}
       continue-on-error: true
     - name: Re-run test failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (1st)
+      timeout-minutes: 120
       if: ${{ contains(matrix.test-flags1, '--mpi-script') }}
       id: test1_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Run tests (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' }}
       id: test2
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }}
       continue-on-error: true
     - name: Re-run test failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' && contains(matrix.test-flags2, '--mpi-script')
         }}
       id: test2_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Combine coverage reports
@@ -611,14 +622,14 @@ jobs:
 
         '
     - name: (CONDA) Generate XML version of coverage report
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: '# ls .*
 
         coverage xml
 
         '
     - name: Upload coverage report
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         name: ${{ env.YGG_GHA_JOB_ID }}
         token: ${{ secrets.CODECOV_TOKEN }}
@@ -634,7 +645,7 @@ jobs:
     - name: Create a log of Python package versions
       if: ${{ always() }}
       run: python utils/setup_test_env.py log
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@v3
       if: ${{ success() }}
       with:
         name: environment-log-${{ env.YGG_GHA_JOB_ID }}
@@ -666,6 +677,8 @@ jobs:
         - true
         install-mpi:
         - false
+        install-sbml:
+        - false
       fail-fast: false
     services:
       rabbitmq:
@@ -675,17 +688,22 @@ jobs:
         options: --health-cmd "rabbitmqctl node_health_check" --health-interval 10s
           --health-timeout 5s --health-retries 5
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set C installation based on matrix value
-      if: matrix.install-c != true && matrix.install-method != 'conda'
+      if: matrix.install-c != true && matrix.install-method != 'conda' && matrix.install-method
+        != 'mamba'
       run: echo "INSTALLC=0" >> $GITHUB_ENV
       shell: bash -l {0}
     - name: Set MPI installation based on matrix value
       if: matrix.install-mpi != true
       run: echo "INSTALLMPI=0" >> $GITHUB_ENV
       shell: bash -l {0}
+    - name: Set SBML installation based on matrix value
+      if: matrix.install-sbml == true
+      run: echo "INSTALLSBML=1" >> $GITHUB_ENV
+      shell: bash -l {0}
     - name: Store an ID string for the job
       run: echo "YGG_GHA_JOB_ID=${{ runner.os }}-${{ matrix.install-method }}-py${{
         matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
@@ -710,11 +728,11 @@ jobs:
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLMPI
         == 1
     - name: Install R
-      uses: r-lib/actions/setup-r@v1
+      uses: r-lib/actions/setup-r@v2
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLR
         == 1
     - name: (LINUX,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
       with:
         path: ~/.cache/pip
@@ -723,7 +741,7 @@ jobs:
 
           '
     - name: (MACOS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
       with:
         path: ~/Library/Caches/pip
@@ -732,7 +750,7 @@ jobs:
 
           '
     - name: (WINDOWS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
       with:
         path: ~\AppData\Local\pip\Cache
@@ -741,7 +759,7 @@ jobs:
 
           '
     - name: (PIP) Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       if: matrix.install-method == 'pip'
       with:
         python-version: ${{ matrix.python-version }}
@@ -777,77 +795,66 @@ jobs:
       run: yggconfig --c-compiler=/usr/local/opt/llvm/bin/clang --c++-compiler=/usr/local/opt/llvm/bin/clang++
     - name: (WINDOWS,CONDA) Set up miniconda base environment
       uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda' && runner.os == 'Windows'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba')
       with:
+        auto-update-conda: true
+        python-version: ${{ matrix.python-version }}
         channels: conda-forge
         use-only-tar-bz2: true
-        activate-environment: ''
+        channel-priority: strict
+        miniforge-variant: Mambaforge
+        use-mamba: true
     - name: (WINDOWS,CONDA) Set up MSVC Compiler
       uses: ilammy/msvc-dev-cmd@v1
-      if: matrix.install-method == 'conda' && runner.os == 'Windows' && env.INSTALLC
-        == 1
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'Windows' && env.INSTALLC == 1
       with:
         toolset: 14.0
-    - name: (UNIX,CONDA) Add miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
-
-        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
-
-        '
-      shell: bash -l {0}
-    - name: (MACOS,CONDA) Change ownership of conda on MacOS
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
-      run: sudo chown -R $(whoami) $CONDA
-      shell: bash
     - name: (CONDA) Build package in base environment
       id: conda_build
       continue-on-error: true
-      if: matrix.install-method == 'conda'
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
+
+        '
     - name: (CONDA) Build package in base environment (re-attempt)
-      if: ${{ steps.conda_build.outcome == 'failure' && matrix.install-method == 'conda'}}
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
-    - name: (UNIX,CONDA) Remove miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+      if: ${{ steps.conda_build.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
 
         '
-      shell: bash -l {0}
-    - name: (CONDA) Set up miniconda test environment
-      uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda'
-      with:
-        auto-update-conda: true
-        python-version: ${{ matrix.python-version }}
-        channels: conda-forge
-        use-only-tar-bz2: true
-        channel-priority: strict
     - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
         llvm (7)
-      if: runner.os == 'macOS' && matrix.install-method == 'conda'
+      if: runner.os == 'macOS' && (matrix.install-method == 'conda' || matrix.install-method
+        == 'mamba')
       run: ./utils/setup_old_mac_sdk.sh
     - name: (CONDA) Install dependencies & package from existing build
       id: conda_install
       continue-on-error: true
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (CONDA) Install dependencies & package from existing build (re-attempt)
-      if: ${{ steps.conda_install.outcome == 'failure' && matrix.install-method ==
-        'conda'}}
+      if: ${{ steps.conda_install.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (MACOS,CONDA) Set allow_multiple_omp config
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'macOS'
       run: yggconfig --allow-multiple-omp
     - name: Verify installation
       run: python utils/setup_test_env.py verify
     - name: Compile libraries
       run: yggcompile
     - name: Run cmake test in command prompt
+      timeout-minutes: 120
       if: runner.os == 'Windows'
       shell: cmd /C CALL {0}
       run: 'cmake -h
@@ -856,37 +863,45 @@ jobs:
 
         '
     - name: Run tests (1st)
+      timeout-minutes: 120
       id: test1
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }}
       continue-on-error: true
     - name: Re-run test failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (1st)
+      timeout-minutes: 120
       if: ${{ contains(matrix.test-flags1, '--mpi-script') }}
       id: test1_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Run tests (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' }}
       id: test2
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }}
       continue-on-error: true
     - name: Re-run test failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' && contains(matrix.test-flags2, '--mpi-script')
         }}
       id: test2_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Combine coverage reports
@@ -897,14 +912,14 @@ jobs:
 
         '
     - name: (CONDA) Generate XML version of coverage report
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: '# ls .*
 
         coverage xml
 
         '
     - name: Upload coverage report
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         name: ${{ env.YGG_GHA_JOB_ID }}
         token: ${{ secrets.CODECOV_TOKEN }}
@@ -920,7 +935,7 @@ jobs:
     - name: Create a log of Python package versions
       if: ${{ always() }}
       run: python utils/setup_test_env.py log
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@v3
       if: ${{ success() }}
       with:
         name: environment-log-${{ env.YGG_GHA_JOB_ID }}
@@ -951,6 +966,8 @@ jobs:
         - true
         install-mpi:
         - false
+        install-sbml:
+        - false
       fail-fast: false
     services:
       rabbitmq:
@@ -963,17 +980,22 @@ jobs:
       run:
         shell: bash -l {0}
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set C installation based on matrix value
-      if: matrix.install-c != true && matrix.install-method != 'conda'
+      if: matrix.install-c != true && matrix.install-method != 'conda' && matrix.install-method
+        != 'mamba'
       run: echo "INSTALLC=0" >> $GITHUB_ENV
       shell: bash -l {0}
     - name: Set MPI installation based on matrix value
       if: matrix.install-mpi != true
       run: echo "INSTALLMPI=0" >> $GITHUB_ENV
       shell: bash -l {0}
+    - name: Set SBML installation based on matrix value
+      if: matrix.install-sbml == true
+      run: echo "INSTALLSBML=1" >> $GITHUB_ENV
+      shell: bash -l {0}
     - name: Store an ID string for the job
       run: echo "YGG_GHA_JOB_ID=${{ runner.os }}-${{ matrix.install-method }}-py${{
         matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
@@ -998,11 +1020,11 @@ jobs:
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLMPI
         == 1
     - name: Install R
-      uses: r-lib/actions/setup-r@v1
+      uses: r-lib/actions/setup-r@v2
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLR
         == 1
     - name: (LINUX,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
       with:
         path: ~/.cache/pip
@@ -1011,7 +1033,7 @@ jobs:
 
           '
     - name: (MACOS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
       with:
         path: ~/Library/Caches/pip
@@ -1020,7 +1042,7 @@ jobs:
 
           '
     - name: (WINDOWS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
       with:
         path: ~\AppData\Local\pip\Cache
@@ -1029,7 +1051,7 @@ jobs:
 
           '
     - name: (PIP) Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       if: matrix.install-method == 'pip'
       with:
         python-version: ${{ matrix.python-version }}
@@ -1065,77 +1087,66 @@ jobs:
       run: yggconfig --c-compiler=/usr/local/opt/llvm/bin/clang --c++-compiler=/usr/local/opt/llvm/bin/clang++
     - name: (WINDOWS,CONDA) Set up miniconda base environment
       uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda' && runner.os == 'Windows'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba')
       with:
+        auto-update-conda: true
+        python-version: ${{ matrix.python-version }}
         channels: conda-forge
         use-only-tar-bz2: true
-        activate-environment: ''
+        channel-priority: strict
+        miniforge-variant: Mambaforge
+        use-mamba: true
     - name: (WINDOWS,CONDA) Set up MSVC Compiler
       uses: ilammy/msvc-dev-cmd@v1
-      if: matrix.install-method == 'conda' && runner.os == 'Windows' && env.INSTALLC
-        == 1
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'Windows' && env.INSTALLC == 1
       with:
         toolset: 14.0
-    - name: (UNIX,CONDA) Add miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
-
-        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
-
-        '
-      shell: bash -l {0}
-    - name: (MACOS,CONDA) Change ownership of conda on MacOS
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
-      run: sudo chown -R $(whoami) $CONDA
-      shell: bash
     - name: (CONDA) Build package in base environment
       id: conda_build
       continue-on-error: true
-      if: matrix.install-method == 'conda'
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
+
+        '
     - name: (CONDA) Build package in base environment (re-attempt)
-      if: ${{ steps.conda_build.outcome == 'failure' && matrix.install-method == 'conda'}}
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
-    - name: (UNIX,CONDA) Remove miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+      if: ${{ steps.conda_build.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
 
         '
-      shell: bash -l {0}
-    - name: (CONDA) Set up miniconda test environment
-      uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda'
-      with:
-        auto-update-conda: true
-        python-version: ${{ matrix.python-version }}
-        channels: conda-forge
-        use-only-tar-bz2: true
-        channel-priority: strict
     - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
         llvm (7)
-      if: runner.os == 'macOS' && matrix.install-method == 'conda'
+      if: runner.os == 'macOS' && (matrix.install-method == 'conda' || matrix.install-method
+        == 'mamba')
       run: ./utils/setup_old_mac_sdk.sh
     - name: (CONDA) Install dependencies & package from existing build
       id: conda_install
       continue-on-error: true
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (CONDA) Install dependencies & package from existing build (re-attempt)
-      if: ${{ steps.conda_install.outcome == 'failure' && matrix.install-method ==
-        'conda'}}
+      if: ${{ steps.conda_install.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (MACOS,CONDA) Set allow_multiple_omp config
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'macOS'
       run: yggconfig --allow-multiple-omp
     - name: Verify installation
       run: python utils/setup_test_env.py verify
     - name: Compile libraries
       run: yggcompile
     - name: Run cmake test in command prompt
+      timeout-minutes: 120
       if: runner.os == 'Windows'
       shell: cmd /C CALL {0}
       run: 'cmake -h
@@ -1144,37 +1155,45 @@ jobs:
 
         '
     - name: Run tests (1st)
+      timeout-minutes: 120
       id: test1
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }}
       continue-on-error: true
     - name: Re-run test failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (1st)
+      timeout-minutes: 120
       if: ${{ contains(matrix.test-flags1, '--mpi-script') }}
       id: test1_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Run tests (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' }}
       id: test2
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }}
       continue-on-error: true
     - name: Re-run test failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' && contains(matrix.test-flags2, '--mpi-script')
         }}
       id: test2_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Combine coverage reports
@@ -1185,14 +1204,14 @@ jobs:
 
         '
     - name: (CONDA) Generate XML version of coverage report
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: '# ls .*
 
         coverage xml
 
         '
     - name: Upload coverage report
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         name: ${{ env.YGG_GHA_JOB_ID }}
         token: ${{ secrets.CODECOV_TOKEN }}
@@ -1208,7 +1227,7 @@ jobs:
     - name: Create a log of Python package versions
       if: ${{ always() }}
       run: python utils/setup_test_env.py log
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@v3
       if: ${{ success() }}
       with:
         name: environment-log-${{ env.YGG_GHA_JOB_ID }}
@@ -1248,19 +1267,26 @@ jobs:
         - false
         install-mpi:
         - false
+        install-sbml:
+        - false
       fail-fast: false
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set C installation based on matrix value
-      if: matrix.install-c != true && matrix.install-method != 'conda'
+      if: matrix.install-c != true && matrix.install-method != 'conda' && matrix.install-method
+        != 'mamba'
       run: echo "INSTALLC=0" >> $GITHUB_ENV
       shell: bash -l {0}
     - name: Set MPI installation based on matrix value
       if: matrix.install-mpi != true
       run: echo "INSTALLMPI=0" >> $GITHUB_ENV
       shell: bash -l {0}
+    - name: Set SBML installation based on matrix value
+      if: matrix.install-sbml == true
+      run: echo "INSTALLSBML=1" >> $GITHUB_ENV
+      shell: bash -l {0}
     - name: Store an ID string for the job
       run: echo "YGG_GHA_JOB_ID=${{ runner.os }}-${{ matrix.install-method }}-py${{
         matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{
@@ -1285,11 +1311,11 @@ jobs:
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLMPI
         == 1
     - name: Install R
-      uses: r-lib/actions/setup-r@v1
+      uses: r-lib/actions/setup-r@v2
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLR
         == 1
     - name: (LINUX,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
       with:
         path: ~/.cache/pip
@@ -1298,7 +1324,7 @@ jobs:
 
           '
     - name: (MACOS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
       with:
         path: ~/Library/Caches/pip
@@ -1307,7 +1333,7 @@ jobs:
 
           '
     - name: (WINDOWS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
       with:
         path: ~\AppData\Local\pip\Cache
@@ -1316,7 +1342,7 @@ jobs:
 
           '
     - name: (PIP) Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       if: matrix.install-method == 'pip'
       with:
         python-version: ${{ matrix.python-version }}
@@ -1352,77 +1378,66 @@ jobs:
       run: yggconfig --c-compiler=/usr/local/opt/llvm/bin/clang --c++-compiler=/usr/local/opt/llvm/bin/clang++
     - name: (WINDOWS,CONDA) Set up miniconda base environment
       uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda' && runner.os == 'Windows'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba')
       with:
+        auto-update-conda: true
+        python-version: ${{ matrix.python-version }}
         channels: conda-forge
         use-only-tar-bz2: true
-        activate-environment: ''
+        channel-priority: strict
+        miniforge-variant: Mambaforge
+        use-mamba: true
     - name: (WINDOWS,CONDA) Set up MSVC Compiler
       uses: ilammy/msvc-dev-cmd@v1
-      if: matrix.install-method == 'conda' && runner.os == 'Windows' && env.INSTALLC
-        == 1
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'Windows' && env.INSTALLC == 1
       with:
         toolset: 14.0
-    - name: (UNIX,CONDA) Add miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
-
-        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
-
-        '
-      shell: bash -l {0}
-    - name: (MACOS,CONDA) Change ownership of conda on MacOS
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
-      run: sudo chown -R $(whoami) $CONDA
-      shell: bash
     - name: (CONDA) Build package in base environment
       id: conda_build
       continue-on-error: true
-      if: matrix.install-method == 'conda'
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
+
+        '
     - name: (CONDA) Build package in base environment (re-attempt)
-      if: ${{ steps.conda_build.outcome == 'failure' && matrix.install-method == 'conda'}}
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python
-        ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
-    - name: (UNIX,CONDA) Remove miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: 'echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
+      if: ${{ steps.conda_build.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
+      run: 'conda activate base
+
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python
+        ${{ matrix.python-version }} --use-mamba
 
         '
-      shell: bash -l {0}
-    - name: (CONDA) Set up miniconda test environment
-      uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda'
-      with:
-        auto-update-conda: true
-        python-version: ${{ matrix.python-version }}
-        channels: conda-forge
-        use-only-tar-bz2: true
-        channel-priority: strict
     - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda
         llvm (7)
-      if: runner.os == 'macOS' && matrix.install-method == 'conda'
+      if: runner.os == 'macOS' && (matrix.install-method == 'conda' || matrix.install-method
+        == 'mamba')
       run: ./utils/setup_old_mac_sdk.sh
     - name: (CONDA) Install dependencies & package from existing build
       id: conda_install
       continue-on-error: true
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (CONDA) Install dependencies & package from existing build (re-attempt)
-      if: ${{ steps.conda_install.outcome == 'failure' && matrix.install-method ==
-        'conda'}}
+      if: ${{ steps.conda_install.outcome == 'failure' && (matrix.install-method ==
+        'conda' || matrix.install-method == 'mamba')}}
       run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+        --use-mamba
     - name: (MACOS,CONDA) Set allow_multiple_omp config
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') &&
+        runner.os == 'macOS'
       run: yggconfig --allow-multiple-omp
     - name: Verify installation
       run: python utils/setup_test_env.py verify
     - name: Compile libraries
       run: yggcompile
     - name: Run cmake test in command prompt
+      timeout-minutes: 120
       if: runner.os == 'Windows'
       shell: cmd /C CALL {0}
       run: 'cmake -h
@@ -1431,37 +1446,45 @@ jobs:
 
         '
     - name: Run tests (1st)
+      timeout-minutes: 120
       id: test1
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }}
       continue-on-error: true
     - name: Re-run test failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (1st)
+      timeout-minutes: 120
       if: ${{ contains(matrix.test-flags1, '--mpi-script') }}
       id: test1_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Run tests (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' }}
       id: test2
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }}
       continue-on-error: true
     - name: Re-run test failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none
         --second-attempt
     - name: Run generated test script (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' && contains(matrix.test-flags2, '--mpi-script')
         }}
       id: test2_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
     - name: Combine coverage reports
@@ -1472,14 +1495,14 @@ jobs:
 
         '
     - name: (CONDA) Generate XML version of coverage report
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: '# ls .*
 
         coverage xml
 
         '
     - name: Upload coverage report
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         name: ${{ env.YGG_GHA_JOB_ID }}
         token: ${{ secrets.CODECOV_TOKEN }}
@@ -1495,7 +1518,7 @@ jobs:
     - name: Create a log of Python package versions
       if: ${{ always() }}
       run: python utils/setup_test_env.py log
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@v3
       if: ${{ success() }}
       with:
         name: environment-log-${{ env.YGG_GHA_JOB_ID }}
@@ -1506,11 +1529,11 @@ jobs:
     env:
       BUILDDOCS: 1
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.8
     - name: Install dependencies
@@ -1544,11 +1567,11 @@ jobs:
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.8
     - name: Install dependencies
@@ -1576,14 +1599,15 @@ jobs:
     - test_rmq_conda
     - test_stripped
     - docs
-    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+    if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) || github.event.schedule
+      == '0 10 1 * *'
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.9
     - name: Login to DockerHub
@@ -1607,11 +1631,11 @@ jobs:
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.8
     - name: Install dependencies
diff --git a/tests/communication/test_ForkComm.py b/tests/communication/test_ForkComm.py
index 9c0dc6165..0101dccc8 100644
--- a/tests/communication/test_ForkComm.py
+++ b/tests/communication/test_ForkComm.py
@@ -94,7 +94,8 @@ def n_msg_expected(self, ncomm, send_pattern, recv_pattern):
              and (recv_pattern == 'cycle'))):
             return ncomm
         return 1
-    
+
+    @pytest.mark.flaky(max_runs=5)
     def test_send_recv_eof_no_close(self, send_comm, recv_comm, do_send_recv):
         r"""Test send/recv of EOF message with no close."""
         recv_comm.close_on_eof_recv = False
@@ -110,7 +111,7 @@ def test_send_recv_filter_eof(self, run_once, filtered_comms, send_comm,
                      send_params={'method': 'send_eof'},
                      recv_params={'flag': False,
                                   'kwargs': {'timeout': 2 * timeout}})
-        assert(recv_comm.is_closed)
+        assert recv_comm.is_closed
         
     def test_send_recv_filter_recv_filter(self, filtered_comms,
                                           msg_filter_recv, send_comm,
@@ -137,12 +138,12 @@ def test_send_recv_after_close(self, commtype, send_comm, recv_comm,
             recv_comm.bind()
         send_comm.close()
         recv_comm.close()
-        assert(send_comm.is_closed)
-        assert(recv_comm.is_closed)
+        assert send_comm.is_closed
+        assert recv_comm.is_closed
         flag = send_comm.send([testing_options['msg'] for _ in range(ncomm)])
-        assert(not flag)
+        assert not flag
         flag, msg_recv = recv_comm.recv()
-        assert(not flag)
+        assert not flag
         
     def test_async_gather(self, testing_options, send_pattern, recv_pattern,
                           send_comm, recv_comm, map_sent2recv, timeout):
@@ -152,13 +153,13 @@ def test_async_gather(self, testing_options, send_pattern, recv_pattern,
         test_msg = testing_options['msg']
         send_comm.comm_list[0].send(test_msg[0])
         flag, msg_recv = recv_comm.recv()
-        assert(flag)
-        assert(recv_comm.is_empty_recv(msg_recv))
+        assert flag
+        assert recv_comm.is_empty_recv(msg_recv)
         for msg_send, comm in zip(test_msg[1:], send_comm.comm_list[1:]):
-            assert(comm.send(msg_send))
+            assert comm.send(msg_send)
         flag, msg_recv = recv_comm.recv(timeout=timeout)
-        assert(flag)
-        assert(msg_recv == map_sent2recv(test_msg))
+        assert flag
+        assert msg_recv == map_sent2recv(test_msg)
 
 
 class TestForkCommList(TestForkComm):
diff --git a/tests/communication/test_RMQAsyncComm.py b/tests/communication/test_RMQAsyncComm.py
index bb80b3d66..031efa913 100644
--- a/tests/communication/test_RMQAsyncComm.py
+++ b/tests/communication/test_RMQAsyncComm.py
@@ -1,12 +1,11 @@
 import pytest
-import flaky
 from tests.communication.test_CommBase import TestComm as base_class
 from yggdrasil.communication import new_comm
 from yggdrasil.communication.RMQComm import RMQComm
 from tests import timeout_decorator
 
 
-@flaky.flaky
+@pytest.mark.flaky_optin
 @timeout_decorator(timeout=60)
 class TestRMQAsyncComm(base_class):
     r"""Test for RMQAsyncComm communication class."""
@@ -29,8 +28,8 @@ def test_reconnect_recv(self, send_comm, recv_comm, do_send_recv):
                                    reply_text="Test shutdown")
         recv_comm._reconnecting.started.wait(5)
         recv_comm._reconnecting.stopped.wait(5)
-        assert(recv_comm.times_connected > 1)
-        assert(recv_comm._reconnecting.has_stopped())
+        assert recv_comm.times_connected > 1
+        assert recv_comm._reconnecting.has_stopped()
         recv_comm._opening.stopped.wait(5)
         do_send_recv(send_comm, recv_comm)
         send_comm.printStatus()
@@ -82,8 +81,8 @@ def test_reconnect_send(self, send_comm, recv_comm, do_send_recv):
                                    reply_text="Test shutdown")
         send_comm._reconnecting.started.wait(5)
         send_comm._reconnecting.stopped.wait(10)
-        assert(send_comm.times_connected > 1)
-        assert(send_comm._reconnecting.has_stopped())
+        assert send_comm.times_connected > 1
+        assert send_comm._reconnecting.has_stopped()
         do_send_recv(send_comm, recv_comm)
         send_comm.printStatus()
         recv_comm.printStatus()
diff --git a/tests/conftest.py b/tests/conftest.py
index d18794ded..863cd66ea 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -27,7 +27,9 @@
      "tests that take a long time to run", None),
     ("extra_example", "--extra-examples",
      "tests for superfluous examples", None),
-    ("production_run", "--production-run", None)
+    ("production_run", "--production-run", None),
+    ("remote_service", "--remote-service",
+     "tests that must connect to a running remote service", None)
 ]
 _params = {
     "example_name": [],
@@ -107,7 +109,7 @@ def setup_ci(args):
         package_dir = os.path.join(x, 'yggdrasil')
         if os.path.isdir(package_dir):
             break
-    assert(os.path.isdir(package_dir))
+    assert os.path.isdir(package_dir)
     args += ['-v',
              '--import-mode=importlib',
              f'--cov={package_dir}',
@@ -115,8 +117,8 @@ def setup_ci(args):
              '--cov-config=.coveragerc',
              '--ignore=yggdrasil/rapidjson/']
     # f'--rootdir={package_dir}']
-    if not any(x.startswith('--with-mpi') for x in args):
-        args += ['--reruns=2', '--reruns-delay=1', '--timeout=900']
+    # if not any(x.startswith('--with-mpi') for x in args):
+    #     args += ['--reruns=2', '--reruns-delay=1', '--timeout=900']
     # Additional checks
     if not os.path.isfile('setup.cfg'):
         raise RuntimeError("The CI tests must be run from the root "
@@ -193,7 +195,9 @@ def pytest_cmdline_preparse(args, dont_exit=False):
         remove_option(args, 'mpi_nproc')
         run_process = True
         prefix = ['mpiexec', '-n', str(pargs.mpi_nproc)]
-        if os.environ.get('CI', False) and platform._is_linux:
+        print(mpi_flavor())
+        if ((os.environ.get('CI', False) and platform._is_linux
+             and mpi_flavor() == 'openmpi')):
             prefix.append('--oversubscribe')
         if '--with-mpi' not in args:
             args.append('--with-mpi')
@@ -234,7 +238,7 @@ def pytest_cmdline_preparse(args, dont_exit=False):
         # for k in x_args_copy:
         #     if k.split('=')[0] not in x_args_keys:
         #         x_args.append(k)
-        assert(any((k.split('=', 1)[0] == '--write-script') for k in x_args))
+        assert (any((k.split('=', 1)[0] == '--write-script') for k in x_args))
         if not pargs.second_attempt:
             pytest_cmdline_preparse(x_args, dont_exit=True)
     # Run test in separate process
@@ -318,6 +322,8 @@ def add_options_to_parser(parser):
                      help="Don't capture output or log messages from tests.")
     parser.addoption('--end-yggdrasil-opts', action="store_true",
                      help="Internal use only")
+    parser.addoption('--rerun-flaky', action="store_true",
+                     help="Re-run flaky tests.")
 
 
 def pytest_configure(config):
@@ -338,6 +344,11 @@ def pytest_configure(config):
         "markers", ("related_language(name): mark test as being related "
                     "to a language. The language may or may not be "
                     "installed, but must be enabled for the test to run."))
+    config.addinivalue_line(
+        "markers", ("flaky_optin(name,condition=None,reruns=1,"
+                    "reruns_delay=0): mark test as "
+                    "flaky without automatically re-running on "
+                    "failure unless --rerun-flaky is specified."))
 
 
 # def pytest_runtest_setup(item):
@@ -373,6 +384,7 @@ def pytest_collection_modifyitems(config, items):
     selected_suites = config.getoption('--suite')
     enabled = config.getoption("--language")
     disabled = config.getoption("--skip-language")
+    rerun_flaky = config.getoption("--rerun-flaky")
     for item in items:
         # Suites
         suites = [mark.args[0] for mark in item.iter_markers(name="suite")]
@@ -440,6 +452,11 @@ def pytest_collection_modifyitems(config, items):
                     pytest.mark.skip(
                         reason=(f"test requires languages {absent_langs!r} "
                                 f"NOT be installed")))
+        # Flaky markers
+        if rerun_flaky:
+            for mark in item.iter_markers(name="flaky_optin"):
+                item.add_marker(
+                    pytest.mark.flaky(*mark.args, **mark.kwargs))
 
 
 def pytest_generate_tests(metafunc):
@@ -640,6 +657,8 @@ def config_env(pytestconfig):
     if second_attempt:
         production_run = False
         debug = True
+    if pytestconfig.getoption("--rerun-flaky"):
+        os.environ['YGGDRASIL_RERUN_FLAKY'] = '1'
     from yggdrasil import config
     with config.temp_config(production_run=production_run,
                             debug=debug, default_comm=default_comm,
@@ -708,7 +727,7 @@ def get_service_manager_skips(service_type, partial_commtype=None,
     out.append(
         (not cls.is_installed(),
          f"Service type '{service_type}' not installed."))
-    assert(not check_running)
+    assert not check_running
     # if check_running and cls.is_installed():
     #     cli = IntegrationServiceManager(service_type=service_type,
     #                                     commtype=partial_commtype,
@@ -744,7 +763,8 @@ def running_service(pytestconfig, check_service_manager_settings,
             break
 
     @contextlib.contextmanager
-    def running_service_w(service_type, partial_commtype=None):
+    def running_service_w(service_type, partial_commtype=None,
+                          track_memory=False):
         from yggdrasil.services import (
             IntegrationServiceManager)
         if ((((service_type, partial_commtype) == ('flask', 'rmq'))
@@ -759,6 +779,8 @@ def running_service_w(service_type, partial_commtype=None):
             args.append(f"--commtype={partial_commtype}")
         args += ["start", f"--model-repository={model_repo}",
                  f"--log-level={log_level}"]
+        if track_memory:
+            args.append("--track-memory")
         process_kws = {}
         if with_coverage:
             script_path = os.path.expanduser(os.path.join('~', 'run_server.py'))
@@ -775,13 +797,16 @@ def running_service_w(service_type, partial_commtype=None):
             if partial_commtype is not None:
                 lines[-1] += f'commtype=\'{partial_commtype}\''
             lines[-1] += ')'
-            lines += ['assert(not srv.is_running)',
+            lines += ['assert not srv.is_running',
                       f'srv.start_server(with_coverage={with_coverage},',
                       f'                 log_level={log_level},'
-                      f'                 model_repository=\'{model_repo}\')']
+                      f'                 model_repository=\'{model_repo}\','
+                      f'                 track_memory={track_memory})']
             with open(script_path, 'w') as fd:
                 fd.write('\n'.join(lines))
             args = [sys.executable, script_path]
+            # args = 'ulimit -v 256000; ' + ' '.join(args)
+            # process_kws['shell'] = True
         verify_flask = (service_type == 'flask')
         if verify_flask:
             # Flask is the default, verify that it is selected
@@ -790,14 +815,14 @@ def running_service_w(service_type, partial_commtype=None):
                                         commtype=partial_commtype,
                                         for_request=True)
         if verify_flask:
-            assert(cli.service_type == 'flask')
-        assert(not cli.is_running)
+            assert cli.service_type == 'flask'
+        assert not cli.is_running
         p = subprocess.Popen(args, **process_kws)
         try:
             cli.wait_for_server()
             yield cli
             cli.stop_server()
-            assert(not cli.is_running)
+            assert not cli.is_running
             p.wait(10)
         finally:
             if p.returncode is None:  # pragma: debug
@@ -916,11 +941,11 @@ def recv_element():
                     return True
                 msg_list.append(msg_recv)
             else:
-                assert(msg_recv == recv_inst.eof_msg)
+                assert msg_recv == recv_inst.eof_msg
             return (not flag)
         wait_on_function(recv_element, timeout=timeout)
         if expected_result is not None:
-            assert(msg_list == nested_approx(expected_result))
+            assert msg_list == nested_approx(expected_result)
         return msg_list
     return wrapped_recv_message_list
 
@@ -1178,7 +1203,7 @@ def close_comm():
     def close_comm_w(comm):
         comm.close()
         comm.disconnect()
-        assert(comm.is_closed)
+        assert comm.is_closed
         del comm
     return close_comm_w
 
@@ -1345,6 +1370,19 @@ def cleanup_communicators(communicator_types):
 _mpi_error_exchange = None
 
 
+def mpi_flavor():
+    r"""Return the MPI flavor."""
+    if shutil.which('mpicc'):
+        result = subprocess.check_output("mpicc -v", shell=True).decode(
+            "utf-8")
+        print('mpi_flavor', result)
+        if "MPICH" in result:
+            return 'mpich'
+        # elif "Open MPI" in result:
+        return 'openmpi'
+    return None
+
+
 @pytest.fixture(scope="session")
 def mpi_comm():
     r"""MPI communicator."""
@@ -1395,7 +1433,7 @@ def new_mpi_exchange():
 def adv_global_mpi_tag():
     def adv_global_mpi_tag_w(value=1):
         global _mpi_error_exchange
-        assert(_mpi_error_exchange is not None)
+        assert _mpi_error_exchange is not None
         out = _mpi_error_exchange.global_tag
         _mpi_error_exchange.global_tag += value
         return out
@@ -1406,7 +1444,7 @@ def adv_global_mpi_tag_w(value=1):
 def sync_mpi_exchange():
     def sync_mpi_exchange_w(*args, **kwargs):
         global _mpi_error_exchange
-        assert(_mpi_error_exchange is not None)
+        assert _mpi_error_exchange is not None
         return _mpi_error_exchange.sync(*args, **kwargs)
     return sync_mpi_exchange_w
 
diff --git a/tests/drivers/test_CMakeModelDriver.py b/tests/drivers/test_CMakeModelDriver.py
index 01bdaf67d..f735de72f 100644
--- a/tests/drivers/test_CMakeModelDriver.py
+++ b/tests/drivers/test_CMakeModelDriver.py
@@ -20,14 +20,14 @@ def test_CMakeConfigure(scripts):
     builddir = sourcedir
     # Test get_output_file
     out = CMakeConfigure.get_output_file(src, dont_build=True)
-    assert(out == builddir)
+    assert out == builddir
     out = CMakeConfigure.get_output_file(src, dont_build=True,
                                          builddir='.', working_dir=sourcedir)
-    assert(out == builddir)
+    assert out == builddir
     # Test get_flags
     out_A = CMakeConfigure.get_flags(dont_link=True)
     out_B = CMakeConfigure.get_flags(dont_link=True, outfile='.')
-    assert(out_A == out_B)
+    assert out_A == out_B
 
 
 @pytest.mark.related_language('cmake')
@@ -41,16 +41,16 @@ def test_CMakeBuilder(scripts):
     if platform._is_win:  # pragma: windows
         out += '.exe'
     # Test get_output_file
-    assert(CMakeBuilder.get_output_file(obj) == out)
-    assert(CMakeBuilder.get_output_file(obj, target='clean') == 'clean')
-    assert(CMakeBuilder.get_output_file(builddir, target=target) == out)
+    assert CMakeBuilder.get_output_file(obj) == out
+    assert CMakeBuilder.get_output_file(obj, target='clean') == 'clean'
+    assert CMakeBuilder.get_output_file(builddir, target=target) == out
     with pytest.raises(RuntimeError):
         CMakeBuilder.get_output_file(builddir)
     # Test get_flags
     out_A = CMakeBuilder.get_flags(target=target, working_dir=builddir)
     out_B = CMakeBuilder.get_flags(target=target,
                                    outfile=os.path.join('.', os.path.basename(out)))
-    assert(out_A == out_B)
+    assert out_A == out_B
 
 
 @pytest.mark.language('cmake')
@@ -63,7 +63,7 @@ def test_create_include():
     for fname in [fname_dll, fname_lib]:
         with open(fname, 'w') as fd:
             fd.write('')
-        assert(os.path.isfile(fname))
+        assert os.path.isfile(fname)
     testlist = [(['-DYGG'], [], ['ADD_DEFINITIONS(-DYGG)']),
                 (['-Wall'], [], ['ADD_DEFINITIONS(-Wall)']),
                 (['/nologo'], [], ['ADD_DEFINITIONS(/nologo)']),
@@ -96,7 +96,7 @@ def test_create_include():
                                             **kws)
         for x in lines:
             try:
-                assert(x in out)
+                assert x in out
             except AssertionError:  # pragma: debug
                 print("Could not find '%s':" % x)
                 pprint.pprint(out)
@@ -185,8 +185,8 @@ def test_run_model_gcc(self, run_model_instance):
 
     def test_sbdir(self, instance, sourcedir, builddir):
         r"""Test that source/build directories set correctly."""
-        assert(instance.sourcedir == sourcedir)
-        assert(instance.builddir == builddir)
+        assert instance.sourcedir == sourcedir
+        assert instance.builddir == builddir
 
     def test_write_wrappers(self, instance):
         r"""Test write_wrappers method with verbosity and existing
@@ -218,6 +218,7 @@ def instance_kwargs(self, testing_options, timeout, sourcedir,
     
     # Disable instance args?
 
+    # @pytest.mark.skip(reason="determine what lines this is testing")
     def test_call_compiler(self, python_class, instance):
         r"""Test call_compiler without full path."""
         # instance.cleanup()
diff --git a/tests/drivers/test_CompiledModelDriver.py b/tests/drivers/test_CompiledModelDriver.py
index d6e5ccfd1..0d48e872a 100644
--- a/tests/drivers/test_CompiledModelDriver.py
+++ b/tests/drivers/test_CompiledModelDriver.py
@@ -14,7 +14,7 @@ def test_get_compatible_tool():
     r"""Test get_compatible_tool when default provided."""
     with pytest.raises(ValueError):
         CompiledModelDriver.get_compatible_tool('invalid', 'compiler', 'c')
-    assert(CompiledModelDriver.get_compatible_tool(
+    assert (CompiledModelDriver.get_compatible_tool(
         'invalid', 'compiler', 'c', default=None) is None)
 
 
@@ -50,17 +50,17 @@ def test_get_compilation_tool():
         if platform._is_win:
             vals += [toolname.upper(), toolfile.upper()]
         for v in vals:
-            assert(CompiledModelDriver.get_compilation_tool(tooltype, v) == out)
+            assert CompiledModelDriver.get_compilation_tool(tooltype, v) == out
         with pytest.raises(ValueError):
             CompiledModelDriver.get_compilation_tool('compiler', 'invalid')
     else:
         with pytest.raises(NotImplementedError):
             CModelDriver.get_tool('compiler')
-        assert(CModelDriver.get_tool(
+        assert (CModelDriver.get_tool(
             'compiler', default='invalid') == 'invalid')
-    assert(CompiledModelDriver.get_compilation_tool('compiler', 'invalid',
-                                                    default='invalid')
-           == 'invalid')
+    assert (CompiledModelDriver.get_compilation_tool('compiler', 'invalid',
+                                                     default='invalid')
+            == 'invalid')
 
 
 def test_CompilationToolBase():
@@ -104,7 +104,7 @@ def test_append_flags(self, python_class):
                      ['a', 'b', '0', '1', 'c'])]
         for (out, key, val), kws, res in kws_list:
             python_class.append_flags(out, key, val, **kws)
-            assert(out == res)
+            assert out == res
 
     def test_create_flag(self, python_class):
         r"""Test create_flag."""
@@ -113,7 +113,7 @@ def test_create_flag(self, python_class):
                      ('-set-this', False, []),
                      ('-set-this', None, [])]
         for (key, val, out) in test_args:
-            assert(python_class.create_flag(key, val) == out)
+            assert python_class.create_flag(key, val) == out
 
     def test_not_implemented(self):
         r"""Test raising of NotImplementedErrors for incomplete classes."""
@@ -121,7 +121,7 @@ def test_not_implemented(self):
 
     def test_get_flags(self, python_class):
         r"""Test get_flags."""
-        assert(python_class.get_flags(flags='hello') == ['hello'])
+        assert python_class.get_flags(flags='hello') == ['hello']
 
     def test_get_search_path(self, class_name, python_class):
         r"""Test get_search_path."""
@@ -154,14 +154,14 @@ def instance_kwargs(self):
     def test_call(self, python_class):
         r"""Test call."""
         out = 'test123'
-        assert(not shutil.which(python_class.toolname))
-        assert(not (os.path.isfile(out) or os.path.isdir(out)))
+        assert not shutil.which(python_class.toolname)
+        assert not (os.path.isfile(out) or os.path.isdir(out))
         with pytest.raises(RuntimeError):
             python_class.call('args', out=out, dont_link=True)
 
     def test_linker(self, python_class):
         r"""Test linker."""
-        assert(python_class.linker() is False)
+        assert python_class.linker() is False
 
     def test_archiver(self, python_class):
         r"""Test archiver."""
@@ -170,8 +170,8 @@ def test_archiver(self, python_class):
         
     def test_get_flags(self, python_class):
         r"""Test get_flags."""
-        assert(python_class.get_flags(flags='hello', libtype='object')
-               == ['hello', '-DWITH_YGGDRASIL'])
+        assert (python_class.get_flags(flags='hello', libtype='object')
+                == ['hello', '-DWITH_YGGDRASIL'])
         
     def test_get_executable_command(self, python_class):
         r"""Test get_executable_command."""
@@ -245,8 +245,8 @@ def test_get_dependency_info(self, python_class):
             python_class.get_dependency_info(dep, default='default')
         with pytest.raises(KeyError):
             python_class.get_dependency_info('invalid')
-        assert(python_class.get_dependency_info('invalid', default='default')
-               == 'default')
+        assert (python_class.get_dependency_info('invalid', default='default')
+                == 'default')
 
     def test_get_dependency_source(self, python_class):
         r"""Test get_dependency_source."""
@@ -258,8 +258,8 @@ def test_get_dependency_source(self, python_class):
             python_class.get_dependency_source(dep, default='default')
         with pytest.raises(ValueError):
             python_class.get_dependency_source('invalid')
-        assert(python_class.get_dependency_source(__file__) == __file__)
-        assert(python_class.get_dependency_source(
+        assert python_class.get_dependency_source(__file__) == __file__
+        assert (python_class.get_dependency_source(
             'invalid', default='default') == 'default')
 
     def test_get_dependency_object(self, python_class):
@@ -272,8 +272,8 @@ def test_get_dependency_object(self, python_class):
             python_class.get_dependency_object(dep, default='default')
         with pytest.raises(ValueError):
             python_class.get_dependency_object('invalid')
-        assert(python_class.get_dependency_object(__file__) == __file__)
-        assert(python_class.get_dependency_object(
+        assert python_class.get_dependency_object(__file__) == __file__
+        assert (python_class.get_dependency_object(
             'invalid', default='default') == 'default')
 
     def test_get_dependency_library(self, python_class):
@@ -292,17 +292,17 @@ def test_get_dependency_library(self, python_class):
                 python_class.get_dependency_library(dep, libtype=libtype)
         with pytest.raises(ValueError):
             python_class.get_dependency_library('invalid')
-        assert(python_class.get_dependency_library(__file__) == __file__)
-        assert(python_class.get_dependency_library(
+        assert python_class.get_dependency_library(__file__) == __file__
+        assert (python_class.get_dependency_library(
             'invalid', default='default') == 'default')
 
     def test_get_dependency_include_dirs(self, python_class):
         r"""Test get_dependency_include_dirs."""
         with pytest.raises(ValueError):
             python_class.get_dependency_include_dirs('invalid')
-        assert(python_class.get_dependency_include_dirs(__file__)
-               == [os.path.dirname(__file__)])
-        assert(python_class.get_dependency_include_dirs(
+        assert (python_class.get_dependency_include_dirs(__file__)
+                == [os.path.dirname(__file__)])
+        assert (python_class.get_dependency_include_dirs(
             'invalid', default='default') == ['default'])
 
     def test_get_dependency_order(self, python_class):
@@ -357,8 +357,8 @@ def test_executable_command(self, python_class):
     def test_compiler_call(self, python_class):
         r"""Test compiler call."""
         tool = python_class.get_tool('compiler')
-        assert(tool.call('args', out='test',
-                         dry_run=True, skip_flags=True) == '')
+        assert (tool.call('args', out='test',
+                          dry_run=True, skip_flags=True) == '')
         src = [x + tool.get_language_ext()[0] for x in ['args1', 'args2']]
         with pytest.raises(ValueError):
             tool.call(src, out='out1', dont_link=True)
@@ -424,10 +424,10 @@ def test_compile_model(self, instance, source):
         if not instance.is_build_tool:
             instance.compile_model(out=instance.model_file,
                                    overwrite=False)
-            assert(os.path.isfile(instance.model_file))
+            assert os.path.isfile(instance.model_file)
             instance.compile_model(out=instance.model_file,
                                    overwrite=False)
-            assert(os.path.isfile(instance.model_file))
+            assert os.path.isfile(instance.model_file)
             os.remove(instance.model_file)
 
     def test_call_linker(self, instance):
@@ -441,5 +441,5 @@ def test_parse_arguments(self, python_class, instance):
         r"""Run test to initialize driver using the executable."""
         x = os.path.splitext(instance.source_files[0])[0] + '.out'
         new_inst = python_class('test_name', [x], skip_compile=True)
-        assert(new_inst.model_file == x)
-        assert(new_inst.source_files == instance.source_files[:1])
+        assert new_inst.model_file == x
+        assert new_inst.source_files == instance.source_files[:1]
diff --git a/tests/languages/test_YggInterface.py b/tests/languages/test_YggInterface.py
index be50ec8f3..52bf6dd77 100644
--- a/tests/languages/test_YggInterface.py
+++ b/tests/languages/test_YggInterface.py
@@ -1,7 +1,6 @@
 import pytest
 import os
 import numpy as np
-import flaky
 import copy
 from yggdrasil.communication import get_comm
 from yggdrasil.interface import YggInterface
@@ -40,17 +39,17 @@ def __exit__(self, type, value, traceback):
 
 def test_maxMsgSize():
     r"""Test max message size."""
-    assert(YggInterface.maxMsgSize() == YGG_MSG_MAX)
+    assert YggInterface.maxMsgSize() == YGG_MSG_MAX
 
 
 def test_eof_msg():
     r"""Test eof message signal."""
-    assert(YggInterface.eof_msg() == constants.YGG_MSG_EOF)
+    assert YggInterface.eof_msg() == constants.YGG_MSG_EOF
 
 
 def test_bufMsgSize():
     r"""Test buf message size."""
-    assert(YggInterface.bufMsgSize() == constants.YGG_MSG_BUF)
+    assert YggInterface.bufMsgSize() == constants.YGG_MSG_BUF
 
 
 def test_init():
@@ -103,8 +102,8 @@ def do_send_recv(language='python', fmt='%f\\n%d', msg=[float(1.0), np.int32(2)]
             o.send_eof()
             o.close(linger=True)
             # Input
-            assert(i.recv() == (True, converter(msg)))
-            assert(i.recv() == (False, converter(constants.YGG_MSG_EOF)))
+            assert i.recv() == (True, converter(msg))
+            assert i.recv() == (False, converter(constants.YGG_MSG_EOF))
     finally:
         iodrv.terminate()
 
@@ -125,12 +124,12 @@ def test_YggInit_backwards():
 
 def test_YggInit_variables():
     r"""Test Matlab interface for variables."""
-    assert(YggInterface.YggInit('YGG_MSG_MAX') == YGG_MSG_MAX)
-    assert(YggInterface.YggInit('YGG_MSG_EOF') == constants.YGG_MSG_EOF)
-    assert(YggInterface.YggInit('YGG_MSG_EOF')
-           == YggInterface.YggInit('CIS_MSG_EOF'))
-    assert(YggInterface.YggInit('YGG_MSG_EOF')
-           == YggInterface.YggInit('PSI_MSG_EOF'))
+    assert YggInterface.YggInit('YGG_MSG_MAX') == YGG_MSG_MAX
+    assert YggInterface.YggInit('YGG_MSG_EOF') == constants.YGG_MSG_EOF
+    assert (YggInterface.YggInit('YGG_MSG_EOF')
+            == YggInterface.YggInit('CIS_MSG_EOF'))
+    assert (YggInterface.YggInit('YGG_MSG_EOF')
+            == YggInterface.YggInit('PSI_MSG_EOF'))
 
 
 class TestYggClass(base_class):
@@ -294,7 +293,7 @@ def input_file(self, filename, testing_options, direction, filecomm):
         if filecomm and (direction == 'input'):
             with open(filename, 'wb') as fd:
                 fd.write(testing_options['contents'])
-            assert(os.path.isfile(filename))
+            assert os.path.isfile(filename)
         try:
             yield
         finally:
@@ -374,42 +373,42 @@ def test_msg(self, filecomm, testing_options, instance, timeout,
             if filecomm:
                 for msg_recv in testing_options['recv']:
                     msg_flag, msg_recv0 = instance.recv(timeout)
-                    assert(msg_flag)
-                    assert(msg_recv0 == nested_approx(msg_recv))
+                    assert msg_flag
+                    assert msg_recv0 == nested_approx(msg_recv)
                 msg_flag, msg_recv0 = instance.recv(timeout)
-                assert(not msg_flag)
+                assert not msg_flag
             else:
                 for msg_send, msg_recv in zip(testing_options['send'],
                                               testing_options['recv']):
                     msg_flag = test_comm.send(msg_send)
-                    assert(msg_flag)
+                    assert msg_flag
                     msg_flag, msg_recv0 = instance.recv(timeout)
-                    assert(msg_flag)
-                    assert(msg_recv0 == nested_approx(msg_recv))
+                    assert msg_flag
+                    assert msg_recv0 == nested_approx(msg_recv)
         else:
             if filecomm:
                 for msg in testing_options['send']:
                     msg_flag = instance.send(msg)
-                    assert(msg_flag)
+                    assert msg_flag
                 instance.send_eof()
                 # Read temp file
                 wait_on_function(lambda: not iodriver.ocomm.is_open)
-                assert(os.path.isfile(filename))
+                assert os.path.isfile(filename)
                 if testing_options.get('exact_contents', True):
                     with open(filename, 'rb') as fd:
                         res = fd.read()
-                        assert(res == testing_options['contents'])
+                        assert res == testing_options['contents']
             else:
                 for msg_send, msg_recv in zip(testing_options['send'],
                                               testing_options['recv']):
                     msg_flag = instance.send(msg_send)
-                    assert(msg_flag)
+                    assert msg_flag
                     msg_flag, msg_recv0 = test_comm.recv(timeout)
-                    assert(msg_flag)
-                    assert(msg_recv0 == nested_approx(msg_recv))
+                    assert msg_flag
+                    assert msg_recv0 == nested_approx(msg_recv)
 
 
-@flaky.flaky
+@pytest.mark.flaky_optin
 class TestYggRpcClient(TestYggClass):
     r"""Test client-side RPC communication with Python."""
 
@@ -506,7 +505,7 @@ def test_msg(self, filecomm, testing_options, instance, timeout,
         for msg_send, msg_recv in zip(testing_options['send'],
                                       testing_options['recv']):
             msg_flag = send_comm.send(msg_send)
-            assert(msg_flag)
+            assert msg_flag
             msg_flag, msg_recv0 = recv_comm.recv(timeout)
-            assert(msg_flag)
-            assert(msg_recv0 == nested_approx(msg_recv))
+            assert msg_flag
+            assert msg_recv0 == nested_approx(msg_recv)
diff --git a/tests/test_services.py b/tests/test_services.py
index 5e77801bf..8b1fdfa25 100644
--- a/tests/test_services.py
+++ b/tests/test_services.py
@@ -13,7 +13,7 @@ def _make_ids(ids):
     return ','.join([str(x) for x in ids])
 
 
-@pytest.mark.skip("heroku app disabled")
+@pytest.mark.remote_service
 @pytest.mark.language('c')
 @pytest.mark.language('c++')
 def test_call_integration_remote():
@@ -26,14 +26,16 @@ def test_call_integration_remote():
     yamls.remove(test_yml)
     yamls.remove(copy_yml)
     yamls.append(remote_yml)
-    address = 'https://model-service-demo.herokuapp.com/'
+    # address = 'https://model-service-demo.herokuapp.com/'
+    # address = "https://model-service-demo.fly.dev/"
+    address = "https://model-service-demo.onrender.com/"
     service_type = 'flask'
     cli = IntegrationServiceManager(service_type=service_type,
                                     for_request=True,
                                     address=address)
-    cli.wait_for_server()
+    cli.wait_for_server(timeout=600.0)
     if not cli.is_running:  # pragma: debug
-        pytest.skip("Heroku app is not running.")
+        pytest.skip("Web service app is not running.")
     try:
         shutil.copy(copy_yml, remote_yml)
         with open(remote_yml, 'a') as fd:
@@ -56,7 +58,10 @@ class TestServices(object):
     @pytest.fixture(params=itertools.product(['flask', 'rmq'], [None, 'rmq']),
                     ids=_make_ids, scope="class", autouse=True)
     def running_service(self, request, running_service):
-        with running_service(request.param[0], request.param[1]) as cli:
+        track_memory = (request.param[0] == 'flask'
+                        and request.param[1] is None)
+        with running_service(request.param[0], request.param[1],
+                             track_memory=track_memory) as cli:
             self.cli = cli
             yield cli
             self.cli = None
diff --git a/tests/test_timing.py b/tests/test_timing.py
index 4ddac83a5..fcfb3cc31 100644
--- a/tests/test_timing.py
+++ b/tests/test_timing.py
@@ -2,7 +2,6 @@
 import os
 import sys
 import copy
-import flaky
 from yggdrasil import tools, timing, platform
 from tests import TestClassBase as base_class
 
@@ -32,7 +31,7 @@ def test_get_source():
     for lang in lang_list:
         for d in dir_list:
             fname = timing.get_source(lang, d)
-            assert(os.path.isfile(fname))
+            assert os.path.isfile(fname)
 
 
 def test_platform_error():
@@ -164,7 +163,7 @@ def test_json(self, check_filename, get_raw_data, instance):
         x = instance.load(as_json=True)
         instance.save(x, overwrite=True)
         new_text = get_raw_data()
-        assert(new_text == old_text)
+        assert new_text == old_text
 
     def test_save(self, check_filename, get_raw_data, instance):
         r"""Test save with/without overwrite."""
@@ -173,7 +172,7 @@ def test_save(self, check_filename, get_raw_data, instance):
             instance.save(instance.data)
         instance.save(instance.data, overwrite=True)
         new_text = get_raw_data()
-        assert(new_text == old_text)
+        assert new_text == old_text
 
     def test_scaling_count(self, check_filename, instance, count, size,
                            nrep):
@@ -334,14 +333,14 @@ def time_run_kwargs(self, nrep):
         r"""dict: Keyword arguments for time_run."""
         return {'nrep': nrep, 'overwrite': True}
 
-    @flaky.flaky
+    @pytest.mark.flaky_optin
     def test_pyperf_func(self, dont_use_pyperf, instance, count, size):
         r"""Test pyperf_func."""
         if dont_use_pyperf:
             pytest.skip("Don't use pyperf")
         timing.pyperf_func(1, instance, count, size, 0)
 
-    @flaky.flaky
+    @pytest.mark.flaky_optin
     def test_run_overwrite(self, time_run, python_class,
                            instance_args, instance_kwargs, entry_name):
         r"""Test performing a run twice, the second time with ovewrite."""
@@ -350,4 +349,4 @@ def test_run_overwrite(self, time_run, python_class,
         instance = python_class(*instance_args, **instance_kwargs)
         time_run(instance)
         instance.remove_entry(entry_name)
-        assert(not instance.has_entry(entry_name))
+        assert not instance.has_entry(entry_name)
diff --git a/tests/test_yamlfile.py b/tests/test_yamlfile.py
index 556d81b76..0fd0d886b 100644
--- a/tests/test_yamlfile.py
+++ b/tests/test_yamlfile.py
@@ -2,7 +2,6 @@
 import tempfile
 import os
 import yaml
-import flaky
 import io as sio
 from jsonschema.exceptions import ValidationError
 from yggdrasil import yamlfile
@@ -29,17 +28,17 @@ def test_load_yaml():
     try:
         # Dictionary
         out = yamlfile.load_yaml(dict_write)
-        assert(out == dict_read)
+        assert out == dict_read
         # File name
         out = yamlfile.load_yaml(fname)
-        assert(out == dict_read)
+        assert out == dict_read
         # Open file object
         with open(fname, 'r') as fd:
             out = yamlfile.load_yaml(fd)
-            assert(out == dict_read)
+            assert out == dict_read
         # Open stream
         out = yamlfile.load_yaml(sio.StringIO(contents))
-        assert(out == dict_read)
+        assert out == dict_read
     finally:
         # Remove file
         if os.path.isfile(fname):
@@ -60,16 +59,16 @@ def test_parse_component_error():
         yamlfile.parse_component({}, 'invalid', 'invalid')
 
 
-@flaky.flaky(max_runs=3)
+@pytest.mark.flaky_optin(max_runs=3)
 def test_load_yaml_git():
     r"""Test loading a yaml from a remote git repository."""
     import git
     yml = "https://github.com/cropsinsilico/example-fakemodel/fakemodel.yml"
     with pytest.raises(Exception):
         yamlfile.load_yaml(yml)
-    assert('model' in yamlfile.load_yaml('git:' + yml))
+    assert 'model' in yamlfile.load_yaml('git:' + yml)
     yml = "cropsinsilico/example-fakemodel/fakemodel.yml"
-    assert('model' in yamlfile.load_yaml('git:' + yml))
+    assert 'model' in yamlfile.load_yaml('git:' + yml)
     git.rmtree("cropsinsilico")
     
 
diff --git a/requirements_testing.txt b/utils/requirements/requirements_testing.txt
similarity index 65%
rename from requirements_testing.txt
rename to utils/requirements/requirements_testing.txt
index 915ad9939..9fce71869 100644
--- a/requirements_testing.txt
+++ b/utils/requirements/requirements_testing.txt
@@ -1,13 +1,13 @@
+ccache; platform_system != 'Windows'  # [conda]
 codecov
+conda-tree       # [conda]
 coverage
 flake8
 flaky
-nose
 pytest
 pytest-cov
-pytest-timeout
-pytest-mpi
 pytest-drop-dup-tests  # [pip]
-pytest-rerunfailures
-pytest-repeat
 pytest-mpi
+pytest-repeat
+pytest-rerunfailures
+pytest-timeout
diff --git a/utils/setup_test_env.py b/utils/setup_test_env.py
index d3fd60154..3ef761b3e 100644
--- a/utils/setup_test_env.py
+++ b/utils/setup_test_env.py
@@ -5,15 +5,18 @@
 import pprint
 import shutil
 import subprocess
-import warnings
 import difflib
 import copy
 from datetime import datetime
 PYVER = ('%s.%s' % sys.version_info[:2])
 PY2 = (sys.version_info[0] == 2)
+if os.environ.get('PRE_CONDA_BIN', False):
+    os.environ['PATH'] = (os.environ['PRE_CONDA_BIN']
+                          + os.pathsep
+                          + os.environ['PATH'])
 _is_osx = (sys.platform == 'darwin')
 _is_linux = ('linux' in sys.platform)
-_is_win = (sys.platform in ['win32', 'cygwin'])
+_is_win = (sys.platform in ['win32', 'cygwin', 'msys'])
 _is_unix = (_is_osx or _is_linux)
 _on_gha = bool(os.environ.get('GITHUB_ACTIONS', False))
 _on_travis = bool(os.environ.get('TRAVIS_OS_NAME', False))
@@ -27,22 +30,15 @@
 CONDA_ROOT = None
 try:
     CONDA_CMD_WHICH = shutil.which('conda')
-    YGG_CMD_WHICH = shutil.which('yggdrasil')
 except AttributeError:
     if _is_win:
         CONDA_CMD_WHICH = None
-        YGG_CMD_WHICH = None
     else:
         try:
             CONDA_CMD_WHICH = subprocess.check_output(
                 ['which', 'conda']).strip().decode('utf-8')
         except subprocess.CalledProcessError:
             CONDA_CMD_WHICH = None
-        try:
-            YGG_CMD_WHICH = subprocess.check_output(
-                ['which', 'yggdrasil']).strip().decode('utf-8')
-        except subprocess.CalledProcessError:
-            YGG_CMD_WHICH = None
 if (not CONDA_PREFIX):
     if CONDA_CMD_WHICH:
         CONDA_PREFIX = os.path.dirname(os.path.dirname(CONDA_CMD_WHICH))
@@ -62,35 +58,441 @@
 if CONDA_CMD_WHICH:
     if _is_win:
         CONDA_CMD = 'call conda'
+        MAMBA_CMD = 'call mamba'
     else:
         CONDA_CMD = 'conda'
+        MAMBA_CMD = 'mamba'
     CONDA_ROOT = os.path.dirname(os.path.dirname(CONDA_CMD_WHICH))
 elif os.environ.get('CONDA', None):
     if _is_win:
         CONDA_CMD = 'call %s' % os.path.join(os.environ['CONDA'],
                                              'condabin', 'conda.bat')
+        MAMBA_CMD = 'call %s' % os.path.join(os.environ['CONDA'],
+                                             'condabin', 'mamba.bat')
     else:
         CONDA_CMD = os.path.join(os.environ['CONDA'], 'bin', 'conda')
+        MAMBA_CMD = os.path.join(os.environ['CONDA'], 'bin', 'mamba')
     CONDA_ROOT = os.environ['CONDA']
 else:
     CONDA_CMD = None
+    MAMBA_CMD = None
 PYTHON_CMD = sys.executable
-SUMMARY_CMDS = ["%s --version" % PYTHON_CMD,
-                "%s -m pip list" % PYTHON_CMD]
-if CONDA_ENV:
-    SUMMARY_CMDS += ["echo 'CONDA_PREFIX=%s'" % CONDA_PREFIX,
-                     "%s info" % CONDA_CMD,
-                     "%s list" % CONDA_CMD,
-                     "%s config --show-sources" % CONDA_CMD]
 
 
-def call_conda_command(args, **kwargs):
+class SetupParam(object):
+    r"""Storage for setup parameters.
+
+    Args:
+        method (str): Method that should be used to build and install
+            the package. Valid values include 'conda' and 'pip'.
+        install_opts (dict, optional): Mapping from language/package
+            to bool specifying whether or not the language/package
+            should be installed. If not provided, get_install_opts is
+            used to create it.
+        env_created (bool, optional): If True, the env is assumed to
+            not yet exist as it will be created. Defaults to False.
+        **kwargs: Additional keyword arguments are parsed according to
+            the values specified in _args.
+
+    """
+
+    _args = [
+        ('method', [], {
+            'choices': ['conda', 'pip', 'mamba'],
+            'help': "Method that should be used to install packages"}),
+        (('--env-method', ), ['auto'], {
+            'choices': ['conda', 'mamba', 'virtualenv', None],
+            'default': None,
+            'help': ("Method that should be used to create an "
+                     "environment")}),
+        (('--python', '--python-version'), [], {
+            'type': str,
+            'help': "Python version that should be installed"}),
+        ('--target-os', [], {
+            'choices': ['win', 'osx', 'linux'],
+            'default': None,
+            'help': ("Operating system that should be targeted if "
+                     "different from the current OS.")}),
+        ('--for-development', ['install'], {
+            'action': 'store_true',
+            'help': ("Install dependencies used during development "
+                     "and that would be missed when installing in "
+                     "development mode.")}),
+        ('--windows-package-manager', ['install'], {
+            'default': 'vcpkg',
+            'choices': ['vcpkg', 'choco'],
+            'help': "Package manager that should be used on Windows."}),
+        (('--env-name', '-n', '--name'), [], {
+            'default': None,
+            'help': ("Conda or virtualenv environment that packages "
+                     "should be installed in.")}),
+        ('--verbose', ['run'], {
+            'action': 'store_true',
+            'help': "Turn up verbosity of output."}),
+        ('--always-yes', ['run'], {
+            'action': 'store_true',
+            'help': "Don't ask for user input to run commands."}),
+        (('--only-python', '--python-only'), ['install'], {
+            'action': 'store_true',
+            'help': "Only install python dependencies."}),
+        ('--use-mamba', [], {
+            'action': 'store_true',
+            'help': "Use mamba in place of conda"}),
+        ('--user', ['run', 'install'], {
+            'action': 'store_true',
+            'help': "Install in user mode."}),
+        ('--dry-run', ['run'], {
+            'action': 'store_true',
+            'help': "Don't actually run any commands"}),
+        ('--deps-method', ['install'], {
+            'type': str,
+            'choices': ["all", "env", "unique", "supplemental",
+                        "conda_recipe"],
+            'default': 'env',
+            'help': (
+                "How the method should be used to select"
+                " dependencies. Options:\n"
+                "    all: Do not deselect any dependencies based"
+                " on their method.\n"
+                "    env: Select dependencies with methods"
+                " that are valid for the select environment.\n"
+                "    unique: Select dependencies that can only be"
+                " installed by the specified method.\n"
+                "    supplemental: Select dependencies that cannot"
+                " be installed by the specified method.\n"
+                "    conda_recipe: Select dependencies that would"
+                " occur in a conda recipe.")}),
+        ('--fallback-to-conda', ['install'], {
+            'action': 'store_true',
+            'default': None,
+            'help': ("Fallback to installing non-python dependencies "
+                     "using mamba/conda")}),
+    ]
+
+    def __init__(self, method=None, install_opts=None,
+                 env_created=False, **kwargs):
+        self.method = method
+        self.install_opts = get_install_opts(install_opts)
+        for k in self.args_to_copy():
+            if k in kwargs:
+                setattr(self, k, kwargs.pop(k))
+            else:
+                setattr(self, k, self.get_default(k))
+        assert not kwargs
+        self.conda_flags = ''
+        self.pip_flags = ''
+        if not self.python:
+            self.python = PYVER
+        # Modified inputs vars:
+        #   use_mamba, method, fallback_to_conda, install_opts,
+        #   target_os
+        if self.target_os is None:
+            self.target_os = self.install_opts['os']
+        else:
+            assert self.target_os in ['linux', 'osx', 'win', 'any']
+            if self.target_os != self.install_opts['os']:
+                assert self.dry_run
+            self.install_opts['os'] = self.target_os
+        if self.method is None:
+            if self.use_mamba:
+                self.method = 'conda'
+            elif self.env_method in ('conda', 'mamba'):
+                self.method = self.env_method
+            elif self.env_method == 'virtualenv':
+                self.method = 'pip'
+            elif CONDA_ENV:
+                self.method = 'conda'
+            else:
+                self.method = 'pip'
+        elif self.method.startswith('mamba'):
+            self.use_mamba = True
+            self.method = self.method.replace('mamba', 'conda')
+        if self.always_yes:
+            self.conda_flags += ' -y'
+            # self.pip_flags += ' -y'
+        if self.verbose:
+            self.conda_flags += ' -vvv'
+            self.pip_flags += ' --verbose'
+        # else:
+        #     self.conda_flags += '-q'
+        if self.method.endswith('-dev'):
+            self.method_base = self.method.split('-dev')[0]
+            self.for_development = True
+        else:
+            self.method_base = self.method
+        self.conda_exe_config = CONDA_CMD
+        if self.use_mamba:
+            self.conda_exe = MAMBA_CMD
+            # self.conda_build = f"{CONDA_CMD} mambabuild"
+            # self.build_pkgs = ["boa"]
+        else:
+            self.conda_exe = CONDA_CMD
+            # self.conda_build = f"{CONDA_CMD} build"
+            # self.build_pkgs = ["conda-build", "conda-verify"]
+        if self.fallback_to_conda is None:
+            self.fallback_to_conda = ((self.method_base == 'conda')
+                                      or (_is_win and _on_appveyor)
+                                      or self.install_opts['lpy'])
+        if self.env_method is None:
+            if self.fallback_to_conda:
+                if self.use_mamba:
+                    self.env_method = 'mamba'
+                else:
+                    self.env_method = 'conda'
+            else:
+                self.env_method = 'virtualenv'
+        if self.env_name and self.env_method in ('conda', 'mamba'):
+            self.conda_env = self.env_name
+        else:
+            self.conda_env = None
+        self.python_cmd = PYTHON_CMD
+        if self.conda_env:
+            self.python_cmd = locate_conda_exe(
+                self.conda_env, 'python',
+                allow_missing=env_created,
+                use_mamba=self.use_mamba)
+            self.conda_flags += f' --name {self.conda_env}'
+        if self.for_development:
+            self.install_opts['dev'] = True
+        # Methods that can be used to install deps
+        self.valid_methods = ['skip']
+        if self.deps_method == 'all':
+            self.valid_methods += ['python', 'pip', 'pip_skip',
+                                   'conda', 'conda_skip', 'cran',
+                                   'brew', 'apt', 'choco', 'vcpkg']
+        elif self.deps_method in ['env', 'supplemental']:
+            self.valid_methods += ['python', 'pip', 'pip_skip']
+            if self.fallback_to_conda:
+                self.valid_methods += ['conda', 'conda_skip']
+            elif not self.only_python:
+                self.valid_methods.append('cran')
+                if self.install_opts['os'] == 'linux':
+                    self.valid_methods.append('apt')
+                elif self.install_opts['os'] == 'osx':
+                    self.valid_methods.append('brew')
+                elif self.install_opts['os'] == 'win':
+                    self.valid_methods.append(self.windows_package_manager)
+                    if 'vcpkg' not in self.valid_methods:
+                        self.valid_methods.append('vcpkg')
+            if self.deps_method == 'supplemental':
+                if not (self.for_development or self.method == 'pip'):
+                    # Pip extras installed directly as extras do not
+                    # seem to work when installing from a sdist
+                    for k in ['python', self.method_base,
+                              f'{self.method_base}_skip']:
+                        if k in self.valid_methods:
+                            self.valid_methods.remove(k)
+                self.valid_methods.append(f"{self.method_base}_supp")
+        elif self.deps_method == 'unique':
+            self.valid_methods += [f'{self.method_base}_skip',
+                                   self.method_base]
+        elif self.deps_method == 'conda_recipe':
+            self.valid_methods += ['conda', 'conda_skip',
+                                   'conda_recipe', 'python']
+        if self.only_python:
+            for k in ['cran', 'apt', 'brew', 'choco', 'vcpkg']:
+                if k in self.valid_methods:
+                    self.valid_methods.remove(k)
+        # print(f"deps_method = {self.deps_method}, "
+        #       f"valid_methods = {self.valid_methods}")
+
+    @classmethod
+    def find_args(cls, x):
+        x_try = [x, '--' + x.replace('_', '-')]
+        for k, types, v in cls._args:
+            if isinstance(k, tuple):
+                if any(xx in k for xx in x_try):
+                    return (k, types, v)
+            else:
+                if k in x_try:
+                    return (k, types, v)
+        raise KeyError(x)
+
+    @classmethod
+    def get_default(cls, x):
+        k, types, v = cls.find_args(x)
+        if 'default' in v:
+            return v['default']
+        if v.get('action', None) == 'store_true':
+            return False
+        return None
+
+    @classmethod
+    def args_to_copy(cls, keep_method=False):
+        r"""list: Arguments to copy."""
+        out = []
+        for x in cls._args:
+            base = x[0]
+            if isinstance(base, tuple):
+                base = base[0]
+            out.append(base.lstrip('-').replace('-', '_'))
+        if not keep_method:
+            out.remove('method')
+        return out
+
+    @classmethod
+    def from_args(cls, args, install_opts, env_created=False,
+                  require_env_name=False, **kwargs):
+        if env_created:
+            require_env_name = True
+        method = getattr(args, 'method', None)
+        if require_env_name and getattr(args, 'env_name', None) is None:
+            assert method is not None
+            args.env_name = method + args.python.replace('.', '')
+        cls.extract_install_opts_from_args(args, install_opts)
+        for k in cls.args_to_copy():
+            if hasattr(args, k):
+                kwargs[k] = getattr(args, k)
+        kwargs['env_created'] = env_created
+        return cls(method=method, install_opts=install_opts, **kwargs)
+
+    @staticmethod
+    def extract_install_opts_from_args(args, install_opts):
+        new_opts = {}
+        for k, v in install_opts.items():
+            if k == 'no_sudo':
+                new_opts[k] = bool(getattr(args, k, False))
+            elif v and getattr(args, f'dont_install_{k}', False):
+                new_opts[k] = False
+            elif (not v) and getattr(args, f'install_{k}', False):
+                new_opts[k] = True
+        install_opts.update(new_opts)
+    
+    @staticmethod
+    def add_parser_args(parser, skip=None, skip_types=None,
+                        skip_all=False, include=None,
+                        install_opts=None, additional_args=None,
+                        **kwargs):
+        r"""Add arguments to a parser for installation options.
+
+        Args:
+            parser (argparse.ArgumentParser): Parser to add arguments to.
+            skip_all (bool, optional): If True, only install_opts
+                will be added. Defaults to False.
+            skip (list, optional): Arguments that should not be added.
+                Defaults to an empty list.
+            skip_types (list, optional): Type of argument that should
+                be skipped. Defaults to an empty list.
+            include (list, optional): Arguments that should be included
+                even if the type if skipped. Defaults to an empty list.
+            install_opts (dict, optional): Existing installation options
+                that should be used to set the flags. Create using
+                get_install_opts if not provided.
+            additional_args (list, optional): Additional arguments that
+                should be added after positional arguments.
+            **kwargs: Additional keyword arguments are checked for
+                parameters pertaining to individual arguments.
+
+        """
+        if skip is None:
+            skip = []
+        if skip_types is None:
+            skip_types = []
+        skip_types.append('auto')
+        if include is None:
+            include = []
+
+        def args_match(a_args, match):
+            for x in a_args:
+                x = x.strip('-')
+                if x in match or x.replace('-', '_') in match:
+                    return True
+            return False
+
+        args_req = []
+        args_opt = []
+
+        def add_argument(*a_args, **a_kwargs):
+            base = a_args[0].lstrip('-').replace('-', '_')
+            if f"{base}_args" in kwargs:
+                a_args = kwargs.pop(f"{base}_args")
+                a_kwargs['dest'] = base
+            for k in ['choices', 'default', 'help']:
+                if f"{base}_{k}" in kwargs:
+                    a_kwargs[k] = kwargs.pop(f"{base}_{k}")
+            if kwargs.pop(f"{base}_optional", False):
+                assert not any(x.startswith('--') for x in a_args)
+                a_args = tuple(['--' + x for x in a_args])
+            if kwargs.pop(f"{base}_required", False):
+                assert all(x.startswith('--') for x in a_args)
+                a_args = (base, )
+            # if additional_args and a_args[0].startswith('--'):
+            #     for kk, vv in additional_args:
+            #         parser.add_argument(*kk, **vv)
+            #     additional_args.clear()
+            if a_args[0].startswith('--'):
+                args_opt.append((a_args, a_kwargs))
+            else:
+                args_req.append((a_args, a_kwargs))
+            # parser.add_argument(*a_args, **a_kwargs)
+
+        if skip_all:
+            if include:
+                for k, types, v in SetupParam._args:
+                    if not isinstance(k, tuple):
+                        k = (k, )
+                    if args_match(k, include):
+                        add_argument(*k, **v)
+        else:
+            for k, types, v in SetupParam._args:
+                if not isinstance(k, tuple):
+                    k = (k, )
+                if (((any(x in types for x in skip_types)
+                      or args_match(k, skip))
+                     and not args_match(k, include))):
+                    continue
+                add_argument(*k, **v)
+        if additional_args:
+            for k, v in additional_args:
+                if k[0].startswith('--'):
+                    args_opt.append((k, v))
+                else:
+                    args_req.append((k, v))
+        for k, v in args_req + args_opt:
+            parser.add_argument(*k, **v)
+        if kwargs:
+            pprint.pprint(kwargs)
+        assert not kwargs  # Use all keyword arguments
+        add_install_opts_args(parser, install_opts=install_opts)
+
+
+def get_summary_commands(param=None, **kwargs):
+    r"""Get commands to use to summarize the state of the environment.
+
+    Args:
+        use_mamba (bool, optional): If True, use mamba in place of conda.
+        param (SetupParam, optional): Parameters controlling setup. If
+            not provided, parameters will be generated from kwargs.
+        **kwargs: Keyword arguments are passed to SetupParam if param
+            is not provided.
+
+    Returns:
+        list: Commands.
+
+    """
+    if param is None:
+        param = SetupParam(**kwargs)
+    out = [f"{param.python_cmd} --version",
+           f"{param.python_cmd} -m pip list"]
+    if CONDA_ENV:
+        flags = ''
+        if param.conda_env:
+            flags = f'--name {param.conda_env}'
+        out += [f"echo 'CONDA_PREFIX={CONDA_PREFIX}'",
+                f"{param.conda_exe} info",
+                f"{param.conda_exe} list {flags}",
+                f"{param.conda_exe_config} config --show-sources"]
+    return out
+
+
+def call_conda_command(args, use_mamba=False, **kwargs):
     r"""Function for calling conda commands as the conda script is not
     available on subprocesses for windows unless invoked via the shell.
 
     Args:
         args (list): Command arguments.
         **kwargs: Additional keyword arguments are passed to subprocess.check_output.
+        use_mamba (bool, optional): If True, use mamba in place of conda.
 
     Returns:
         str: The output from the command.
@@ -102,15 +504,30 @@ def call_conda_command(args, **kwargs):
     return subprocess.check_output(args, **kwargs).decode("utf-8")
 
 
-def call_script(lines, force_bash=False):
+def call_script(lines, force_bash=False, verbose=False, dry_run=False,
+                call_kws=None):
     r"""Write lines to a script and call it.
 
     Args:
         lines (list): Lines that should be written to the script.
         force_bash (bool, optional): If True, bash will be used, even
             on windows. Defaults to False.
+        verbose (bool, optional): If True, each line will be printed before
+            it is executed.
+        dry_run (bool, optional): If True, don't actually run any
+            commands. Defaults to False.
+        call_kws (dict, optional): Keyword arguments that should be
+            passed to the subprocess call.
 
     """
+    # if _on_gha:
+    verbose = True
+    if call_kws is None:
+        call_kws = {}
+    if dry_run:
+        lines_str = '\n\t'.join(lines)
+        print(f"Dry run:\n\t{lines_str}")
+        return
     if not lines:
         return
     # Split lines that should be allowed to fail
@@ -126,6 +543,10 @@ def call_script(lines, force_bash=False):
         allow_failure = lines[0].endswith('# [ALLOW FAIL]')
         if allow_failure:
             lines = [lines[0].split('#')[0].strip()]
+        if verbose:
+            for i in range(len(lines) - 1, 0, -1):
+                line_str = lines[i].replace('"', '\\"')
+                lines.insert(i, f'echo "CALLING: {line_str}"')
         if _is_win and (not force_bash):  # pragma: windows
             script_ext = '.bat'
             error_check = 'if %errorlevel% neq 0 exit /b %errorlevel%'
@@ -144,7 +565,6 @@ def call_script(lines, force_bash=False):
             with open(fname, 'w') as fd:
                 fd.write('\n'.join(lines))
                 
-            call_kws = {}
             if _is_win:  # pragma: windows
                 call_cmd = [os.environ['COMSPEC'], '/c', 'call', fname]
             else:
@@ -159,18 +579,23 @@ def call_script(lines, force_bash=False):
                 os.remove(fname)
 
 
-def conda_env_exists(name):
+def conda_env_exists(name, use_mamba=False):
     r"""Determine if a conda environment already exists.
 
     Args:
         name (str): Name of the environment to check.
+        use_mamba (bool, optional): If true, use mamba.
 
     Returns:
         bool: True the the environment exits, False otherwise.
 
     """
-    args = [CONDA_CMD, 'info', '--envs']
-    out = call_conda_command(args)
+    if use_mamba:
+        cmd = MAMBA_CMD
+    else:
+        cmd = CONDA_CMD
+    args = [cmd, 'info', '--envs']
+    out = call_conda_command(args, use_mamba=use_mamba)
     envs = []
     for x in out.splitlines():
         if x.startswith('#') or (not x):
@@ -179,32 +604,79 @@ def conda_env_exists(name):
     return (name in envs)
 
 
-def locate_conda_exe(conda_env, name):
-    r"""Determine the full path to an executable in a specific conda environment.
+def locate_conda_bin(conda_env, use_mamba=False):
+    r"""Determine the full path to the bin directory in a specific
+    conda environment.
 
     Args:
         conda_env (str): Name of conda environment that executable should be
             returned for.
-        name (str): Name of the executable to locate.
+        use_mamba (bool, optional): If True, use mamba in place of conda.
 
     Returns:
-        str: Full path to the executable.
+        str: Full path to the directory.
 
     """
-    assert(CONDA_ROOT)
+    if conda_env is None:
+        conda_env = CONDA_ENV
+    assert CONDA_ROOT
     conda_prefix = os.path.join(CONDA_ROOT, 'envs')
-    if (sys.platform in ['win32', 'cygwin']):
-        if not name.endswith('.exe'):
-            name += '.exe'
-        if name.startswith('python'):
-            out = os.path.join(conda_prefix, conda_env, name)
-        else:
-            out = os.path.join(conda_prefix, conda_env,
-                               'Scripts', name)
+    if sys.platform in ['win32', 'cygwin']:
+        out = os.path.join(conda_prefix, conda_env, 'Scripts')
     else:
-        out = os.path.join(conda_prefix, conda_env, 'bin', name)
+        out = os.path.join(conda_prefix, conda_env, 'bin')
+    return out
+
+
+def locate_exe(name):
+    r"""Locate an executable.
+
+    Args:
+        name (str): Name of the executable to locate.
+
+    Returns:
+        str: Full path to the executable.
+
+    """
+    try:
+        return shutil.which('yggdrasil')
+    except AttributeError:
+        try:
+            return subprocess.check_output(
+                ['which', 'yggdrasil']).strip().decode('utf-8')
+        except subprocess.CalledProcessError:
+            pass
+    return None
+
+
+def locate_conda_exe(conda_env, name, use_mamba=False,
+                     allow_missing=False):
+    r"""Determine the full path to an executable in a specific conda
+    environment.
+
+    Args:
+        conda_env (str): Name of conda environment that executable
+            should be returned for.
+        name (str): Name of the executable to locate.
+        use_mamba (bool, optional): If True, use mamba in place of
+            conda.
+        allow_missing (bool, optional): If True, don't raise an error
+            if the executable dosn't exist. Defaults to False.
+
+    Returns:
+        str: Full path to the executable.
+
+    """
+    if sys.platform in ['win32', 'cygwin'] and not name.endswith('.exe'):
+        name += '.exe'
+    out = os.path.join(
+        locate_conda_bin(conda_env, use_mamba=use_mamba), name)
+    if sys.platform in ['win32', 'cygwin'] and name.startswith('python'):
+        out = os.path.dirname(out)
+    if allow_missing:
+        return out
     try:
-        assert(os.path.isfile(out))
+        assert os.path.isfile(out)
     except AssertionError:
         out = os.path.expanduser(os.path.join('~', '.conda', 'envs', name))
         if not os.path.isfile(out):
@@ -212,7 +684,7 @@ def locate_conda_exe(conda_env, name):
     return out
 
 
-def get_install_opts(old=None):
+def get_install_opts(old=None, empty=False):
     r"""Get optional language/package installation options from CI
     environment variables.
 
@@ -226,11 +698,31 @@ def get_install_opts(old=None):
             should be installed.
 
     """
-    if _on_ci:
+    if empty:
+        new = {
+            'c': False,
+            'lpy': False,
+            'r': False,
+            'fortran': False,
+            'zmq': False,
+            'sbml': False,
+            'astropy': False,
+            'rmq': False,
+            'trimesh': False,
+            'pygments': False,
+            'omp': False,
+            'docs': False,
+            'no_sudo': False,
+            'mpi': False,
+            'dev': False,
+            'testing': False,
+            'empty': True,
+        }
+    elif _on_ci:
         new = {
             'c': (os.environ.get('INSTALLC', '0') == '1'),
             'lpy': (os.environ.get('INSTALLLPY', '0') == '1'),
-            'R': (os.environ.get('INSTALLR', '0') == '1'),
+            'r': (os.environ.get('INSTALLR', '0') == '1'),
             'fortran': (os.environ.get('INSTALLFORTRAN', '0') == '1'),
             'zmq': (os.environ.get('INSTALLZMQ', '0') == '1'),
             'sbml': (os.environ.get('INSTALLSBML', '0') == '1'),
@@ -242,6 +734,8 @@ def get_install_opts(old=None):
             'docs': (os.environ.get('BUILDDOCS', '0') == '1'),
             'no_sudo': False,
             'mpi': (os.environ.get('INSTALLMPI', '0') == '1'),
+            'dev': False,
+            'testing': True,
         }
         if not _is_win:
             new['c'] = True  # c compiler usually installed by default
@@ -249,7 +743,7 @@ def get_install_opts(old=None):
         new = {
             'c': True,
             'lpy': False,
-            'R': True,
+            'r': True,
             'fortran': True,
             'zmq': True,
             'sbml': False,
@@ -261,8 +755,12 @@ def get_install_opts(old=None):
             'docs': False,
             'no_sudo': False,
             'mpi': False,
+            'dev': False,
+            'testing': True,
         }
-    if _is_win:
+    if empty:
+        new['os'] = 'any'
+    elif _is_win:
         new['os'] = 'win'
     elif _is_osx:
         new['os'] = 'osx'
@@ -274,20 +772,26 @@ def get_install_opts(old=None):
         out = old.copy()
     for k, v in new.items():
         out.setdefault(k, v)
-    if not out['c']:
-        out.update(fortran=False, zmq=False)
-    if out['docs']:
-        out['R'] = True  # Allow roxygen
+    if not out.get('empty', False):
+        if not out['c']:
+            out.update(fortran=False, zmq=False)
+        if out['docs']:
+            out['r'] = True  # Allow roxygen
     return out
 
 
-def add_install_opts_args(parser):
+def add_install_opts_args(parser, install_opts=None):
     r"""Add arguments to a parser for installation options.
 
     Args:
         parser (argparse.ArgumentParser): Parser to add arguments to.
+        install_opts (dict, optional): Existing installation options
+            that should be used to set the flags. Create using
+            get_install_opts if not provided.
 
     """
+    if install_opts is None:
+        install_opts = get_install_opts()
     for k, v in install_opts.items():
         if k in ['os']:
             continue
@@ -306,58 +810,74 @@ def add_install_opts_args(parser):
                 help=("Install %s" % k))
 
 
-def create_env(method, python, name=None, packages=None, init=_on_ci):
-    r"""Setup a test environment on a CI resource.
+def create_env(env_method, python, param=None, name=None, packages=None,
+               init=_on_ci, populate=False, **kwargs):
+    r"""Setup an environment for yggdrasil installation.
 
     Args:
-        method (str): Method that should be used to create an environment.
-            Supported values currently include 'conda' & 'virtualenv'.
+        env_method (str): Method that should be used to create an
+            environment. Supported values currently include 'conda',
+            'mamba', and 'virtualenv'.
         python (str): Version of Python that should be tested.
-        name (str, optional): Name that should be used for the environment.
-            Defaults to None and will be craeted based on the method and
-            Python version.
-        packages (list, optional): Packages that should be installed in the new
-            environment. Defaults to None and is ignored.
-        init (bool, optional): If True, the environment management program is
-            first configured as if it is one CI so that, some interactive
-            aspects will be disabled. Default is set based on the presence of
-            CI environment variables (it currently checks for Github Actions,
-            Travis CI, and Appveyor)
-
-    Raises:
-        ValueError: If method is not 'conda' or 'pip'.
+        param (SetupParam, optional): Parameters defining setup. If
+            not provided, one will be created from kwargs.
+        name (str, optional): Name that should be used for the
+            environment. Defaults to None and will be craeted based
+            on the method and Python version.
+        packages (list, optional): Packages that should be installed
+            in the new environment. Defaults to None and is ignored.
+        init (bool, optional): If True, the environment management
+            program is first configured as if it is on CI so that some
+            interactive aspects will be disabled. Default is set based
+            on the presence of CI environment variables (it currently
+            checks for Github Actions, Travis CI, and Appveyor).
+        populate (bool, optional): If True, the environment will be
+            populated. Defaults to False.
 
     """
-    cmds = ["echo Creating test environment using %s..." % method]
-    major, minor = [int(x) for x in python.split('.')][:2]
+    if param is None:
+        if name:
+            kwargs['env_name'] = name
+        param = SetupParam(env_method=env_method, python=python,
+                           **kwargs)
+    python = param.python
+    if name is None:
+        name = param.env_name
     if name is None:
-        name = '%s%s' % (method, python.replace('.', ''))
+        name = env_method + python.replace('.', '')
+    cmds = [f"echo Creating test environment using {env_method}..."]
+    major, minor = [int(x) for x in python.split('.')][:2]
     if packages is None:
         packages = []
-    if 'requests' not in packages:
-        # Not strictly required, but useful for determine the versions of
-        # dependencies required by packages during testing
-        packages.append('requests')
-    if method == 'conda':
-        if conda_env_exists(name):
-            print("Conda env with name '%s' already exists." % name)
-            return
-        if init:
+    # if 'requests' not in packages:
+    #     # Not strictly required, but useful for determine the versions of
+    #     # dependencies required by packages during testing
+    #     packages.append('requests')
+    if param.env_method in ('conda', 'mamba'):
+        conda_exe_config = param.conda_exe_config
+        conda_exe = param.conda_exe
+        if (((not param.dry_run)
+             and conda_env_exists(name, use_mamba=param.use_mamba))):
+            print(f"Conda env with name '{name}' already exists.")
+            if not populate:
+                return
+        else:
+            if init:
+                cmds += [
+                    # Configure conda
+                    f"{conda_exe_config} config --set always_yes yes --set changeps1 no",
+                    f"{conda_exe_config} config --set channel_priority strict",
+                    f"{conda_exe_config} config --prepend channels conda-forge",
+                    f"{conda_exe_config} update -q {param.env_method}",
+                    # f"{conda_exe_config} config --set allow_conda_downgrades true",
+                    # f"{conda_exe} install -n root conda=4.9",
+                ]
             cmds += [
-                # Configure conda
-                "%s config --set always_yes yes --set changeps1 no" % CONDA_CMD,
-                "%s config --set channel_priority strict" % CONDA_CMD,
-                "%s config --add channels conda-forge" % CONDA_CMD,
-                "%s update -q conda" % CONDA_CMD,
-                # "%s config --set allow_conda_downgrades true" % CONDA_CMD,
-                # "%s install -n root conda=4.9" % CONDA_CMD,
+                (f"{conda_exe} create -q -n {name} python={python} "
+                 + ' '.join(packages))
             ]
-        cmds += [
-            "%s create -q -n %s python=%s %s" % (CONDA_CMD, name, python,
-                                                 ' '.join(packages))
-        ]
-    elif method == 'virtualenv':
-        python_cmd = PYTHON_CMD
+    elif param.env_method == 'virtualenv':
+        python_cmd = param.python_cmd
         if (sys.version_info[0] != major) or (sys.version_info[1] != minor):
             if _is_osx:
                 try:
@@ -369,49 +889,66 @@ def create_env(method, python, name=None, packages=None, init=_on_ci):
                 except AttributeError:
                     python_cmd = 'python%d' % major
             else:  # pragma: debug
-                raise RuntimeError(("The version of Python (%d.%d) does not match the "
-                                    "desired version (%s) and virtualenv cannot create "
-                                    "an environment with a different version of Python.")
-                                   % (sys.version_info[0], sys.version_info[1], python))
+                raise RuntimeError(
+                    f"The version of Python "
+                    f"{sys.version_info[0]}.{sys.version_info[1]}) "
+                    f"does not match the desired version "
+                    f"({param.python}) and virtualenv cannot create "
+                    f"an environment with a different version of "
+                    f"Python.")
         cmds += [
-            "%s -m pip install --upgrade pip virtualenv" % python_cmd,
-            "virtualenv -p %s %s" % (python_cmd, name)
+            f"{python_cmd} -m pip install --upgrade pip virtualenv",
+            f"virtualenv -p {python_cmd} {name}"
         ]
+        if populate or packages:
+            if param.target_os == 'win':
+                cmds.append(f".\\{name}\\Scripts\\activate")
+            else:
+                cmds.append(f"source {name}/bin/activate")
         if packages:
-            cmds.append("%s -m pip install %s" % (python_cmd, ' '.join(packages)))
+            cmds.append(f"{python_cmd} -m pip install " + ' '.join(packages))
     else:  # pragma: debug
-        raise ValueError("Unsupport environment management method: '%s'"
-                         % method)
-    call_script(cmds)
+        raise ValueError(f"Unsupported environment management method:"
+                         f" '{param.env_method}'")
+    if populate:
+        cmds += install_pkg(param.method, param=param,
+                            return_commands=True)
+    call_script(cmds, verbose=param.verbose,
+                dry_run=param.dry_run)
 
 
-def build_pkg(method, python=None, return_commands=False,
-              verbose=False):
+def build_pkg(method, param=None, return_commands=False, **kwargs):
     r"""Build the package on a CI resource.
 
     Args:
         method (str): Method that should be used to build the package.
-            Valid values include 'conda' and 'pip'.
-        python (str, optional): Version of Python that package should be
-            built for. Defaults to None if not provided and the current
-            version of Python will be used.
-        return_commands (bool, optional): If True, the commands necessary to
-            build the package are returned instead of running them. Defaults
-            to False.
-        verbose (bool, optional): If True, setup steps are run with verbosity
-            turned up. Defaults to False.
+            Valid values include 'conda', 'mamba', and 'pip'.
+        param (SetupParam, optional): Parameters defining setup. If
+            not provided, one will be created from kwargs.
+        return_commands (bool, optional): If True, the commands
+            necessary to build the package are returned instead of
+            running them. Defaults to False.
+        **kwargs: Additional keyword arguments are passed to
+            SetupParam.
 
     """
-    if python is None:
-        python = PYVER
+    if param is None:
+        param = SetupParam(method, **kwargs)
     cmds = []
     # Upgrade pip and setuptools and wheel to get clean install
     upgrade_pkgs = ['wheel', 'setuptools']
     if not _is_win:
         upgrade_pkgs.insert(0, 'pip')
-    # cmds += ["%s -m pip install --upgrade %s" % (PYTHON_CMD, ' '.join(upgrade_pkgs))]
-    if method == 'conda':
-        if verbose:
+    if param.method == 'conda':
+        conda_env = CONDA_ENV
+        conda_idx = CONDA_INDEX
+        if param.use_mamba:
+            conda_build = f"{CONDA_CMD} mambabuild"
+            build_pkgs = ["boa"]
+        else:
+            conda_build = f"{CONDA_CMD} build"
+            build_pkgs = ["conda-build", "conda-verify"]
+        if param.verbose:
             build_flags = ''
         else:
             build_flags = '-q'
@@ -419,12 +956,13 @@ def build_pkg(method, python=None, return_commands=False,
         # version of Python used in the environment)
         # https://github.com/conda/conda/issues/9124
         # https://github.com/conda/conda/issues/7758#issuecomment-660328841
-        assert(CONDA_ENV == 'base')
-        assert(CONDA_INDEX)
+        assert conda_env == 'base' or param.dry_run
+        assert conda_idx
         if _on_gha:
             cmds += [
-                "%s config --add channels conda-forge" % CONDA_CMD,
-                "%s update -q conda" % CONDA_CMD,
+                f"{param.conda_exe_config} config --prepend channels"
+                f" conda-forge",
+                f"{param.conda_exe} update -q {param.method}",
             ]
         if _is_win and _on_gha:
             # The tests issue a command that is too long for the
@@ -432,597 +970,269 @@ def build_pkg(method, python=None, return_commands=False,
             # package on Github Actions
             build_flags += ' --no-test'
         cmds += [
-            "%s clean --all" % CONDA_CMD]  # Might invalidate cache
+            f"{param.conda_exe} clean --all"]  # Might invalidate cache
         if not (_is_win and _on_gha):
-            cmds += [
-                # "%s deactivate" % CONDA_CMD,
-                "%s update --all" % CONDA_CMD]
+            cmds += [f"{param.conda_exe} update --all"]
         cmds += [
-            "%s install -q -n base conda-build conda-verify" % CONDA_CMD,
-            "%s build %s --python %s %s" % (
-                CONDA_CMD, 'recipe', python, build_flags),
-            "%s index %s" % (CONDA_CMD, CONDA_INDEX),
-            # "%s activate %s" % (CONDA_CMD, CONDA_ENV),
+            f"{param.conda_exe} install -q -n base " + ' '.join(build_pkgs),
+            f"{conda_build} recipe --python {param.python} {build_flags}"
         ]
-    elif method == 'pip':
-        if verbose:
+        cmds.append(f"{param.conda_exe} index {conda_idx}")
+    elif param.method == 'pip':
+        if param.verbose:
             build_flags = ''
         else:
             build_flags = '--quiet'
         # Install from source dist
-        cmds += ["%s -m pip install --upgrade %s" % (PYTHON_CMD, ' '.join(upgrade_pkgs))]
-        cmds += ["%s setup.py %s sdist" % (PYTHON_CMD, build_flags)]
+        cmds += [f"{param.python_cmd} -m pip install --upgrade "
+                 + ' '.join(upgrade_pkgs)]
+        cmds += [f"{param.python_cmd} setup.py {build_flags} sdist"]
     else:  # pragma: debug
-        raise ValueError("Method must be 'conda' or 'pip', not '%s'"
-                         % method)
+        raise ValueError(f"Method must be 'conda', 'mamba', or 'pip', not"
+                         f" '{param.method}'")
+    summary_cmds = get_summary_commands(param)
+    if cmds:
+        cmds += summary_cmds
     if return_commands:
         return cmds
-    cmds = SUMMARY_CMDS + cmds + SUMMARY_CMDS
-    call_script(cmds)
-    if method == 'conda':
-        assert(CONDA_INDEX and os.path.isdir(CONDA_INDEX))
-
-
-def create_env_yaml(filename='environment.yml', name='env', channels=None,
-                    install_opts=None, target_os=None, python_version=None,
-                    **kwargs):
-    r"""Create an environment.yml file.
-
-    Args:
-        filename (str, optional): Path where the generate file should be
-            saved. Defaults to 'environment.yml'.
-        name (str, optional): Name of environment that should be
-            created by the file. Defaults to 'env'.
-        channels (list, optional): Conda channels that should be used in the
-            environment file. Defaults to []. 'conda-forge' will be added
-            if it is not already present.
-        install_opts (dict, optional): Mapping from language/package to bool
-            specifying whether or not the language/package should be installed.
-            If not provided, get_install_opts is used to create it.
-        target_os (str, optional): Operating system that the generated file
-            should target. Defaults to the current OS.
-        python_version (str, optional): Python version that environment
-            should target. Defaults to the current Python version if not
-            provided.
-        **kwargs: Additional keyword arguments are passed to itemize_deps.
-
-    """
-    import yaml
-    import platform
-    from install_from_requirements import prune
-    if channels is None:
-        channels = []
-    if 'conda-forge' not in channels:
-        channels.append('conda-forge')
-    if python_version is None:
-        python_version = ".".join(platform.python_version_tuple()[:2])
-    environment = {'python_version': python_version}
-    if target_os is not None:
-        if target_os == 'linux':
-            environment["os_name"] = 'Linux'
-        elif target_os == 'osx':
-            environment["os_name"] = 'Darwin'
-        elif target_os == 'win':
-            environment["os_name"] = 'Windows'
-    install_opts = get_install_opts(install_opts)
-    pkgs = itemize_deps('conda', install_opts=install_opts, **kwargs)
-    deps = prune(pkgs['requirements'] + pkgs['requirements_conda'],
-                 install_opts=install_opts, excl_method='pip',
-                 additional_packages=pkgs['conda'],
-                 skip_packages=pkgs['skip'],
-                 return_list=True, environment=environment,
-                 verbose=True)
-    deps.insert(0, 'python=' + python_version)
-    data = {'name': name,
-            'channels': channels,
-            'dependencies': deps}
-    with open(filename, 'w') as fd:
-        yaml.dump(data, fd, Dumper=yaml.SafeDumper)
-    return filename
-
-
-def itemize_deps(method, for_development=False,
-                 skip_test_deps=False, include_dev_deps=False, include_doc_deps=False,
-                 windows_package_manager='vcpkg', install_opts=None,
-                 fallback_to_conda=None):
-    r"""Get lists of dependencies.
-    
-    Args:
-        method (str): Method that should be used to install the package dependencies.
-            Valid values include 'conda' and 'pip'.
-        for_development (bool, optional): If True, dependencies are installed that would
-            be missed when installing in development mode. Defaults to False.
-        skip_test_deps (bool, optional): If True, dependencies required for running
-            tests will not be installed. Defaults to False.
-        include_dev_deps (bool, optional): If True, dependencies used during development
-            will be installed. Defaults to False.
-        include_doc_deps (bool, optional): If True, dependencies used during generating
-            documentation will be installed. Defaults to False.
-        windows_package_manager (str, optional): Name of the package
-            manager that should be used on windows. Defaults to 'vcpkg'.
-        install_opts (dict, optional): Mapping from language/package to bool
-            specifying whether or not the language/package should be installed.
-            If not provided, get_install_opts is used to create it.
-        fallback_to_conda (bool, optional): If True, conda will be used to install
-            non-python dependencies and Python dependencies that cannot be installed
-            via pip. Defaults to False.
-
-    Returns:
-        dict: Dependencies grouped by the package manager that should be
-            used.
+    if cmds:
+        cmds = summary_cmds + cmds
+        if param.use_mamba and not shutil.which('mamba'):
+            cmds_after = cmds
+            cmds = get_summary_commands()
+            cmds += [
+                f"{CONDA_CMD} config --prepend channels conda-forge",
+                f"{CONDA_CMD} config --remove channels defaults",
+                f"{CONDA_CMD} config --set channel_priority strict",
+            ]
+            if not (_is_win and _on_gha):
+                cmds += [f"{CONDA_CMD} update --all"]
+            cmds += [f"{CONDA_CMD} install mamba -c conda-forge"]
+            cmds += cmds_after
+        call_script(cmds, verbose=param.verbose,
+                    dry_run=param.dry_run)
+    if param.method == 'conda':  # and not param.use_mamba:
+        print(f"CONDA_IDX = {conda_idx}")
+        assert (conda_idx and os.path.isdir(conda_idx))
 
-    """
-    from install_from_requirements import (
-        get_pip_dependency_version, DependencyNotFound)
-    out = {'conda': [], 'pip': [], 'os': [], 'skip': [],
-           'apt': [], 'brew': [], 'choco': [], 'vcpkg': [],
-           'requirements': ['requirements_optional.txt'],
-           'requirements_conda': [], 'requirements_pip': []}
-    if fallback_to_conda is None:
-        fallback_to_conda = ((method == 'conda')
-                             or ((install_opts['os'] == 'win')
-                                 and _on_appveyor)
-                             or install_opts['lpy'])
-    print('ITEMIZE_DEPS', fallback_to_conda)
-    if method == 'conda':
-        out['default'] = out['conda']
-    elif method == 'pip':
-        out['default'] = out['pip']
-    else:  # pragma: debug
-        raise ValueError("Method must be 'conda' or 'pip', not '%s'"
-                         % method)
-    # In the case of a development environment install requirements that
-    # would be missed when installing in development mode
-    if for_development:
-        if method == 'conda':
-            out['requirements'] += ['requirements.txt',
-                                    'requirements_condaonly.txt']
-        elif method == 'pip':
-            # requirements.txt not needed because dev install will
-            # pick up and install those deps
-            out['requirements'] += ['requirements_piponly.txt']
-        out['default'].append('ipython')
-        include_dev_deps = True
-    if not skip_test_deps:
-        out['requirements'].append('requirements_testing.txt')
-    if include_dev_deps:
-        out['requirements'].append('requirements_development.txt')
-    if (method == 'pip') and fallback_to_conda:
-        out['requirements_conda'] += ['requirements_condaonly.txt']
-    elif (method == 'conda'):
-        out['requirements_pip'] += ['requirements.txt',
-                                    'requirements_piponly.txt']
-    # Required for non-strict channel priority
-    # https://github.com/conda-forge/conda-forge.github.io/pull/670
-    # https://conda.io/projects/conda/en/latest/user-guide/concepts/ ...
-    #    packages.html?highlight=openblas#installing-numpy-with-blas-variants
-    # if fallback_to_conda:
-    #     conda_pkgs.append("\"blas=*=openblas\"")
-    # Installing via pip causes import error on Windows and
-    # a conflict when installing LPy
-    out['conda'] += ['scipy', os.environ.get('NUMPY', 'numpy')]
-    for k in ['matplotlib', 'jsonschema']:
-        if os.environ.get(k.upper(), k) != k:
-            out['default'].append(os.environ[k.upper()])
-    if install_opts['os'] == 'linux':
-        out['os'] += ["strace", "valgrind"]
-    elif install_opts['os'] == 'osx':
-        out['os'] += ["valgrind"]
-    if install_opts['omp'] and (not fallback_to_conda):
-        if install_opts['os'] == 'linux':
-            out['os'].append('libomp-dev')
-        elif install_opts['os'] == 'osx':
-            out['os'] += ['libomp', 'llvm']
-        elif install_opts['os'] == 'win':
-            pass
-    if install_opts['mpi'] and (not fallback_to_conda):
-        if install_opts['os'] == 'linux':
-            out['os'] += ['openmpi-bin', 'libopenmpi-dev']
-        elif install_opts['os'] == 'osx':
-            out['os'].append('openmpi')
-        elif install_opts['os'] == 'win':
-            pass
-    elif ((install_opts['os'] == 'win') and install_opts['mpi']
-          and (method == 'conda')):
-        # Force mpi4py to be installed last on Windows to avoid
-        # conflicts
-        out['skip'].append('mpi4py')
-    if install_opts['fortran'] and (not fallback_to_conda):
-        # Fortran is not installed via conda on linux/macos
-        if install_opts['os'] == 'linux':
-            out['os'].append("gfortran")
-        elif install_opts['os'] == 'osx':
-            out['os'].append("gcc")
-            out['os'].append("gfortran")
-        elif install_opts['os'] == 'win' and (not fallback_to_conda):
-            out['choco'] += ["mingw"]
-            # out['vcpkg'].append("vcpkg-gfortran")
-    if install_opts['R'] and (not fallback_to_conda):
-        # TODO: Test split installation where r-base is installed from
-        # conda and the R dependencies are installed from CRAN?
-        if install_opts['os'] == 'linux':
-            if not shutil.which('R'):
-                out['os'] += ["r-base", "r-base-dev"]
-            out['os'] += ["libudunits2-dev"]
-        elif install_opts['os'] == 'osx':
-            if not shutil.which('R'):
-                out['os'] += ["r"]
-            out['os'] += ["udunits"]
-        elif install_opts['os'] == 'win':
-            if not shutil.which('R'):
-                out['choco'] += [
-                    "r.project --params \"\'/AddToPath\'\"",
-                    "rtools"]
-        else:
-            raise NotImplementedError("Could not determine "
-                                      "R installation method.")
-    if install_opts['zmq'] and (not fallback_to_conda):
-        if install_opts['os'] == 'linux':
-            out['os'] += ["libczmq-dev", "libzmq3-dev"]
-        elif install_opts['os'] == 'osx':
-            out['os'] += ["czmq", "zmq"]
-        elif install_opts['os'] == 'win':
-            out['vcpkg'] += ["czmq", "zeromq"]
-        else:
-            raise NotImplementedError("Could not determine "
-                                      "ZeroMQ installation method.")
-    if include_doc_deps or install_opts['docs']:
-        out['requirements'].append('requirements_documentation.txt')
-        if not fallback_to_conda:
-            out['os'].append("doxygen")
-            if install_opts['os'] == 'linux':
-                out['os'] += ["pandoc"]
-            elif install_opts['os'] == 'osx':
-                out['os'] += ["pandoc-citeproc", "Caskroom/cask/mactex"]
-            else:
-                NotImplementedError("Could not determine "
-                                    "pandoc installation method.")
-    if install_opts['sbml'] and fallback_to_conda:
-        # Until the sbml package is updated to allow numpy != 1.19.3,
-        # sbml will need to be installed separately without deps in order
-        # to work in a conda env
-        numpy_ver = 'numpy==1.19.3'
-        try:
-            new_numpy_ver = get_pip_dependency_version(
-                'libroadrunner', 'numpy')
-            if new_numpy_ver != numpy_ver:
-                warnings.warn(
-                    "libroadrunner has updated it's numpy requirement. "
-                    "Assuming the new requirement is not strict (%s), "
-                    "this code can be removed and libroadrunner can be "
-                    "installed normally (numpy should be installed with "
-                    "conda if a conda environment is being used to avoid "
-                    "inconsistencies)." % new_numpy_ver)
-            numpy_ver = new_numpy_ver
-        except (ImportError, ModuleNotFoundError, DependencyNotFound):
-            pass
-        out['conda'].insert(0, numpy_ver.replace('==', '>='))
-        out['skip'].append('libroadrunner')
-    if install_opts['astropy'] and fallback_to_conda and _on_travis:
-        out['conda'].insert(0, 'astropy>=4.1')
-    if ((install_opts['fortran'] and fallback_to_conda
-         and (_on_travis or (_on_gha and (install_opts['os'] == 'osx'))))):
-        out['conda'].append('fortran-compiler')
-    if not fallback_to_conda:
-        out['default'] += out['conda']
-    # Determine package manager based on OS
-    if install_opts['os'] == 'linux':
-        out['apt'] += out['os']
-    elif install_opts['os'] == 'osx':
-        out['brew'] += out['os']
-    elif install_opts['os'] == 'win':
-        if windows_package_manager == 'choco':
-            out['choco'] += out['os']
-        elif windows_package_manager == 'vcpkg':
-            out['vcpkg'] += out['os']
-        else:
-            raise NotImplementedError("Invalid package manager: '%s'"
-                                      % windows_package_manager)
-    out.pop('os')
-    return out
 
+def preinstall_deps(method, param=None, return_commands=False,
+                    no_packages=False, **kwargs):
+    r"""Pre-install packages with test specific versions.
 
-def install_deps(method, return_commands=False, verbose=False,
-                 install_opts=None, conda_env=None,
-                 always_yes=False, only_python=False, fallback_to_conda=None, **kwargs):
-    r"""Install the package dependencies.
-    
     Args:
-        method (str): Method that should be used to install the package dependencies.
-            Valid values include 'conda' and 'pip'.
-        return_commands (bool, optional): If True, the commands necessary to
-            install the dependencies are returned instead of running them. Defaults
-            to False.
-        verbose (bool, optional): If True, setup steps are run with verbosity
-            turned up. Defaults to False.
-        install_opts (dict, optional): Mapping from language/package to bool
-            specifying whether or not the language/package should be installed.
-            If not provided, get_install_opts is used to create it.
-        conda_env (str, optional): Name of the conda environment that packages
-            should be installed in. Defaults to None and is ignored.
-        always_yes (bool, optional): If True, conda commands are called with -y flag
-            so that user interaction is not required. Defaults to False.
-        only_python (bool, optional): If True, only Python packages will be installed.
-            Defaults to False.
-        fallback_to_conda (bool, optional): If True, conda will be used to install
-            non-python dependencies and Python dependencies that cannot be installed
-            via pip. Defaults to False.
-        **kwargs: Additional keyword arguments are passed to itemize_deps.
+        method (str): Method that should be used to install the
+            package dependencies. Valid values include 'conda',
+            'mamba', and 'pip'.
+        param (SetupParam, optional): Parameters defining setup. If
+            not provided, one will be created from kwargs.
+        return_commands (bool, optional): If True, the commands
+            necessary to install the dependencies are returned instead
+            of running them. Defaults to False.
+        no_package (bool, optional): If True, no packages are
+            uninstalled or installed. Defaults to False.
+        **kwargs: Additional keyword arguments are passed to
+            SetupParam.
 
     """
-    from install_from_requirements import install_from_requirements
-    install_opts = get_install_opts(install_opts)
-    python_cmd = PYTHON_CMD
-    conda_flags = ''
-    if conda_env:
-        python_cmd = locate_conda_exe(conda_env, 'python')
-        conda_flags += ' --name %s' % conda_env
-    if always_yes:
-        conda_flags += ' -y'
-    # Determine if conda should be used for base dependencies
-    if fallback_to_conda is None:
-        fallback_to_conda = ((method == 'conda')
-                             or (_is_win and _on_appveyor)
-                             or install_opts['lpy'])
-    # Get list of packages
-    pkgs = itemize_deps(method, fallback_to_conda=fallback_to_conda,
-                        install_opts=install_opts, **kwargs)
-    pprint.pprint(pkgs)
+    if param is None:
+        param = SetupParam(method, **kwargs)
+    conda_prefix = '$CONDA_PREFIX'
+    conda_root = CONDA_ROOT
+    cmds = []
     # Uninstall default numpy and matplotlib to allow installation
     # of specific versions
-    cmds = ["%s -m pip uninstall -y numpy matplotlib" % python_cmd]
+    if not no_packages:
+        # Installing via pip causes import error on Windows and
+        #  a conflict when installing LPy
+        # TODO: Get this from YggRequirements
+        version_specified = ['scipy', 'numpy',
+                             'matplotlib', 'jsonschema']
+        if param.method != 'conda':
+            cmds += [f"{param.python_cmd} -m pip uninstall -y "
+                     + ' '.join(version_specified)]
     # Refresh channel
     # https://github.com/conda/conda/issues/8051
-    if fallback_to_conda and _on_gha:
+    if param.fallback_to_conda and _on_gha:
         cmds += [
-            "%s config --set channel_priority strict" % CONDA_CMD,
-            # "%s install -n root conda=4.9" % CONDA_CMD,
-            # "%s config --set allow_conda_downgrades true" % CONDA_CMD,
-            "%s config --remove channels conda-forge" % CONDA_CMD,
-            "%s config --add channels conda-forge" % CONDA_CMD,
+            f"{param.conda_exe_config} config --set channel_priority strict",
+            # These commands will not be valid for mamba
+            # f"{param.conda_exe} install -n root conda=4.9",
+            # f"{param.conda_exe_config} config --set "
+            # f" allow_conda_downgrades true",
+            f"{param.conda_exe_config} config --remove channels conda-forge",
+            f"{param.conda_exe_config} config --prepend channels conda-forge",
         ]
-    if fallback_to_conda:
-        cmds.append("%s update --all" % CONDA_CMD)
-    # if install_opts['R'] and (not fallback_to_conda) and (not only_python):
-    #     # TODO: Test split installation where r-base is installed from
-    #     # conda and the R dependencies are installed from CRAN?
-    #     if _is_linux:
-    #         if install_opts['no_sudo']:
-    #             cmds += [
-    #                 ("add-apt-repository 'deb https://cloud"
-    #                  ".r-project.org/bin/linux/ubuntu xenial-cran35/'"),
-    #                 ("apt-key adv --keyserver keyserver.ubuntu.com "
-    #                  "--recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9")]
-    #         else:
-    #             cmds += [
-    #                 ("sudo add-apt-repository 'deb https://cloud"
-    #                  ".r-project.org/bin/linux/ubuntu xenial-cran35/'"),
-    #                 ("sudo apt-key adv --keyserver keyserver.ubuntu.com "
-    #                  "--recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9")]
-    # if install_opts['zmq'] and (not fallback_to_conda):
-    #     cmds.append("echo Installing ZeroMQ...")
-    #     if _is_linux:
-    #         cmds.append("./ci/install-czmq-linux.sh")
-    #     elif _is_osx:
-    #         cmds.append("bash ci/install-czmq-osx.sh")
-    #     # elif _is_win:
-    #     #     cmds += ["call ci\\install-czmq-windows.bat",
-    #     #              "echo \"%PATH%\""]
-    #     else:
-    #         raise NotImplementedError("Could not determine "
-    #                                   "ZeroMQ installation method.")
-    if _on_gha and _is_linux and fallback_to_conda:
-        conda_prefix = '$CONDA_PREFIX'
-        if conda_env:
-            conda_prefix = os.path.join(CONDA_ROOT, 'envs', conda_env)
+    if param.fallback_to_conda and not no_packages:
+        cmds.append(f"{param.conda_exe} update --all")
+    if _on_gha and _is_unix and param.fallback_to_conda:
+        if param.conda_env:
+            conda_prefix = os.path.join(conda_root, 'envs',
+                                        param.conda_env)
         # Do both to ensure that the path is set for the installation
         # and in following steps
         cmds += [
-            "export LD_LIBRARY_PATH=%s/lib:$LD_LIBRARY_PATH" % conda_prefix,
+            f"export LD_LIBRARY_PATH={conda_prefix}/lib:$LD_LIBRARY_PATH",
             "echo -n \"LD_LIBRARY_PATH=\" >> $GITHUB_ENV",
-            "echo %s/lib:$LD_LIBRARY_PATH >> $GITHUB_ENV" % conda_prefix
+            f"echo {conda_prefix}/lib:$LD_LIBRARY_PATH >> $GITHUB_ENV"
         ]
-    # Install dependencies using package manager(s)
-    if not only_python:
-        if pkgs['apt']:
-            if install_opts['no_sudo']:
-                cmds += ["apt -y update"]
-                cmds += ["apt-get -y install %s" % ' '.join(pkgs['apt'])]
-            else:
-                cmds += ["sudo apt update"]
-                cmds += ["sudo apt-get install %s" % ' '.join(pkgs['apt'])]
-        if pkgs['brew']:
-            if 'gcc' in pkgs['brew']:
-                cmds += ["brew reinstall gcc"]
-                pkgs['brew'].remove('gcc')
-            if 'valgrind' in pkgs['brew']:
-                # There seems to be a bug with this installation on GHA
-                # cmds += ["brew tap LouisBrunner/valgrind",
-                #          "brew install --HEAD LouisBrunner/valgrind/valgrind"]
-                pkgs['brew'].remove('valgrind')
-            pkgs_from_src = []
-            if _on_travis:
-                for k in ['zmq', 'czmq', 'zeromq']:
-                    if k in pkgs['brew']:
-                        pkgs_from_src.append(k)
-                        pkgs['brew'].remove(k)
-            if pkgs_from_src:
-                cmds += ["brew install --build-from-source %s" % ' '.join(pkgs_from_src)]
-            if pkgs['brew']:
-                cmds += ["brew install %s" % ' '.join(pkgs['brew'])]
-        if pkgs['choco']:
-            # cmds += ["choco install %s" % ' '.join(pkgs['choco'])]
-            for x in pkgs['choco']:
-                cmds.append("choco install %s --force" % x)
-        if pkgs['vcpkg']:
-            cmds += ["%s install %s --triplet x64-windows"
-                     % ('vcpkg.exe', ' '.join(pkgs['vcpkg']))]
-    # Install via requirements
-    req_kwargs = dict(conda_env=conda_env, python_cmd=python_cmd,
-                      install_opts=install_opts, append_cmds=cmds,
-                      skip_packages=pkgs['skip'], verbose=verbose,
-                      verbose_prune=True)
-    install_from_requirements(method, pkgs['requirements'],
-                              additional_packages=pkgs['default'],
-                              **req_kwargs)
-    pkgs.pop(method, None)  # Remove so that they are not installed twice
-    if fallback_to_conda:
-        install_from_requirements('conda', pkgs['requirements_conda'],
-                                  additional_packages=pkgs.get('conda', []),
-                                  unique_to_method=True, **req_kwargs)
-    install_from_requirements('pip', pkgs['requirements_pip'],
-                              additional_packages=pkgs.get('pip', []),
-                              unique_to_method=True, **req_kwargs)
-    if 'libroadrunner' in pkgs['skip']:
-        pip_flags = '--no-dependencies'
-        if verbose:
-            pip_flags += ' --verbose'
-        cmds.append('%s -m pip install %s \"libroadrunner<2.0.7\"'
-                    % (python_cmd, pip_flags))
-    if install_opts['lpy']:
-        if verbose:
-            install_flags = '-vvv'
-        else:
-            install_flags = '-q'
-        install_flags += conda_flags
-        if fallback_to_conda:
-            cmds += ["%s install %s openalea.lpy boost=1.66.0 -c openalea"
-                     % (CONDA_CMD, install_flags)]
-        else:  # pragma: debug
-            raise RuntimeError("Could not detect conda environment. "
-                               "Cannot proceed with a conda deployment "
-                               "(required for LPy).")
-    # if _is_win and install_opts['mpi'] and fallback_to_conda:
-    #     if verbose:
-    #         install_flags = '-vvv'
-    #     else:
-    #         install_flags = '-q'
-    #     install_flags = '-vv'
-    #     install_flags += conda_flags
-    #     # This is required as the install script for mpi4py aborts without
-    #     # an error message when called inside a Python subprocess. This seems
-    #     # to occur during cleanup for the installation process as the mpi4py
-    #     # installation is functional. Possibly triggered by the activation script?
-    #     cmds += ["%s install %s mpi4py # [ALLOW FAIL]" % (CONDA_CMD, install_flags)]
     if return_commands:
         return cmds
-    cmds = SUMMARY_CMDS + cmds + SUMMARY_CMDS
-    call_script(cmds)
+    if cmds:
+        cmds += get_summary_commands(param)
+        call_script(cmds, verbose=param.verbose,
+                    dry_run=param.dry_run)
+
+
+def install_deps(method, param=None, return_commands=False,
+                 do_preinstall=False, req=None, **kwargs):
+    r"""Install the package dependencies.
+    
+    Args:
+        method (str): Method that should be used to install the
+            package dependencies. Valid values include 'conda',
+            'mamba', and 'pip'.
+        param (SetupParam, optional): Parameters defining setup. If
+            not provided, one will be created from kwargs.
+        return_commands (bool, optional): If True, the commands
+            necessary to install the dependencies are returned instead
+            of running them. Defaults to False.
+        do_preinstall (bool, optional): If True, steps are taken to
+            prepare for installation. Defaults to False.
+        req (YggRequirements, optional): Existing set of requirements
+            to use.
+        **kwargs: Additional keyword arguments are passed to
+            SetupParam.
+
+    """
+    from manage_requirements import install_requirements
+    if param is None:
+        kwargs.setdefault("deps_method", "supplemental")
+        param = SetupParam(method, **kwargs)
+    cmds = []
+    if do_preinstall:
+        cmds += preinstall_deps(param.method_base, param=param,
+                                return_commands=True)
+        if cmds:
+            cmds += get_summary_commands(param)
+    cmds += install_requirements(param,
+                                 return_commands=True,
+                                 req=req)
+    summary_cmds = get_summary_commands(param=param)
+    if cmds:
+        cmds += summary_cmds
+    if return_commands:
+        return cmds
+    if cmds:
+        cmds = summary_cmds + cmds
+        call_script(cmds, verbose=param.verbose,
+                    dry_run=param.dry_run)
+    return cmds
 
 
-def install_pkg(method, python=None, without_build=False,
-                without_deps=False, verbose=False,
-                skip_test_deps=False, include_dev_deps=False, include_doc_deps=False,
-                windows_package_manager='vcpkg', install_opts=None, conda_env=None,
-                always_yes=False, only_python=False, fallback_to_conda=None):
+def install_pkg(method, param=None, without_build=False,
+                without_deps=False, install_deps_before=False,
+                return_commands=False, **kwargs):
     r"""Build and install the package and its dependencies on a CI
     resource.
 
     Args:
         method (str): Method that should be used to build and install
             the package. Valid values include 'conda' and 'pip'.
-        python (str, optional): Version of Python that package should be
-            built for. Defaults to None if not provided and the current
-            version of Python will be used. This will be ignored if
-            without_build is True.
-        without_build (bool, optional): If True, the package will not be
-            built prior to install. Defaults to False.
-        without_deps (bool, optional): If True the package dependencies will
-            no be installed prior to installing the package. Defaults to
+        param (SetupParam, optional): Parameters defining setup. If
+            not provided, one will be created from kwargs.
+        without_build (bool, optional): If True, the package will not
+            be built prior to install. Defaults to False.
+        without_deps (bool, optional): If True the package
+            dependencies will not be installed prior to installing
+            the package. Defaults to False.
+        install_deps_before (bool, optional): If True, install deps
+            before the package is installed. Set to true in the case
+            of pip or dev environments to handle non-Python
+            dependencies before yggdrasil is installed. Defaults to
             False.
-        verbose (bool, optional): If True, setup steps are run with verbosity
-            turned up. Defaults to False.
-        skip_test_deps (bool, optional): If True, dependencies required for running
-            tests will not be installed. Defaults to False.
-        include_dev_deps (bool, optional): If True, dependencies used during development
-            will be installed. Defaults to False.
-        include_doc_deps (bool, optional): If True, dependencies used during generating
-            documentation will be installed. Defaults to False.
-        windows_package_manager (str, optional): Name of the package
-            manager that should be used on windows. Defaults to 'vcpkg'.
-        install_opts (dict, optional): Mapping from language/package to bool
-            specifying whether or not the language/package should be installed.
-            If not provided, get_install_opts is used to create it.
-        conda_env (str, optional): Name of the conda environment that packages
-            should be installed in. Defaults to None and is ignored.
-        always_yes (bool, optional): If True, conda commands are called with -y flag
-            so that user interaction is not required. Defaults to False.
-        only_python (bool, optional): If True, only Python packages will be installed.
-            Defaults to False.
-        fallback_to_conda (bool, optional): If True, conda will be used to install
-            non-python dependencies and Python dependencies that cannot be installed
-            via pip. Defaults to False.
+        return_commands (bool, optional): If True, the commands
+            necessary to install the package are returned instead of
+            running them. Defaults to False.
+        **kwargs: Additional keyword arguments are passed to
+            SetupParam.
 
     Raises:
         ValueError: If method is not 'conda' or 'pip'.
 
     """
-    install_opts = get_install_opts(install_opts)
-    python_cmd = PYTHON_CMD
-    conda_flags = ''
-    if conda_env:
-        python_cmd = locate_conda_exe(conda_env, 'python')
-        conda_flags += ' --name %s' % conda_env
-    if always_yes:
-        conda_flags += ' -y'
-    cmds = []
-    if method.endswith('-dev'):
-        method_base = method.split('-dev')[0]
-        for_development = True
+    from manage_requirements import YggRequirements
+    if param is None:
+        param = SetupParam(method, **kwargs)
+    if param.method != 'conda' and not param.only_python:
+        # For pip and dev environments, non-Python deps should be
+        #   installed before yggdrasil
+        install_deps_before = True
+    summary_cmds = get_summary_commands(param)
+    cmds = [x for x in summary_cmds]  # Make sure this a copy
+    if param.for_development:
         without_build = True
-    else:
-        method_base = method
-        for_development = False
     if not without_build:
-        cmds += build_pkg(method_base, python=python,
-                          return_commands=True, verbose=verbose)
-        cmds += SUMMARY_CMDS
+        cmds += build_pkg(param.method, param=param,
+                          return_commands=True)
+    cmds_deps = []
+    cmds += preinstall_deps(param.method_base,
+                            param=param,
+                            return_commands=True)
+    req = YggRequirements.from_file()
     if not without_deps:
-        cmds += install_deps(method_base, return_commands=True, verbose=verbose,
-                             for_development=for_development,
-                             skip_test_deps=skip_test_deps,
-                             include_dev_deps=include_dev_deps,
-                             include_doc_deps=include_doc_deps,
-                             install_opts=install_opts,
-                             windows_package_manager=windows_package_manager,
-                             conda_env=conda_env, always_yes=always_yes,
-                             only_python=only_python,
-                             fallback_to_conda=fallback_to_conda)
-        cmds += SUMMARY_CMDS
+        cmds_deps += install_deps(param.method_base, param=param,
+                                  return_commands=True, req=req)
+    if install_deps_before:
+        cmds += cmds_deps
+    extras = [
+        x for x in req.extras(methods=['python', param.method_base])
+        if param.install_opts[x]]
     # Install yggdrasil
-    if method == 'conda':
-        assert(CONDA_INDEX and os.path.isdir(CONDA_INDEX))
+    if param.for_development:
+        # Call setup.py in separate process from the package directory
+        # cmds += [f"{param.python_cmd} setup.py develop"]
+        pass
+    elif param.method == 'conda':
+        conda_exe_config = CONDA_CMD
+        if param.use_mamba:
+            conda_exe = MAMBA_CMD
+            conda_idx = CONDA_INDEX  # 'local'
+        else:
+            conda_exe = CONDA_CMD
+            conda_idx = CONDA_INDEX
+        if not (conda_idx and os.path.isdir(conda_idx)):
+            print(f"conda_idx = {conda_idx}")
+        assert (conda_idx and os.path.isdir(conda_idx))
         # Install from conda build
         # Assumes that the target environment is active
-        if verbose:
-            install_flags = '-vvv'
-        else:
-            install_flags = '-q'
-        install_flags += conda_flags
+        install_flags = param.conda_flags
+        if not param.use_mamba:
+            install_flags += ' --update-deps'
         if _is_win:
-            index_channel = CONDA_INDEX
+            index_channel = conda_idx
         else:
-            index_channel = "file:/%s" % CONDA_INDEX
+            index_channel = f"file:/{conda_idx}"
+        ygg_pkgs = ['yggdrasil']
+        ygg_pkgs += [f'yggdrasil.{x}' for x in extras]
         cmds += [
-            "%s config --add channels %s" % (CONDA_CMD, index_channel),
+            f"{conda_exe_config} config --prepend channels {index_channel}",
             # Related issues if this stops working again
             # https://github.com/conda/conda/issues/466#issuecomment-378050252
-            "%s install %s --update-deps -c %s yggdrasil" % (
-                CONDA_CMD, install_flags, index_channel)
+            f"{conda_exe} install {install_flags} -c"
+            f" {index_channel} {' '.join(ygg_pkgs)}"
             # Required for non-strict channel priority
             # https://github.com/conda-forge/conda-forge.github.io/pull/670
             # https://conda.io/projects/conda/en/latest/user-guide/concepts/ ...
-            #    packages.html?highlight=openblas#installing-numpy-with-blas-variants
-            # "%s install %s --update-deps -c %s yggdrasil \"blas=*=openblas\"" % (
-            #     CONDA_CMD, install_flags, index_channel)
+            # packages.html?highlight=openblas#installing-numpy-with-blas-variants
+            # f"{conda_exe} install {install_flags} --update-deps -c
+            #   {index_channel} yggdrasil \"blas=*=openblas\""
         ]
-        if _is_win and install_opts['mpi']:
-            cmds[-1] = cmds[-1] + ' mpi4py # [ALLOW FAIL]'
-    elif method == 'pip':
-        if verbose:
-            install_flags = '--verbose'
-        else:
-            install_flags = ''
+        if 'mpi' in extras:
+            assert ' install ' in cmds[-1]
+            cmds[-1] += " # [ALLOW FAIL]"
+        cmds += summary_cmds
+    elif param.method == 'pip':
         if _is_win:  # pragma: windows
             cmds += [
                 "for %%a in (\"dist\\*.tar.gz\") do set YGGSDIST=%%a",
@@ -1031,38 +1241,62 @@ def install_pkg(method, python=None, without_build=False,
             sdist = "%YGGSDIST%"
         else:
             sdist = "dist/*.tar.gz"
+        # if extras:
+        #     sdist += f"[{','.join(extras)}]"
         cmds += [
-            "%s -m pip install %s %s" % (python_cmd, install_flags, sdist),
-            "%s create_coveragerc.py" % python_cmd
+            f"{param.python_cmd} -m pip install"
+            f" {param.pip_flags} {sdist}",
+            f"{param.python_cmd} create_coveragerc.py"
         ]
-    elif method.endswith('-dev'):
-        # Call setup.py in separate process from the package directory
-        # cmds += ["%s setup.py develop" % python_cmd]
-        pass
+        cmds += summary_cmds
     else:  # pragma: debug
-        raise ValueError("Invalid method: '%s'" % method)
-    # Print summary of what was installed
-    if not YGG_CMD_WHICH:
-        cmds = SUMMARY_CMDS + cmds + SUMMARY_CMDS
-        call_script(cmds)
-        if method.endswith('-dev'):
-            print(call_conda_command([python_cmd, '-m', 'pip', 'install',
-                                      '--editable', '.'],
-                                     cwd=_pkg_dir))
+        raise ValueError(f"Invalid method: '{param.method}'")
+    yggdrasil_installed = False
+    if param.method == 'conda':
+        try:
+            locate_conda_exe(param.conda_env, 'yggdrasil',
+                             use_mamba=param.use_mamba)
+            yggdrasil_installed = True
+        except AssertionError:
+            pass
+    else:
+        yggdrasil_installed = (locate_exe('yggdrasil') is not None)
+    if yggdrasil_installed and not param.dry_run:
+        cmds = []
+    if not install_deps_before:
+        cmds += cmds_deps
+    if (((param.dry_run or (not yggdrasil_installed))
+         and param.for_development)):
+        src = '.'
+        if extras:
+            src += f"[{','.join(extras)}]"
+        cmds += [
+            f"cd {_pkg_dir}",
+            f"{param.python_cmd} -m pip install --editable {src}",
+            f"cd {os.getcwd()}"]
     # Follow up if on Unix as R installation may require sudo
-    if install_opts['R'] and _is_unix:
-        R_cmd = ["ygginstall", "r"]
-        if not install_opts['no_sudo']:
-            R_cmd.append("--sudoR")
-        subprocess.check_call(R_cmd)
-    if method == 'conda':
+    if param.install_opts['r'] and _is_unix:
+        # TODO: Fix location of R executable
+        R_cmd = f"{param.python_cmd} -m yggdrasil install r"
+        if not param.install_opts['no_sudo']:
+            R_cmd += ' --sudoR'
+        cmds.append(R_cmd)
+    call_kws = {}
+    if param.method == 'conda':
         env = copy.copy(os.environ)
-        if (not install_opts['no_sudo']) and install_opts['R']:
+        if (not param.install_opts['no_sudo']) and param.install_opts['r']:
             env['YGG_USE_SUDO_FOR_R'] = '1'
         src_dir = os.path.join(os.getcwd(),
                                os.path.dirname(os.path.dirname(__file__)))
-        subprocess.check_call([python_cmd, "create_coveragerc.py"],
-                              cwd=src_dir, env=env)
+        cmds += [
+            f"cd {src_dir}",
+            f"{param.python_cmd} create_coveragerc.py",
+            f"cd {os.getcwd()}"]
+        call_kws['env'] = env
+    if return_commands:
+        return cmds
+    call_script(cmds, verbose=param.verbose, dry_run=param.dry_run,
+                call_kws=call_kws)
 
 
 def verify_pkg(install_opts=None):
@@ -1074,20 +1308,21 @@ def verify_pkg(install_opts=None):
             If not provided, get_install_opts is used to create it.
 
     """
+    call_script(get_summary_commands())
     install_opts = get_install_opts(install_opts)
     if _is_win and (not install_opts['zmq']):
         install_opts['c'] = False
         install_opts['fortran'] = False
     elif (not install_opts['fortran']) and shutil.which('gfortran'):
         install_opts['fortran'] = True
-    # if (not install_opts['R']) and shutil.which('Rscript'):
+    # if (not install_opts['r']) and shutil.which('Rscript'):
     #     if not (_on_gha and _is_linux):
     #         # The installation on GHA-ubuntu machines requires sudo
     #         # and so installation will not be complete unless it is
     #         # enabled explicitly. This does not seem to be True on
     #         # GHA-macos builds and R is not installed by default on
     #         # Travis/Appveyor.
-    #         install_opts['R'] = True
+    #         install_opts['r'] = True
     if (not install_opts['c']) and shutil.which('gcc') and (not _is_win):
         install_opts['c'] = True
     src_dir = os.path.join(os.getcwd(),
@@ -1105,9 +1340,9 @@ def verify_pkg(install_opts=None):
                            "this source code.\n"
                            "\tSource version: %s\n\tBuild  version: %s"
                            % (src_version, bld_version))
-    if install_opts['R']:
-        assert(shutil.which("R"))
-        assert(shutil.which("Rscript"))
+    if install_opts['r']:
+        assert shutil.which("R")
+        assert shutil.which("Rscript")
     subprocess.check_call(["flake8", "yggdrasil"], cwd=src_dir)
     if not os.path.isfile(".coveragerc"):
         raise RuntimeError(".coveragerc file dosn't exist.")
@@ -1121,13 +1356,13 @@ def verify_pkg(install_opts=None):
     sys.stdout.flush()
     from yggdrasil.tools import is_lang_installed, is_comm_installed
     errors = []
-    for name in ['c', 'R', 'fortran', 'sbml', 'lpy']:
+    for name in ['c', 'r', 'fortran', 'sbml', 'lpy']:
         flag = install_opts[name]
         if flag and (not is_lang_installed(name)):
             errors.append("Language '%s' should be installed, but is not."
                           % name)
         elif (not flag) and is_lang_installed(name):
-            if name in ['R']:
+            if name in ['r']:
                 # Allow R to be installed even if the settings is not as
                 # packages may be installed from CRAN, unless there is an
                 # error which can occur when a new release of a dependency
@@ -1145,14 +1380,32 @@ def verify_pkg(install_opts=None):
             errors.append("Comm '%s' should be installed, but is not." % name)
         elif (not flag) and is_comm_installed(name, language=language):
             errors.append("Comm '%s' should NOT be installed, but is." % name)
+    if install_opts['mpi'] and not shutil.which('mpiexec'):
+        paths = ["/usr/local", os.environ.get('CONDA', False),
+                 os.environ.get('CONDA_PREFIX', False),
+                 '/Library/Frameworks/Python.framework/Versions/Current']
+        cmds = []
+        for x in paths:
+            if not x:
+                continue
+            cmds.append(f"ls {os.path.join(x, 'bin')}")
+            for k in ['mpiexec', 'mpirun', 'mpicc']:
+                cmds.append(f"find {x} -xdev -name '*{k}*'")
+        cmds += ['conda-tree whoneeds -t mpich',
+                 'conda-tree whoneeds -t mpi4py',
+                 'conda-tree whoneeds -t libgfortran',
+                 'conda-tree whoneeds -t clang',
+                 'conda-tree depends -t yggdrasil']
+        call_script(cmds)
+        errors.append("mpiexec could not be found")
     if errors:
         raise AssertionError("One or more languages was not installed as "
                              "expected\n\t%s" % "\n\t".join(errors))
     if _is_win:  # pragma: windows
         if os.environ.get('HOMEDRIVE', None):
-            assert(os.path.expanduser('~').startswith(os.environ['HOMEDRIVE']))
+            assert os.path.expanduser('~').startswith(os.environ['HOMEDRIVE'])
         else:
-            assert(os.path.expanduser('~').lower().startswith('c:'))
+            assert os.path.expanduser('~').lower().startswith('c:')
 
 
 def log_environment(new_filename='new_environment_log.txt',
@@ -1180,9 +1433,9 @@ def log_environment(new_filename='new_environment_log.txt',
             "%s --version >> %s" % (PYTHON_CMD, new_filename),
             "%s -m pip list >> %s" % (PYTHON_CMD, new_filename)]
     if shutil.which('conda'):
-        cmds.append("%s list >> %s" % (CONDA_CMD, new_filename))
+        cmds.append(f"{CONDA_CMD} list >> {new_filename}")
     call_script(cmds)
-    assert(os.path.isfile(new_filename))
+    assert os.path.isfile(new_filename)
     if os.path.isfile(old_filename):
         with open(new_filename, 'r') as fd:
             new_contents = fd.read()
@@ -1204,112 +1457,82 @@ def log_environment(new_filename='new_environment_log.txt',
     # Environment creation
     parser_env = subparsers.add_parser(
         'env', help="Setup an environment for testing.")
-    parser_env.add_argument(
-        'method', choices=['conda', 'virtualenv'],
-        help=("Method that should be used to create "
-              "the test environment."))
-    parser_env.add_argument(
-        'python',
-        help="Version of python that should be tested.")
-    parser_env.add_argument(
-        '--env-name', default=None,
-        help="Name that should be used for the environment.")
+    SetupParam.add_parser_args(
+        parser_env,
+        env_method_required=True,
+        python_required=True,
+        env_name_help="Name that should be used for the environment.",
+        include=['env_method'],
+        skip=['target_os', 'method'],
+        skip_types=['install'])
+    # Development environment setup
+    parser_dev = subparsers.add_parser(
+        'devenv',
+        help=("Create and populated a development environment "
+              "for testing yggdrasil."))
+    SetupParam.add_parser_args(
+        parser_dev,
+        python_required=True,
+        include=['env_method'],
+        env_method_default='mamba',
+        env_name_help="Name that should be used for the environment.",
+        skip=['for_development', 'deps_method', 'user'])
+    # Multiple env creation
+    parser_devmat = subparsers.add_parser(
+        'devenv-matrix', help="Setup a matrix of environments.")
+    SetupParam.add_parser_args(
+        parser_devmat,
+        include=['env_method'],
+        env_method_default='mamba',
+        skip=['for_development', 'deps_method', 'user',
+              'method', 'python', 'env_name'],
+        additional_args=[
+            (('--method', '--methods'),
+             {'nargs': '+', 'default': ['mamba', 'pip'],
+              'choices': ['conda', 'pip', 'mamba'],
+              'help': ("Method(s) that should be used to "
+                       "install dependencies in the environments.")}),
+            (('--python', '--pythons', '--version', '--versions'),
+             {'nargs': '+', 'default': ['3.7'],
+              'help': "Python version(s) for environments."}),
+        ])
     # Build package
     parser_bld = subparsers.add_parser(
         'build', help="Build the package.")
-    parser_bld.add_argument(
-        'method', choices=['conda', 'pip'],
-        help=("Method that should be used to build the package."))
-    parser_bld.add_argument(
-        '--python', default=None,
-        help="Version of python that package should be built for.")
-    parser_bld.add_argument(
-        '--verbose', action='store_true',
-        help="Turn up verbosity of output.")
+    SetupParam.add_parser_args(parser_bld,
+                               skip=['target_os', 'env_name'],
+                               skip_types=['install'])
     # Install dependencies
     parser_dep = subparsers.add_parser(
         'deps', help="Install the package dependencies.")
-    parser_dep.add_argument(
-        'method', choices=['conda', 'pip'],
-        help=("Method that should be used to install the package dependencies."))
-    parser_dep.add_argument(
-        '--verbose', action='store_true',
-        help="Turn up verbosity of output.")
-    parser_dep.add_argument(
-        '--for-development', action='store_true',
-        help=("Install dependencies used during development and "
-              "that would be missed when installing in development mode. "
-              "Implies --include-dev-deps"))
-    parser_dep.add_argument(
-        '--skip-test-deps', action='store_true',
-        help="Don't install dependencies used for testing.")
-    parser_dep.add_argument(
-        '--include-dev-deps', action='store_true',
-        help="Install dependencies used during development.")
-    parser_dep.add_argument(
-        '--include-doc-deps', action='store_true',
-        help="Install dependencies used during doc generation.")
-    parser_dep.add_argument(
-        '--windows-package-manager', default='vcpkg',
-        help="Package manager that should be used on Windows.",
-        choices=['vcpkg', 'choco'])
-    parser_dep.add_argument(
-        '--conda-env', default=None,
-        help="Conda environment that dependencies should be installed in.")
-    parser_dep.add_argument(
-        '--always-yes', action='store_true',
-        help="Pass -y to conda commands to avoid user interaction.")
-    parser_dep.add_argument(
-        '--only-python', '--python-only', action='store_true',
-        help="Only install python dependencies.")
-    add_install_opts_args(parser_dep)
+    SetupParam.add_parser_args(parser_dep, skip=['python'],
+                               deps_method_default="supplemental")
     # Install package
     parser_pkg = subparsers.add_parser(
         'install', help="Install the package.")
-    parser_pkg.add_argument(
-        'method', choices=['conda', 'pip', 'conda-dev', 'pip-dev'],
-        help=("Method that should be used to install the package."))
-    parser_pkg.add_argument(
-        '--python', default=None,
-        help="Version of python that package should be built/installed for.")
-    parser_pkg.add_argument(
-        '--without-build', action='store_true',
-        help=("Perform installation steps without building first. (Assumes "
-              "the package has already been built)."))
-    parser_pkg.add_argument(
-        '--without-deps', action='store_true',
-        help=("Perform installation steps without installing dependencies first. "
-              "(Assumes they have already been installed)."))
-    parser_pkg.add_argument(
-        '--verbose', action='store_true',
-        help="Turn up verbosity of output.")
-    parser_pkg.add_argument(
-        '--skip-test-deps', action='store_true',
-        help="Don't install dependencies used for testing.")
-    parser_pkg.add_argument(
-        '--include-dev-deps', action='store_true',
-        help="Install dependencies used during development.")
-    parser_pkg.add_argument(
-        '--include-doc-deps', action='store_true',
-        help="Install dependencies used during doc generation.")
-    parser_pkg.add_argument(
-        '--windows-package-manager', default='vcpkg',
-        help="Package manager that should be used on Windows.",
-        choices=['vcpkg', 'choco'])
-    parser_pkg.add_argument(
-        '--conda-env', default=None,
-        help="Conda environment that the package should be installed in.")
-    parser_pkg.add_argument(
-        '--always-yes', action='store_true',
-        help="Pass -y to conda commands to avoid user interaction.")
-    parser_pkg.add_argument(
-        '--only-python', '--python-only', action='store_true',
-        help="Only install python dependencies.")
-    add_install_opts_args(parser_pkg)
+    SetupParam.add_parser_args(
+        parser_pkg,
+        deps_method_default="supplemental",
+        method_choices=['conda', 'pip', 'mamba',
+                        'conda-dev', 'pip-dev', 'mamba-dev'],
+        method_help="Method that should be used to install yggdrasil.",
+        additional_args=[
+            (('--without-build', ),
+             {'action': 'store_true',
+              'help': ("Perform installation steps without building "
+                       "first (assuming the package was already "
+                       "built).")}),
+            (('--without-deps', ),
+             {'action': 'store_true',
+              'help': ("Perform installation steps without installing "
+                       "dependencies first (assuming the depdnencies "
+                       "were already installed).")}),
+        ])
     # Installation verification
     parser_ver = subparsers.add_parser(
         'verify', help="Verify that the package was installed correctly.")
-    add_install_opts_args(parser_ver)
+    SetupParam.add_parser_args(parser_ver, skip_all=True,
+                               install_opts=install_opts)
     # Environment logging
     parser_log = subparsers.add_parser(
         'log', help="Create a log of the Python environment.")
@@ -1320,91 +1543,47 @@ def log_environment(new_filename='new_environment_log.txt',
         '--old-filename', default='old_environment_log.txt',
         help=("File containing previous environment log that the new "
               "log should be diffed against."))
-    # Create environment.yml
-    parser_yml = subparsers.add_parser(
-        'env-yaml', help="Create an environment.yml file.")
-    parser_yml.add_argument(
-        '--filename', default='environment.yml',
-        help="File where the environment yaml should be saved.")
-    parser_yml.add_argument(
-        '--name', '-n', default='ygg',
-        help="Name of environment.")
-    parser_yml.add_argument(
-        '--channels', '--channel', '-c', nargs='*',
-        help="Name of conda channels that should be used.")
-    parser_yml.add_argument(
-        '--for-development', action='store_true',
-        help=("Install dependencies used during development and "
-              "that would be missed when installing in development mode. "
-              "Implies --include-dev-deps"))
-    parser_yml.add_argument(
-        '--skip-test-deps', action='store_true',
-        help="Don't install dependencies used for testing.")
-    parser_yml.add_argument(
-        '--include-dev-deps', action='store_true',
-        help="Install dependencies used during development.")
-    parser_yml.add_argument(
-        '--include-doc-deps', action='store_true',
-        help="Install dependencies used during doc generation.")
-    parser_yml.add_argument(
-        '--target-os', choices=['win', 'osx', 'linux'],
-        help=("Operating system that environment should target if "
-              "different from the current OS."))
-    parser_yml.add_argument(
-        '--python-version', '--python', type=str,
-        help="Python version that environment should use.")
-    add_install_opts_args(parser_yml)
     # Call methods
     args = parser.parse_args()
-    if args.operation in ['deps', 'install', 'verify', 'env-yaml']:
-        new_opts = {}
-        for k, v in install_opts.items():
-            if k == 'no_sudo':
-                new_opts[k] = bool(getattr(args, k, False))
-            elif v and getattr(args, 'dont_install_%s' % k, False):
-                new_opts[k] = False
-            elif (not v) and getattr(args, 'install_%s' % k, False):
-                new_opts[k] = True
-        install_opts.update(new_opts)
     if args.operation in ['env', 'setup']:
-        create_env(args.method, args.python, name=args.env_name)
+        param = SetupParam.from_args(args, install_opts,
+                                     env_created=True)
+        create_env(args.env_method, args.python, param=param)
+    elif args.operation == 'devenv':
+        args.for_development = True
+        args.deps_method = 'env'
+        param = SetupParam.from_args(args, install_opts,
+                                     env_created=True)
+        create_env(args.env_method, args.python, param=param,
+                   populate=True)
+    elif args.operation == 'devenv-matrix':
+        args.for_development = True
+        args.deps_method = 'env'
+        methods = args.method
+        pythons = args.python
+        for method in methods:
+            for python in pythons:
+                args.method = method
+                args.python = python
+                args.env_name = None
+                param = SetupParam.from_args(args, install_opts,
+                                             env_created=True)
+                create_env(args.env_method, args.python, param=param,
+                           populate=True)
     elif args.operation == 'build':
-        build_pkg(args.method, python=args.python,
-                  verbose=args.verbose)
+        param = SetupParam.from_args(args, install_opts)
+        build_pkg(args.method, param=param)
     elif args.operation == 'deps':
-        install_deps(args.method, verbose=args.verbose,
-                     skip_test_deps=args.skip_test_deps,
-                     include_dev_deps=args.include_dev_deps,
-                     include_doc_deps=args.include_doc_deps,
-                     for_development=args.for_development,
-                     windows_package_manager=args.windows_package_manager,
-                     install_opts=install_opts,
-                     conda_env=args.conda_env, always_yes=args.always_yes,
-                     only_python=args.only_python)
+        param = SetupParam.from_args(args, install_opts)
+        install_deps(args.method, param=param, do_preinstall=True)
     elif args.operation == 'install':
-        install_pkg(args.method, python=args.python,
+        param = SetupParam.from_args(args, install_opts)
+        install_pkg(args.method, param=param, python=args.python,
                     without_build=args.without_build,
-                    without_deps=args.without_deps,
-                    verbose=args.verbose,
-                    skip_test_deps=args.skip_test_deps,
-                    include_dev_deps=args.include_dev_deps,
-                    include_doc_deps=args.include_doc_deps,
-                    windows_package_manager=args.windows_package_manager,
-                    install_opts=install_opts,
-                    conda_env=args.conda_env, always_yes=args.always_yes,
-                    only_python=args.only_python)
+                    without_deps=args.without_deps)
     elif args.operation == 'verify':
-        verify_pkg(install_opts=install_opts)
+        param = SetupParam.from_args(args, install_opts)
+        verify_pkg(install_opts=param.install_opts)
     elif args.operation == 'log':
         log_environment(new_filename=args.new_filename,
                         old_filename=args.old_filename)
-    elif args.operation == 'env-yaml':
-        install_opts['os'] = args.target_os
-        create_env_yaml(filename=args.filename, name=args.name,
-                        channels=args.channels, target_os=args.target_os,
-                        python_version=args.python_version,
-                        for_development=args.for_development,
-                        skip_test_deps=args.skip_test_deps,
-                        include_dev_deps=args.include_dev_deps,
-                        include_doc_deps=args.include_doc_deps,
-                        install_opts=install_opts)
diff --git a/utils/test-install-base.yml b/utils/test-install-base.yml
index 0b2cfef95..4374718c2 100644
--- a/utils/test-install-base.yml
+++ b/utils/test-install-base.yml
@@ -8,12 +8,10 @@ name: Test of package installation & execution
       - '*'
   schedule:
     - cron: "0 10 * * 1"  # 3AM PST
+    - cron: "0 10 1 * *"  # 3AM PST once a month
 
 env:
   NUMPY: numpy
-  MATPLOTLIB: matplotlib
-  MPLBACKEND: agg
-  JSONSCHEMA: jsonschema
   INSTALLR: 1
   INSTALLC: 1
   INSTALLFORTRAN: 1
@@ -38,7 +36,7 @@ jobs:
       os-versions: ${{ steps.set-os-versions.outputs.versions }}
     steps:
       - id: set-os-versions
-        run: echo "::set-output name=versions::{\"ubuntu\":\"latest\", \"macos\":\"latest\", \"windows\":\"2019\"}"
+        run: echo "versions={\"ubuntu\":\"latest\", \"macos\":\"latest\", \"windows\":\"2019\"}" >> $GITHUB_OUTPUT
   test_pip:
     needs: params
     name: Test (${{ matrix.python-version }}, ${{ matrix.os-base }}, ${{ matrix.install-method }}, ${{ matrix.test-flags1 }}, ${{ matrix.test-flags2 }}), Install C = ${{ matrix.install-c }}
@@ -53,6 +51,7 @@ jobs:
         test-flags2: [""]
         install-c: [true]
         install-mpi: [true]
+        install-sbml: [false]
         include:
           - os-base: windows
             python-version: 3.7
@@ -61,20 +60,25 @@ jobs:
             test-flags2: ""
             install-c: false
             install-mpi: false
+            install-sbml: false
       fail-fast: false
 
     steps: &steps
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set C installation based on matrix value
-      if: matrix.install-c != true && matrix.install-method != 'conda'
+      if: matrix.install-c != true && matrix.install-method != 'conda' && matrix.install-method != 'mamba'
       run: echo "INSTALLC=0" >> $GITHUB_ENV
       shell: bash -l {0}
     - name: Set MPI installation based on matrix value
       if: matrix.install-mpi != true
       run: echo "INSTALLMPI=0" >> $GITHUB_ENV
       shell: bash -l {0}
+    - name: Set SBML installation based on matrix value
+      if: matrix.install-sbml == true
+      run: echo "INSTALLSBML=1" >> $GITHUB_ENV
+      shell: bash -l {0}
     - name: Store an ID string for the job
       run: echo "YGG_GHA_JOB_ID=${{ runner.os }}-${{ matrix.install-method }}-py${{ matrix.python-version }}-C${{ env.INSTALLC }}-RMQ${{ env.INSTALLRMQ }}${{ env.YGG_GHA_JOB_ID_SUFFIX }}" >> $GITHUB_ENV
       shell: bash -l {0}
@@ -97,14 +101,10 @@ jobs:
       uses: mpi4py/setup-mpi@v1
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLMPI == 1
     - name: Install R
-      uses: r-lib/actions/setup-r@v1
+      uses: r-lib/actions/setup-r@v2
       if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLR == 1
-    # - name: (WINDOWS,PIP) Set VCPKG_ROOT on Windows
-    #   if: runner.os == 'Windows' && matrix.install-method == 'pip' && env.INSTALLC == 1
-    #   run: echo "VCPKG_ROOT=C:\\vcpkg" >> $GITHUB_ENV
-    #   shell: bash -l {0}
     - name: (LINUX,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Linux') && matrix.install-method == 'pip'
       with:
         path: ~/.cache/pip
@@ -112,7 +112,7 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-
     - name: (MACOS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'macOS') && matrix.install-method == 'pip'
       with:
         path: ~/Library/Caches/pip
@@ -120,7 +120,7 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-
     - name: (WINDOWS,PIP) Cache pip
-      uses: actions/cache@v2
+      uses: actions/cache@v3
       if: startsWith(runner.os, 'Windows') && matrix.install-method == 'pip'
       with:
         path: ~\AppData\Local\pip\Cache
@@ -128,13 +128,12 @@ jobs:
         restore-keys: |
           ${{ runner.os }}-pip-
     - name: (PIP) Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       if: matrix.install-method == 'pip'
       with:
         python-version: ${{ matrix.python-version }}
     - name: (PIP) Set up Virtual Env
       if: env.USEVIRTUALENV == 1 && matrix.install-method == 'pip'
-      # uses: syphar/restore-virtualenv@v1
       run: |
         python utils/setup_test_env.py env virtualenv ${{ matrix.python-version }} --env-name=test
         echo "source activate ygg" > ~/.bashrc
@@ -157,11 +156,11 @@ jobs:
     # ========================================================
 
     # ========================================================
-    # Conda specific installation steps
+    # Conda/Mamba specific installation steps
     # ========================================================
     # - name: (CONDA) Cache conda
-    #   uses: actions/cache@v1
-    #   if: matrix.install-method == 'conda'
+    #   uses: actions/cache@v3
+    #   if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
     #   env:
     #     # Increase this value to reset cache if file not changed
     #     CACHE_NUMBER: 0
@@ -172,12 +171,15 @@ jobs:
     #       hashFiles('recipe/meta.yaml') }}
     - name: (WINDOWS,CONDA) Set up miniconda base environment
       uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda' && runner.os == 'Windows'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba')  #  && runner.os == 'Windows'
       with:
+        auto-update-conda: true
+        python-version: ${{ matrix.python-version }}
         channels: conda-forge
         use-only-tar-bz2: true
-        activate-environment: ''
-        # auto-activate-base: false
+        channel-priority: strict
+        miniforge-variant: Mambaforge
+        use-mamba: true
     # The compiler must be initialized after the base environment for
     # conda so that it dosn't interfere with the base environment
     # initialization
@@ -185,69 +187,52 @@ jobs:
     # conda?
     - name: (WINDOWS,CONDA) Set up MSVC Compiler
       uses: ilammy/msvc-dev-cmd@v1
-      if: matrix.install-method == 'conda' && runner.os == 'Windows' && env.INSTALLC == 1
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') && runner.os == 'Windows' && env.INSTALLC == 1
       with:
         toolset: 14.0
-    # This step and the next allows conda to be called before the
-    # conda-incubator/setup-miniconda@v2 action is used to create the
-    # test environment so that the package can be built in the base
-    # environment.
-    - name: (UNIX,CONDA) Add miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
-      run: |
-        echo "NON_CONDA_PATH=$PATH" >> $GITHUB_ENV
-        echo "PATH=$CONDA/bin:$PATH" >> $GITHUB_ENV
-      shell: bash -l {0}
-    - name: (MACOS,CONDA) Change ownership of conda on MacOS
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
-      run: sudo chown -R $(whoami) $CONDA
-      shell: bash
+    # - name: (UNIX,CONDA) Add miniconda bin to path
+    #   if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') && runner.os != 'Windows'
+    #   run: |
+    #     echo "PRE_CONDA_BIN=$CONDA/bin" >> $GITHUB_ENV
+    #   shell: bash -l {0}
+    # - name: (WINDOWS,CONDA) Add miniconda bin to path
+    #   if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') && runner.os == 'Windows'
+    #   run: |
+    #     echo "PRE_CONDA_BIN=$CONDA\\Scripts" >> $GITHUB_ENV
     - name: (CONDA) Build package in base environment
       id: conda_build
       continue-on-error: true
-      if: matrix.install-method == 'conda'
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
+      run: |
+        conda activate base
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python ${{ matrix.python-version }} --use-mamba
     - name: (CONDA) Build package in base environment (re-attempt)
-      if: ${{ steps.conda_build.outcome == 'failure' && matrix.install-method == 'conda'}}
-      run: python utils/setup_test_env.py build ${{ matrix.install-method }} --python ${{ matrix.python-version }}
-      shell: bash --noprofile --norc -eo pipefail {0}
-    - name: (UNIX,CONDA) Remove miniconda bin to path
-      if: matrix.install-method == 'conda' && runner.os != 'Windows'
+      if: ${{ steps.conda_build.outcome == 'failure' && (matrix.install-method == 'conda' || matrix.install-method == 'mamba')}}
       run: |
-        echo "PATH=$NON_CONDA_PATH" >> $GITHUB_ENV
-      shell: bash -l {0}
-    - name: (CONDA) Set up miniconda test environment
-      uses: conda-incubator/setup-miniconda@v2
-      if: matrix.install-method == 'conda'
-      with:
-        # TODO: Move autoupdate into script?
-        auto-update-conda: true
-        python-version: ${{ matrix.python-version }}
-        channels: conda-forge
-        use-only-tar-bz2: true
-        channel-priority: strict
+        conda activate base
+        python utils/setup_test_env.py build ${{ matrix.install-method }} --python ${{ matrix.python-version }} --use-mamba
     - name: (MACOS,CONDA) Install old MacOSX SDK for compatibility with the conda llvm (7)
-      if: runner.os == 'macOS' && matrix.install-method == 'conda'
+      if: runner.os == 'macOS' && (matrix.install-method == 'conda' || matrix.install-method == 'mamba')
       run: ./utils/setup_old_mac_sdk.sh
     - name: (CONDA) Install dependencies & package from existing build
       id: conda_install
       continue-on-error: true
-      if: matrix.install-method == 'conda'
-      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build --use-mamba
     - name: (CONDA) Install dependencies & package from existing build (re-attempt)
-      if: ${{ steps.conda_install.outcome == 'failure' && matrix.install-method == 'conda'}}
-      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build
+      if: ${{ steps.conda_install.outcome == 'failure' && (matrix.install-method == 'conda' || matrix.install-method == 'mamba')}}
+      run: python utils/setup_test_env.py install ${{ matrix.install-method }} --without-build --use-mamba
     - name: (MACOS,CONDA) Set allow_multiple_omp config
-      if: matrix.install-method == 'conda' && runner.os == 'macOS'
+      if: (matrix.install-method == 'conda' || matrix.install-method == 'mamba') && runner.os == 'macOS'
       run: yggconfig --allow-multiple-omp
     # ========================================================
-    
+
     - name: Verify installation
       run: python utils/setup_test_env.py verify
     - name: Compile libraries
       run: yggcompile
     - name: Run cmake test in command prompt
+      timeout-minutes: 120
       if: runner.os == 'Windows'
       shell: cmd /C CALL {0}
       run: |
@@ -255,37 +240,45 @@ jobs:
         pytest --ci --cov-append --nocapture tests/drivers/test_CMakeModelDriver.py
 
     - name: Run tests (1st)
+      timeout-minutes: 120
       id: test1
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }}
       continue-on-error: true
     - name: Re-run test failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags1 }} --last-failed --last-failed-no-failures=none --second-attempt
 
     - name: Run generated test script (1st)
+      timeout-minutes: 120
       if: ${{ contains(matrix.test-flags1, '--mpi-script') }}
       id: test1_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (1st)
+      timeout-minutes: 120
       if: ${{ steps.test1_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
 
     - name: Run tests (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' }}
       id: test2
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }}
       continue-on-error: true
     - name: Re-run test failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2.outcome == 'failure' }}
       run: pytest --ci --cov-append ${{ matrix.test-flags2 }} --last-failed --last-failed-no-failures=none --second-attempt
 
     - name: Run generated test script (2nd)
+      timeout-minutes: 120
       if: ${{ matrix.test-flags2 != '' && contains(matrix.test-flags2, '--mpi-script') }}
       id: test2_script
       run: ./run_mpi.sh
       continue-on-error: true
     - name: Re-run test script failures (2nd)
+      timeout-minutes: 120
       if: ${{ steps.test2_script.outcome == 'failure' }}
       run: ./run_mpi.sh --last-failed --last-failed-no-failures=none --second-attempt
 
@@ -301,12 +294,12 @@ jobs:
         # ls .*
         coverage combine --append
     - name: (CONDA) Generate XML version of coverage report
-      if: matrix.install-method == 'conda'
+      if: matrix.install-method == 'conda' || matrix.install-method == 'mamba'
       run: |
         # ls .*
         coverage xml
     - name: Upload coverage report
-      uses: codecov/codecov-action@v1
+      uses: codecov/codecov-action@v3
       with:
         name: ${{ env.YGG_GHA_JOB_ID }}
         token: ${{ secrets.CODECOV_TOKEN }}
@@ -322,7 +315,7 @@ jobs:
     - name: Create a log of Python package versions
       if: ${{ always() }}
       run: python utils/setup_test_env.py log
-    - uses: actions/upload-artifact@v2
+    - uses: actions/upload-artifact@v3
       if: ${{ success() }}
       with:
         name: environment-log-${{ env.YGG_GHA_JOB_ID }}
@@ -342,6 +335,7 @@ jobs:
         test-flags2: [--suite=examples --parametrize-example-name=backwards]
         install-c: [true]
         install-mpi: [true]
+        install-sbml: [false]
         include:
           - os-base: ubuntu
             python-version: 3.6
@@ -350,6 +344,7 @@ jobs:
             test-flags2: ""
             install-c: true
             install-mpi: false
+            install-sbml: false
           - os-base: ubuntu
             python-version: 3.8
             install-method: conda
@@ -357,13 +352,15 @@ jobs:
             test-flags2: ""
             install-c: true
             install-mpi: false
+            install-sbml: false
           - os-base: ubuntu
             python-version: 3.9
             install-method: conda
-            test-flags1: --long-running
-            test-flags2: ""
+            test-flags1: --long-running --remote-service
+            test-flags2: --suites examples --parametrize-language=sbml
             install-c: true
             install-mpi: false
+            install-sbml: true
           - os-base: macos
             python-version: 3.8
             install-method: conda
@@ -371,6 +368,7 @@ jobs:
             test-flags2: tests/test_tools.py
             install-c: true
             install-mpi: false
+            install-sbml: false
       fail-fast: false
     defaults:
       run:
@@ -395,6 +393,7 @@ jobs:
         test-flags2: [""]
         install-c: [true]
         install-mpi: [false]
+        install-sbml: [false]
       fail-fast: false
     services: &rmq_services
       rabbitmq:
@@ -421,6 +420,7 @@ jobs:
         test-flags2: [tests/test_services.py --nocapture]
         install-c: [true]
         install-mpi: [false]
+        install-sbml: [false]
       fail-fast: false
     services: *rmq_services
     defaults:
@@ -452,6 +452,7 @@ jobs:
         test-flags2: [""]
         install-c: [false]
         install-mpi: [false]
+        install-sbml: [false]
       fail-fast: false
     steps: *steps
 
@@ -461,11 +462,11 @@ jobs:
     env:
       BUILDDOCS: 1
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.8
     - name: Install dependencies
@@ -490,11 +491,11 @@ jobs:
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.8
     - name: Install dependencies
@@ -515,14 +516,14 @@ jobs:
   docker:
     name: Build and Publish Docker images
     needs: [test_pip, test_conda, test_rmq_pip, test_rmq_conda, test_stripped, docs]
-    if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+    if: (github.event_name == 'push' && startsWith(github.ref, 'refs/tags')) || github.event.schedule == '0 10 1 * *'
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.9
     - name: Login to DockerHub
@@ -541,11 +542,11 @@ jobs:
     if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v2
+    - uses: actions/checkout@v3
       with:
         submodules: true
     - name: Set up Python
-      uses: actions/setup-python@v2
+      uses: actions/setup-python@v4
       with:
         python-version: 3.8
     - name: Install dependencies