pex 2.64.1__py2.py3-none-any.whl → 2.69.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pex might be problematic. Click here for more details.

Files changed (69) hide show
  1. pex/bin/pex.py +2 -1
  2. pex/build_backend/configuration.py +5 -5
  3. pex/build_backend/wrap.py +2 -19
  4. pex/cli/commands/lock.py +4 -2
  5. pex/cli/commands/run.py +10 -11
  6. pex/cli/pex.py +11 -4
  7. pex/dist_metadata.py +29 -2
  8. pex/docs/html/_pagefind/fragment/en_4250138.pf_fragment +0 -0
  9. pex/docs/html/_pagefind/fragment/en_7125dad.pf_fragment +0 -0
  10. pex/docs/html/_pagefind/fragment/en_785d562.pf_fragment +0 -0
  11. pex/docs/html/_pagefind/fragment/en_8e94bb8.pf_fragment +0 -0
  12. pex/docs/html/_pagefind/fragment/{en_17782b6.pf_fragment → en_a0396bb.pf_fragment} +0 -0
  13. pex/docs/html/_pagefind/fragment/en_a8a3588.pf_fragment +0 -0
  14. pex/docs/html/_pagefind/fragment/en_c07d988.pf_fragment +0 -0
  15. pex/docs/html/_pagefind/fragment/en_d718411.pf_fragment +0 -0
  16. pex/docs/html/_pagefind/index/en_a2e3c5e.pf_index +0 -0
  17. pex/docs/html/_pagefind/pagefind-entry.json +1 -1
  18. pex/docs/html/_pagefind/pagefind.en_4ce1afa9e3.pf_meta +0 -0
  19. pex/docs/html/_static/documentation_options.js +1 -1
  20. pex/docs/html/api/vars.html +5 -5
  21. pex/docs/html/buildingpex.html +5 -5
  22. pex/docs/html/genindex.html +5 -5
  23. pex/docs/html/index.html +5 -5
  24. pex/docs/html/recipes.html +5 -5
  25. pex/docs/html/scie.html +5 -5
  26. pex/docs/html/search.html +5 -5
  27. pex/docs/html/whatispex.html +5 -5
  28. pex/hashing.py +71 -9
  29. pex/interpreter_constraints.py +1 -1
  30. pex/jobs.py +13 -6
  31. pex/pep_376.py +21 -6
  32. pex/pep_427.py +30 -8
  33. pex/pex_builder.py +1 -4
  34. pex/pip/local_project.py +6 -14
  35. pex/pip/tool.py +3 -3
  36. pex/pip/vcs.py +93 -44
  37. pex/pip/version.py +7 -0
  38. pex/resolve/configured_resolve.py +13 -5
  39. pex/resolve/lock_downloader.py +1 -0
  40. pex/resolve/locker.py +30 -14
  41. pex/resolve/lockfile/create.py +2 -7
  42. pex/resolve/pre_resolved_resolver.py +1 -7
  43. pex/resolve/project.py +233 -47
  44. pex/resolve/resolver_configuration.py +1 -1
  45. pex/resolve/resolver_options.py +14 -9
  46. pex/resolve/venv_resolver.py +221 -65
  47. pex/resolver.py +59 -55
  48. pex/scie/__init__.py +40 -1
  49. pex/scie/model.py +2 -0
  50. pex/scie/science.py +25 -3
  51. pex/sdist.py +219 -0
  52. pex/version.py +1 -1
  53. pex/wheel.py +16 -12
  54. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/METADATA +4 -4
  55. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/RECORD +60 -59
  56. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/entry_points.txt +1 -0
  57. pex/docs/html/_pagefind/fragment/en_1048255.pf_fragment +0 -0
  58. pex/docs/html/_pagefind/fragment/en_3f7efc3.pf_fragment +0 -0
  59. pex/docs/html/_pagefind/fragment/en_40667cd.pf_fragment +0 -0
  60. pex/docs/html/_pagefind/fragment/en_55ee2f4.pf_fragment +0 -0
  61. pex/docs/html/_pagefind/fragment/en_d6d92dd.pf_fragment +0 -0
  62. pex/docs/html/_pagefind/fragment/en_d834316.pf_fragment +0 -0
  63. pex/docs/html/_pagefind/fragment/en_ec2ce54.pf_fragment +0 -0
  64. pex/docs/html/_pagefind/index/en_17effb2.pf_index +0 -0
  65. pex/docs/html/_pagefind/pagefind.en_49ec86cf86.pf_meta +0 -0
  66. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/WHEEL +0 -0
  67. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/licenses/LICENSE +0 -0
  68. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/pylock/pylock.toml +0 -0
  69. {pex-2.64.1.dist-info → pex-2.69.0.dist-info}/top_level.txt +0 -0
pex/scie/model.py CHANGED
@@ -303,6 +303,8 @@ class ScieOptions(object):
303
303
  python_version = attr.ib(
304
304
  default=None
305
305
  ) # type: Optional[Union[Tuple[int, int], Tuple[int, int, int]]]
306
+ pbs_free_threaded = attr.ib(default=False) # type: bool
307
+ pbs_debug = attr.ib(default=False) # type: bool
306
308
  pbs_stripped = attr.ib(default=False) # type: bool
307
309
  hash_algorithms = attr.ib(default=()) # type: Tuple[str, ...]
308
310
  science_binary = attr.ib(default=None) # type: Optional[Union[File, Url]]
pex/scie/science.py CHANGED
@@ -24,6 +24,7 @@ from pex.hashing import Sha256
24
24
  from pex.os import is_exe
25
25
  from pex.pep_440 import Version
26
26
  from pex.pex import PEX
27
+ from pex.pex_info import PexInfo
27
28
  from pex.result import Error, try_
28
29
  from pex.scie.model import (
29
30
  File,
@@ -37,6 +38,7 @@ from pex.scie.model import (
37
38
  )
38
39
  from pex.sysconfig import SysPlatform
39
40
  from pex.third_party.packaging.specifiers import SpecifierSet
41
+ from pex.third_party.packaging.utils import parse_wheel_filename
40
42
  from pex.third_party.packaging.version import InvalidVersion
41
43
  from pex.tracer import TRACER
42
44
  from pex.typing import TYPE_CHECKING
@@ -66,7 +68,7 @@ class Manifest(object):
66
68
 
67
69
 
68
70
  SCIENCE_RELEASES_URL = "https://github.com/a-scie/lift/releases"
69
- MIN_SCIENCE_VERSION = Version("0.14.0")
71
+ MIN_SCIENCE_VERSION = Version("0.15.1")
70
72
  SCIENCE_REQUIREMENT = SpecifierSet("~={min_version}".format(min_version=MIN_SCIENCE_VERSION))
71
73
 
72
74
 
@@ -104,6 +106,16 @@ class Filenames(Enum["Filenames.Value"]):
104
106
  Filenames.seal()
105
107
 
106
108
 
109
+ def _is_free_threaded_pex(pex_info):
110
+ # type: (PexInfo) -> bool
111
+ for distribution in pex_info.distributions:
112
+ _, _, _, tags = parse_wheel_filename(os.path.basename(distribution))
113
+ for tag in tags:
114
+ if tag.abi.startswith(("cp", "abi3")) and "t" in tag.abi:
115
+ return True
116
+ return False
117
+
118
+
107
119
  def create_manifests(
108
120
  configuration, # type: ScieConfiguration
109
121
  name, # type: str
@@ -270,6 +282,7 @@ def create_manifests(
270
282
  }
271
283
 
272
284
  configure_binding_args = [Filenames.PEX.placeholder, Filenames.CONFIGURE_BINDING.placeholder]
285
+ pbs_free_threaded = _is_free_threaded_pex(pex_info) or configuration.options.pbs_free_threaded
273
286
  for interpreter in configuration.interpreters:
274
287
  lift = lift_template.copy()
275
288
 
@@ -285,10 +298,17 @@ def create_manifests(
285
298
  interpreter.platform.qualified_file_name("{name}-lift.toml".format(name=name)),
286
299
  )
287
300
 
301
+ version_str = interpreter.version_str
302
+ if Provider.PythonBuildStandalone is interpreter.provider:
303
+ if configuration.options.pbs_debug:
304
+ version_str += "d"
305
+ if pbs_free_threaded:
306
+ version_str += "t"
307
+
288
308
  interpreter_config = {
289
309
  "id": "python-distribution",
290
310
  "provider": interpreter.provider.value,
291
- "version": interpreter.version_str,
311
+ "version": version_str,
292
312
  "lazy": configuration.options.style is ScieStyle.LAZY,
293
313
  }
294
314
  if interpreter.release:
@@ -297,7 +317,9 @@ def create_manifests(
297
317
  interpreter_config["base_url"] = "/".join(
298
318
  (configuration.options.assets_base_url, "providers", str(interpreter.provider))
299
319
  )
300
- if Provider.PythonBuildStandalone is interpreter.provider:
320
+ if Provider.PythonBuildStandalone is interpreter.provider and not (
321
+ configuration.options.pbs_debug or pbs_free_threaded
322
+ ):
301
323
  interpreter_config.update(
302
324
  flavor=(
303
325
  "install_only_stripped"
pex/sdist.py ADDED
@@ -0,0 +1,219 @@
1
+ # Copyright 2025 Pex project contributors.
2
+ # Licensed under the Apache License, Version 2.0 (see LICENSE).
3
+
4
+ from __future__ import absolute_import
5
+
6
+ import copy
7
+ import os.path
8
+ import sys
9
+ import tarfile
10
+ from tarfile import TarInfo
11
+
12
+ from pex.compatibility import commonpath
13
+ from pex.typing import TYPE_CHECKING, cast
14
+
15
+ if TYPE_CHECKING:
16
+ from typing import Any, Dict, Optional, Text, TypeVar
17
+
18
+
19
+ class FilterError(tarfile.TarError):
20
+ pass
21
+
22
+
23
+ class AbsolutePathError(FilterError):
24
+ pass
25
+
26
+
27
+ class OutsideDestinationError(FilterError):
28
+ pass
29
+
30
+
31
+ class SpecialFileError(FilterError):
32
+ pass
33
+
34
+
35
+ class AbsoluteLinkError(FilterError):
36
+ pass
37
+
38
+
39
+ class LinkOutsideDestinationError(FilterError):
40
+ pass
41
+
42
+
43
+ _REALPATH_KWARGS = (
44
+ {"strict": getattr(os.path, "ALLOW_MISSING", False)} if sys.version_info[:2] >= (3, 10) else {}
45
+ ) # type: Dict[str, Any]
46
+
47
+
48
+ if TYPE_CHECKING:
49
+ _Text = TypeVar("_Text", str, Text)
50
+
51
+
52
+ def _realpath(path):
53
+ # type: (_Text) -> _Text
54
+ return os.path.realpath(path, **_REALPATH_KWARGS)
55
+
56
+
57
+ def _get_filtered_attrs(
58
+ member, # type: TarInfo
59
+ dest_path, # type: Text
60
+ for_data=True, # type: bool
61
+ ):
62
+ # type: (...) -> Dict[str, Any]
63
+
64
+ # N.B.: Copied from CPython 3.14 stdlib tarfile.py
65
+ # Modifications:
66
+ # + Exception types replicated with error messages placed at call site.
67
+ # + `os.path.realpath` -> `_realpath` to deal with `strict` parameter.
68
+ # + `os.path.commonpath` -> `pex.compatibility.commonpath`
69
+ # + `mode = None` guarded by `sys.version_info[:2] >= (3, 12)` with commentary.
70
+ # + `{uid,gid,uname,gname} = None` guarded by `sys.version_info[:2] >= (3, 12)` with commentary.
71
+
72
+ new_attrs = {} # type: Dict[str, Any]
73
+ name = member.name
74
+ dest_path = _realpath(dest_path)
75
+ # Strip leading / (tar's directory separator) from filenames.
76
+ # Include os.sep (target OS directory separator) as well.
77
+ if name.startswith(("/", os.sep)):
78
+ name = new_attrs["name"] = member.path.lstrip("/" + os.sep)
79
+ if os.path.isabs(name):
80
+ # Path is absolute even after stripping.
81
+ # For example, 'C:/foo' on Windows.
82
+ raise AbsolutePathError("member {name!r} has an absolute path".format(name=member.name))
83
+ # Ensure we stay in the destination
84
+ target_path = _realpath(os.path.join(dest_path, name))
85
+ if commonpath([target_path, dest_path]) != dest_path:
86
+ raise OutsideDestinationError(
87
+ "{name!r} would be extracted to {path!r}, which is outside the destination".format(
88
+ name=member.name, path=target_path
89
+ )
90
+ )
91
+ # Limit permissions (no high bits, and go-w)
92
+ mode = member.mode # type: Optional[int]
93
+ if mode is not None:
94
+ # Strip high bits & group/other write bits
95
+ mode = mode & 0o755
96
+ if for_data:
97
+ # For data, handle permissions & file types
98
+ if member.isreg() or member.islnk():
99
+ if not mode & 0o100:
100
+ # Clear executable bits if not executable by user
101
+ mode &= ~0o111
102
+ # Ensure owner can read & write
103
+ mode |= 0o600
104
+ elif member.isdir() or member.issym():
105
+ if sys.version_info[:2] >= (3, 12):
106
+ # Ignore mode for directories & symlinks
107
+ mode = None
108
+ else:
109
+ # Retain stripped mode since older Pythons do not support None.
110
+ pass
111
+ else:
112
+ # Reject special files
113
+ raise SpecialFileError("{name!r} is a special file".format(name=member.name))
114
+ if mode != member.mode:
115
+ new_attrs["mode"] = mode
116
+ if for_data:
117
+ if sys.version_info[:2] >= (3, 12):
118
+ # Ignore ownership for 'data'
119
+ if member.uid is not None:
120
+ new_attrs["uid"] = None
121
+ if member.gid is not None:
122
+ new_attrs["gid"] = None
123
+ if member.uname is not None:
124
+ new_attrs["uname"] = None
125
+ if member.gname is not None:
126
+ new_attrs["gname"] = None
127
+ else:
128
+ # Retain uid/gid/uname/gname since older Pythons do not support None.
129
+ pass
130
+
131
+ # Check link destination for 'data'
132
+ if member.islnk() or member.issym():
133
+ if os.path.isabs(member.linkname):
134
+ raise AbsoluteLinkError(
135
+ "{name!r} is a link to an absolute path".format(name=member.name)
136
+ )
137
+ normalized = os.path.normpath(member.linkname)
138
+ if normalized != member.linkname:
139
+ new_attrs["linkname"] = normalized
140
+ if member.issym():
141
+ target_path = os.path.join(dest_path, os.path.dirname(name), member.linkname)
142
+ else:
143
+ target_path = os.path.join(dest_path, member.linkname)
144
+ target_path = _realpath(target_path)
145
+ if commonpath([target_path, dest_path]) != dest_path:
146
+ raise LinkOutsideDestinationError(
147
+ "{name!r} would link to {path!r}, which is outside the destination".format(
148
+ name=member.name, path=target_path
149
+ )
150
+ )
151
+ return new_attrs
152
+
153
+
154
+ def _replace(
155
+ member, # type: TarInfo
156
+ attrs, # type: Dict[str, Any]
157
+ ):
158
+ # type: (...) -> TarInfo
159
+
160
+ replace = getattr(member, "replace", None)
161
+ if replace:
162
+ attrs["deep"] = False
163
+ return cast(TarInfo, replace(**attrs))
164
+
165
+ result = copy.copy(member)
166
+ for attr, value in attrs.items():
167
+ setattr(result, attr, value)
168
+ return result
169
+
170
+
171
+ def _data_filter(
172
+ member, # type: TarInfo
173
+ dest_path, # type: Text
174
+ ):
175
+ # type: (...) -> TarInfo
176
+ new_attrs = _get_filtered_attrs(member, dest_path, True)
177
+ if new_attrs:
178
+ return _replace(member, new_attrs)
179
+ return member
180
+
181
+
182
+ _EXTRACTALL_DATA_FILTER_KWARGS = {"filter": "data"} # type: Dict[str, Any]
183
+
184
+
185
+ class InvalidSourceDistributionError(ValueError):
186
+ pass
187
+
188
+
189
+ def extract_tarball(
190
+ tarball_path, # type: Text
191
+ dest_dir, # type: _Text
192
+ ):
193
+ # type: (...) -> _Text
194
+
195
+ with tarfile.open(tarball_path) as tf:
196
+ if sys.version_info[:2] >= (3, 12):
197
+ tf.extractall(dest_dir, **_EXTRACTALL_DATA_FILTER_KWARGS)
198
+ else:
199
+ for tar_info in tf: # type: ignore[unreachable]
200
+ tar_info = _data_filter(tar_info, dest_dir)
201
+ tf.extract(tar_info, dest_dir)
202
+
203
+ listing = os.listdir(dest_dir)
204
+ if len(listing) != 1:
205
+ raise InvalidSourceDistributionError(
206
+ "Expected one top-level project directory to be extracted from {project}, "
207
+ "found {count}: {listing}".format(
208
+ project=tarball_path, count=len(listing), listing=", ".join(listing)
209
+ )
210
+ )
211
+
212
+ project_dir = os.path.join(dest_dir, listing[0])
213
+ if not os.path.isdir(project_dir):
214
+ raise InvalidSourceDistributionError(
215
+ "Expected one top-level project directory to be extracted from {project}, "
216
+ "found file: {path}".format(project=tarball_path, path=listing[0])
217
+ )
218
+
219
+ return project_dir
pex/version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # Copyright 2015 Pex project contributors.
2
2
  # Licensed under the Apache License, Version 2.0 (see LICENSE).
3
3
 
4
- __version__ = "2.64.1"
4
+ __version__ = "2.69.0"
pex/wheel.py CHANGED
@@ -42,7 +42,7 @@ class WHEEL(object):
42
42
  """
43
43
 
44
44
  @classmethod
45
- def _from_metadata_files(cls, metadata_files):
45
+ def from_metadata_files(cls, metadata_files):
46
46
  # type: (MetadataFiles) -> WHEEL
47
47
 
48
48
  metadata_bytes = metadata_files.read("WHEEL")
@@ -52,7 +52,18 @@ class WHEEL(object):
52
52
  wheel=metadata_files.render_description(metadata_file_name="WHEEL")
53
53
  )
54
54
  )
55
- metadata = parse_message(metadata_bytes)
55
+
56
+ # Some WHEEL metadata in the wild has blank lines in between headers when it should not.
57
+ # Since WHEEL metadata, unlike METADATA, does not use the message body to convey metadata,
58
+ # this should be safe.
59
+ #
60
+ # See here for initial discovery case:
61
+ # https://github.com/pex-tool/pex/issues/2998#issuecomment-3492998265
62
+ normalized_metadata = b"".join(
63
+ line for line in metadata_bytes.splitlines(True) if line.strip()
64
+ )
65
+
66
+ metadata = parse_message(normalized_metadata)
56
67
  return cls(files=metadata_files, metadata=metadata)
57
68
 
58
69
  _CACHE = {} # type: Dict[Tuple[Text, Optional[ProjectName]], WHEEL]
@@ -73,7 +84,7 @@ class WHEEL(object):
73
84
  raise WheelMetadataLoadError(
74
85
  "Could not find any metadata in {wheel}.".format(wheel=location)
75
86
  )
76
- wheel = cls._from_metadata_files(metadata_files)
87
+ wheel = cls.from_metadata_files(metadata_files)
77
88
  cls._CACHE[(location, project_name)] = wheel
78
89
  return wheel
79
90
 
@@ -84,7 +95,7 @@ class WHEEL(object):
84
95
  project_name = distribution.metadata.project_name
85
96
  wheel = cls._CACHE.get((location, project_name))
86
97
  if not wheel:
87
- wheel = cls._from_metadata_files(distribution.metadata.files)
98
+ wheel = cls.from_metadata_files(distribution.metadata.files)
88
99
  cls._CACHE[(location, project_name)] = wheel
89
100
  return wheel
90
101
 
@@ -144,14 +155,7 @@ class Wheel(object):
144
155
  if wheel:
145
156
  metadata = wheel
146
157
  else:
147
- wheel_data = metadata_files.read("WHEEL")
148
- if not wheel_data:
149
- raise WheelMetadataLoadError(
150
- "Could not find WHEEL metadata in {source}.".format(
151
- source=cls._source(location, metadata_files)
152
- )
153
- )
154
- metadata = WHEEL(files=metadata_files, metadata=parse_message(wheel_data))
158
+ metadata = WHEEL.from_metadata_files(metadata_files)
155
159
 
156
160
  wheel_metadata_dir = os.path.dirname(metadata_files.metadata.rel_path)
157
161
  if not wheel_metadata_dir.endswith(".dist-info"):
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pex
3
- Version: 2.64.1
3
+ Version: 2.69.0
4
4
  Summary: The PEX packaging toolchain.
5
5
  Home-page: https://github.com/pex-tool/pex
6
- Download-URL: https://github.com/pex-tool/pex/releases/download/v2.64.1/pex
6
+ Download-URL: https://github.com/pex-tool/pex/releases/download/v2.69.0/pex
7
7
  Author: The PEX developers
8
8
  Author-email: developers@pex-tool.org
9
9
  License-Expression: Apache-2.0
10
- Project-URL: Changelog, https://github.com/pex-tool/pex/blob/v2.64.1/CHANGES.md
10
+ Project-URL: Changelog, https://github.com/pex-tool/pex/blob/v2.69.0/CHANGES.md
11
11
  Project-URL: Documentation, https://docs.pex-tool.org/
12
- Project-URL: Source, https://github.com/pex-tool/pex/tree/v2.64.1
12
+ Project-URL: Source, https://github.com/pex-tool/pex/tree/v2.69.0
13
13
  Keywords: package,executable,virtualenv,lock,freeze
14
14
  Classifier: Development Status :: 5 - Production/Stable
15
15
  Classifier: Intended Audience :: Developers