Skip to content

blext.pydeps

blext.pydeps.blext_deps

Tools for managing wheel-based dependencies.

BLExtDeps

Bases: BaseModel

All Python dependencies needed by a Blender extension.

pydeps_graph cached property

pydeps_graph: DiGraph

Dependency-graph representation of self.pydeps.

from_uv_lock classmethod

from_uv_lock(
	uv_lock: frozendict[str, Any],
	*,
	module_name: str,
	min_glibc_version: tuple[int, int] | None = None,
	min_macos_version: tuple[int, int] | None = None,
	valid_python_tags: frozenset[str] | None = None,
	valid_abi_tags: frozenset[str] | None = None,
) -> Self

Create from a uv.lock file.

PARAMETER DESCRIPTION
uv_lock

Result of parsing a uv.lock file with ex. tomllib.

TYPE: frozendict[str, Any]

module_name

Name of the top-level Python module, which depends on everything else. Should be identical to: - Script Extensions: The module name without .py, such that <module_name>.py exists. - Project Extensions: The package folder name, such that <module_name>/__init__.py exists and has the extension's register() method. - BLExtSpec.id: The

TYPE: str

Source code in blext/pydeps/blext_deps.py
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
@classmethod
def from_uv_lock(
	cls,
	uv_lock: frozendict[str, typ.Any],
	*,
	module_name: str,
	min_glibc_version: tuple[int, int] | None = None,
	min_macos_version: tuple[int, int] | None = None,
	valid_python_tags: frozenset[str] | None = None,
	valid_abi_tags: frozenset[str] | None = None,
) -> typ.Self:
	"""Create from a `uv.lock` file.

	Parameters:
		uv_lock: Result of parsing a `uv.lock` file with ex. `tomllib`.
		module_name: Name of the top-level Python module, which depends on everything else.
			Should be identical to:
			- **Script Extensions**: The module name without `.py`, such that `<module_name>.py` exists.
			- **Project Extensions**: The package folder name, such that `<module_name>/__init__.py` exists and has the extension's `register()` method.
			- `BLExtSpec.id`: The
	"""

	@functools.cache
	def nrm_name(pkg_name: str) -> str:
		return pkg_name.replace('-', '_').lower()

	####################
	# - Stage 1: Parse [[package]]
	####################
	## - Projects: [[package]] list has extension package AND only upstream dependencies.
	## - Single-File Scripts: [[package]] list has only upstream dependencies.
	if 'package' not in uv_lock:
		pydeps: dict[tuple[str, str], PyDep] = {}  ## No PyDeps
	else:
		pydeps = {
			# TODO: Index by a tuple (name, version) to deal with deps that have differing versions across Blender versions
			(nrm_name(package['name']), package['version']): PyDep(  # pyright: ignore[reportAny]
				name=nrm_name(package['name']),  # pyright: ignore[reportAny]
				version=package['version'],  # pyright: ignore[reportAny]
				registry=package['source']['registry'],  # pyright: ignore[reportAny]
				wheels=frozenset(
					{
						PyDepWheel(
							url=wheel_info['url'],  # pyright: ignore[reportAny]
							registry=package['source']['registry'],  # pyright: ignore[reportAny]
							hash=wheel_info.get('hash'),  # pyright: ignore[reportAny]
							size=wheel_info.get('size'),  # pyright: ignore[reportAny]
						)
						for wheel_info in package.get('wheels', [])  # pyright: ignore[reportAny]
						if 'url' in wheel_info
					}
				),
				pydep_markers=frozendict(
					{
						nrm_name(dependency['name']): (  # pyright: ignore[reportAny]
							PyDepMarker(marker_str=dependency['marker'])  # pyright: ignore[reportAny]
							if 'marker' in dependency
							else None
						)
						for dependency in [  # pyright: ignore[reportAny]
							# Always include mandatory dependencies.
							*package.get('dependencies', []),  # pyright: ignore[reportAny]
							# Always include "all" optional dependencies.
							## - uv has already worked out which optional deps are needed.
							## - Unused [optional-dependencies] simply aren't in uv.lock.
							## - So, it's safe to pretend that they are all normal dependencies.
							*[
								resolved_opt_dependency
								for resolved_opt_dependencies in package.get(  # pyright: ignore[reportAny]
									'optional-dependencies',
									{},
								).values()
								for resolved_opt_dependency in resolved_opt_dependencies  # pyright: ignore[reportAny]
							],
						]
					}
				),
			)
			for package in uv_lock['package']  # pyright: ignore[reportAny]
			if (
				# Package must have a name.
				'name' in package
				# Package must have a version.
				and 'version' in package
				# Package must have a registry URL.
				and 'source' in package
				and 'registry' in package['source']
				# Package must not be the "current" package.
				## - We've made the decision not to consider the root (L0) package as a PyDep.
				and module_name != nrm_name(package['name'])  # pyright: ignore[reportAny]
			)
		}

	####################
	# - Stage 2: Parse Target (L1) Dependencies
	####################
	## The L0 (root) PyDep is the blext project itself, pulling in all other PyDeps.
	## The L1 (target) PyDeps are all of the immediate user-requested PyDeps (were 'uv add'ed).
	## L1 PyDeps come in "groups" such as '_' (default), 'dev', and also Blender version-specific.

	target_pydeps: dict[str, PyDepMarker | None] = {}

	# Single-File Scripts: [manifest] table has top-level dependencies - make a "fake" PyDep.
	if 'manifest' in uv_lock:
		# Parse Mandatory Dependencies
		## - Always found in 'manifest.requirements'.
		if 'requirements' in uv_lock['manifest']:
			target_pydeps = {
				nrm_name(dependency['name']): None  ## pyright: ignore[reportAny]
				for dependency in uv_lock['manifest']['requirements']  # pyright: ignore[reportAny]
			}
		else:
			msg = '`uv.lock` has a `[manifest]`, but no `manifest.requirements`. Was it generated correctly?'
			raise RuntimeError(msg)

	# Projects: Find L1 Deps from the L0 Dep (the root package == the blext project)
	elif 'package' in uv_lock:
		# Find the Root Package
		root_package: dict[str, typ.Any] = next(
			package
			for package in uv_lock['package']  # pyright: ignore[reportAny]
			if 'name' in package and module_name == nrm_name(package['name'])  # pyright: ignore[reportAny]
		)

		# Parse Target Dependencies
		## - Always found in root_package['metadata']['requires-dist'].
		## - In `pyproject.toml`, they are given as 'project.dev-dependencies'.
		if (
			'metadata' in root_package
			and 'requires-dist' in root_package['metadata']
		):
			target_pydeps = {
				nrm_name(dependency['name']): PyDepMarker(  # pyright: ignore[reportAny]
					marker_str=dependency['marker']  # pyright: ignore[reportAny]
				)
				if 'marker' in dependency
				else None
				for dependency in root_package['metadata']['requires-dist']  # pyright: ignore[reportAny]
			}
			## NOTE: It's safe to ignore 'extra'. uv.lock's resolution already managed this.

	return cls(
		pydeps=frozendict(pydeps),
		target_pydeps=frozendict(target_pydeps),
		min_glibc_version=min_glibc_version,
		min_macos_version=min_macos_version,
		valid_python_tags=valid_python_tags,
		valid_abi_tags=valid_abi_tags,
	)

pydeps_by

pydeps_by(
	*,
	pkg_name: str,
	bl_version: BLVersion,
	bl_platform: BLPlatform,
	err_msgs: dict[BLPlatform, list[str]],
) -> frozendict[str, PyDep]

All Python dependencies needed by the given Python environment.

Source code in blext/pydeps/blext_deps.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
def pydeps_by(
	self,
	*,
	pkg_name: str,
	bl_version: extyp.BLVersion,
	bl_platform: extyp.BLPlatform,
	err_msgs: dict[extyp.BLPlatform, list[str]],
) -> frozendict[str, PyDep]:
	"""All Python dependencies needed by the given Python environment."""

	# Buckle up kids!
	## I confused myself repeatedly, so I wrote some comments to help myself.
	def filter_edge(
		node_upstream: tuple[str, str],
		node_downstream: tuple[str, str],
	) -> bool:
		"""Checks if each marker is `None`, or valid, on the edge between upstream/downstream node."""
		pydep_marker: PyDepMarker | None = self.pydeps_graph[node_upstream][  # pyright: ignore[reportUnknownMemberType]
			node_downstream
		]['marker']

		return pydep_marker is None or pydep_marker.is_valid_for(
			pkg_name=pkg_name,
			bl_version=bl_version,
			bl_platform=bl_platform,
		)

	if bl_platform in bl_version.valid_bl_platforms:
		# Deduce which pydep_name the user asked for.
		## **If** the user specified markers, then we check and respect them.
		## **Don't** include targets that are vendored by Blender.
		valid_pydep_target_names = {
			pydep_target_name
			for pydep_target_name, pydep_marker in self.target_pydeps.items()
			if (
				pydep_target_name not in bl_version.vendored_site_packages
				and (
					pydep_marker is None
					or pydep_marker.is_valid_for(
						pkg_name=pkg_name,
						bl_version=bl_version,
						bl_platform=bl_platform,
					)
				)
			)
		}
		## NOTE: This allow Blender-vendored pydeps to have only sdist.
		## - Blender does a lot of its own building of dependencies.
		## - This is sensible. Look what we have to do to play with pydeps.
		## - But, not all of Blender's site-package have wheels.
		## - In fact, not all of them are on PyPi at all.
		## - This makes it difficult to duplicate Blender Python environment.
		## - (Need it be so?)
		## - We deal with this by never letting them progress to "finding wheels"...
		## - ...since no wheels would be able to be found.
		## - This works. Kind of. Egh.
		##
		## Dear Blender Devs: I beg you to run a PyPi repo for your homebrew builds.
		## - That way we can all just pin your repo...
		## - ...and easily duplicate Blender's Python environment...
		## - ...without destructive `sys.path` flooding.
		## - How-To for Gitea: https://docs.gitea.com/1.18/packages/packages/pypi
		## - I love you all <3. And thank you for coming to my TED Talk.

		# Compute (pydep_name, pydep_version) from each target pydep_name.
		## There should be exactly one of these, otherwise something is very wrong.
		## That's why you're here, isn't it? *Sigh*.
		valid_pydep_targets: set[tuple[str, str]] = {
			next(
				iter(
					(pydep_target_name, pydep_version)
					for pydep_name, pydep_version in self.pydeps
					if pydep_target_name == pydep_name
				)
			)
			for pydep_target_name in valid_pydep_target_names
		}

		# Compute non-vendored ancestors of all target (pydep_name, pydep_version).
		## No need to refer to Knuth. Just `nx.ancestors` and `nx.subgraph_view`.
		valid_pydep_ancestors: set[tuple[str, str]] = {
			(pydep_ancestor_name, pydep_ancestor_version)
			for pydep_target_name, pydep_target_version in sorted(
				valid_pydep_targets
			)
			for pydep_ancestor_name, pydep_ancestor_version in nx.ancestors(  # pyright: ignore[reportUnknownMemberType, reportUnknownVariableType]
				nx.subgraph_view(
					self.pydeps_graph,  # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
					filter_edge=filter_edge,
				),
				(pydep_target_name, pydep_target_version),
			)
			if pydep_ancestor_name not in bl_version.vendored_site_packages
		}
		# Assemble a mapping from name to PyDep, indexed by by name and version
		pydeps: frozendict[str, PyDep] = frozendict(
			{
				pydep_name: self.pydeps[(pydep_name, pydep_version)]
				for pydep_name, pydep_version in sorted(
					valid_pydep_targets | valid_pydep_ancestors,
				)
			}
		)

		# PyDeps that overlap with vendored site-packages must be identical.
		for pydep_name, pydep in pydeps.items():
			if (
				pydep_name in bl_version.vendored_site_packages
				and pydep.version != bl_version.vendored_site_packages[pydep_name]
			):
				err_msgs[bl_platform].extend(
					[
						f'**Conflict** [{bl_platform}]: Requested version of **{pydep_name}** conflicts with vendored `site-packages` of Blender `{bl_version.version}`.',
						f'> **Provided by Blender `{bl_version.version}`**: `{pydep_name}=={bl_version.vendored_site_packages[pydep_name]}`',
						'>',
						f'> **Requested**: `{pydep_name}=={pydep.version}`',
						'',
					]
				)

		# After a long journey, you've found a return statement.
		## But will you ever be the same?
		## The elves have offered to let you sail West with them.
		## For now, have a cookie.
		return frozendict(
			{
				pydep_name: pydep
				for pydep_name, pydep in pydeps.items()
				if pydep_name not in bl_version.vendored_site_packages
			}
		)

	msg = f'The given `bl_platform` in `{bl_platform}` is not supported by the given Blender version (`{bl_version.version}`).'
	raise ValueError(msg)

wheels_by

wheels_by(
	*,
	pkg_name: str,
	bl_version: BLVersion,
	bl_platforms: frozenset[BLPlatform],
) -> frozenset[PyDepWheel]

All wheels needed for a Blender extension.

Notes

Computed from self.validated_graph.

Source code in blext/pydeps/blext_deps.py
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
def wheels_by(
	self,
	*,
	pkg_name: str,
	bl_version: extyp.BLVersion,
	bl_platforms: frozenset[extyp.BLPlatform],
) -> frozenset[PyDepWheel]:
	"""All wheels needed for a Blender extension.

	Notes:
		Computed from `self.validated_graph`.
	"""
	# I know it looks like a monster, but give it a chance, eh?
	## Even monsters deserve love. [1]
	## [1]: Shrek (2001)
	err_msgs: dict[extyp.BLPlatform, list[str]] = {
		bl_platform: [] for bl_platform in sorted(bl_platforms)
	}
	wheels = {
		bl_platform: {
			pydep: pydep.select_wheel(
				bl_platform=bl_platform,
				min_glibc_version=(
					bl_version.min_glibc_version
					if self.min_glibc_version is None
					else self.min_glibc_version
				),
				min_macos_version=(
					bl_version.min_macos_version
					if self.min_macos_version is None
					else self.min_macos_version
				),
				valid_python_tags=(
					bl_version.valid_python_tags
					if self.valid_python_tags is None
					else self.valid_python_tags
				),
				valid_abi_tags=(
					bl_version.valid_abi_tags
					if self.valid_abi_tags is None
					else self.valid_abi_tags
				),
				target_descendants=frozenset(
					{  # pyright: ignore[reportUnknownArgumentType]
						node
						for node in nx.descendants(  # pyright: ignore[reportUnknownVariableType, reportUnknownMemberType]
							self.pydeps_graph,  # pyright: ignore[reportUnknownArgumentType, reportUnknownMemberType]
							(pydep.name, pydep.version),
						)
						if self.pydeps_graph.out_degree(node) == 0  # pyright: ignore[reportUnknownMemberType, reportUnknownArgumentType]
					}
				),
				err_msgs=err_msgs,
			)
			for pydep in self.pydeps_by(
				pkg_name=pkg_name,
				bl_version=bl_version,
				bl_platform=bl_platform,
				err_msgs=err_msgs,
			).values()
		}
		for bl_platform in sorted(bl_platforms)
	}

	# Return the monster if every pydep/platform has a wheel.
	## Otherwise, say what's missing, why, and what can be done about it.
	num_missing_wheels = sum(
		1 if wheel is None else 0
		for _, pydeps_wheels in wheels.items()
		for _, wheel in pydeps_wheels.items()
	)
	if (
		not any(err_msgs_by_platform for err_msgs_by_platform in err_msgs.values())
		and num_missing_wheels == 0
	):
		return frozenset(
			wheel
			for _, pydeps_wheels in wheels.items()
			for _, wheel in pydeps_wheels.items()
			if wheel is not None
		)

	raise ValueError(
		*[
			err_msg
			for bl_platform_err_msgs in err_msgs.values()
			for err_msg in bl_platform_err_msgs
		],
		f'**Missing Wheels** for `{bl_version.version}`: {num_missing_wheels}',
	)

blext.pydeps.download

Tools for managing wheel-based dependencies.

download_wheel

download_wheel(
	wheel_url: str,
	wheel_path: Path,
	*,
	wheel: PyDepWheel,
	cb_update_wheel_download: Callable[
		[PyDepWheel, Path, int], list[None] | None
	] = lambda *_: None,
	cb_finish_wheel_download: Callable[
		[PyDepWheel, Path], list[None] | None
	] = lambda *_: None,
) -> None

Download a Python wheel.

Notes

This function is designed to be run in a background thread.

PARAMETER DESCRIPTION
wheel_url

URL to download the wheel from.

TYPE: str

wheel_path

Path to download the wheel to.

TYPE: Path

wheel

The wheel spec to be downloaded.

TYPE: PyDepWheel

cb_update_wheel_download

Callback to trigger whenever more data has been downloaded.

TYPE: Callable[[PyDepWheel, Path, int], list[None] | None] DEFAULT: lambda *_: None

cb_finish_wheel_download

Callback to trigger whenever a wheel has finished downloading.

TYPE: Callable[[PyDepWheel, Path], list[None] | None] DEFAULT: lambda *_: None

Source code in blext/pydeps/download.py
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
def download_wheel(
	wheel_url: str,
	wheel_path: Path,
	*,
	wheel: PyDepWheel,
	cb_update_wheel_download: typ.Callable[
		[PyDepWheel, Path, int], list[None] | None
	] = lambda *_: None,  # pyright: ignore[reportUnknownLambdaType]
	cb_finish_wheel_download: typ.Callable[
		[PyDepWheel, Path], list[None] | None
	] = lambda *_: None,  # pyright: ignore[reportUnknownLambdaType]
) -> None:
	"""Download a Python wheel.

	Notes:
		This function is designed to be run in a background thread.

	Parameters:
		wheel_url: URL to download the wheel from.
		wheel_path: Path to download the wheel to.
		wheel: The wheel spec to be downloaded.
		cb_update_wheel_download: Callback to trigger whenever more data has been downloaded.
		cb_finish_wheel_download: Callback to trigger whenever a wheel has finished downloading.
	"""
	with (
		urllib.request.urlopen(wheel_url, timeout=10) as www_wheel,  # pyright: ignore[reportAny]
		wheel_path.open('wb') as f_wheel,
	):
		raw_data_iterator: collections.abc.Iterator[bytes] = iter(
			functools.partial(
				www_wheel.read,  # pyright: ignore[reportAny]
				DOWNLOAD_CHUNK_BYTES,
			),
			b'',
		)
		for raw_data in raw_data_iterator:
			if SIGNAL_ABORT:
				wheel_path.unlink()
				return

			_ = f_wheel.write(raw_data)
			_ = cb_update_wheel_download(wheel, wheel_path, len(raw_data))

	if not wheel.is_download_valid(wheel_path):
		wheel_path.unlink()
		msg = f'Hash of downloaded wheel at path {wheel_path} did not match expected hash: {wheel.hash}'
		raise ValueError(msg)

	_ = cb_finish_wheel_download(wheel, wheel_path)

download_wheels

download_wheels(
	wheels: frozenset[PyDepWheel],
	*,
	path_wheels: Path,
	cb_start_wheel_download: Callable[
		[PyDepWheel, Path], Any
	] = lambda *_: None,
	cb_update_wheel_download: Callable[
		[PyDepWheel, Path, int], Any
	] = lambda *_: None,
	cb_finish_wheel_download: Callable[
		[PyDepWheel, Path], Any
	] = lambda *_: None,
) -> None

Download universal and binary wheels for all platforms defined in pyproject.toml.

Each blender-supported platform requires specifying a valid list of PyPi platform constraints. These will be used as an allow-list when deciding which binary wheels may be selected for ex. 'mac'.

It is recommended to start with the most compatible platform tags, then work one's way up to the newest. Depending on how old the compatibility should stretch, one may have to omit / manually compile some wheels.

There is no exhaustive list of valid platform tags - though this should get you started: - https://stackoverflow.com/questions/49672621/what-are-the-valid-values-for-platform-abi-and-implementation-for-pip-do - Examine https://pypi.org/project/pillow/#files for some widely-supported tags.

PARAMETER DESCRIPTION
wheels

Wheels to download.

TYPE: frozenset[PyDepWheel]

path_wheels

Folder within which to download wheels to.

TYPE: Path

cb_start_wheel_download

Callback to trigger when starting a wheel download.

TYPE: Callable[[PyDepWheel, Path], Any] DEFAULT: lambda *_: None

cb_update_wheel_download

Callback to trigger when a wheel download should update.

TYPE: Callable[[PyDepWheel, Path, int], Any] DEFAULT: lambda *_: None

cb_finish_wheel_download

Callback to trigger when a wheel download has finished.

TYPE: Callable[[PyDepWheel, Path], Any] DEFAULT: lambda *_: None

Source code in blext/pydeps/download.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def download_wheels(
	wheels: frozenset[PyDepWheel],
	*,
	path_wheels: Path,
	cb_start_wheel_download: typ.Callable[
		[PyDepWheel, Path], typ.Any
	] = lambda *_: None,  # pyright: ignore[reportUnknownLambdaType]
	cb_update_wheel_download: typ.Callable[
		[PyDepWheel, Path, int], typ.Any
	] = lambda *_: None,  # pyright: ignore[reportUnknownLambdaType]
	cb_finish_wheel_download: typ.Callable[
		[PyDepWheel, Path], typ.Any
	] = lambda *_: None,  # pyright: ignore[reportUnknownLambdaType]
) -> None:
	"""Download universal and binary wheels for all platforms defined in `pyproject.toml`.

	Each blender-supported platform requires specifying a valid list of PyPi platform constraints.
	These will be used as an allow-list when deciding which binary wheels may be selected for ex. 'mac'.

	It is recommended to start with the most compatible platform tags, then work one's way up to the newest.
	Depending on how old the compatibility should stretch, one may have to omit / manually compile some wheels.

	There is no exhaustive list of valid platform tags - though this should get you started:
	- https://stackoverflow.com/questions/49672621/what-are-the-valid-values-for-platform-abi-and-implementation-for-pip-do
	- Examine https://pypi.org/project/pillow/#files for some widely-supported tags.

	Parameters:
		wheels: Wheels to download.
		path_wheels: Folder within which to download wheels to.
		cb_start_wheel_download: Callback to trigger when starting a wheel download.
		cb_update_wheel_download: Callback to trigger when a wheel download should update.
		cb_finish_wheel_download: Callback to trigger when a wheel download has finished.
	"""
	global SIGNAL_ABORT  # noqa: PLW0603

	path_wheels = path_wheels.resolve()
	wheel_paths_current = frozenset(
		{path_wheel.resolve() for path_wheel in path_wheels.rglob('*.whl')}
	)

	# Compute PyDepWheels to Download
	## - Missing: Will be downloaded.
	## - Superfluous: Will be deleted.
	wheels_to_download = {
		path_wheels / wheel.filename: wheel
		for wheel in wheels
		if path_wheels / wheel.filename not in wheel_paths_current
	}

	# Download Missing PyDepWheels
	if wheels_to_download:
		with concurrent.futures.ThreadPoolExecutor(
			max_workers=DOWNLOAD_THREADS
		) as pool:
			futures: set[concurrent.futures.Future[None]] = set()
			for path_wheel, wheel in sorted(
				wheels_to_download.items(),
				key=lambda el: el[1].filename,
			):
				cb_start_wheel_download(wheel, path_wheel)
				futures.add(
					pool.submit(
						download_wheel,
						str(wheel.url),
						path_wheel,
						wheel=wheel,
						cb_update_wheel_download=cb_update_wheel_download,
						cb_finish_wheel_download=cb_finish_wheel_download,
					)
				)

			try:
				for future in concurrent.futures.as_completed(futures):
					try:
						future.result()
					except (
						urllib.error.URLError,
						urllib.error.HTTPError,
						urllib.error.ContentTooShortError,
					) as ex:
						SIGNAL_ABORT = True  # pyright: ignore[reportConstantRedefinition]
						pool.shutdown(wait=True, cancel_futures=True)

						SIGNAL_ABORT = False  # pyright: ignore[reportConstantRedefinition]
						msg = (
							'A wheel download aborted with the following message: {ex}'
						)
						raise ValueError(msg) from ex

			except KeyboardInterrupt:
				SIGNAL_ABORT = True  # pyright: ignore[reportConstantRedefinition]
				pool.shutdown(wait=True, cancel_futures=True)

				SIGNAL_ABORT = False  # pyright: ignore[reportConstantRedefinition]
				sys.exit(1)

blext.pydeps.pydep

Implements PyDep and `PyDepMarker.

PyDep

Bases: BaseModel

A Python dependency.

select_wheel

select_wheel(
	*,
	bl_platform: BLPlatform,
	min_glibc_version: tuple[int, int],
	min_macos_version: tuple[int, int],
	valid_python_tags: frozenset[str],
	valid_abi_tags: frozenset[str],
	err_msgs: dict[BLPlatform, list[str]],
	target_descendants: frozenset[str],
) -> PyDepWheel | None

Select the best wheel to satisfy this dependency.

Source code in blext/pydeps/pydep.py
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def select_wheel(  # noqa: PLR0913
	self,
	*,
	bl_platform: extyp.BLPlatform,
	min_glibc_version: tuple[int, int],
	min_macos_version: tuple[int, int],
	valid_python_tags: frozenset[str],
	valid_abi_tags: frozenset[str],
	err_msgs: dict[extyp.BLPlatform, list[str]],
	target_descendants: frozenset[str],
) -> PyDepWheel | None:
	"""Select the best wheel to satisfy this dependency."""
	####################
	# - Step 0: Find All Valid Wheels
	####################
	## - Any wheel in this list should work just fine.
	semivalid_wheels = [
		wheel
		for wheel in self.wheels
		# The wheel must work in an environment that supports the given python tags.
		if wheel.works_with_python_tags(valid_python_tags)
		# The wheel must work in an environment that supports the given ABI tags.
		and wheel.works_with_abi_tags(valid_abi_tags)
		and wheel.works_with_platform(
			bl_platform=bl_platform,
			min_glibc_version=None,
			min_macos_version=None,
		)
	]
	valid_wheels = [
		wheel
		for wheel in semivalid_wheels
		# The wheel must work in an environment provided by a given Blender platform.
		if wheel.works_with_platform(
			bl_platform=bl_platform,
			min_glibc_version=min_glibc_version,
			min_macos_version=min_macos_version,
		)
	]

	####################
	# - Step 1: Select "Best" Wheel
	####################
	## - Prefer some wheels over others in a platform-specific manner.
	## - For instance, one might want to prefer higher `glibc` versions when available.
	match bl_platform:
		case extyp.BLPlatform.linux_x64 | extyp.BLPlatform.linux_arm64:
			if valid_wheels:
				return sorted(
					valid_wheels,
					key=lambda wheel: wheel.sort_key_preferred_linux,
				)[0]

			osver_str = 'glibc'
			min_osver_str = '.'.join(str(i) for i in min_glibc_version)
			semivalid_wheel_osver_strs = {
				semivalid_wheel: ', '.join(
					'.'.join(str(i) for i in glibc_version)
					for glibc_version in semivalid_wheel.glibc_versions.values()
					if glibc_version is not None
				)
				for semivalid_wheel in semivalid_wheels
			}

		case extyp.BLPlatform.macos_x64 | extyp.BLPlatform.macos_arm64:
			if valid_wheels:
				return sorted(
					valid_wheels,
					key=lambda wheel: wheel.sort_key_preferred_mac,
				)[0]

			osver_str = 'macos'
			min_osver_str = '.'.join(str(i) for i in min_macos_version)
			semivalid_wheel_osver_strs = {
				semivalid_wheel: ', '.join(
					'.'.join(str(i) for i in macos_version)
					for macos_version in semivalid_wheel.macos_versions.values()
					if macos_version is not None
				)
				for semivalid_wheel in semivalid_wheels
			}

		case extyp.BLPlatform.windows_x64 | extyp.BLPlatform.windows_arm64:
			if valid_wheels:
				return sorted(
					valid_wheels,
					key=lambda wheel: wheel.sort_key_preferred_windows,
				)[0]

			# osver_str = ''
			# min_osver_str = ''
			# semivalid_wheel_osver_strs = {}

	err_msgs[bl_platform].extend(
		[
			f'**{self.name}** not found for `{bl_platform}`.',
			f'> **Extension Supports**: `{osver_str} >= {min_osver_str}`'  # pyright: ignore[reportPossiblyUnboundVariable]
			if not bl_platform.is_windows
			else '>',
			'>',
			'> ----' if not bl_platform.is_windows else '>',
			'> **Rejected Wheels**:'
			if semivalid_wheels
			else '> **Rejected Wheels**: No candidates were found.',
			*(
				[
					f'> - {semivalid_wheel.filename}: `{osver_str} >= {semivalid_wheel_osver_strs[semivalid_wheel]}`'  # pyright: ignore[reportPossiblyUnboundVariable]
					for semivalid_wheel in semivalid_wheels
				]
				if not bl_platform.is_windows
				else []
			),
			'>',
			'> ----',
			'> **Remedies**:',
			f'> 1. **Remove** `{bl_platform}` from `tool.blext.supported_platforms`.',
			f'> 2. **Remove** `{"==".join(next(iter(target_descendants)))}` from `project.dependencies`.'
			if len(target_descendants) == 1
			else f'> 2. **Remove** `{self.name}=={self.version}` from `project.dependencies`.',
			f'> 3. **Raise** `{osver_str}` version in `tool.blext.min_{osver_str}_version`.'  # pyright: ignore[reportPossiblyUnboundVariable]
			if semivalid_wheels and not bl_platform.is_windows
			else '>',
			'',
		]
	)
	return None

blext.pydeps.pydep_marker

Implements PyDep and `PyDepMarker.

PyDepMarker

Bases: BaseModel

A platform-specific criteria for installing a particular wheel.

marker cached property

marker: Marker

Parsed marker whose evaluate() method checks it against a Python environment.

is_valid_for

is_valid_for(
	*,
	pkg_name: str,
	bl_version: BLVersion,
	bl_platform: BLPlatform,
) -> bool

Whether this marker will evaluate True under the targeted Python environment.

Notes

pkg_name is included, since the way that uv encodes conflicts between extras is to add the package name, like so: - extra-11-simple-proj-bl-4-3 - extra-11-simple-proj-bl-4-4 - extra-{len(pkg_name)}-{pkg_name}-{pymarker_extra}

Presumably, this prevents name-conflicts.

PARAMETER DESCRIPTION
pkg_name

The name of the root package, defined with standard _. - Used for the package-encoded version of the extras.

TYPE: str

bl_version

The Blender version to check validity for.

TYPE: BLVersion

bl_platform

The Blender platform to check validity for.

TYPE: BLPlatform

See Also

extyp.BLVersion.pymarker_encoded_package_extra: For more information about how and why uv-generated extras require pkg_name to be known.

Source code in blext/pydeps/pydep_marker.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
def is_valid_for(
	self,
	*,
	pkg_name: str,
	bl_version: extyp.BLVersion,
	bl_platform: extyp.BLPlatform,
) -> bool:
	"""Whether this marker will evaluate `True` under the targeted Python environment.

	Notes:
		`pkg_name` is included, since the way that `uv` encodes conflicts between `extra`s is to add the package name, like so:
		- `extra-11-simple-proj-bl-4-3`
		- `extra-11-simple-proj-bl-4-4`
		- `extra-{len(pkg_name)}-{pkg_name}-{pymarker_extra}`

		Presumably, this prevents name-conflicts.

	Parameters:
		pkg_name: The name of the root package, defined with standard `_`.
			- Used for the package-encoded version of the extras.
		bl_version: The Blender version to check validity for.
		bl_platform: The Blender platform to check validity for.

	See Also:
		`extyp.BLVersion.pymarker_encoded_package_extra`: For more information about how and why `uv`-generated `extra`s require `pkg_name` to be known.

	"""
	return any(
		self.marker.evaluate(environment=pymarker_environment)
		for pymarker_environment in [
			# dict() each 'environment' by-platform.
			## - 'pymarker_environment' encodes the 'extra' corresponding to the Blender version.
			## - For example: {'extra': 'bl4-2'}
			## - By specifying 'pkg_name', 'pymarker_environment' also has 'extra' by-package.
			## - For example: {'extra': 'extra-11-simple-proj-bl4-2'}
			dict(pymarker_environment)
			for pymarker_environment in bl_version.pymarker_environments(
				pkg_name=pkg_name
			)[bl_platform]
		]
	)

blext.pydeps.pydep_wheel

Defines an abstraction for a Blender extension wheel.

PyDepWheel

Bases: BaseModel

Representation of a Python dependency.

abi_tags cached property

abi_tags: frozenset[str]

The ABI tags of the wheel.

build cached property

build: str | None

The build-tag of the project represented by the wheel, if any.

filename cached property

filename: str

Parse the filename of the wheel file.

glibc_versions cached property

glibc_versions: frozendict[str, tuple[int, int] | None]

Minimum GLIBC versions supported by this wheel.

Notes
  • The smallest available GLIBC version indicates the minimum GLIBC support for this wheel.
  • Non-manylinux platform tags will always map to None.

macos_versions cached property

macos_versions: frozendict[str, tuple[int, int] | None]

Minimum MacOS versions supported by this wheel.

Notes
  • The smallest available MacOS version indicates the minimum GLIBC support for this wheel.
  • Non-macosx platform tags will always map to None.

platform_tags cached property

platform_tags: frozenset[str]

The platform tags of the wheel.

Notes

Legacy manylinux tags (such as 2014) are normalized to their explicit PEP600 equivalents (ex. 2014 -> 2_17).

This is done to avoid irregularities in how glibc versions are parsed for manylinux wheels in later methods.

See Also
  • PEP600: https://peps.python.org/pep-0600/

pretty_bl_platforms cached property

pretty_bl_platforms: str

Retrieve prettified, unfiltered bl_platforms for this wheel.

project cached property

project: str

The name of the project represented by the wheel.

Name is normalized to use '_' instead of '-'.

python_tags cached property

python_tags: frozenset[str]

The Python tags of the wheel.

sort_key_preferred_linux cached property

sort_key_preferred_linux: int

Priority to assign to this wheel when selecting one of several Linux wheels.

Notes

Higher values will be chosen over lower values. The value should be deterministic from the platform tags.

sort_key_preferred_mac cached property

sort_key_preferred_mac: int

Priority to assign to this wheel when selecting one of several MacOS wheels.

Notes

Higher values will be chosen over lower values. The value should be deterministic from the platform tags.

sort_key_preferred_windows cached property

sort_key_preferred_windows: int

Priority to assign to this wheel when selecting one of several Windows wheels.

Notes

Higher values will be chosen over lower values. The value should be deterministic from the platform tags.

sort_key_size cached property

sort_key_size: int

Priority to assign to this wheel sorting by size.

Notes
  • Higher values will sort later in the list.
  • When self.size is None, then the "size" will be set to 0.

version cached property

version: str

The version of the project represented by the wheel.

windows_versions cached property

windows_versions: frozendict[str, Literal['win32'] | None]

Windows ABI versions supported by this wheel.

Notes
  • In terms of ABI, there is only one on Windows: win32.
  • Non-win platform tags will always map to None.

is_download_valid

is_download_valid(wheel_path: Path) -> bool

Check whether a downloaded file is, in fact, this wheel.

Notes

Implemented by comparing the file hash to the expected hash.

RAISES DESCRIPTION
ValueError

If the hash digest of wheel_path does not match self.hash.

Source code in blext/pydeps/pydep_wheel.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
def is_download_valid(self, wheel_path: Path) -> bool:
	"""Check whether a downloaded file is, in fact, this wheel.

	Notes:
		Implemented by comparing the file hash to the expected hash.

	Raises:
		ValueError: If the hash digest of `wheel_path` does not match `self.hash`.
	"""
	if wheel_path.is_file():
		with wheel_path.open('rb', buffering=0) as f:
			file_digest = hashlib.file_digest(f, 'sha256').hexdigest()

		return 'sha256:' + file_digest == self.hash
	return False

works_with_abi_tags

works_with_abi_tags(valid_abi_tags: frozenset[str]) -> bool

Does this wheel work with a runtime that supports abi_tags?

Notes
  • It is very strongly recommended to always pass "none" as one of the abi_tags, since this is the ABI tag of pure-Python wheels.
  • This method doesn't guarantee directly that the wheel will run. It only guarantees that there is no mismatch in Python ABI tags between what the environment supports, and what the wheel supports.
PARAMETER DESCRIPTION
valid_abi_tags

List of ABI tags supported by a runtime environment.

TYPE: frozenset[str]

RETURNS DESCRIPTION
bool

Whether the Python tags of the environment, and the wheel, are compatible.

Source code in blext/pydeps/pydep_wheel.py
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
def works_with_abi_tags(self, valid_abi_tags: frozenset[str]) -> bool:
	"""Does this wheel work with a runtime that supports `abi_tags`?

	Notes:
		- It is very strongly recommended to always pass `"none"` as one of the `abi_tags`,
			since this is the ABI tag of pure-Python wheels.
		- This method doesn't guarantee directly that the wheel will run.
			It only guarantees that there is no mismatch in Python ABI tags between
			what the environment supports, and what the wheel supports.

	Parameters:
		valid_abi_tags: List of ABI tags supported by a runtime environment.

	Returns:
		Whether the Python tags of the environment, and the wheel, are compatible.
	"""
	return len(valid_abi_tags & self.abi_tags) > 0

works_with_platform

works_with_platform(
	*,
	bl_platform: BLPlatform,
	min_glibc_version: tuple[int, int] | None,
	min_macos_version: tuple[int, int] | None,
) -> bool

Whether this wheel ought to run on the given platform.

PARAMETER DESCRIPTION
bl_platform

The Blender platform that the wheel would be run on.

TYPE: BLPlatform

min_glibc_version

The minimum version of glibc available. - Note: Only relevant for Linux-based bl_platforms.

TYPE: tuple[int, int] | None

min_macos_version

The minimum version of macos available. - Note: Only relevant for MacOS-based bl_platforms.

TYPE: tuple[int, int] | None

Notes

extyp.BLPlatform only denotes a partial set of compatibility constraints, namely, particular OS / CPU architecture combinations.

It does not a sufficient set of compatibility constraints to be able to say, for instance, "this wheel will work with any Linux version of Blender".

A version of Blender versions comes with a Python runtime environment that imposes very important constraints such as: - Supported Python tags. - Supported ABI tags.

To deduce final wheel compatibility, both the BLPlatform and the information derived from the Blender version must be checked.

Source code in blext/pydeps/pydep_wheel.py
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
def works_with_platform(
	self,
	*,
	bl_platform: extyp.BLPlatform,
	min_glibc_version: tuple[int, int] | None,
	min_macos_version: tuple[int, int] | None,
) -> bool:
	"""Whether this wheel ought to run on the given platform.

	Parameters:
		bl_platform: The Blender platform that the wheel would be run on.
		min_glibc_version: The minimum version of `glibc` available.
			- **Note**: Only relevant for Linux-based `bl_platform`s.
		min_macos_version: The minimum version of `macos` available.
			- **Note**: Only relevant for MacOS-based `bl_platform`s.

	Notes:
		`extyp.BLPlatform` only denotes a _partial_ set of compatibility constraints,
		namely, particular OS / CPU architecture combinations.

		It does **not** a sufficient set of compatibility constraints to be able to say,
		for instance, "this wheel will work with any Linux version of Blender".

		A version of Blender versions comes with a Python runtime environment that imposes
		very important constraints such as:
		- Supported Python tags.
		- Supported ABI tags.

		To deduce final wheel compatibility, _both_ the BLPlatform _and_ the information derived
		from the Blender version must be checked.
	"""
	####################
	# - Step 0: Check 'any'
	####################
	## - 'any' denotes a pure-python wheel, which works on all platforms (by definition)
	if 'any' in self.platform_tags:
		return True

	####################
	# - Step 1: Check Architecture and OS
	####################
	## - This information is contained in the prefix and suffix of each platform tag.

	# At least one platform tag must end with one of bl_platform's valid architectures.
	arch_matches = any(
		platform_tag.endswith(pypi_arch)
		for platform_tag in self.platform_tags
		for pypi_arch in bl_platform.pypi_arches
	)

	# At least one platform tag must start with one of bl_platform's valid wheel prefixes.
	os_matches = any(
		platform_tag.startswith(bl_platform.wheel_platform_tag_prefix)
		for platform_tag in self.platform_tags
	)

	if not (arch_matches and os_matches):
		return False

	####################
	# - Step 2: Check OS Version
	####################
	## - The minimum `glibc` / `macos` versions must be checked, if applicable.

	match bl_platform:
		case extyp.BLPlatform.linux_x64 | extyp.BLPlatform.linux_arm64:
			return (
				True
				if min_glibc_version is None
				else any(
					glibc_version[0] < min_glibc_version[0]
					or (
						glibc_version[0] == min_glibc_version[0]
						and glibc_version[1] <= min_glibc_version[1]
					)
					for glibc_version in self.glibc_versions.values()
					if glibc_version is not None
				)
			)

		case extyp.BLPlatform.macos_x64 | extyp.BLPlatform.macos_arm64:
			return (
				True
				if min_macos_version is None
				else any(
					macos_version[0] < min_macos_version[0]
					or (
						macos_version[0] == min_macos_version[0]
						and macos_version[1] <= min_macos_version[1]
					)
					for macos_version in self.macos_versions.values()
					if macos_version is not None
				)
			)

		case extyp.BLPlatform.windows_x64 | extyp.BLPlatform.windows_arm64:
			return True

works_with_python_tags

works_with_python_tags(
	valid_python_tags: frozenset[str],
) -> bool

Does this wheel work with a runtime that supports python_tags?

Notes

This method doesn't guarantee directly that the wheel will run. It only guarantees that there is no mismatch in Python tags between what the environment supports, and what the wheel supports.

PARAMETER DESCRIPTION
valid_python_tags

List of Python tags supported by a runtime environment.

TYPE: frozenset[str]

RETURNS DESCRIPTION
bool

Whether the Python tags of the environment, and the wheel, are compatible.

Source code in blext/pydeps/pydep_wheel.py
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
def works_with_python_tags(self, valid_python_tags: frozenset[str]) -> bool:
	"""Does this wheel work with a runtime that supports `python_tags`?

	Notes:
		This method doesn't guarantee directly that the wheel will run.
		It only guarantees that there is no mismatch in Python tags between
		what the environment supports, and what the wheel supports.

	Parameters:
		valid_python_tags: List of Python tags supported by a runtime environment.

	Returns:
		Whether the Python tags of the environment, and the wheel, are compatible.
	"""
	return len(valid_python_tags & self.python_tags) > 0

blext.pydeps.uv

Tools for managing wheel-based dependencies.

parse_requirements_txt

parse_requirements_txt(
	path_uv_lock: Path,
	*,
	path_uv_exe: Path | None = None,
	include_hashes: bool = False,
	include_dev: bool = False,
	include_editable: bool = False,
	include_comment_header: bool = False,
) -> tuple[str, ...]

Get Python dependencies of a project as lines of a requirements.txt file.

Notes
  • Runs uv export with various options, under the hood.
  • Always runs with --locked, to ensure that uv.lock is unaltered by this function.
PARAMETER DESCRIPTION
path_uv_lock

Where to generate the uv.lock file.

TYPE: Path

path_uv_exe

Path to the uv executable.

TYPE: Path | None DEFAULT: None

include_hashes

Include specific allowed wheel hashes in the generated requirements.txt.

TYPE: bool DEFAULT: False

include_dev

Include dependencies marked as "development only".

  • In the context of extensions, development dependencies should not be included in the extension.

TYPE: bool DEFAULT: False

include_editable

Include "editable" dependencies, for instance from local filesystem paths.

TYPE: bool DEFAULT: False

include_comment_header

Include a comment describing how uv generated the file.

TYPE: bool DEFAULT: False

Source code in blext/pydeps/uv.py
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
def parse_requirements_txt(
	path_uv_lock: Path,
	*,
	path_uv_exe: Path | None = None,
	include_hashes: bool = False,
	include_dev: bool = False,
	include_editable: bool = False,
	include_comment_header: bool = False,
) -> tuple[str, ...]:
	"""Get Python dependencies of a project as lines of a `requirements.txt` file.

	Notes:
		- Runs `uv export` with various options, under the hood.
		- Always runs with `--locked`, to ensure that `uv.lock` is unaltered by this function.

	Parameters:
		path_uv_lock: Where to generate the `uv.lock` file.
		path_uv_exe: Path to the `uv` executable.
		include_hashes: Include specific allowed wheel hashes in the generated `requirements.txt`.
		include_dev: Include dependencies marked as "development only".

			- In the context of extensions, development dependencies **should not** be included in the extension.
		include_editable: Include "editable" dependencies, for instance from local filesystem paths.
		include_comment_header: Include a comment describing how `uv` generated the file.
	"""
	# Find uv Executable
	path_uv_lock = path_uv_lock.resolve()

	# Lock UV
	uv_export_cmd_args = [
		*(['--no-dev'] if not include_dev else []),
		*(['--no-hashes'] if not include_hashes else []),
		*(['--no-editable'] if not include_editable else []),
		*(['--no-header'] if not include_comment_header else []),
		'--locked',
	]
	if path_uv_lock.name == 'uv.lock':
		with contextlib.chdir(path_uv_lock.parent):
			result = subprocess.run(
				[
					str(path_uv_exe),
					'export',
					*uv_export_cmd_args,
				],
				check=True,
				text=True,
				stdout=subprocess.PIPE,
				stderr=subprocess.DEVNULL,
			)
	elif path_uv_lock.name.endswith('.py.lock'):
		result = subprocess.run(
			[
				str(path_uv_exe),
				'export',
				*uv_export_cmd_args,
				'--script',
				str(path_uv_lock.parent / path_uv_lock.name.removesuffix('.lock')),
			],
			check=True,
			text=True,
			stdout=subprocess.PIPE,
			stderr=subprocess.DEVNULL,
		)
	else:
		msg = f"Couldn't find `uv.lock` file at {path_uv_lock}."
		raise ValueError(msg)

	return tuple(result.stdout.split('\n'))

parse_uv_lock

parse_uv_lock(
	path_uv_lock: Path,
	*,
	path_uv_exe: Path,
	force_update: bool = True,
) -> frozendict[str, Any]

Parse a uv.lock file.

Notes

A uv.lock file contains the platform-independent dependency resolution for a Python project managed with uv. By working directly with uv's lockfiles, accessing data such as size, hash, and download URLs for wheels may be done in a lightweight manner, ex. without the need for a venv.

PARAMETER DESCRIPTION
path_uv_lock

Path to the uv lockfile. If it doesn't exist, then it will be generated

TYPE: Path

RETURNS DESCRIPTION
frozendict[str, Any]

The dictionary parsed from uv.lock.

Source code in blext/pydeps/uv.py
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
def parse_uv_lock(
	path_uv_lock: Path,
	*,
	path_uv_exe: Path,
	force_update: bool = True,
) -> frozendict[str, typ.Any]:
	"""Parse a `uv.lock` file.

	Notes:
		A `uv.lock` file contains the platform-independent dependency resolution for a Python project managed with `uv`.
		By working directly with `uv`'s lockfiles, accessing data such as size, hash, and download URLs for wheels may be done in a lightweight manner, ex. without the need for a `venv`.

	Parameters:
		path_uv_lock: Path to the `uv` lockfile.
			If it doesn't exist, then it will be generated

	Returns:
		The dictionary parsed from `uv.lock`.
	"""
	if path_uv_lock.name == 'uv.lock' or path_uv_lock.name.endswith('.py.lock'):
		# Generate/Update the Lockfile
		## - By default, the lockfile is only generated if it doesn't (yet) exist.
		## - Otherwise, setting it requires a forced update.
		if force_update or not path_uv_lock.is_file():
			update_uv_lock(
				path_uv_lock,
				path_uv_exe=path_uv_exe,
			)

		# Parse the Lockfile
		if path_uv_lock.is_file():
			with path_uv_lock.open('rb') as f:
				return deepfreeze(tomllib.load(f))  # pyright: ignore[reportAny]

		msg = f'Generating `{path_uv_lock}` failed, likely because it is not located within a valid `uv` project.'
		raise ValueError(msg)

	msg = f'The path `{path_uv_lock}` MUST refer to a file named `uv.lock`.'
	raise ValueError(msg)

update_uv_lock

update_uv_lock(
	path_uv_lock: Path, *, path_uv_exe: Path
) -> None

Run uv lock within a uv project, which generates / update the lockfile uv.lock.

PARAMETER DESCRIPTION
path_uv_lock

Where to generate the uv.lock file.

TYPE: Path

path_uv_exe

Path to the uv executable.

TYPE: Path

Source code in blext/pydeps/uv.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def update_uv_lock(
	path_uv_lock: Path,
	*,
	path_uv_exe: Path,
) -> None:
	"""Run `uv lock` within a `uv` project, which generates / update the lockfile `uv.lock`.

	Parameters:
		path_uv_lock: Where to generate the `uv.lock` file.
		path_uv_exe: Path to the `uv` executable.
	"""
	path_uv_lock = path_uv_lock.resolve()
	if path_uv_lock.name == 'uv.lock':
		with contextlib.chdir(path_uv_lock.parent):
			_ = subprocess.run(
				[str(path_uv_exe), 'lock'],
				check=True,
				stdout=subprocess.DEVNULL,
				stderr=subprocess.DEVNULL,
			)
	elif path_uv_lock.name.endswith('.py.lock'):
		_ = subprocess.run(
			[
				str(path_uv_exe),
				'lock',
				'--script',
				str(path_uv_lock.parent / path_uv_lock.name.removesuffix('.lock')),
			],
			check=True,
			stdout=subprocess.DEVNULL,
			stderr=subprocess.DEVNULL,
		)
	else:
		msg = f"Couldn't find `uv.lock` file at {path_uv_lock}."
		raise ValueError(msg)