Repository: pex-tool/pex

Issue:
Black fails to lock with new resolver and universal style
```
❯ pex3 lock create --style=universal --resolver-version=pip-2020-resolver black==22.1.0
Traceback (most recent call last):
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/bin/pex3", line 8, in <module>
    sys.exit(main())
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/cli/pex.py", line 30, in main
    result = catch(command.run)
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/result.py", line 101, in catch
    return func(*args, **kwargs)
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/cli/command.py", line 84, in run
    return subcommand_func(self)
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/cli/commands/lock.py", line 244, in _create
    create(
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/resolve/lockfile/__init__.py", line 234, in create
    downloaded = resolver.download(
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/resolver.py", line 1183, in download
    build_requests, download_results = _download_internal(
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/resolver.py", line 1085, in _download_internal
    download_results = download_request.download_distributions(
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/resolver.py", line 136, in download_distributions
    return list(
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/jobs.py", line 512, in execute_parallel
    yield spawn_result.spawned_job.await_result()
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/jobs.py", line 217, in await_result
    return result_func()
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/resolver.py", line 88, in lock
    locked_resolve = LockedResolve.create(
  File "/Users/ericarellano/.cache/pants/pants_dev_deps/Darwin.arm64.arm.py39.venv/lib/python3.9/site-packages/pex/resolve/locked_resolve.py", line 391, in create
    raise ValueError(
ValueError: No distribution metadata found for dataclasses==0.8.
Given distribution metadata for:
black==22.1
click==8.0.4
colorama==0.4.4
dataclasses==0.6
importlib-metadata==4.11.2
mypy-extensions==0.4.3
pathspec==0.9
platformdirs==2.5.1
tomli==2.0.1
typed-ast==1.5.2
typing-extensions==4.1.1
typing==3.7.4.3
zipp==3.7
```

It works if you remove either the universal style or the new resolver.


Discussion:
The relevant Pip log section:
```
...
2022-03-12T10:37:00,057 1 location(s) to search for versions of dataclasses:
2022-03-12T10:37:00,057 * https://pypi.org/simple/dataclasses/
2022-03-12T10:37:00,057 Fetching project page and analyzing links: https://pypi.org/simple/dataclasses/
2022-03-12T10:37:00,057 Getting page https://pypi.org/simple/dataclasses/
2022-03-12T10:37:00,058 Found index url https://pypi.org/simple
2022-03-12T10:37:00,059 Looking up "https://pypi.org/simple/dataclasses/" in the cache
2022-03-12T10:37:00,059 Request header has "max_age" as 0, cache bypassed
2022-03-12T10:37:00,090 https://pypi.org:443 "GET /simple/dataclasses/ HTTP/1.1" 304 0
2022-03-12T10:37:00,096   Found link https://files.pythonhosted.org/packages/c2/1a/f071ef9c0ca0de699888a0216f03e461ca52a6606b4e4e67a3fd0945cc57/dataclasses-0.1-py3-none-any.whl#sha256=7453d237c07b54fbe7f34269cb50bdd14b247f161e2e40fd36cd0d572696ec91 (from https://pypi.org/simple/dataclasses/), version: 0.1
2022-03-12T10:37:00,096   Found link https://files.pythonhosted.org/packages/18/f6/c65a5cafa61d5e1aeb56b5b0ce08eeb7a49255e7af612f4ffb68d5afbf32/dataclasses-0.1.tar.gz#sha256=a20c07c7fb59b0f1b7484ba4845ce1d3a5fd9fa28ac07b4c1b7b17d4b2f6c8da (from https://pypi.org/simple/dataclasses/), version: 0.1
2022-03-12T10:37:00,096   Found link https://files.pythonhosted.org/packages/63/3b/97b1df59a8bbf6366a10ae37e8794681993729c15726f093d81d500041a7/dataclasses-0.2-py3-none-any.whl#sha256=88ef405c36c11623db398a3efc28bb05cc219717def604b3011f51497d36d6e2 (from https://pypi.org/simple/dataclasses/), version: 0.2
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/b2/1b/c9440e565f8582d6abc7fa40d037e1b5f6d66e938a8629b5efdb15cf3d7e/dataclasses-0.2.tar.gz#sha256=cb0862ffeed52910182bffa56b50c439b790ae6169b59897c045d332cc030395 (from https://pypi.org/simple/dataclasses/), version: 0.2
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/c5/0f/509bf941c92e2e52330e736f7a7c66eb5147fb86f6c41d64a5819ccbc847/dataclasses-0.3-py3-none-any.whl#sha256=5e9c6144acb2113b6d7fdcbfa352a9639322c38752323503ef04710d8b8e68ce (from https://pypi.org/simple/dataclasses/), version: 0.3
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/a5/84/548d2f6c7cbd17a3ba89f1cadd6bb18f3b990b56b08cfe90d42044eb641c/dataclasses-0.3.tar.gz#sha256=5ea21f4d75eee0a6c316b76782f0426e6ac63915815de99eda7313ce35e14ecb (from https://pypi.org/simple/dataclasses/), version: 0.3
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/1a/5c/1b3c26c66fe52d05519a6240eb8d27481dc955dd303cd10cba47412c3566/dataclasses-0.4-py3-none-any.whl#sha256=068953b730c80cbb13ca6aac6ceedaa5d483fb6081a372ac4788aa5179ed9597 (from https://pypi.org/simple/dataclasses/), version: 0.4
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/f7/46/d95afb5e1291f6ec18175b249a8fb2f168b1b19c7f9dcce0e1584924be22/dataclasses-0.4.tar.gz#sha256=0f75133f21f6c9bd0fe82bc75d9908e46f531682911c9cffa75bce0e40ef09ef (from https://pypi.org/simple/dataclasses/), version: 0.4
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/e6/aa/06d69f3084fcf49bc02db6f39a8126a7d5a1f3ebfdf4ae69bcbc68141e01/dataclasses-0.5-py3-none-any.whl#sha256=7127de1976d5c6d4f374c9826aefe6564b612690a60ca957cbd5c64483326dc1 (from https://pypi.org/simple/dataclasses/), version: 0.5
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/47/6b/78d693919f14438cea61ecf1ff20cbfaace33f41c95618fd9a848aa2a844/dataclasses-0.5.tar.gz#sha256=3f20420c953ecf4c4df68f3aaee48dea69a87935aaeceffce1176b5366b08f1e (from https://pypi.org/simple/dataclasses/), version: 0.5
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/26/2f/1095cdc2868052dd1e64520f7c0d5c8c550ad297e944e641dbf1ffbb9a5d/dataclasses-0.6-py3-none-any.whl#sha256=454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f (from https://pypi.org/simple/dataclasses/), version: 0.6
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/59/e4/2f921edfdf1493bdc07b914cbea43bc334996df4841a34523baf73d1fb4f/dataclasses-0.6.tar.gz#sha256=6988bd2b895eef432d562370bb707d540f32f7360ab13da45340101bc2307d84 (from https://pypi.org/simple/dataclasses/), version: 0.6
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/e1/d2/6f02df2616fd4016075f60157c7a0452b38d8f7938ae94343911e0fb0b09/dataclasses-0.7-py3-none-any.whl#sha256=3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836 (from https://pypi.org/simple/dataclasses/) (requires-python:>=3.6, <3.7), version: 0.7
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/7a/71/fdbab71f1f714e03ead2d264bf444f88379bc09b2937d54ec83894057f80/dataclasses-0.7.tar.gz#sha256=494a6dcae3b8bcf80848eea2ef64c0cc5cd307ffc263e17cdf42f3e5420808e6 (from https://pypi.org/simple/dataclasses/) (requires-python:>=3.6, <3.7), version: 0.7
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/fe/ca/75fac5856ab5cfa51bbbcefa250182e50441074fdc3f803f6e76451fab43/dataclasses-0.8-py3-none-any.whl#sha256=0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf (from https://pypi.org/simple/dataclasses/) (requires-python:>=3.6, <3.7), version: 0.8
2022-03-12T10:37:00,097   Found link https://files.pythonhosted.org/packages/1f/12/7919c5d8b9c497f9180db15ea8ead6499812ea8264a6ae18766d93c59fe5/dataclasses-0.8.tar.gz#sha256=8479067f342acf957dc82ec415d355ab5edb7e7646b90dc6e2fd1d96ad084c97 (from https://pypi.org/simple/dataclasses/) (requires-python:>=3.6, <3.7), version: 0.8
2022-03-12T10:37:00,098 Given no hashes to check 6 links for project 'dataclasses': discarding no candidates
2022-03-12T10:37:00,098 Collecting dataclasses>=0.6
2022-03-12T10:37:00,099   Created temporary directory: /tmp/pip-unpack-lk8xscb3
2022-03-12T10:37:00,100   Looking up "https://files.pythonhosted.org/packages/fe/ca/75fac5856ab5cfa51bbbcefa250182e50441074fdc3f803f6e76451fab43/dataclasses-0.8-py3-none-any.whl" in the cache
2022-03-12T10:37:00,100   Current age based on date: 872
2022-03-12T10:37:00,100   Ignoring unknown cache-control directive: immutable
2022-03-12T10:37:00,100   Freshness lifetime from max-age: 365000000
2022-03-12T10:37:00,100   The response is "fresh", returning cached response
2022-03-12T10:37:00,100   365000000 > 872
2022-03-12T10:37:00,100   Using cached dataclasses-0.8-py3-none-any.whl (19 kB)
2022-03-12T10:37:00,101   Added dataclasses>=0.6 from https://files.pythonhosted.org/packages/fe/ca/75fac5856ab5cfa51bbbcefa250182e50441074fdc3f803f6e76451fab43/dataclasses-0.8-py3-none-any.whl#sha256=0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf (from black==22.1.0) to build tracker '/tmp/pip-req-tracker-2u5rjsgq'
2022-03-12T10:37:00,101   Removed dataclasses>=0.6 from https://files.pythonhosted.org/packages/fe/ca/75fac5856ab5cfa51bbbcefa250182e50441074fdc3f803f6e76451fab43/dataclasses-0.8-py3-none-any.whl#sha256=0201d89fa866f68c8ebd9d08ee6ff50c0b255f8ec63a71c16fda7af82bb887bf (from black==22.1.0) from build tracker '/tmp/pip-req-tracker-2u5rjsgq'
...
2022-03-12T10:37:01,392 Collecting dataclasses>=0.6
2022-03-12T10:37:01,392   Created temporary directory: /tmp/pip-unpack-hxkf3k7z
2022-03-12T10:37:01,393   Looking up "https://files.pythonhosted.org/packages/e1/d2/6f02df2616fd4016075f60157c7a0452b38d8f7938ae94343911e0fb0b09/dataclasses-0.7-py3-none-any.whl" in the cache
2022-03-12T10:37:01,393   Current age based on date: 871
2022-03-12T10:37:01,393   Ignoring unknown cache-control directive: immutable
2022-03-12T10:37:01,393   Freshness lifetime from max-age: 365000000
2022-03-12T10:37:01,393   The response is "fresh", returning cached response
2022-03-12T10:37:01,393   365000000 > 871
2022-03-12T10:37:01,394   Using cached dataclasses-0.7-py3-none-any.whl (18 kB)
2022-03-12T10:37:01,394   Added dataclasses>=0.6 from https://files.pythonhosted.org/packages/e1/d2/6f02df2616fd4016075f60157c7a0452b38d8f7938ae94343911e0fb0b09/dataclasses-0.7-py3-none-any.whl#sha256=3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836 (from black==22.1.0) to build tracker '/tmp/pip-req-tracker-2u5rjsgq'
2022-03-12T10:37:01,394   Removed dataclasses>=0.6 from https://files.pythonhosted.org/packages/e1/d2/6f02df2616fd4016075f60157c7a0452b38d8f7938ae94343911e0fb0b09/dataclasses-0.7-py3-none-any.whl#sha256=3459118f7ede7c8bea0fe795bff7c6c2ce287d01dd226202f7c9ebc0610a7836 (from black==22.1.0) from build tracker '/tmp/pip-req-tracker-2u5rjsgq'
2022-03-12T10:37:01,395   Created temporary directory: /tmp/pip-unpack-nyaapdsx
2022-03-12T10:37:01,396   Looking up "https://files.pythonhosted.org/packages/26/2f/1095cdc2868052dd1e64520f7c0d5c8c550ad297e944e641dbf1ffbb9a5d/dataclasses-0.6-py3-none-any.whl" in the cache
2022-03-12T10:37:01,396   Current age based on date: 871
2022-03-12T10:37:01,396   Ignoring unknown cache-control directive: immutable
2022-03-12T10:37:01,396   Freshness lifetime from max-age: 365000000
2022-03-12T10:37:01,396   The response is "fresh", returning cached response
2022-03-12T10:37:01,396   365000000 > 871
2022-03-12T10:37:01,397   Using cached dataclasses-0.6-py3-none-any.whl (14 kB)
2022-03-12T10:37:01,397   Added dataclasses>=0.6 from https://files.pythonhosted.org/packages/26/2f/1095cdc2868052dd1e64520f7c0d5c8c550ad297e944e641dbf1ffbb9a5d/dataclasses-0.6-py3-none-any.whl#sha256=454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f (from black==22.1.0) to build tracker '/tmp/pip-req-tracker-2u5rjsgq'
2022-03-12T10:37:01,397   Removed dataclasses>=0.6 from https://files.pythonhosted.org/packages/26/2f/1095cdc2868052dd1e64520f7c0d5c8c550ad297e944e641dbf1ffbb9a5d/dataclasses-0.6-py3-none-any.whl#sha256=454a69d788c7fda44efd71e259be79577822f5e3f53f029a22d08004e951dc9f (from black==22.1.0) from build tracker '/tmp/pip-req-tracker-2u5rjsgq'
```

So the current signal Pex's Pip Locker uses is `Added {req} from {url} ...` and there is one of these for each of dataclasses 0.6, 0.7 and 0.8. Of these though, Pip only downloads one, dataclasses 0.6. That decision looks like it's encapsulated in these log lines:
```
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/black-22.1.0-py3-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/click-8.0.4-py3-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/dataclasses-0.6-py3-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/mypy_extensions-0.4.3-py2.py3-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/pathspec-0.9.0-py2.py3-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/platformdirs-2.5.1-py3-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/tomli-2.0.1-py3-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/typed_ast-1.5.2-cp39-cp39-win_amd64.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/typing-3.7.4.3-py2-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/typing_extensions-4.1.1-py3-none-any.whl
2022-03-12T10:37:01,976 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/colorama-0.4.4-py2.py3-none-any.whl
2022-03-12T10:37:01,977 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/importlib_metadata-4.11.2-py3-none-any.whl
2022-03-12T10:37:01,977 Saved /tmp/tmp1u9wc1lc/home.jsirois..pyenv.versions.3.9.10.bin.python3.9/zipp-3.7.0-py3-none-any.whl
2022-03-12T10:37:01,977 Successfully downloaded black click dataclasses mypy-extensions pathspec platformdirs tomli typed-ast typing typing-extensions colorama importlib-metadata zipp
```

So the Locker looks like it needs to either post-filter resolved requirements to just include those saved or else download any not saved. The latter effectively means launching several rounds of resolves (what if downloading dataclasses 0.8 revealed a different subgraph?!). It's not immediately clear to me which is correct, but the former sticks with current expectations - that Pip will pick exactly one answer for each project in a resolve.

Resolving patch:
diff --git a/pex/pip.py b/pex/pip.py
--- a/pex/pip.py
+++ b/pex/pip.py
@@ -52,6 +52,7 @@
         Pattern,
         Protocol,
         Sequence,
+        Set,
         Tuple,
         TypeVar,
         Union,
@@ -303,10 +304,25 @@ def analyze(self, line):
 
 
 class Locker(_LogAnalyzer):
-    def __init__(self, lock_request):
-        # type: (LockRequest) -> None
+    def __init__(
+        self,
+        lock_request,  # type: LockRequest
+        download_dir,  # type: str
+    ):
+        # type: (...) -> None
         self._lock_request = lock_request
 
+        self._saved_re = re.compile(
+            r"Saved (?:{download_dir}){dir_sep}(?P<filename>.+)$".format(
+                download_dir="|".join(
+                    re.escape(path)
+                    for path in frozenset((download_dir, os.path.realpath(download_dir)))
+                ),
+                dir_sep=re.escape(os.path.sep),
+            )
+        )
+        self._saved = set()  # type: Set[Pin]
+
         self._resolved_requirements = []  # type: List[ResolvedRequirement]
         self._links = defaultdict(OrderedSet)  # type: DefaultDict[Pin, OrderedSet[PartialArtifact]]
         self._done_building_re = None  # type: Optional[Pattern]
@@ -402,29 +418,41 @@ def analyze(self, line):
                     via=via,
                 )
             )
-        elif self.style in (LockStyle.SOURCES, LockStyle.UNIVERSAL):
-            match = re.search(r"Found link (?P<url>[^\s]+)(?: \(from .*\))?, version: ", line)
+        else:
+            match = self._saved_re.search(line)
             if match:
-                project_name_and_version, partial_artifact = self._extract_resolve_data(
-                    match.group("url")
-                )
-                self._links[project_name_and_version].add(partial_artifact)
-            elif LockStyle.UNIVERSAL == self.style:
-                match = re.search(
-                    r"Skipping link: none of the wheel's tags \([^)]+\) are compatible \(run pip "
-                    r"debug --verbose to show compatible tags\): (?P<url>[^\s]+) ",
-                    line,
+                self._saved.add(
+                    Pin.canonicalize(ProjectNameAndVersion.from_filename(match.group("filename")))
                 )
+            elif self.style in (LockStyle.SOURCES, LockStyle.UNIVERSAL):
+                match = re.search(r"Found link (?P<url>[^\s]+)(?: \(from .*\))?, version: ", line)
                 if match:
                     project_name_and_version, partial_artifact = self._extract_resolve_data(
                         match.group("url")
                     )
                     self._links[project_name_and_version].add(partial_artifact)
+                elif LockStyle.UNIVERSAL == self.style:
+                    match = re.search(
+                        r"Skipping link: none of the wheel's tags \([^)]+\) are compatible \(run "
+                        r"pip debug --verbose to show compatible tags\): (?P<url>[^\s]+) ",
+                        line,
+                    )
+                    if match:
+                        project_name_and_version, partial_artifact = self._extract_resolve_data(
+                            match.group("url")
+                        )
+                        self._links[project_name_and_version].add(partial_artifact)
         return self.Continue()
 
     def analysis_completed(self):
         # type: () -> None
-        self._lock_request.resolve_handler(self._resolved_requirements)
+        self._lock_request.resolve_handler(
+            tuple(
+                resolved_requirement
+                for resolved_requirement in self._resolved_requirements
+                if resolved_requirement.pin in self._saved
+            )
+        )
 
 
 class _LogScrapeJob(Job):
@@ -933,7 +961,7 @@ def spawn_download_distributions(
     ):
         # type: (...) -> Job
         target = target or targets.current()
-        locker = Locker(lock_request) if lock_request else None
+        locker = Locker(lock_request, download_dir) if lock_request else None
 
         if not use_wheel:
             if not build:


Test patch:
diff --git a/tests/integration/cli/commands/test_issue_1665.py b/tests/integration/cli/commands/test_issue_1665.py
new file mode 100644
--- /dev/null
+++ b/tests/integration/cli/commands/test_issue_1665.py
@@ -0,0 +1,38 @@
+# Copyright 2022 Pants project contributors (see CONTRIBUTORS.md).
+# Licensed under the Apache License, Version 2.0 (see LICENSE).
+
+import os
+import sys
+
+import pytest
+
+from pex.cli.testing import run_pex3
+from pex.testing import IS_PYPY, run_pex_command
+from pex.typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+    from typing import Any
+
+
+@pytest.mark.skipif(
+    IS_PYPY or sys.version_info[:3] < (3, 6, 2),
+    reason="The black 22.1.0 distribution requires Python >= 3.6.2",
+)
+def test_lock_black(tmpdir):
+    # type: (Any) -> None
+
+    lock = os.path.join(str(tmpdir), "lock")
+    run_pex3(
+        "lock",
+        "create",
+        "--resolver-version",
+        "pip-2020-resolver",
+        "--style",
+        "universal",
+        "black==22.1.0",
+        "-o",
+        lock,
+    ).assert_success()
+    result = run_pex_command(args=["--lock", lock, "-c", "black", "--", "--version"])
+    result.assert_success()
+    assert " 22.1.0 " in result.output