Repository: tox-dev/tox

Issue:
tox4: tox fails to parse tox.ini when a testenv contains `deps = .[test]`
This may be related to #1929 (or even a dupe), but this time the failure is caused by a configuration directly in the `tox.ini` file, not in a requirements file.

repo
https://github.com/plone/plone.event

error caused by
https://github.com/plone/plone.event/blob/dfbaaf6f1ac79603f59de6492c20f1c28ce6fcfb/tox.ini#L8

repro
```
❯ tox4 -rvv -e py38
ROOT: 154 D setup logging to DEBUG on pid 13625 [tox/report.py:211]
ROOT: 159 W remove tox env folder /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38 [tox/tox_env/api.py:202]
ROOT: 194 I find interpreter for spec PythonSpec(implementation=CPython, major=3, minor=8) [virtualenv/discovery/builtin.py:62]
ROOT: 195 I proposed PythonInfo(spec=CPython3.6.9.final.0-64, exe=/usr/bin/python3, platform=linux, version='3.6.9 (default, Jan 26 2021, 15:33:00) \n[GCC 8.4.0]', encoding_fs_io=utf-8-UTF-8) [virtualenv/discovery/builtin.py:69]
ROOT: 195 D discover PATH[0]=/home/jugmac00/.cargo/bin [virtualenv/discovery/builtin.py:114]
ROOT: 196 D filesystem is case-sensitive [virtualenv/info.py:28]
ROOT: 196 D discover PATH[1]=/home/jugmac00/.local/bin [virtualenv/discovery/builtin.py:114]
ROOT: 196 D discover PATH[2]=/home/jugmac00/bin [virtualenv/discovery/builtin.py:114]
ROOT: 196 D discover PATH[3]=/usr/local/sbin [virtualenv/discovery/builtin.py:114]
ROOT: 196 D discover PATH[4]=/usr/local/bin [virtualenv/discovery/builtin.py:114]
ROOT: 197 D discover PATH[5]=/usr/sbin [virtualenv/discovery/builtin.py:114]
ROOT: 197 D discover PATH[6]=/usr/bin [virtualenv/discovery/builtin.py:114]
ROOT: 197 D Attempting to acquire lock 140519792202136 on /home/jugmac00/.local/share/virtualenv/py_info/1/df0893f56f349688326838aaeea0de204df53a132722cbd565e54b24a8fec5f6.lock [filelock.py:270]
ROOT: 197 D Lock 140519792202136 acquired on /home/jugmac00/.local/share/virtualenv/py_info/1/df0893f56f349688326838aaeea0de204df53a132722cbd565e54b24a8fec5f6.lock [filelock.py:274]
ROOT: 197 D got python info of /usr/bin/python3.8 from /home/jugmac00/.local/share/virtualenv/py_info/1/df0893f56f349688326838aaeea0de204df53a132722cbd565e54b24a8fec5f6.json [virtualenv/app_data/via_disk_folder.py:135]
ROOT: 198 D Attempting to release lock 140519792202136 on /home/jugmac00/.local/share/virtualenv/py_info/1/df0893f56f349688326838aaeea0de204df53a132722cbd565e54b24a8fec5f6.lock [filelock.py:315]
ROOT: 198 D Lock 140519792202136 released on /home/jugmac00/.local/share/virtualenv/py_info/1/df0893f56f349688326838aaeea0de204df53a132722cbd565e54b24a8fec5f6.lock [filelock.py:318]
ROOT: 198 I proposed PathPythonInfo(spec=CPython3.8.8.final.0-64, exe=/usr/bin/python3.8, platform=linux, version='3.8.8 (default, Feb 20 2021, 21:09:14) \n[GCC 7.5.0]', encoding_fs_io=utf-8-utf-8) [virtualenv/discovery/builtin.py:69]
ROOT: 198 D accepted PathPythonInfo(spec=CPython3.8.8.final.0-64, exe=/usr/bin/python3.8, platform=linux, version='3.8.8 (default, Feb 20 2021, 21:09:14) \n[GCC 7.5.0]', encoding_fs_io=utf-8-utf-8) [virtualenv/discovery/builtin.py:71]
ROOT: 228 I create virtual environment via CPython3Posix(dest=/home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38, clear=True, no_vcs_ignore=False, global=False) [virtualenv/run/session.py:52]
ROOT: 228 D delete /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38 [virtualenv/create/creator.py:169]
ROOT: 228 D create folder /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin [virtualenv/util/path/_sync.py:25]
ROOT: 228 D create folder /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages [virtualenv/util/path/_sync.py:25]
ROOT: 228 D write /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/pyvenv.cfg [virtualenv/create/pyenv_cfg.py:34]
ROOT: 228 D     home = /usr [virtualenv/create/pyenv_cfg.py:38]
ROOT: 229 D     implementation = CPython [virtualenv/create/pyenv_cfg.py:38]
ROOT: 229 D     version_info = 3.8.8.final.0 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 229 D     virtualenv = 20.4.2 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 229 D     include-system-site-packages = false [virtualenv/create/pyenv_cfg.py:38]
ROOT: 229 D     base-prefix = /usr [virtualenv/create/pyenv_cfg.py:38]
ROOT: 229 D     base-exec-prefix = /usr [virtualenv/create/pyenv_cfg.py:38]
ROOT: 229 D     base-executable = /usr/bin/python3.8 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 229 D symlink /usr/bin/python3.8 to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/python [virtualenv/util/path/_sync.py:44]
ROOT: 229 D create virtualenv import hook file /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/_virtualenv.pth [virtualenv/create/via_global_ref/api.py:95]
ROOT: 229 D create /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/_virtualenv.py [virtualenv/create/via_global_ref/api.py:98]
ROOT: 230 D ============================== target debug ============================== [virtualenv/run/session.py:54]
ROOT: 230 D debug via /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/python /home/jugmac00/.local/lib/python3.6/site-packages/virtualenv/create/debug.py [virtualenv/create/creator.py:224]
ROOT: 230 D {
  "sys": {
    "executable": "/home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/python",
    "_base_executable": "/home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/python",
    "prefix": "/home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38",
    "base_prefix": "/usr",
    "real_prefix": null,
    "exec_prefix": "/home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38",
    "base_exec_prefix": "/usr",
    "path": [
      "/usr/lib/python38.zip",
      "/usr/lib/python3.8",
      "/usr/lib/python3.8/lib-dynload",
      "/home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages"
    ],
    "meta_path": [
      "<class '_virtualenv._Finder'>",
      "<class '_frozen_importlib.BuiltinImporter'>",
      "<class '_frozen_importlib.FrozenImporter'>",
      "<class '_frozen_importlib_external.PathFinder'>"
    ],
    "fs_encoding": "utf-8",
    "io_encoding": "utf-8"
  },
  "version": "3.8.8 (default, Feb 20 2021, 21:09:14) \n[GCC 7.5.0]",
  "makefile_filename": "/usr/lib/python3.8/config-3.8-x86_64-linux-gnu/Makefile",
  "os": "<module 'os' from '/usr/lib/python3.8/os.py'>",
  "site": "<module 'site' from '/usr/lib/python3.8/site.py'>",
  "datetime": "<module 'datetime' from '/usr/lib/python3.8/datetime.py'>",
  "math": "<module 'math' (built-in)>",
  "json": "<module 'json' from '/usr/lib/python3.8/json/__init__.py'>"
} [virtualenv/run/session.py:55]
ROOT: 259 I add seed packages via FromAppData(download=False, pip=bundle, setuptools=bundle, wheel=bundle, via=copy, app_data_dir=/home/jugmac00/.local/share/virtualenv) [virtualenv/run/session.py:59]
py38: 262 D got embed update of distribution wheel from /home/jugmac00/.local/share/virtualenv/wheel/3.8/embed/1/wheel.json [virtualenv/app_data/via_disk_folder.py:135]
py38: 262 D got embed update of distribution setuptools from /home/jugmac00/.local/share/virtualenv/wheel/3.8/embed/1/setuptools.json [virtualenv/app_data/via_disk_folder.py:135]
py38: 262 D got embed update of distribution pip from /home/jugmac00/.local/share/virtualenv/wheel/3.8/embed/1/pip.json [virtualenv/app_data/via_disk_folder.py:135]
py38: 265 D using periodically updated wheel /home/jugmac00/.local/share/virtualenv/wheel/house/setuptools-53.0.0-py3-none-any.whl [virtualenv/seed/wheels/periodic_update.py:53]
py38: 265 D install wheel from wheel /home/jugmac00/.local/lib/python3.6/site-packages/virtualenv/seed/wheels/embed/wheel-0.36.2-py2.py3-none-any.whl via CopyPipInstall [virtualenv/seed/embed/via_app_data/via_app_data.py:49]
py38: 265 D install pip from wheel /home/jugmac00/.local/lib/python3.6/site-packages/virtualenv/seed/wheels/embed/pip-21.0.1-py3-none-any.whl via CopyPipInstall [virtualenv/seed/embed/via_app_data/via_app_data.py:49]
py38: 265 D install setuptools from wheel /home/jugmac00/.local/share/virtualenv/wheel/house/setuptools-53.0.0-py3-none-any.whl via CopyPipInstall [virtualenv/seed/embed/via_app_data/via_app_data.py:49]
py38: 265 D Attempting to acquire lock 140519764970800 on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any.lock [filelock.py:270]
py38: 266 D Attempting to acquire lock 140519765108104 on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/pip-21.0.1-py3-none-any.lock [filelock.py:270]
py38: 266 D Attempting to acquire lock 140519765108440 on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any.lock [filelock.py:270]
py38: 266 D Lock 140519764970800 acquired on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any.lock [filelock.py:274]
py38: 266 D Lock 140519765108104 acquired on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/pip-21.0.1-py3-none-any.lock [filelock.py:274]
py38: 266 D Lock 140519765108440 acquired on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any.lock [filelock.py:274]
py38: 266 D Attempting to release lock 140519764970800 on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any.lock [filelock.py:315]
py38: 267 D Attempting to release lock 140519765108104 on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/pip-21.0.1-py3-none-any.lock [filelock.py:315]
py38: 267 D Lock 140519764970800 released on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any.lock [filelock.py:318]
py38: 267 D Attempting to release lock 140519765108440 on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any.lock [filelock.py:315]
py38: 267 D Lock 140519765108104 released on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/pip-21.0.1-py3-none-any.lock [filelock.py:318]
py38: 267 D Lock 140519765108440 released on /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any.lock [filelock.py:318]
py38: 267 D copy /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any/wheel-0.36.2.virtualenv to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/wheel-0.36.2.virtualenv [virtualenv/util/path/_sync.py:52]
py38: 267 D copy /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/pip-21.0.1-py3-none-any/pip-21.0.1.virtualenv to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/pip-21.0.1.virtualenv [virtualenv/util/path/_sync.py:52]
py38: 268 D copy directory /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/pkg_resources to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/pkg_resources [virtualenv/util/path/_sync.py:52]
py38: 268 D copy directory /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any/wheel-0.36.2.dist-info to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/wheel-0.36.2.dist-info [virtualenv/util/path/_sync.py:52]
py38: 268 D copy directory /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/pip-21.0.1-py3-none-any/pip-21.0.1.dist-info to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/pip-21.0.1.dist-info [virtualenv/util/path/_sync.py:52]
py38: 272 D copy directory /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/wheel-0.36.2-py2.py3-none-any/wheel to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/wheel [virtualenv/util/path/_sync.py:52]
py38: 272 D copy directory /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/pip-21.0.1-py3-none-any/pip to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/pip [virtualenv/util/path/_sync.py:52]
py38: 278 D copy /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/distutils-precedence.pth to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/distutils-precedence.pth [virtualenv/util/path/_sync.py:52]
py38: 278 D copy directory /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/_distutils_hack to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/_distutils_hack [virtualenv/util/path/_sync.py:52]
py38: 281 D changing mode of /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/wheel3 to 755 [distlib/util.py:566]
py38: 281 D changing mode of /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/wheel3.8 to 755 [distlib/util.py:566]
py38: 281 D changing mode of /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/wheel to 755 [distlib/util.py:566]
py38: 281 D copy directory /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/setuptools to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/setuptools [virtualenv/util/path/_sync.py:52]
py38: 282 D changing mode of /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/wheel-3.8 to 755 [distlib/util.py:566]
py38: 287 D generated console scripts wheel wheel-3.8 wheel3 wheel3.8 [virtualenv/seed/embed/via_app_data/pip_install/base.py:48]
py38: 320 D copy directory /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/setuptools-53.0.0.dist-info to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/setuptools-53.0.0.dist-info [virtualenv/util/path/_sync.py:52]
py38: 322 D copy /home/jugmac00/.local/share/virtualenv/wheel/3.8/image/1/CopyPipInstall/setuptools-53.0.0-py3-none-any/setuptools-53.0.0.virtualenv to /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/lib/python3.8/site-packages/setuptools-53.0.0.virtualenv [virtualenv/util/path/_sync.py:52]
py38: 323 D generated console scripts  [virtualenv/seed/embed/via_app_data/pip_install/base.py:48]
py38: 345 D changing mode of /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/pip-3.8 to 755 [distlib/util.py:566]
py38: 345 D changing mode of /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/pip3 to 755 [distlib/util.py:566]
py38: 345 D changing mode of /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/pip to 755 [distlib/util.py:566]
py38: 345 D changing mode of /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/bin/pip3.8 to 755 [distlib/util.py:566]
py38: 345 D generated console scripts pip pip3.8 pip-3.8 pip3 [virtualenv/seed/embed/via_app_data/pip_install/base.py:48]
ROOT: 345 I add activators for Bash, CShell, Fish, PowerShell, Python, Xonsh [virtualenv/run/session.py:66]
ROOT: 346 D write /home/jugmac00/All/output_plone/plone/plone.event/.tox/4/py38/pyvenv.cfg [virtualenv/create/pyenv_cfg.py:34]
ROOT: 346 D     home = /usr [virtualenv/create/pyenv_cfg.py:38]
ROOT: 346 D     implementation = CPython [virtualenv/create/pyenv_cfg.py:38]
ROOT: 346 D     version_info = 3.8.8.final.0 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 346 D     virtualenv = 20.4.2 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 347 D     include-system-site-packages = false [virtualenv/create/pyenv_cfg.py:38]
ROOT: 347 D     base-prefix = /usr [virtualenv/create/pyenv_cfg.py:38]
ROOT: 347 D     base-exec-prefix = /usr [virtualenv/create/pyenv_cfg.py:38]
ROOT: 347 D     base-executable = /usr/bin/python3.8 [virtualenv/create/pyenv_cfg.py:38]
ROOT: 349 E internal error [tox/session/cmd/run/single.py:53]
Traceback (most recent call last):
  File "/home/jugmac00/.local/lib/python3.6/site-packages/packaging/requirements.py", line 98, in __init__
    req = REQUIREMENT.parseString(requirement_string)
  File "/home/jugmac00/.local/lib/python3.6/site-packages/pyparsing.py", line 1955, in parseString
    raise exc
  File "/home/jugmac00/.local/lib/python3.6/site-packages/pyparsing.py", line 3250, in parseImpl
    raise ParseException(instring, loc, self.errmsg, self)
pyparsing.ParseException: Expected W:(abcd...), found '.'  (at char 0), (line:1, col:1)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/tox_env/python/req_file.py", line 125, in _expand_non_flag
    req = Requirement(requirement)
  File "/home/jugmac00/.local/lib/python3.6/site-packages/packaging/requirements.py", line 102, in __init__
    requirement_string[e.loc : e.loc + 8], e.msg
packaging.requirements.InvalidRequirement: Parse error at "'.[test]'": Expected W:(abcd...)

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/session/cmd/run/single.py", line 41, in _evaluate
    tox_env.ensure_setup(recreate=recreate)
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/tox_env/api.py", line 179, in ensure_setup
    self.setup()
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/tox_env/python/virtual_env/api.py", line 109, in setup
    super().setup()
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/tox_env/python/api.py", line 162, in setup
    super().setup()
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/tox_env/runner.py", line 81, in setup
    self.before_package_install()
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/tox_env/python/runner.py", line 49, in before_package_install
    requirement_file_content = requirements_file.validate_and_expand()
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/tox_env/python/req_file.py", line 115, in validate_and_expand
    self._expand_non_flag(at, ini_dir, line, result)
  File "/home/jugmac00/.local/lib/python3.6/site-packages/tox/tox_env/python/req_file.py", line 136, in _expand_non_flag
    raise ValueError(f"{at}: {line}") from exc
ValueError: 2: .[test]
  py38: FAIL code 2 (0.19 seconds)
  evaluation failed :( (0.22 seconds)
```

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1998?src=pr&el=h1) Report
> Merging [#1998](https://codecov.io/gh/tox-dev/tox/pull/1998?src=pr&el=desc) (170ce42) into [rewrite](https://codecov.io/gh/tox-dev/tox/commit/00723c507f37e99bd78edc82aa389c5af396b762?el=desc) (00723c5) will **increase** coverage by `0.16%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tox-dev/tox/pull/1998/graphs/tree.svg?width=650&height=150&src=pr&token=DYodAwDCZ5)](https://codecov.io/gh/tox-dev/tox/pull/1998?src=pr&el=tree)

```diff
@@             Coverage Diff              @@
##           rewrite     #1998      +/-   ##
============================================
+ Coverage    99.83%   100.00%   +0.16%     
============================================
  Files          147       145       -2     
  Lines         8276      8137     -139     
  Branches       823       798      -25     
============================================
- Hits          8262      8137     -125     
+ Misses           3         0       -3     
+ Partials        11         0      -11     
```

| Flag | Coverage Δ | |
|---|---|---|
| tests | `100.00% <100.00%> (+0.16%)` | :arrow_up: |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags#carryforward-flags-in-the-pull-request-comment) to find out more.

| [Impacted Files](https://codecov.io/gh/tox-dev/tox/pull/1998?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/tox/tox\_env/python/pip/req\_file.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L3B5dGhvbi9waXAvcmVxX2ZpbGUucHk=) | `100.00% <100.00%> (ø)` | |
| [tests/tox\_env/python/pip/test\_req\_file.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-dGVzdHMvdG94X2Vudi9weXRob24vcGlwL3Rlc3RfcmVxX2ZpbGUucHk=) | `100.00% <100.00%> (ø)` | |
| [src/tox/tox\_env/api.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L2FwaS5weQ==) | `100.00% <0.00%> (ø)` | |
| [src/tox/execute/request.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-c3JjL3RveC9leGVjdXRlL3JlcXVlc3QucHk=) | `100.00% <0.00%> (ø)` | |
| [src/tox/util/pep517/backend.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-c3JjL3RveC91dGlsL3BlcDUxNy9iYWNrZW5kLnB5) | `100.00% <0.00%> (ø)` | |
| [...ecute/local\_sub\_process/read\_via\_thread\_windows.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-c3JjL3RveC9leGVjdXRlL2xvY2FsX3N1Yl9wcm9jZXNzL3JlYWRfdmlhX3RocmVhZF93aW5kb3dzLnB5) | | |
| [tests/conftest.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-dGVzdHMvY29uZnRlc3QucHk=) | | |
| [src/tox/tox\_env/python/api.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-c3JjL3RveC90b3hfZW52L3B5dGhvbi9hcGkucHk=) | `100.00% <0.00%> (+0.77%)` | :arrow_up: |
| [src/tox/config/loader/str\_convert.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-c3JjL3RveC9jb25maWcvbG9hZGVyL3N0cl9jb252ZXJ0LnB5) | `100.00% <0.00%> (+1.47%)` | :arrow_up: |
| [src/tox/util/spinner.py](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree#diff-c3JjL3RveC91dGlsL3NwaW5uZXIucHk=) | `100.00% <0.00%> (+2.27%)` | :arrow_up: |
| ... and [1 more](https://codecov.io/gh/tox-dev/tox/pull/1998/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tox-dev/tox/pull/1998?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tox-dev/tox/pull/1998?src=pr&el=footer). Last update [00723c5...170ce42](https://codecov.io/gh/tox-dev/tox/pull/1998?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/changelog/1933.bugfix.rst b/docs/changelog/1933.bugfix.rst
new file mode 100644
index 00000000..320be733
--- /dev/null
+++ b/docs/changelog/1933.bugfix.rst
@@ -0,0 +1,1 @@
+Support for extras with paths for python deps and requirement files - by :user:`gaborbernat`.
diff --git a/docs/conf.py b/docs/conf.py
index ed435703..8436ce2e 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -87,7 +87,7 @@ nitpick_ignore = []
 
 
 def skip_member(app: Sphinx, what: str, name: str, obj: Any, would_skip: bool, options: Options) -> bool:  # noqa: U100
-    return True if name in options.get("exclude-members", set()) else would_skip
+    return name in options.get("exclude-members", set()) or would_skip
 
 
 def process_signature(
diff --git a/src/tox/config/cli/parse.py b/src/tox/config/cli/parse.py
index 63bd6826..73a332d9 100644
--- a/src/tox/config/cli/parse.py
+++ b/src/tox/config/cli/parse.py
@@ -16,10 +16,11 @@ def get_options(*args: str) -> Tuple[Parsed, Handlers, Optional[Sequence[str]],
     pos_args: Optional[Tuple[str, ...]] = None
     try:  # remove positional arguments passed to parser if specified, they are pulled directly from sys.argv
         pos_arg_at = args.index("--")
-        pos_args = tuple(args[pos_arg_at + 1 :])
-        args = args[:pos_arg_at]
     except ValueError:
         pass
+    else:
+        pos_args = tuple(args[pos_arg_at + 1 :])
+        args = args[:pos_arg_at]
 
     guess_verbosity, log_handler, source = _get_base(args)
     parsed, cmd_handlers = _get_all(args)
diff --git a/src/tox/config/cli/parser.py b/src/tox/config/cli/parser.py
index ac27cac9..df8d6163 100644
--- a/src/tox/config/cli/parser.py
+++ b/src/tox/config/cli/parser.py
@@ -182,7 +182,7 @@ class ToxParser(ArgumentParserWithEnvAndConfig):
 
     def add_argument(self, *args: str, of_type: Optional[Type[Any]] = None, **kwargs: Any) -> Action:
         result = super().add_argument(*args, **kwargs)
-        if self.of_cmd is None and (result.dest not in ("help",)):
+        if self.of_cmd is None and result.dest != "help":
             self._arguments.append((args, of_type, kwargs))
             if hasattr(self, "_cmd") and self._cmd is not None and hasattr(self._cmd, "choices"):
                 for parser in {id(v): v for k, v in self._cmd.choices.items()}.values():
diff --git a/src/tox/config/loader/ini/factor.py b/src/tox/config/loader/ini/factor.py
index d1e956e4..281dbf4d 100644
--- a/src/tox/config/loader/ini/factor.py
+++ b/src/tox/config/loader/ini/factor.py
@@ -1,16 +1,14 @@
 """
 Expand tox factor expressions to tox environment list.
 """
-import itertools
 import re
+from itertools import chain, groupby, product
 from typing import Iterator, List, Optional, Tuple
 
 
 def filter_for_env(value: str, name: Optional[str]) -> str:
     current = (
-        set(itertools.chain.from_iterable([(i for i, _ in a) for a in find_factor_groups(name)]))
-        if name is not None
-        else set()
+        set(chain.from_iterable([(i for i, _ in a) for a in find_factor_groups(name)])) if name is not None else set()
     )
     overall = []
     for factors, content in expand_factors(value):
@@ -68,12 +66,12 @@ def find_factor_groups(value: str) -> Iterator[List[Tuple[str, bool]]]:
 
 def expand_env_with_negation(value: str) -> Iterator[str]:
     """transform '{py,!pi}-{a,b},c' to ['py-a', 'py-b', '!pi-a', '!pi-b', 'c']"""
-    for key, group in itertools.groupby(re.split(r"((?:{[^}]+})+)|,", value), key=bool):
+    for key, group in groupby(re.split(r"((?:{[^}]+})+)|,", value), key=bool):
         if key:
             group_str = "".join(group).strip()
             elements = re.split(r"{([^}]+)}", group_str)
             parts = [re.sub(r"\s+", "", elem).split(",") for elem in elements]
-            for variant in itertools.product(*parts):
+            for variant in product(*parts):
                 variant_str = "".join(variant)
                 yield variant_str
 
diff --git a/src/tox/config/loader/str_convert.py b/src/tox/config/loader/str_convert.py
index 8524afaf..a973d7e4 100644
--- a/src/tox/config/loader/str_convert.py
+++ b/src/tox/config/loader/str_convert.py
@@ -37,16 +37,12 @@ class StrConvert(Convert[str]):
     @staticmethod
     def to_dict(value: str, of_type: Tuple[Type[Any], Type[Any]]) -> Iterator[Tuple[str, str]]:  # noqa: U100
         for row in value.split("\n"):
-            row = row.strip()
-            if row:
-                try:
-                    at = row.index("=")
-                except ValueError as exc:
-                    raise TypeError(f"dictionary lines must be of form key=value, found {row}") from exc
+            if row.strip():
+                key, sep, value = row.partition("=")
+                if sep:
+                    yield key.strip(), value.strip()
                 else:
-                    key = row[:at].strip()
-                    value = row[at + 1 :].strip()
-                    yield key, value
+                    raise TypeError(f"dictionary lines must be of form key=value, found {row!r}")
 
     @staticmethod
     def to_command(value: str) -> Command:
@@ -57,11 +53,7 @@ class StrConvert(Convert[str]):
             args: List[str] = []
             for arg in splitter:
                 # on Windows quoted arguments will remain quoted, strip it
-                if (
-                    len(arg) > 1
-                    and (arg.startswith('"') and arg.endswith('"'))
-                    or (arg.startswith("'") and arg.endswith("'"))
-                ):
+                if len(arg) > 1 and arg[0] == arg[-1] and arg.startswith(("'", '"')):
                     arg = arg[1:-1]
                 args.append(arg)
         else:
diff --git a/src/tox/config/of_type.py b/src/tox/config/of_type.py
index 82c184c9..b555e887 100644
--- a/src/tox/config/of_type.py
+++ b/src/tox/config/of_type.py
@@ -2,6 +2,7 @@
 Group together configuration values that belong together (such as base tox configuration, tox environment configs)
 """
 from abc import ABC, abstractmethod
+from itertools import product
 from typing import TYPE_CHECKING, Any, Callable, Generic, Iterable, List, Mapping, Optional, Type, TypeVar, Union, cast
 
 from tox.config.loader.api import Loader
@@ -92,16 +93,12 @@ class ConfigDynamicDefinition(ConfigDefinition[T]):
         chain: List[str],
     ) -> T:
         if self._cache is _PLACE_HOLDER:
-            found = False
-            for key in self.keys:
-                for loader in loaders:
-                    try:
-                        value = loader.load(key, self.of_type, self.kwargs, conf, self.env_name, chain)
-                        found = True
-                    except KeyError:
-                        continue
-                    break
-                if found:
+            for key, loader in product(self.keys, loaders):
+                try:
+                    value = loader.load(key, self.of_type, self.kwargs, conf, self.env_name, chain)
+                except KeyError:
+                    continue
+                else:
                     break
             else:
                 value = self.default(conf, self.env_name) if callable(self.default) else self.default
diff --git a/src/tox/config/set_env.py b/src/tox/config/set_env.py
index 8a4d794a..ce836516 100644
--- a/src/tox/config/set_env.py
+++ b/src/tox/config/set_env.py
@@ -6,12 +6,11 @@ Replacer = Callable[[str, List[str]], str]
 class SetEnv:
     def __init__(self, raw: str) -> None:
         self.replacer: Replacer = lambda s, c: s
-        lines = raw.splitlines()
         self._later: List[str] = []
         self._raw: Dict[str, str] = {}
         from .loader.ini.replace import find_replace_part
 
-        for line in lines:
+        for line in raw.splitlines():
             if line.strip():
                 try:
                     key, value = self._extract_key_value(line)
@@ -29,12 +28,11 @@ class SetEnv:
 
     @staticmethod
     def _extract_key_value(line: str) -> Tuple[str, str]:
-        try:
-            at = line.index("=")
-        except ValueError:
+        key, sep, value = line.partition("=")
+        if sep:
+            return key.strip(), value.strip()
+        else:
             raise ValueError(f"invalid line {line!r} in set_env")
-        key, value = line[:at], line[at + 1 :]
-        return key.strip(), value.strip()
 
     def load(self, item: str, chain: Optional[List[str]] = None) -> str:
         if chain is None:
@@ -57,11 +55,7 @@ class SetEnv:
         while self._later:
             line = self._later.pop(0)
             expanded_line = self.replacer(line, [])
-            sub_raw = {}
-            for sub_line in expanded_line.splitlines():
-                if sub_line:
-                    key, value = self._extract_key_value(sub_line)
-                    sub_raw[key] = value
+            sub_raw = dict(self._extract_key_value(sub_line) for sub_line in expanded_line.splitlines() if sub_line)
             self._raw.update(sub_raw)
             yield from sub_raw.keys()
 
diff --git a/src/tox/provision.py b/src/tox/provision.py
index 94d23fca..ff2ac5fd 100644
--- a/src/tox/provision.py
+++ b/src/tox/provision.py
@@ -81,10 +81,11 @@ def provision(state: State) -> Union[int, bool]:
         package_name = canonicalize_name(package.name)
         try:
             dist = distribution(package_name)  # type: ignore[no-untyped-call]
-            if not package.specifier.contains(dist.version, prereleases=True):
-                missing.append((package, dist.version))
         except PackageNotFoundError:
             missing.append((package, "N/A"))
+        else:
+            if not package.specifier.contains(dist.version, prereleases=True):
+                missing.append((package, dist.version))
     if not missing:
         return False
     deps = ", ".join(f"{p} ({ver})" for p, ver in missing)
@@ -107,7 +108,7 @@ def run_provision(deps: List[Requirement], state: State) -> int:  # noqa
     tox_env = cast(PythonRun, state.tox_env(provision_tox_env))
     env_python = tox_env.env_python()
     logging.info("will run in a automatically provisioned python environment under %s", env_python)
-    recreate = state.options.no_recreate_provision is False if state.options.recreate else False
+    recreate = state.options.recreate and not state.options.no_recreate_provision
     try:
         tox_env.setup(recreate=recreate)
     except Skip as exception:
diff --git a/src/tox/session/cmd/run/parallel.py b/src/tox/session/cmd/run/parallel.py
index a41c6d9d..cf0e53d4 100644
--- a/src/tox/session/cmd/run/parallel.py
+++ b/src/tox/session/cmd/run/parallel.py
@@ -36,9 +36,9 @@ def parse_num_processes(str_value: str) -> Optional[int]:
     try:
         value = int(str_value)
     except ValueError as exc:
-        raise ArgumentTypeError(f"value must be a positive number, is {str_value}") from exc
+        raise ArgumentTypeError(f"value must be a positive number, is {str_value!r}") from exc
     if value < 0:
-        raise ArgumentTypeError(f"value must be positive, is {value}")
+        raise ArgumentTypeError(f"value must be positive, is {value!r}")
     return value
 
 
diff --git a/src/tox/tox_env/package.py b/src/tox/tox_env/package.py
index 3d1b9ea0..daf7dbb8 100644
--- a/src/tox/tox_env/package.py
+++ b/src/tox/tox_env/package.py
@@ -31,7 +31,7 @@ class PackageToxEnv(ToxEnv, ABC):
         self, conf: EnvConfigSet, core: CoreConfigSet, options: "Parsed", journal: EnvJournal, log_handler: ToxHandler
     ) -> None:
         super().__init__(conf, core, options, journal, log_handler)
-        self.recreate_package = options.no_recreate_pkg is False if options.recreate else False
+        self.recreate_package = options.recreate and not options.no_recreate_pkg
         self._envs: Set[str] = set()
         self._lock = Lock()
 
diff --git a/src/tox/tox_env/python/pip/req_file.py b/src/tox/tox_env/python/pip/req_file.py
index 5f10cc9d..cd916e75 100644
--- a/src/tox/tox_env/python/pip/req_file.py
+++ b/src/tox/tox_env/python/pip/req_file.py
@@ -3,7 +3,7 @@ import os
 import re
 from abc import ABC, abstractmethod
 from pathlib import Path
-from typing import Any, Dict, Iterable, List, Optional, Sequence, Tuple, Union
+from typing import Any, Dict, Iterable, Iterator, List, Optional, Sequence, Tuple, Union
 
 from packaging.requirements import InvalidRequirement, Requirement
 
@@ -19,7 +19,7 @@ def is_url(name: str) -> bool:
 
 
 def get_url_scheme(url: str) -> Optional[str]:
-    return None if ":" not in url else url.split(":", 1)[0].lower()
+    return None if ":" not in url else url.partition(":")[0].lower()
 
 
 NO_ARG = {
@@ -108,25 +108,27 @@ class RequirementWithFlags(Requirement, Flags):
 
 
 class PathReq(PipRequirementEntry):
-    def __init__(self, path: Path) -> None:
+    def __init__(self, path: Path, extras: List[str]) -> None:
         self.path = path
+        self.extras = extras
 
     def as_args(self) -> Iterable[str]:
-        return (str(self.path),)
+        return (str(self),)
 
     def __eq__(self, other: Any) -> bool:
-        return isinstance(other, self.__class__) and self.path == other.path
+        return isinstance(other, self.__class__) and self.path == other.path and self.extras == other.extras
 
     def __str__(self) -> str:
-        return str(self.path)
+        extra_group = f"[{','.join(self.extras)}]" if self.extras else ""
+        return f"{self.path}{extra_group}"
 
 
 class EditablePathReq(PathReq):
     def as_args(self) -> Iterable[str]:
-        return "-e", str(self.path)
+        return ("-e", super().__str__())
 
     def __str__(self) -> str:
-        return f"-e {self.path}"
+        return f"-e {super().__str__()}"
 
 
 class UrlReq(PipRequirementEntry):
@@ -143,6 +145,11 @@ class UrlReq(PipRequirementEntry):
         return self.url
 
 
+# https://www.python.org/dev/peps/pep-0508/#extras
+_EXTRA_PATH = re.compile(r"(.*)\[([-._,\sa-zA-Z0-9]*)]")
+_EXTRA_ELEMENT = re.compile(r"[a-zA-Z0-9]*[-._a-zA-Z0-9]")
+
+
 class PythonDeps:
     """A sub-set form of the requirements files (support tox 3 syntax, and --hash is not valid on CLI)"""
 
@@ -215,17 +222,35 @@ class PythonDeps:
             if is_url(line) or any(line.startswith(f"{v}+") and is_url(line[len(v) + 1 :]) for v in VCS):
                 result.append(UrlReq(line))
             else:
-                path = ini_dir / line
-                try:
-                    is_valid_file = path.exists() and (path.is_file() or path.is_dir())
-                except OSError:  # https://bugs.python.org/issue42855 # pragma: no cover
-                    is_valid_file = False  # pragma: no cover
-                if not is_valid_file:
+                for path, extra in self._path_candidate(ini_dir / line):
+                    try:
+                        if path.exists() and (path.is_file() or path.is_dir()):
+                            result.append(PathReq(path, extra))
+                            break
+                    except OSError:  # https://bugs.python.org/issue42855 # pragma: no cover
+                        continue
+                else:
                     raise ValueError(f"{at}: {line}") from exc
-                result.append(PathReq(path))
         else:
             result.append(req)
 
+    @staticmethod
+    def _path_candidate(path: Path) -> Iterator[Tuple[Path, List[str]]]:
+        yield path, []
+        # if there's a trailing [a,b] section that could mean either a folder or extras, try both
+        match = _EXTRA_PATH.fullmatch(path.name)
+        if match:
+            extras = []
+            for extra in match.group(2).split(","):
+                extra = extra.strip()
+                if not extra:
+                    continue
+                if not _EXTRA_ELEMENT.fullmatch(extra):
+                    break
+                extras.append(extra)
+            else:
+                yield path.parent / match.group(1), extras
+
     def _load_requirement_with_extra(self, line: str) -> Tuple[str, List[str]]:
         return line, []
 
@@ -254,7 +279,7 @@ class PythonDeps:
                 req_file.validate_and_expand()
                 result.append(req_file)
             elif first in ("-e", "--editable"):
-                result.append(EditablePathReq(Path(words[2])))
+                result.append(EditablePathReq(Path(words[2]), []))
             elif first in [
                 "-i",
                 "--index-url",
diff --git a/src/tox/tox_env/python/virtual_env/package/api.py b/src/tox/tox_env/python/virtual_env/package/api.py
index 8e444244..e2b1095d 100644
--- a/src/tox/tox_env/python/virtual_env/package/api.py
+++ b/src/tox/tox_env/python/virtual_env/package/api.py
@@ -62,11 +62,7 @@ class ToxCmdStatus(CmdStatus):
         if status.exit_code is not None:  # pragma: no branch
             return True  # pragma: no cover
         # 2. the backend output reported back that our command is done
-        content = status.out
-        at = content.rfind(b"Backend: Wrote response ")
-        if at != -1 and content.find(b"\n", at) != -1:
-            return True
-        return False
+        return b"\n" in status.out.rpartition(b"Backend: Wrote response ")[0]
 
     def out_err(self) -> Tuple[str, str]:
         status = self._execute_status


Test patch:
diff --git a/src/tox/pytest.py b/src/tox/pytest.py
index f7c84502..8ce123f6 100644
--- a/src/tox/pytest.py
+++ b/src/tox/pytest.py
@@ -461,7 +461,7 @@ class IndexServer:
         self.path = path
 
         self.host, self.port = "localhost", _find_free_port()
-        self._passwd = "".join(random.choice(string.ascii_letters) for _ in range(8))
+        self._passwd = "".join(random.choices(string.ascii_letters, k=8))
 
         def _exe(name: str) -> str:
             return str(Path(scripts_dir) / f"{name}{'.exe' if IS_WIN else ''}")
diff --git a/tests/config/cli/test_parser.py b/tests/config/cli/test_parser.py
index 47d74bb8..23c87509 100644
--- a/tests/config/cli/test_parser.py
+++ b/tests/config/cli/test_parser.py
@@ -47,7 +47,7 @@ def test_parser_color(
     stdout_mock.isatty.return_value = is_atty
 
     if tox_color in ("yes", "no"):
-        expected = True if tox_color == "yes" else False
+        expected = tox_color == "yes"
     elif no_color == "1":
         expected = False
     elif force_color == "1":
diff --git a/tests/config/test_sets.py b/tests/config/test_sets.py
index e2a8cd64..e0bc7dd3 100644
--- a/tests/config/test_sets.py
+++ b/tests/config/test_sets.py
@@ -70,7 +70,7 @@ def test_config_bad_dict(conf_builder: ConfBuilder) -> None:
     config_set.add_config(keys="bad_dict", of_type=Dict[str, str], default={}, desc="bad_dict")
     with pytest.raises(TypeError) as context:
         assert config_set["bad_dict"]
-    assert str(context.value) == "dictionary lines must be of form key=value, found something"
+    assert str(context.value) == "dictionary lines must be of form key=value, found 'something'"
 
 
 def test_config_bad_bool(conf_builder: ConfBuilder) -> None:
diff --git a/tests/tox_env/python/pip/test_req_file.py b/tests/tox_env/python/pip/test_req_file.py
index f73d020d..5f61f44f 100644
--- a/tests/tox_env/python/pip/test_req_file.py
+++ b/tests/tox_env/python/pip/test_req_file.py
@@ -45,6 +45,7 @@ from tox.tox_env.python.pip.req_file import (
         pytest.param("--extra-index-url a", "--extra-index-url a", id="extra-index-url"),
         pytest.param("-e a", "-e a", id="e"),
         pytest.param("--editable a", "-e a", id="editable"),
+        pytest.param("--editable .[extra1,extra2]", "-e .[extra1,extra2]", id="editable extra"),
         pytest.param("-f a", "-f a", id="f"),
         pytest.param("--find-links a", "--find-links a", id="find-links"),
         pytest.param("--trusted-host a", "--trusted-host a", id="trusted-host"),
@@ -138,6 +139,16 @@ def test_requirements_txt(tmp_path: Path, req: str, key: str) -> None:
         assert expanded == []
 
 
+def test_deps_path_with_extra_ok(tmp_path: Path) -> None:
+    result = PythonDeps(".[\t, a1. , B2-\t, C3_, ]", root=tmp_path).unroll()
+    assert result == [f"{tmp_path}[a1.,B2-,C3_]"]
+
+
+def test_deps_path_with_extra_nok(tmp_path: Path) -> None:
+    with pytest.raises(ValueError, match=re.escape(".[\t, a.1]")):
+        PythonDeps(".[\t, a.1]", root=tmp_path).unroll()
+
+
 def test_requirements_txt_local_path_file_protocol(tmp_path: Path) -> None:
     (tmp_path / "downloads").mkdir()
     (tmp_path / "downloads" / "numpy-1.9.2-cp34-none-win32.whl").write_text("1")
@@ -300,24 +311,24 @@ def test_requirement_with_flags_has_args() -> None:
 
 
 def test_path_req(tmp_path: Path) -> None:
-    path_req = PathReq(tmp_path)
+    path_req = PathReq(tmp_path, [])
 
     assert path_req.as_args() == (str(tmp_path),)
     assert str(path_req) == str(tmp_path)
 
-    assert path_req != PathReq(tmp_path / "a")
-    assert path_req == PathReq(tmp_path)
+    assert path_req != PathReq(tmp_path / "a", [])
+    assert path_req == PathReq(tmp_path, [])
     assert path_req != object
 
 
 def test_editable_path_req(tmp_path: Path) -> None:
-    editable_path_req = EditablePathReq(tmp_path)
+    editable_path_req = EditablePathReq(tmp_path, [])
 
     assert editable_path_req.as_args() == ("-e", str(tmp_path))
     assert str(editable_path_req) == f"-e {tmp_path}"
 
-    assert editable_path_req != EditablePathReq(tmp_path / "a")
-    assert editable_path_req == EditablePathReq(tmp_path)
+    assert editable_path_req != EditablePathReq(tmp_path / "a", [])
+    assert editable_path_req == EditablePathReq(tmp_path, [])
     assert editable_path_req != object