Repository: capitalone/edgetest (language: python)

Issue:
Support for pyproject.toml
Based on PEP 518 here: https://peps.python.org/pep-0518/#overview-of-file-formats-considered
It seems like `pyproject.toml` is preferred over setup.cfg. We should include some support here for various types. So far:

- setup.cfg (existing)
- pyproject.toml (new)

@ak-gupta thoughts?


Discussion / hints:
Also this: https://github.com/pypa/setuptools/milestone/7, https://github.com/pypa/setuptools/issues/3214

Fix patch:
diff --git a/README.md b/README.md
index 934b4e5..1ce953a 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
 
 [Full Documentation](https://capitalone.github.io/edgetest/)
 
-`edgetest` is a `tox`-inspired python library that will loop through your project's dependencies, and check if your 
+`edgetest` is a `tox`-inspired python library that will loop through your project's dependencies, and check if your
 project is compatible with the latest version of each dependency. It does this by:
 
 * creating a virtual environment,
@@ -18,6 +18,14 @@ project is compatible with the latest version of each dependency. It does this b
 For example, if you depend on ``pandas>=0.25.1,<=1.0.0``, ``edgetest`` will test your project against the most current
 pandas version (say ``1.3.4``), so you know if you can safely update your dependency to ``pandas>=0.25.1,<=1.3.4`` or not.
 
+
+`edgetest` works with the following types of projects:
+
+- `pyproject.toml`
+- `setup.cfg`
+- and `requirements.txt`
+
+
 Table Of Contents
 -----------------
 
@@ -47,7 +55,8 @@ $ conda install -c conda-forge edgetest
 Getting Started
 ---------------
 
-``edgetest`` allows multi-package, bleeding edge dependency testing. Suppose you have a package, ``mypackage``, with the following ``requirements.txt``:
+``edgetest`` allows multi-package, bleeding edge dependency testing. Suppose you have a package, ``mypackage``, with
+the following ``requirements.txt``:
 
 ```
 pandas>=0.25.1,<=1.0.0
@@ -118,4 +127,3 @@ Roadmap
 -------
 
 Roadmap details can be found [here](https://capitalone.github.io/edgetest/roadmap.html).
-
diff --git a/docs/source/quickstart.rst b/docs/source/quickstart.rst
index 59d1f35..b62bf36 100644
--- a/docs/source/quickstart.rst
+++ b/docs/source/quickstart.rst
@@ -16,6 +16,17 @@ Installation from conda-forge:
 
     $ conda install -c conda-forge edgetest
 
+
+Project support type
+--------------------
+
+``edgetest`` works with the following types of projects:
+
+- ``pyproject.toml``
+- ``setup.cfg``
+- and ``requirements.txt``
+
+
 Usage
 -----
 
diff --git a/docs/source/usage.rst b/docs/source/usage.rst
index feed728..b15e37f 100644
--- a/docs/source/usage.rst
+++ b/docs/source/usage.rst
@@ -26,35 +26,81 @@ This command will
 3. Upgrade ``pandas``, and
 4. Run the ``pytest`` in the environment.
 
-``setup.cfg`` Configuration
----------------------------
 
-You can also specify your testing environment through ``setup.cfg``:
+.. important::
 
-.. code-block:: ini
+    This configuration file can be a standalone file which is distinct from the ``setup.cfg`` or ``pyproject.toml``
+    configurations. Just note it must have a ``edgetest.envs`` otherwise it expects a PEP 517-style ``setup.cfg``
 
-    [edgetest.envs.pandas]
-    upgrade =
-        pandas
 
-If you're using a `PEP-517 style installation configuration <https://setuptools.readthedocs.io/en/latest/userguide/declarative_config.html>`_,
+
+``setup.cfg`` and ``pyproject.toml`` Configuration
+--------------------------------------------------
+
+You can also specify your testing environment through ``setup.cfg`` or ``pyproject.toml``:
+
+.. tabs::
+
+    .. tab:: .cfg
+
+        .. code-block:: ini
+
+            [edgetest.envs.pandas]
+            upgrade =
+                pandas
+
+    .. tab:: .toml
+
+        .. code-block:: toml
+
+            [edgetest.envs.pandas]
+            upgrade = [
+                "pandas"
+            ]
+
+If you're using a `PEP-517 <https://setuptools.pypa.io/en/latest/userguide/declarative_config.html>`_
+or `PEP-621 <https://setuptools.pypa.io/en/latest/userguide/pyproject_config.html>`_ style installation configuration
 your requirements might look like this:
 
-.. code-block:: ini
+.. tabs::
 
-    [options]
-    install_requires =
-        pandas
+    .. tab:: .cfg
 
-To point to ``setup.cfg``, supply the location of the file as your ``--config``:
+        .. code-block:: ini
 
-.. code-block:: console
+            [options]
+            install_requires =
+                pandas
 
-    $ edgetest -c path/to/setup.cfg
+    .. tab:: .toml
+
+        .. code-block:: toml
+
+            [project]
+            dependencies = [
+                "pandas",
+            ]
+
+To point to ``setup.cfg`` or ``pyproject.toml``, supply the location of the file as your ``--config``:
+
+.. tabs::
+
+    .. tab:: .cfg
+
+        .. code-block:: console
+
+            $ edgetest -c path/to/setup.cfg
+
+    .. tab:: .toml
+
+        .. code-block:: console
+
+            $ edgetest -c path/to/pyproject.toml
 
 .. important::
 
-    Using ``setup.cfg`` will allow you to upgrade optional installations.
+    Using ``setup.cfg`` or ``pyproject.toml`` will allow you to upgrade optional installations.
+
 
 Installing extras
 -----------------
@@ -64,7 +110,7 @@ call as follows:
 
 .. tabs::
 
-    .. tab:: Configuration file
+    .. tab:: .cfg
 
         Add an ``extras`` list to your environment:
 
@@ -76,6 +122,16 @@ call as follows:
             extras =
                 tests
 
+    .. tab:: .toml
+
+        Add an ``extras`` list to your environment:
+
+        .. code-block:: toml
+
+            [edgetest.envs.pandas]
+            upgrade = ["myupgrade"]
+            extras = ["tests"]
+
     .. tab:: Requirements parsing
 
         Add ``--extras`` to the CLI call:
@@ -86,6 +142,7 @@ call as follows:
 
         The above command will install ``.[tests, complete]``.
 
+
 Modifying the test command
 --------------------------
 
@@ -93,7 +150,7 @@ To customize your test command, modify the configuration or CLI call as follows:
 
 .. tabs::
 
-    .. tab:: Configuration file
+    .. tab:: .cfg
 
         Add a ``command`` key to your environment:
 
@@ -107,6 +164,17 @@ To customize your test command, modify the configuration or CLI call as follows:
             command =
                 pytest tests -m 'not integration'
 
+    .. tab:: .toml
+
+        Add a ``command`` key to your environment:
+
+        .. code-block:: toml
+
+            [edgetest.envs.pandas]
+            upgrade = ["myupgrade"]
+            extras = ["tests"]
+            command = "pytest tests -m 'not integration'"
+
     .. tab:: Requirements parsing
 
         Add ``--command`` to your CLI call:
@@ -118,6 +186,7 @@ To customize your test command, modify the configuration or CLI call as follows:
                 --extras complete \
                 --command 'pytest tests -m "not integration"'
 
+
 Additional dependencies
 -----------------------
 
@@ -128,7 +197,7 @@ To specify additional ``pip`` dependencies, modify as follows:
 
 .. tabs::
 
-    .. tab:: Configuration file
+    .. tab:: .cfg
 
         Add a ``deps`` list:
 
@@ -144,6 +213,18 @@ To specify additional ``pip`` dependencies, modify as follows:
             deps =
                 scikit-learn
 
+    .. tab:: .toml
+
+        Add a ``deps`` list:
+
+        .. code-block:: toml
+
+            [edgetest.envs.pandas]
+            upgrade = ["myupgrade"]
+            extras = ["tests"]
+            command = "pytest tests -m 'not integration'"
+            deps = ["scikit-learn"]
+
     .. tab:: Requirements parsing
 
         Add a ``deps`` argument to your CLI call; this argument accepts multiple values.
@@ -162,56 +243,80 @@ In both cases, ``scikit-learn`` will be installed with the following command:
 
     $ .edgetest/pandas/bin/python -m pip install scikit-learn
 
+
 Default arguments
 -----------------
 
 If you have default arguments you want to pass to each environment in your configuration,
 you can specify those under the ``edgetest`` section of your configuration:
 
-.. code-block:: ini
+.. tabs::
 
-    [edgetest]
-    extras =
-        tests
-    command =
-        pytest tests -m 'not integration'
+    .. tab:: .cfg
 
-    [edgetest.envs.pandas]
-    upgrade =
-        pandas
+        .. code-block:: ini
 
-    [edgetest.envs.numpy]
-    upgrade =
-        numpy
+            [edgetest]
+            extras =
+                tests
+            command =
+                pytest tests -m 'not integration'
 
-.. important::
+            [edgetest.envs.pandas]
+            upgrade =
+                pandas
+
+            [edgetest.envs.numpy]
+            upgrade =
+                numpy
+
+        .. important::
+
+            You can combine your configuration file with ``requirements.txt``. If you have the following
+            configuration file:
+
+            .. code-block:: ini
+
+                [options]
+                install_requires =
+
+                [edgetest]
+                extras =
+                    tests
+                command =
+                    pytest tests -m 'not integration'
+
+            and the following requirements file:
 
-    You can combine your configuration file with ``requirements.txt``. If you have the following
-    configuration file:
+            .. code-block:: text
 
-    .. code-block:: ini
+                pandas>=0.25.1,<=1.0.0
+                scikit-learn>=0.23.0,<=0.24.2
 
-        [edgetest]
-        extras =
-            tests
-        command =
-            pytest tests -m 'not integration'
 
-    and the following requirements file:
+            the following CLI call
 
-    .. code-block:: text
+            .. code-block:: console
+
+                $ edgetest -c edgetest.cfg -r requirements.txt
+
+            will apply the default arguments to each environment.
 
-        pandas>=0.25.1,<=1.0.0
-        scikit-learn>=0.23.0,<=0.24.2
 
+    .. tab:: .toml
 
-    the following CLI call
+        .. code-block:: toml
 
-    .. code-block:: console
+            [edgetest]
+            extras = ["tests"]
+            command = "pytest tests -m 'not integration'"
+
+            [edgetest.envs.pandas]
+            upgrade = ["pandas"]
 
-        $ edgetest -c edgetest.cfg -r requirements.txt
+            [edgetest.envs.numpy]
+            upgrade = ["numpy"]
 
-    will apply the default arguments to each environment.
 
 Multiple packages
 -----------------
@@ -219,23 +324,45 @@ Multiple packages
 Suppose you have multiple local packages you want to test. You can include the ``package_dir``
 in your testing project directory:
 
-.. code-block:: ini
+.. tabs::
 
-    [edgetest.envs.pandas]
-    package_dir = ../mypackage
-    upgrade =
-        pandas
+    .. tab:: .cfg
 
-    [edgetest.envs.numpy]
-    package_dir = ../myotherpackage
-    upgrade =
-        numpy
+        .. code-block:: ini
 
-After running
+            [edgetest.envs.pandas]
+            package_dir = ../mypackage
+            upgrade =
+                pandas
 
-.. code-block:: console
+            [edgetest.envs.numpy]
+            package_dir = ../myotherpackage
+            upgrade =
+                numpy
 
-    $ edgetest -c path/to/edgetest.cfg
+        After running
+
+        .. code-block:: console
+
+            $ edgetest -c path/to/edgetest.cfg
+
+    .. tab:: .toml
+
+        .. code-block:: toml
+
+            [edgetest.envs.pandas]
+            package_dir = "../mypackage"
+            upgrade = ["pandas"]
+
+            [edgetest.envs.numpy]
+            package_dir = "../myotherpackage"
+            upgrade = ["numpy"]
+
+        After running
+
+        .. code-block:: console
+
+            $ edgetest -c path/to/edgetest.toml
 
 your end output should look something like this:
 
@@ -252,6 +379,7 @@ your end output should look something like this:
 
     Testing multiple local packages is only supported with the configuration file syntax.
 
+
 Running a single environment
 ----------------------------
 
@@ -261,6 +389,30 @@ To run ``edgetest`` for a single environment, supply ``--environment`` or ``-e``
 
     $ edgetest -e pandas
 
+
+Exporting an upgraded config file
+----------------------------------
+
+.. tabs::
+
+    .. tab:: .cfg
+
+        This will overwrite your current ``setup.cfg`` file with the updated requirements.
+
+            .. code-block:: console
+
+                $ edgetest -c /path/to/setup.cfg --export
+
+
+    .. tab:: .toml
+
+        This will overwrite your current ``pyproject.toml`` file with the updated requirements.
+
+            .. code-block:: console
+
+                $ edgetest -c path/to/pyproject.toml --export
+
+
 Exporting an upgraded requirements file
 ---------------------------------------
 
@@ -278,25 +430,17 @@ For instance, ``snowflake-connector-python[pandas]>=2.2.8,<2.3.9`` might be repl
 
 .. tabs::
 
-    .. tab:: Configuration file
+    .. tab:: Configuration file (cfg/toml)
 
         Include the correct ``--requirements`` filepath and use ``--export``:
 
         .. code-block:: console
 
             $ edgetest \
-                -c path/to/config.yaml \
+                -c path/to/edgetest.cfg \
                 --requirements requirements.txt \
                 --export
 
-        .. important::
-
-            This will overwrite your current ``setup.cfg`` file with the updated requirements
-            if you are using PEP-517.
-
-            .. code-block:: console
-
-                $ edgetest -c path/to/setup.cfg --export
 
     .. tab:: Requirements parsing
 
diff --git a/requirements.txt b/requirements.txt
index 829441a..2ba0e67 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -16,6 +16,8 @@ pyparsing==3.0.9
     # via packaging
 tabulate==0.8.10
     # via edgetest (setup.cfg)
+tomlkit==0.11.4
+    # via edgetest (setup.cfg)
 
 # The following packages are considered to be unsafe in a requirements file:
 # setuptools
diff --git a/setup.cfg b/setup.cfg
index 324dcad..372d0e7 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -10,13 +10,13 @@ license = Apache Software License
 maintainer = Akshay Gupta
 maintainer_email = akshay.gupta2@capitalone.com
 url = https://github.com/capitalone/edgetest
-python_requires = 
+python_requires =
 	>=3.7.0
-project_urls = 
+project_urls =
 	Documentation = https://capitalone.github.io/edgetest/
 	Bug Tracker = https://github.com/capitalone/edgetest/issues
 	Source Code = https://github.com/capitalone/edgetest
-classifiers = 
+classifiers =
 	Intended Audience :: Developers
 	Natural Language :: English
 	Operating System :: OS Independent
@@ -31,29 +31,30 @@ classifiers =
 zip_safe = False
 include_package_data = True
 packages = find:
-install_requires = 
+install_requires =
 	Cerberus<=1.3.4,>=1.3.0
 	click<=8.1.3,>=7.0
 	pluggy<=1.0.0,>=1.0.0
 	tabulate<=0.8.10,>=0.8.9
 	packaging<=21.3,>20.6
+	tomlkit<=0.11.4,>=0.11.4
 
 [options.extras_require]
-docs = 
+docs =
 	furo
 	sphinx
 	sphinx-copybutton
 	sphinx-tabs
 	pillow
 	recommonmark
-tests = 
+tests =
 	coverage
 	flake8
 	mypy
 	pydocstyle
 	pytest
 	pytest-cov
-qa = 
+qa =
 	pylint
 	pip-tools
 	pre-commit
@@ -62,12 +63,12 @@ qa =
 	types-click
 	types-pkg_resources
 	types-tabulate
-build = 
+build =
 	build
 	twine
 	wheel
 	bumpver
-dev = 
+dev =
 	coverage
 	flake8
 	mypy
@@ -94,7 +95,7 @@ dev =
 	bumpver
 
 [options.entry_points]
-console_scripts = 
+console_scripts =
 	edgetest = edgetest.interface:cli
 
 [bumpver]
@@ -104,11 +105,11 @@ commit_message = "Bump {old_version} to {new_version}"
 commit = True
 
 [bumpver:file_patterns]
-docs/source/conf.py = 
+docs/source/conf.py =
 	version = "{version}"
-setup.cfg = 
+setup.cfg =
 	current_version = "{version}"
-edgetest/__init__.py = 
+edgetest/__init__.py =
 	__version__ = "{version}"
 
 [aliases]
@@ -141,21 +142,20 @@ allow_redefinition = True
 pylint_minimum_score = 9.5
 
 [tool:pytest]
-markers = 
+markers =
 	unit: mark unit tests that do not require external interfaces and use mocking
 	integration: mark test that interact with an external system
 addopts = --verbose
 
 [edgetest.envs.core]
 python_version = 3.9
-upgrade = 
+upgrade =
 	Cerberus
 	click
 	pluggy
 	tabulate
 	packaging
-extras = 
+extras =
 	tests
-deps = 
+deps =
 	pip-tools
-


Test patch:
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index c6e4420..d3e8efe 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -20,6 +20,7 @@ jobs:
                     - 3.7
                     - 3.8
                     - 3.9
+                    - "3.10"
         steps:
             - uses: actions/checkout@v2
             - name: Set up Python ${{ matrix.python-version }}
diff --git a/edgetest/interface.py b/edgetest/interface.py
index c34105c..de77176 100644
--- a/edgetest/interface.py
+++ b/edgetest/interface.py
@@ -5,6 +5,7 @@
 
 import click
 import pluggy
+from tomlkit import dumps
 
 from . import hookspecs, lib
 from .core import TestPackage
@@ -14,6 +15,8 @@
 from .utils import (
     gen_requirements_config,
     parse_cfg,
+    parse_toml,
+    upgrade_pyproject_toml,
     upgrade_requirements,
     upgrade_setup_cfg,
 )
@@ -109,13 +112,15 @@ def cli(
 ):
     """Create the environments and test.
 
-    If you do not supply a YAML configuration file, this package will search for a
+    If you do not supply a configuration file, this package will search for a
     ``requirements.txt`` file and create a conda environment for each package in that file.
     """
     # Get the hooks
     pm = get_plugin_manager()
-    if config:
+    if config and Path(config).suffix == ".cfg":
         conf = parse_cfg(filename=config, requirements=requirements)
+    elif config and Path(config).suffix == ".toml":
+        conf = parse_toml(filename=config, requirements=requirements)
     else:
         # Find the path to the local directory using the requirements file
         conf = gen_requirements_config(
@@ -125,7 +130,6 @@ def cli(
             command=command,
             package_dir=str(Path(requirements).parent),
         )
-
     # Validate the configuration file
     docstructure = Schema()
     pm.hook.addoption(schema=docstructure)
@@ -183,6 +187,24 @@ def cli(
                 )
                 with open(requirements, "w") as outfile:
                     outfile.write(upgraded)
+        elif config is not None and Path(config).name == "pyproject.toml":
+            parser = upgrade_pyproject_toml(
+                upgraded_packages=testers[-1].upgraded_packages(),
+                filename=config,
+            )
+            with open(config, "w") as outfile:
+                outfile.write(dumps(parser))
+            if "project" not in parser or not parser.get("project").get("dependencies"):
+                click.echo(
+                    "No dependencies in ``pyproject.toml`` to update. Updating "
+                    f"{requirements}"
+                )
+                upgraded = upgrade_requirements(
+                    fname_or_buf=requirements,
+                    upgraded_packages=testers[-1].upgraded_packages(),
+                )
+                with open(requirements, "w") as outfile:
+                    outfile.write(upgraded)
         else:
             click.echo(f"Overwriting the requirements file {requirements}...")
             upgraded = upgrade_requirements(
diff --git a/edgetest/utils.py b/edgetest/utils.py
index c0ad72c..b8a248c 100644
--- a/edgetest/utils.py
+++ b/edgetest/utils.py
@@ -6,10 +6,13 @@
 from copy import deepcopy
 from pathlib import Path
 from subprocess import PIPE, Popen
-from typing import Dict, List, Optional, Tuple
+from typing import Any, Dict, List, Optional, Tuple, Union
 
 from packaging.specifiers import SpecifierSet
 from pkg_resources import parse_requirements
+from tomlkit import TOMLDocument, load
+from tomlkit.container import Container
+from tomlkit.items import Array, Item, String, Table
 
 from .logger import get_logger
 
@@ -64,6 +67,15 @@ def pushd(new_dir: str):
         os.chdir(curr_dir)
 
 
+def _convert_toml_array_to_string(item: Union[Item, Any]) -> str:
+    if isinstance(item, Array):
+        return "\n".join(item)
+    elif isinstance(item, String):
+        return str(item)
+    else:
+        raise ValueError
+
+
 def convert_requirements(requirements: str, conf: Optional[Dict] = None) -> Dict:
     """Generate environments for a newline-separate list of package requirements.
 
@@ -224,6 +236,127 @@ def parse_cfg(filename: str = "setup.cfg", requirements: Optional[str] = None) -
     return output
 
 
+def parse_toml(
+    filename: str = "pyproject.toml", requirements: Optional[str] = None
+) -> Dict:
+    """Generate a configuration from a ``.toml`` style file.
+
+    This function can operate in two ways. First, it will look for tables that
+    start with ``edgetest`` and build a configuration. Suppose
+    you have ``pyproject.toml`` as follows:
+
+    .. code-block:: toml
+
+        [edgetest.envs.pandas]
+        upgrade = [
+            "pandas"
+        ]
+
+    This will result in a configuration that has one testing environment, named
+    ``pandas``, that upgrades the ``pandas`` package.
+
+    If you don't have any tables that start with ``edgetest.envs``, we will look for
+    the installation requirements (the ``dependencies`` key within the ``project`` section).
+    To set global defaults for you environments, use the ``edgetest`` table:
+
+    .. code-block:: toml
+
+        [edgetest]
+        extras = [
+            "tests"
+        ]
+        command = "pytest tests -m 'not integration'"
+
+        [edgetest.envs.pandas]
+        upgrade = [
+            "pandas"
+        ]
+
+    For this single environment file, the above configuration is equivalent to
+
+    .. code-block:: toml
+
+        [edgetest.envs.pandas]
+        extras = [
+            "tests"
+        ]
+        command = "pytest tests -m 'not integration'"
+        upgrade = [
+            "pandas"
+        ]
+
+    Parameters
+    ----------
+    filename : str, optional (default "pyproject.toml")
+        The name of the toml file to read. Defaults to ``pyproject.toml``.
+    requirements : str, optional (default None)
+        An optional path to the requirements text file. If there are no TOML
+        style dependencies or coded environments in the edgetest configuration, this
+        function will look for dependencies in the requirements file.
+
+    Returns
+    -------
+    Dict
+        A configuration dictionary for ``edgetest``.
+    """
+    options: Union[Item, Container, dict]
+    # Read in the configuration file
+    config: TOMLDocument = load(open(filename))
+    # Parse
+    output: Dict = {"envs": []}
+    # Get any global options if necessary. First scan through and pop out any Tables
+    temp_config = deepcopy(config)
+    if "edgetest" in config:
+        for j in config["edgetest"].items():  # type: ignore
+            if isinstance(config["edgetest"][j[0]], Table):  # type: ignore
+                _ = temp_config["edgetest"].pop(  # type: ignore
+                    j[0], None
+                )  # remove Tables from the temp config
+            else:
+                temp_config["edgetest"][j[0]] = _convert_toml_array_to_string(  # type: ignore
+                    temp_config["edgetest"][j[0]]  # type: ignore
+                )
+        options = temp_config["edgetest"]
+    else:
+        options = {}
+
+    # Check envs exists and any other Tables
+    if "edgetest" in config:
+        for section in config["edgetest"]:  # type: ignore
+            if section == "envs":
+                for env in config["edgetest"]["envs"]:  # type: ignore
+                    for item in config["edgetest"]["envs"][env]:  # type: ignore
+                        # If an Array then decompose to a string format
+                        config["edgetest"]["envs"][env][  # type: ignore
+                            item
+                        ] = _convert_toml_array_to_string(
+                            config["edgetest"]["envs"][env][item]  # type: ignore
+                        )
+                    output["envs"].append(dict(config["edgetest"]["envs"][env]))  # type: ignore
+                    output["envs"][-1]["name"] = env
+            elif isinstance(config["edgetest"][section], Table):  # type: ignore
+                output[section] = dict(config["edgetest"][section])  # type: ignore
+
+    if len(output["envs"]) == 0:
+        if config.get("project").get("dependencies"):  # type: ignore
+            output = convert_requirements(
+                requirements="\n".join(config["project"]["dependencies"]), conf=output  # type: ignore # noqa: E501
+            )
+        elif requirements:
+            req_conf = gen_requirements_config(fname_or_buf=requirements)
+            output["envs"] = req_conf["envs"]
+        else:
+            raise ValueError("Please supply a valid list of environments to create.")
+
+    # Apply global environment options (without overwriting)
+    for idx in range(len(output["envs"])):
+        output["envs"][idx] = dict(
+            list(options.items()) + list(output["envs"][idx].items())  # type: ignore
+        )
+
+    return output
+
+
 def upgrade_requirements(
     fname_or_buf: str, upgraded_packages: List[Dict[str, str]]
 ) -> str:
@@ -309,3 +442,40 @@ def upgrade_setup_cfg(
             parser["options.extras_require"][extra] = "\n" + upgraded
 
     return parser
+
+
+def upgrade_pyproject_toml(
+    upgraded_packages: List[Dict[str, str]], filename: str = "pyproject.toml"
+) -> TOMLDocument:
+    """Upgrade the ``pyproject.toml`` file.
+
+    Parameters
+    ----------
+    upgraded_packages : List[Dict[str, str]]
+        A list of packages upgraded in the testing procedure.
+    filename : str, optional (default "pyproject.toml")
+        The name of the configuration file to read. Defaults to ``pyproject.toml``.
+
+    Returns
+    -------
+    TOMLDocument
+        The updated TOMLDocument.
+    """
+    parser: TOMLDocument = load(open(filename))
+    if "project" in parser and parser.get("project").get("dependencies"):  # type: ignore
+        LOG.info(f"Updating the requirements in {filename}")
+        upgraded = upgrade_requirements(
+            fname_or_buf="\n".join(parser["project"]["dependencies"]),  # type: ignore
+            upgraded_packages=upgraded_packages,
+        )
+        parser["project"]["dependencies"] = upgraded.split("\n")  # type: ignore
+    # Update the extras, if necessary
+    if parser.get("project").get("optional-dependencies"):  # type: ignore
+        for extra, dependencies in parser["project"]["optional-dependencies"].items():  # type: ignore # noqa: E501
+            upgraded = upgrade_requirements(
+                fname_or_buf="\n".join(dependencies),
+                upgraded_packages=upgraded_packages,
+            )
+            parser["project"]["optional-dependencies"][extra] = upgraded.split("\n")  # type: ignore
+
+    return parser
diff --git a/tests/test_integration.py b/tests/test_integration_cfg.py
similarity index 100%
rename from tests/test_integration.py
rename to tests/test_integration_cfg.py
diff --git a/tests/test_integration_toml.py b/tests/test_integration_toml.py
new file mode 100644
index 0000000..2392ba9
--- /dev/null
+++ b/tests/test_integration_toml.py
@@ -0,0 +1,144 @@
+"""Create a fake package and test."""
+import configparser
+import sys
+from pathlib import Path
+
+import pytest
+from click.testing import CliRunner
+from tomlkit import load
+
+from edgetest.interface import cli
+
+SETUP_TOML = """
+[project]
+name = "toy_package"
+version = "0.1.0"
+description = "Fake description"
+requires-python = ">=3.7.0"
+dependencies = ["pandas<=1.2.0"]
+
+[project.optional-dependencies]
+tests = ["pytest"]
+
+[edgetest]
+extras = ["tests"]
+"""
+
+
+SETUP_TOML_DASK = """
+[project]
+name = "toy_package"
+version = "0.1.0"
+description = "Fake description"
+requires-python = ">=3.7.0"
+dependencies = ["click<=8.0.0,>=7.1", "dask[dataframe]<=2022.1.0"]
+
+[project.optional-dependencies]
+tests = ["pytest"]
+
+[edgetest.envs.core]
+extras = ["tests"]
+upgrade = ["click", "dask[dataframe]"]
+"""
+
+
+SETUP_PY = """
+from setuptools import setup
+
+setup()
+"""
+
+MODULE_CODE = """
+def main() -> str:
+    print("Hello world")
+"""
+
+TEST_CODE = """
+from toy_package import main
+
+def test_func():
+    main()
+"""
+
+PY_VER = f"python{sys.version_info.major}.{sys.version_info.minor}"
+
+
+@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows")
+@pytest.mark.integration
+def test_toy_package():
+    """Test using edgetest with a toy package."""
+    runner = CliRunner()
+
+    with runner.isolated_filesystem() as loc:
+        with open("pyproject.toml", "w") as outfile:
+            outfile.write(SETUP_TOML)
+        with open("setup.py", "w") as outfile:
+            outfile.write(SETUP_PY)
+        # Make a directory for the module
+        Path(loc, "toy_package").mkdir()
+        with open(Path(loc, "toy_package", "__init__.py"), "w") as outfile:
+            outfile.write(MODULE_CODE)
+        # Make a directory for the tests
+        Path(loc, "tests").mkdir()
+        with open(Path(loc, "tests", "test_main.py"), "w") as outfile:
+            outfile.write(TEST_CODE)
+
+        # Run the CLI
+        result = runner.invoke(cli, ["--config=pyproject.toml"])
+
+        assert result.exit_code == 0
+        assert Path(loc, ".edgetest").is_dir()
+        assert Path(loc, ".edgetest", "pandas").is_dir()
+
+        if not sys.platform == "win32":
+            assert Path(
+                loc, ".edgetest", "pandas", "lib", PY_VER, "site-packages", "pandas"
+            ).is_dir()
+
+
+@pytest.mark.integration
+def test_toy_package_dask():
+    """Test using edgetest with a toy package."""
+    runner = CliRunner()
+
+    with runner.isolated_filesystem() as loc:
+        with open("pyproject.toml", "w") as outfile:
+            outfile.write(SETUP_TOML_DASK)
+        with open("setup.py", "w") as outfile:
+            outfile.write(SETUP_PY)
+        # Make a directory for the module
+        Path(loc, "toy_package").mkdir()
+        with open(Path(loc, "toy_package", "__init__.py"), "w") as outfile:
+            outfile.write(MODULE_CODE)
+        # Make a directory for the tests
+        Path(loc, "tests").mkdir()
+        with open(Path(loc, "tests", "test_main.py"), "w") as outfile:
+            outfile.write(TEST_CODE)
+
+        # Run the CLI
+        result = runner.invoke(cli, ["--config=pyproject.toml", "--export"])
+
+        assert result.exit_code == 0
+        assert Path(loc, ".edgetest").is_dir()
+        assert Path(loc, ".edgetest", "core").is_dir()
+
+        if not sys.platform == "win32":
+            assert Path(
+                loc, ".edgetest", "core", "lib", PY_VER, "site-packages", "dask"
+            ).is_dir()
+            assert Path(
+                loc, ".edgetest", "core", "lib", PY_VER, "site-packages", "pandas"
+            ).is_dir()
+            assert Path(
+                loc, ".edgetest", "core", "lib", PY_VER, "site-packages", "click"
+            ).is_dir()
+
+        config = load(open("pyproject.toml"))
+
+        assert "click" in config["project"]["dependencies"][0]
+        assert "dask" in config["project"]["dependencies"][1]
+        assert config["edgetest"]["envs"]["core"]["extras"] == ["tests"]
+        assert config["edgetest"]["envs"]["core"]["upgrade"] == [
+            "click",
+            "dask[dataframe]",
+        ]
diff --git a/tests/test_interface.py b/tests/test_interface_cfg.py
similarity index 98%
rename from tests/test_interface.py
rename to tests/test_interface_cfg.py
index 2d29dcf..57c47c0 100644
--- a/tests/test_interface.py
+++ b/tests/test_interface_cfg.py
@@ -156,7 +156,7 @@ def test_cli_basic(mock_popen, mock_cpopen, mock_builder):
 
 
 @patch("edgetest.lib.EnvBuilder", autospec=True)
-@patch("edgetest.core.Popen", autospect=True)
+@patch("edgetest.core.Popen", autospec=True)
 @patch("edgetest.utils.Popen", autospec=True)
 def test_cli_reqs(mock_popen, mock_cpopen, mock_builder):
     """Test running tests based on the requirements file."""
@@ -270,7 +270,7 @@ def test_cli_reqs(mock_popen, mock_cpopen, mock_builder):
 
 
 @patch("edgetest.lib.EnvBuilder", autospec=True)
-@patch("edgetest.core.Popen", autospect=True)
+@patch("edgetest.core.Popen", autospec=True)
 @patch("edgetest.utils.Popen", autospec=True)
 def test_cli_setup_reqs_update(mock_popen, mock_cpopen, mock_builder):
     """Test running tests and updating requirements in a ``setup.cfg`` file."""
@@ -296,7 +296,7 @@ def test_cli_setup_reqs_update(mock_popen, mock_cpopen, mock_builder):
 
 
 @patch("edgetest.lib.EnvBuilder", autospec=True)
-@patch("edgetest.core.Popen", autospect=True)
+@patch("edgetest.core.Popen", autospec=True)
 @patch("edgetest.utils.Popen", autospec=True)
 def test_cli_setup_extras_update(mock_popen, mock_cpopen, mock_builder):
     """Test running tests and updating extra installation requirements in a ``setup.cfg`` file."""
@@ -323,7 +323,7 @@ def test_cli_setup_extras_update(mock_popen, mock_cpopen, mock_builder):
     assert out == SETUP_CFG_EXTRAS_UPGRADE
 
 
-@patch("edgetest.core.Popen", autospect=True)
+@patch("edgetest.core.Popen", autospec=True)
 @patch("edgetest.utils.Popen", autospec=True)
 def test_cli_nosetup(mock_popen, mock_cpopen):
     """Test creating a basic environment."""
diff --git a/tests/test_interface_toml.py b/tests/test_interface_toml.py
new file mode 100644
index 0000000..d201914
--- /dev/null
+++ b/tests/test_interface_toml.py
@@ -0,0 +1,416 @@
+"""Test the CLI."""
+
+import platform
+from pathlib import Path
+from unittest.mock import PropertyMock, call, patch
+
+from click.testing import CliRunner
+
+from edgetest.interface import cli
+
+CURR_DIR = Path(__file__).resolve().parent
+
+REQS = """
+myupgrade
+"""
+
+SETUP_TOML = """
+[edgetest.envs.myenv]
+upgrade = ["myupgrade"]
+command = "pytest tests -m 'not integration'"
+"""
+
+SETUP_TOML_REQS = """
+[project]
+dependencies = ["myupgrade<=0.1.5"]
+"""
+
+SETUP_TOML_REQS_UPGRADE = """
+[project]
+dependencies = ["myupgrade<=0.2.0"]
+"""
+
+SETUP_TOML_EXTRAS = """
+[project.optional-dependencies]
+myextra = ["myupgrade<=0.1.5"]
+
+[edgetest.envs.myenv]
+upgrade = ["myupgrade"]
+extras = ["myextra"]
+command = "pytest tests -m 'not integration'"
+"""
+
+
+SETUP_TOML_EXTRAS_UPGRADE = """
+[project.optional-dependencies]
+myextra = ["myupgrade<=0.2.0"]
+
+[edgetest.envs.myenv]
+upgrade = ["myupgrade"]
+extras = ["myextra"]
+command = "pytest tests -m 'not integration'"
+"""
+
+PIP_LIST = """
+[{"name": "myupgrade", "version": "0.2.0"}]
+"""
+
+TABLE_OUTPUT = """
+
+=============  ===============  ===================  =================
+Environment    Passing tests    Upgraded packages    Package version
+=============  ===============  ===================  =================
+myenv          True             myupgrade            0.2.0
+=============  ===============  ===================  =================
+"""
+
+TABLE_OUTPUT_NOTEST = """
+
+=============  ===============  ===================  =================
+Environment    Passing tests    Upgraded packages    Package version
+=============  ===============  ===================  =================
+myenv          False            myupgrade            0.2.0
+=============  ===============  ===================  =================
+"""
+
+TABLE_OUTPUT_REQS = """
+
+================  ===============  ===================  =================
+Environment       Passing tests    Upgraded packages    Package version
+================  ===============  ===================  =================
+myupgrade         True             myupgrade            0.2.0
+all-requirements  True             myupgrade            0.2.0
+================  ===============  ===================  =================
+"""
+
+
+@patch("edgetest.lib.EnvBuilder", autospec=True)
+@patch("edgetest.core.Popen", autospec=True)
+@patch("edgetest.utils.Popen", autospec=True)
+def test_cli_basic(mock_popen, mock_cpopen, mock_builder):
+    """Test creating a basic environment."""
+    mock_popen.return_value.communicate.return_value = (PIP_LIST, "error")
+    type(mock_popen.return_value).returncode = PropertyMock(return_value=0)
+    mock_cpopen.return_value.communicate.return_value = ("output", "error")
+    type(mock_cpopen.return_value).returncode = PropertyMock(return_value=0)
+
+    runner = CliRunner()
+
+    with runner.isolated_filesystem() as loc:
+        with open("pyproject.toml", "w") as outfile:
+            outfile.write(SETUP_TOML)
+
+        result = runner.invoke(cli, ["--config=pyproject.toml"])
+
+    assert result.exit_code == 0
+
+    env_loc = Path(loc) / ".edgetest" / "myenv"
+    if platform.system() == "Windows":
+        py_loc = env_loc / "Scripts" / "python"
+    else:
+        py_loc = env_loc / "bin" / "python"
+
+    mock_builder.return_value.create.assert_called_with(env_loc)
+    assert mock_popen.call_args_list == [
+        call(
+            (f"{str(py_loc)}", "-m", "pip", "install", "."),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (
+                f"{str(py_loc)}",
+                "-m",
+                "pip",
+                "install",
+                "myupgrade",
+                "--upgrade",
+            ),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (f"{str(py_loc)}", "-m", "pip", "list", "--format", "json"),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+    ]
+    assert mock_cpopen.call_args_list == [
+        call(
+            (
+                f"{str(py_loc)}",
+                "-m",
+                "pytest",
+                "tests",
+                "-m",
+                "not integration",
+            ),
+            universal_newlines=True,
+        )
+    ]
+
+    assert result.output == TABLE_OUTPUT
+
+
+@patch("edgetest.lib.EnvBuilder", autospec=True)
+@patch("edgetest.core.Popen", autospec=True)
+@patch("edgetest.utils.Popen", autospec=True)
+def test_cli_reqs(mock_popen, mock_cpopen, mock_builder):
+    """Test running tests based on the requirements file."""
+    mock_popen.return_value.communicate.return_value = (PIP_LIST, "error")
+    type(mock_popen.return_value).returncode = PropertyMock(return_value=0)
+    mock_cpopen.return_value.communicate.return_value = ("output", "error")
+    type(mock_cpopen.return_value).returncode = PropertyMock(return_value=0)
+
+    runner = CliRunner()
+
+    with runner.isolated_filesystem() as loc:
+        with open("requirements.txt", "w") as outfile:
+            outfile.write(REQS)
+
+        result = runner.invoke(cli)
+
+    if platform.system() == "Windows":
+        py_myupgrade_loc = Path(loc) / ".edgetest" / "myupgrade" / "Scripts" / "python"
+        py_allreq_loc = (
+            Path(loc) / ".edgetest" / "all-requirements" / "Scripts" / "python"
+        )
+    else:
+        py_myupgrade_loc = Path(loc) / ".edgetest" / "myupgrade" / "bin" / "python"
+        py_allreq_loc = Path(loc) / ".edgetest" / "all-requirements" / "bin" / "python"
+
+    assert result.exit_code == 0
+
+    assert mock_builder.return_value.create.call_args_list == [
+        call(env_dir=Path(loc) / ".edgetest" / "myupgrade"),
+        call(env_dir=Path(loc) / ".edgetest" / "all-requirements"),
+    ]
+
+    assert mock_popen.call_args_list == [
+        call(
+            (f"{str(py_myupgrade_loc)}", "-m", "pip", "install", "."),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (
+                f"{str(py_myupgrade_loc)}",
+                "-m",
+                "pip",
+                "install",
+                "myupgrade",
+                "--upgrade",
+            ),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (
+                f"{str(py_allreq_loc)}",
+                "-m",
+                "pip",
+                "install",
+                ".",
+            ),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (
+                f"{str(py_allreq_loc)}",
+                "-m",
+                "pip",
+                "install",
+                "myupgrade",
+                "--upgrade",
+            ),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (
+                f"{str(py_myupgrade_loc)}",
+                "-m",
+                "pip",
+                "list",
+                "--format",
+                "json",
+            ),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (
+                f"{str(py_allreq_loc)}",
+                "-m",
+                "pip",
+                "list",
+                "--format",
+                "json",
+            ),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+    ]
+    assert mock_cpopen.call_args_list == [
+        call(
+            (f"{str(py_myupgrade_loc)}", "-m", "pytest"),
+            universal_newlines=True,
+        ),
+        call(
+            (f"{str(py_allreq_loc)}", "-m", "pytest"),
+            universal_newlines=True,
+        ),
+    ]
+
+    assert result.output == TABLE_OUTPUT_REQS
+
+
+@patch("edgetest.lib.EnvBuilder", autospec=True)
+@patch("edgetest.core.Popen", autospec=True)
+@patch("edgetest.utils.Popen", autospec=True)
+def test_cli_setup_reqs_update(mock_popen, mock_cpopen, mock_builder):
+    """Test running tests and updating requirements in a ``pyproject.toml`` file."""
+    mock_popen.return_value.communicate.return_value = (PIP_LIST, "error")
+    type(mock_popen.return_value).returncode = PropertyMock(return_value=0)
+    mock_cpopen.return_value.communicate.return_value = ("output", "error")
+    type(mock_cpopen.return_value).returncode = PropertyMock(return_value=0)
+
+    runner = CliRunner()
+
+    with runner.isolated_filesystem() as loc:
+        with open("pyproject.toml", "w") as outfile:
+            outfile.write(SETUP_TOML_REQS)
+
+        result = runner.invoke(cli, ["--config=pyproject.toml", "--export"])
+
+        with open("pyproject.toml") as infile:
+            out = infile.read()
+
+    assert result.exit_code == 0
+
+    assert out == SETUP_TOML_REQS_UPGRADE
+
+
+@patch("edgetest.lib.EnvBuilder", autospec=True)
+@patch("edgetest.core.Popen", autospec=True)
+@patch("edgetest.utils.Popen", autospec=True)
+def test_cli_setup_extras_update(mock_popen, mock_cpopen, mock_builder):
+    """Test running tests and updating extra installation requirements in a ``pyproject.toml`` file."""
+    mock_popen.return_value.communicate.return_value = (PIP_LIST, "error")
+    type(mock_popen.return_value).returncode = PropertyMock(return_value=0)
+    mock_cpopen.return_value.communicate.return_value = ("output", "error")
+    type(mock_cpopen.return_value).returncode = PropertyMock(return_value=0)
+
+    runner = CliRunner()
+
+    with runner.isolated_filesystem() as loc:
+        with open("pyproject.toml", "w") as outfile:
+            outfile.write(SETUP_TOML_EXTRAS)
+
+        result = runner.invoke(cli, ["--config=pyproject.toml", "--export"])
+
+        print(result.output)
+
+        with open("pyproject.toml") as infile:
+            out = infile.read()
+
+    assert result.exit_code == 0
+
+    assert out == SETUP_TOML_EXTRAS_UPGRADE
+
+
+@patch("edgetest.core.Popen", autospec=True)
+@patch("edgetest.utils.Popen", autospec=True)
+def test_cli_nosetup(mock_popen, mock_cpopen):
+    """Test creating a basic environment."""
+    mock_popen.return_value.communicate.return_value = (PIP_LIST, "error")
+    type(mock_popen.return_value).returncode = PropertyMock(return_value=0)
+    mock_cpopen.return_value.communicate.return_value = ("output", "error")
+    type(mock_cpopen.return_value).returncode = PropertyMock(return_value=0)
+
+    runner = CliRunner()
+
+    with runner.isolated_filesystem() as loc:
+        with open("pyproject.toml", "w") as outfile:
+            outfile.write(SETUP_TOML)
+
+        result = runner.invoke(cli, ["--config=pyproject.toml", "--nosetup"])
+
+    assert result.exit_code == 0
+
+    env_loc = str(Path(loc) / ".edgetest" / "myenv")
+    if platform.system() == "Windows":
+        py_loc = Path(env_loc) / "Scripts" / "python"
+    else:
+        py_loc = Path(env_loc) / "bin" / "python"
+
+    assert mock_popen.call_args_list == [
+        call(
+            (f"{py_loc}", "-m", "pip", "list", "--format", "json"),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+    ]
+    assert mock_cpopen.call_args_list == [
+        call(
+            (f"{py_loc}", "-m", "pytest", "tests", "-m", "not integration"),
+            universal_newlines=True,
+        )
+    ]
+
+    assert (
+        result.output == f"""Using existing environment for myenv...\n{TABLE_OUTPUT}"""
+    )
+
+
+@patch("edgetest.lib.EnvBuilder", autospec=True)
+@patch("edgetest.utils.Popen", autospec=True)
+def test_cli_notest(mock_popen, mock_builder):
+    """Test creating a basic environment."""
+    mock_popen.return_value.communicate.return_value = (PIP_LIST, "error")
+    type(mock_popen.return_value).returncode = PropertyMock(return_value=0)
+
+    runner = CliRunner()
+
+    with runner.isolated_filesystem() as loc:
+        with open("pyproject.toml", "w") as outfile:
+            outfile.write(SETUP_TOML)
+
+        result = runner.invoke(cli, ["--config=pyproject.toml", "--notest"])
+
+    assert result.exit_code == 0
+
+    env_loc = Path(loc) / ".edgetest" / "myenv"
+    if platform.system() == "Windows":
+        py_loc = env_loc / "Scripts" / "python"
+    else:
+        py_loc = env_loc / "bin" / "python"
+
+    mock_builder.return_value.create.assert_called_with(env_loc)
+    assert mock_popen.call_args_list == [
+        call(
+            (f"{str(py_loc)}", "-m", "pip", "install", "."),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (
+                f"{str(py_loc)}",
+                "-m",
+                "pip",
+                "install",
+                "myupgrade",
+                "--upgrade",
+            ),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+        call(
+            (f"{str(py_loc)}", "-m", "pip", "list", "--format", "json"),
+            stdout=-1,
+            universal_newlines=True,
+        ),
+    ]
+
+    assert result.output == f"""Skipping tests for myenv\n{TABLE_OUTPUT_NOTEST}"""
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 14a5f61..4aa6686 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -3,8 +3,17 @@
 from pathlib import Path
 from unittest.mock import mock_open, patch
 
+from tomlkit import array, string
+
 from edgetest.schema import BASE_SCHEMA, EdgetestValidator, Schema
-from edgetest.utils import gen_requirements_config, parse_cfg
+from edgetest.utils import (
+    _convert_toml_array_to_string,
+    gen_requirements_config,
+    parse_cfg,
+    parse_toml,
+    upgrade_pyproject_toml,
+    upgrade_setup_cfg,
+)
 
 REQS = """
 mydep1>=0.1.0,<=0.2.0
@@ -67,6 +76,79 @@
 """
 
 
+TOML_NOREQS = """
+[edgetest.envs.myenv]
+upgrade = [
+    "myupgrade"
+]
+command = "pytest tests -m 'not integration'"
+"""
+
+
+TOML_REQS = """
+[project]
+dependencies = [
+    "myupgrade"
+]
+"""
+
+TOML_REQS_DEFAULTS = """
+[project]
+dependencies = [
+    "myupgrade"
+]
+[edgetest]
+extras = ["tests"]
+command = "pytest tests -m 'not integration'"
+"""
+
+TOML_NOREQS_DEFAULTS = """
+[edgetest]
+extras = ["tests"]
+command = "pytest tests -m 'not integration'"
+
+[edgetest.envs.myenv]
+upgrade = ["myupgrade"]
+command = "pytest tests"
+"""
+
+TOML_CUSTOM = """
+[edgetest]
+extras = ["tests"]
+command = "pytest tests -m 'not integration'"
+
+[edgetest.custom]
+mycustom = "mykey"
+
+[edgetest.envs.myenv]
+upgrade = ["myupgrade"]
+"""
+
+
+CFG_REQS_UPGRADE = """
+[options]
+install_requires =
+    pandas<=1.0.0,>=1.0.0
+    numpy<=1.0.0,>=1.0.0
+
+[options.extras_require]
+tests =
+    pytest<=1.0.0,>=1.0.0
+"""
+
+TOML_REQS_UPGRADE = """
+[project]
+dependencies = [
+    "pandas<=1.0.0,>=1.0.0",
+    "numpy<=1.0.0,>=1.0.0",
+]
+[project.optional-dependencies]
+tests = [
+    "pytest<=1.0.0,>=1.0.0",
+]
+"""
+
+
 @patch("edgetest.utils.Path")
 def test_parse_reqs(mock_pathlib):
     """Test creating a configuration from requirements."""
@@ -216,3 +298,186 @@ def test_parse_custom_cfg(tmpdir):
     validator = EdgetestValidator(schema=schema.schema)
 
     assert validator.validate(cfg)
+
+
+def test_parse_toml(tmpdir):
+    """Test parsing a config with no install requirements."""
+    location = tmpdir.mkdir("mylocation")
+    conf_loc = Path(str(location), "myconfig.toml")
+    with open(conf_loc, "w") as outfile:
+        outfile.write(TOML_NOREQS)
+
+    toml = parse_toml(filename=conf_loc)
+
+    assert toml == {
+        "envs": [
+            {
+                "name": "myenv",
+                "upgrade": "myupgrade",
+                "command": "pytest tests -m 'not integration'",
+            }
+        ]
+    }
+
+    validator = EdgetestValidator(schema=BASE_SCHEMA)
+
+    assert validator.validate(toml)
+
+
+def test_parse_toml_default(tmpdir):
+    """Test parsing a config with no install requirements and defaults."""
+    location = tmpdir.mkdir("mylocation")
+    conf_loc = Path(str(location), "myconfig.toml")
+    with open(conf_loc, "w") as outfile:
+        outfile.write(TOML_NOREQS_DEFAULTS)
+
+    toml = parse_toml(filename=conf_loc)
+
+    assert toml == {
+        "envs": [
+            {
+                "name": "myenv",
+                "upgrade": "myupgrade",
+                "extras": "tests",
+                "command": "pytest tests",
+            }
+        ]
+    }
+
+    validator = EdgetestValidator(schema=BASE_SCHEMA)
+
+    assert validator.validate(toml)
+
+
+def test_parse_toml_reqs(tmpdir):
+    """Test parsing a TOML style config."""
+    location = tmpdir.mkdir("mylocation")
+    conf_loc = Path(str(location), "pyproject.toml")
+    with open(conf_loc, "w") as outfile:
+        outfile.write(TOML_REQS)
+
+    toml = parse_toml(filename=conf_loc)
+
+    assert toml == {
+        "envs": [
+            {"name": "myupgrade", "upgrade": "myupgrade"},
+            {"name": "all-requirements", "upgrade": "myupgrade"},
+        ]
+    }
+
+    validator = EdgetestValidator(schema=BASE_SCHEMA)
+
+    assert validator.validate(toml)
+
+
+def test_parse_toml_reqs_default(tmpdir):
+    """Test parsing a TOML style config with default arguments."""
+    location = tmpdir.mkdir("mylocation")
+    conf_loc = Path(str(location), "pyproject.toml")
+    with open(conf_loc, "w") as outfile:
+        outfile.write(TOML_REQS_DEFAULTS)
+
+    toml = parse_toml(filename=conf_loc)
+
+    assert toml == {
+        "envs": [
+            {
+                "name": "myupgrade",
+                "upgrade": "myupgrade",
+                "extras": "tests",
+                "command": "pytest tests -m 'not integration'",
+            },
+            {
+                "name": "all-requirements",
+                "upgrade": "myupgrade",
+                "extras": "tests",
+                "command": "pytest tests -m 'not integration'",
+            },
+        ]
+    }
+
+    validator = EdgetestValidator(schema=BASE_SCHEMA)
+
+    assert validator.validate(toml)
+
+
+def test_parse_custom_toml(tmpdir):
+    """Test parsing a custom configuration."""
+    location = tmpdir.mkdir("mylocation")
+    conf_loc = Path(str(location), "pyproject.toml")
+    with open(conf_loc, "w") as outfile:
+        outfile.write(TOML_CUSTOM)
+
+    toml = parse_toml(filename=conf_loc)
+
+    assert toml == {
+        "custom": {"mycustom": "mykey"},
+        "envs": [
+            {
+                "name": "myenv",
+                "upgrade": "myupgrade",
+                "extras": "tests",
+                "command": "pytest tests -m 'not integration'",
+            }
+        ],
+    }
+
+    schema = Schema()
+    schema.add_globaloption(
+        "custom", {"type": "dict", "schema": {"mycustom": {"type": "string"}}}
+    )
+
+    validator = EdgetestValidator(schema=schema.schema)
+
+    assert validator.validate(toml)
+
+
+def test_convert_toml_array_to_string():
+    test_array = array("['a','b','c', 'd']")
+    test_string = string("abcd")
+
+    assert _convert_toml_array_to_string(test_array) == "a\nb\nc\nd"
+    assert _convert_toml_array_to_string(test_string) == "abcd"
+
+
+def test_upgrade_setup_cfg(tmpdir):
+    location = tmpdir.mkdir("mylocation")
+    conf_loc = Path(str(location), "setup.cfg")
+    with open(conf_loc, "w") as outfile:
+        outfile.write(CFG_REQS_UPGRADE)
+
+    upgrade_cfg = upgrade_setup_cfg(
+        upgraded_packages=[
+            {"name": "pandas", "version": "2.0.0"},
+            {"name": "numpy", "version": "3.0.0"},
+            {"name": "pytest", "version": "4.0.0"},
+        ],
+        filename=conf_loc,
+    )
+
+    assert (
+        upgrade_cfg["options"]["install_requires"]
+        == "\npandas<=2.0.0,>=1.0.0\nnumpy<=3.0.0,>=1.0.0"
+    )
+    assert upgrade_cfg["options.extras_require"]["tests"] == "\npytest<=4.0.0,>=1.0.0"
+
+
+def test_upgrade_pyproject_toml(tmpdir):
+    location = tmpdir.mkdir("mylocation")
+    conf_loc = Path(str(location), "pyproject.toml")
+    with open(conf_loc, "w") as outfile:
+        outfile.write(TOML_REQS_UPGRADE)
+
+    assert upgrade_pyproject_toml(
+        upgraded_packages=[
+            {"name": "pandas", "version": "2.0.0"},
+            {"name": "numpy", "version": "3.0.0"},
+            {"name": "pytest", "version": "4.0.0"},
+        ],
+        filename=conf_loc,
+    ) == {
+        "project": {
+            "dependencies": ["pandas<=2.0.0,>=1.0.0", "numpy<=3.0.0,>=1.0.0"],
+            "optional-dependencies": {"tests": ["pytest<=4.0.0,>=1.0.0"]},
+        }
+    }