antsibull-nox 0.3.0__py3-none-any.whl → 0.5.0__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.
antsibull_nox/__init__.py CHANGED
@@ -17,7 +17,7 @@ from .config import (
17
17
  from .interpret_config import interpret_config
18
18
  from .sessions.ansible_test import add_ansible_test_session
19
19
 
20
- __version__ = "0.3.0"
20
+ __version__ = "0.5.0"
21
21
 
22
22
 
23
23
  def load_antsibull_nox_toml() -> None:
antsibull_nox/ansible.py CHANGED
@@ -253,8 +253,23 @@ def get_supported_core_versions(
253
253
  return result
254
254
 
255
255
 
256
+ def parse_ansible_core_version(
257
+ version: str | AnsibleCoreVersion,
258
+ ) -> AnsibleCoreVersion:
259
+ """
260
+ Coerce a string or a AnsibleCoreVersion to a AnsibleCoreVersion.
261
+ """
262
+ if version in ("devel", "milestone"):
263
+ # For some reason mypy doesn't notice that
264
+ return t.cast(AnsibleCoreVersion, version)
265
+ if isinstance(version, Version):
266
+ return version
267
+ return Version.parse(version)
268
+
269
+
256
270
  __all__ = [
257
271
  "AnsibleCoreInfo",
258
272
  "get_ansible_core_info",
259
273
  "get_ansible_core_package_name",
274
+ "parse_ansible_core_version",
260
275
  ]
@@ -10,6 +10,8 @@ Data types for collections.
10
10
 
11
11
  from __future__ import annotations
12
12
 
13
+ import base64
14
+ import hashlib
13
15
  from dataclasses import dataclass
14
16
  from pathlib import Path
15
17
 
@@ -98,6 +100,16 @@ class CollectionSource:
98
100
  )
99
101
  return source
100
102
 
103
+ def identifier(self) -> str:
104
+ """
105
+ Compute a source identifier.
106
+ """
107
+ hasher = hashlib.sha256()
108
+ hasher.update(self.name.encode("utf-8"))
109
+ hasher.update(b"::")
110
+ hasher.update(self.source.encode("utf-8"))
111
+ return base64.b32encode(hasher.digest())[:16].decode("ascii")
112
+
101
113
 
102
114
  __all__ = [
103
115
  "CollectionData",
@@ -20,6 +20,7 @@ from pathlib import Path
20
20
 
21
21
  from antsibull_fileutils.yaml import load_yaml_file
22
22
 
23
+ from ..ansible import AnsibleCoreVersion
23
24
  from ..paths import copy_collection as _paths_copy_collection
24
25
  from ..paths import remove_path as _remove
25
26
  from .data import CollectionData, CollectionSource, SetupResult
@@ -47,84 +48,170 @@ class _CollectionSources:
47
48
  """
48
49
  self.sources[name] = source
49
50
 
50
- def get_source(self, name: str) -> CollectionSource:
51
+ @t.overload
52
+ def get_source(
53
+ self, name: str, *, create_default: t.Literal[True] = True
54
+ ) -> CollectionSource: ...
55
+
56
+ @t.overload
57
+ def get_source(
58
+ self, name: str, *, create_default: t.Literal[False]
59
+ ) -> CollectionSource | None: ...
60
+
61
+ def get_source(
62
+ self, name: str, *, create_default: bool = True
63
+ ) -> CollectionSource | None:
51
64
  """
52
65
  Get source for collection.
53
66
  """
54
67
  source = self.sources.get(name)
55
- if source is None:
68
+ if source is None and create_default:
56
69
  source = CollectionSource(name, name)
57
70
  return source
58
71
 
59
72
 
73
+ class _CollectionDownloadCache:
74
+ @staticmethod
75
+ def _parse_galaxy_filename(file: Path) -> tuple[str, str, str]:
76
+ """
77
+ Split filename into (namespace, name, version) tuple.
78
+ """
79
+ if not file.name.endswith(_TARBALL_EXTENSION):
80
+ raise ValueError(
81
+ f"Filename {file.name!r} does not end with {_TARBALL_EXTENSION}"
82
+ )
83
+ parts = file.name[: -len(_TARBALL_EXTENSION)].split("-", 2)
84
+ if len(parts) != 3:
85
+ raise ValueError(
86
+ f"Filename {file.name!r} does not belong to a Galaxy tarball"
87
+ )
88
+ return parts[0], parts[1], parts[2]
89
+
90
+ @staticmethod
91
+ def _parse_cache_filename(file: Path) -> tuple[str, str, str, str] | None:
92
+ """
93
+ Split cache filename into (namespace, name, source_id, version) tuple.
94
+ """
95
+ if not file.name.endswith(_TARBALL_EXTENSION):
96
+ return None
97
+ parts = file.name[: -len(_TARBALL_EXTENSION)].split("-", 3)
98
+ if len(parts) != 4:
99
+ return None
100
+ return parts[0], parts[1], parts[2], parts[3]
101
+
102
+ @staticmethod
103
+ def _encode_cache_filename(
104
+ namespace: str, name: str, version: str, source: CollectionSource
105
+ ) -> str:
106
+ return f"{namespace}-{name}-{source.identifier()}-{version}{_TARBALL_EXTENSION}"
107
+
108
+ def download_collections(
109
+ self, *, destination: Path, sources: list[CollectionSource], runner: Runner
110
+ ) -> None:
111
+ """
112
+ Given a set of collection sources, downloads these and stores them in destination.
113
+ """
114
+ destination.mkdir(exist_ok=True)
115
+ names = ", ".join(sorted(source.name for source in sources))
116
+ print(f"Downloading {names} to {destination}...")
117
+ sources_by_name = {}
118
+ for source in sources:
119
+ sources_by_name[source.name] = source
120
+ if source.name != source.source:
121
+ print(f" Installing {source.name} via {source.source}...")
122
+ with tempfile.TemporaryDirectory(
123
+ prefix="antsibull-nox-galaxy-download"
124
+ ) as dest:
125
+ tempdir = Path(dest)
126
+ command = [
127
+ "ansible-galaxy",
128
+ "collection",
129
+ "download",
130
+ "--no-deps",
131
+ "--download-path",
132
+ str(tempdir),
133
+ "--",
134
+ *(source.source for source in sources),
135
+ ]
136
+ runner(command)
137
+ for file in tempdir.iterdir():
138
+ if file.name.endswith(_TARBALL_EXTENSION) and file.is_file():
139
+ namespace, name, version = self._parse_galaxy_filename(file)
140
+ source_opt = sources_by_name.get(f"{namespace}.{name}")
141
+ if source_opt is None:
142
+ print(
143
+ f"Found unknown collection artifact {file.name!r}, ignoring..."
144
+ )
145
+ continue
146
+ destfile = destination / self._encode_cache_filename(
147
+ namespace, name, version, source_opt
148
+ )
149
+ _remove(destfile)
150
+ shutil.move(file, destfile)
151
+
152
+ def list_downloaded_dir(
153
+ self, *, path: Path
154
+ ) -> dict[tuple[str, str], tuple[Path, str]]:
155
+ """
156
+ List contents of download cache.
157
+
158
+ Returns a dictionary mapping (collection_name, source_id) tuples to
159
+ (tarball_path, version) tuples.
160
+ """
161
+ if not path.is_dir():
162
+ return {}
163
+ result: dict[tuple[str, str], tuple[Path, str]] = {}
164
+ for file in path.iterdir():
165
+ if not file.is_file():
166
+ continue
167
+ parsed = self._parse_cache_filename(file)
168
+ if parsed is None:
169
+ continue
170
+ namespace, name, source_id, version = parsed
171
+ collection_name = f"{namespace}.{name}"
172
+ key = collection_name, source_id
173
+ if key in result:
174
+ # Determine older entry
175
+ old_file = result[key][0]
176
+ old_stat = old_file.stat()
177
+ new_stat = file.stat()
178
+ if new_stat.st_mtime > old_stat.st_mtime:
179
+ older_file = old_file
180
+ result[key] = file, version
181
+ else:
182
+ older_file = file
183
+ # Clean up older entry
184
+ _remove(older_file)
185
+ else:
186
+ result[key] = file, version
187
+ return result
188
+
189
+
60
190
  _COLLECTION_SOURCES = _CollectionSources()
191
+ _COLLECTION_SOURCES_PER_CORE_VERSION: dict[AnsibleCoreVersion, _CollectionSources] = {}
192
+ _COLLECTION_DOWNLOAD_CACHE = _CollectionDownloadCache()
61
193
  _TARBALL_EXTENSION = ".tar.gz"
62
194
  _INSTALLATION_CONFIG_ENV_VAR = "ANTSIBULL_NOX_INSTALL_COLLECTIONS"
63
195
 
64
196
 
65
- def setup_collection_sources(collection_sources: dict[str, CollectionSource]) -> None:
197
+ def setup_collection_sources(
198
+ collection_sources: dict[str, CollectionSource],
199
+ *,
200
+ ansible_core_version: AnsibleCoreVersion | None = None,
201
+ ) -> None:
66
202
  """
67
203
  Setup collection sources.
68
204
  """
205
+ sources = _COLLECTION_SOURCES
206
+ if ansible_core_version is not None:
207
+ sources = _COLLECTION_SOURCES_PER_CORE_VERSION.get(
208
+ ansible_core_version, _COLLECTION_SOURCES
209
+ )
210
+ if sources is _COLLECTION_SOURCES:
211
+ sources = _CollectionSources()
212
+ _COLLECTION_SOURCES_PER_CORE_VERSION[ansible_core_version] = sources
69
213
  for name, source in collection_sources.items():
70
- _COLLECTION_SOURCES.set_source(name, source)
71
-
72
-
73
- def _download_collections(
74
- *, destination: Path, sources: list[CollectionSource], runner: Runner
75
- ) -> None:
76
- destination.mkdir(exist_ok=True)
77
- names = ", ".join(sorted(source.name for source in sources))
78
- print(f"Downloading {names} to {destination}...")
79
- for source in sources:
80
- if source.name != source.source:
81
- print(f" Installing {source.name} via {source.source}...")
82
- with tempfile.TemporaryDirectory(prefix="antsibull-nox-galaxy-download") as dest:
83
- tempdir = Path(dest)
84
- command = [
85
- "ansible-galaxy",
86
- "collection",
87
- "download",
88
- "--no-deps",
89
- "--download-path",
90
- str(tempdir),
91
- "--",
92
- *(source.source for source in sources),
93
- ]
94
- runner(command)
95
- for file in tempdir.iterdir():
96
- if file.name.endswith(_TARBALL_EXTENSION) and file.is_file():
97
- destfile = destination / file.name
98
- _remove(destfile)
99
- shutil.move(file, destfile)
100
-
101
-
102
- def _list_downloaded_dir(*, path: Path) -> dict[str, Path]:
103
- if not path.is_dir():
104
- return {}
105
- result: dict[str, Path] = {}
106
- for file in path.iterdir():
107
- if not file.name.endswith(_TARBALL_EXTENSION) or not file.is_file():
108
- continue
109
- basename = file.name[: -len(_TARBALL_EXTENSION)]
110
- # Format: community-internal_test_tools-0.15.0, community-aws-10.0.0-dev0
111
- parts = basename.split("-", 2)
112
- if len(parts) != 3:
113
- continue
114
- full_name = ".".join(parts[:2])
115
- if full_name in result:
116
- old_stat = result[full_name].stat()
117
- new_stat = file.stat()
118
- if new_stat.st_mtime > old_stat.st_mtime:
119
- older_file = result[full_name]
120
- result[full_name] = file
121
- else:
122
- older_file = file
123
- # Clean up older entry
124
- _remove(older_file)
125
- else:
126
- result[full_name] = file
127
- return result
214
+ sources.set_source(name, source)
128
215
 
129
216
 
130
217
  def _install_from_download_cache(
@@ -137,9 +224,21 @@ def _install_from_download_cache(
137
224
  return destination_dir
138
225
 
139
226
 
227
+ def _get_source(
228
+ name: str, *, ansible_core_version: AnsibleCoreVersion
229
+ ) -> CollectionSource:
230
+ sources_per_version = _COLLECTION_SOURCES_PER_CORE_VERSION.get(ansible_core_version)
231
+ if sources_per_version:
232
+ result = sources_per_version.get_source(name, create_default=False)
233
+ if result is not None:
234
+ return result
235
+ return _COLLECTION_SOURCES.get_source(name)
236
+
237
+
140
238
  def _install_missing(
141
239
  collections: list[str],
142
240
  *,
241
+ ansible_core_version: AnsibleCoreVersion,
143
242
  runner: Runner,
144
243
  ) -> list[CollectionData]:
145
244
  config = os.environ.get(_INSTALLATION_CONFIG_ENV_VAR)
@@ -151,38 +250,49 @@ def _install_missing(
151
250
  f" thus cannot install missing exception{plural_s} {names}..."
152
251
  )
153
252
  return []
154
- sources = [_COLLECTION_SOURCES.get_source(name) for name in collections]
253
+ sources = [
254
+ _get_source(name, ansible_core_version=ansible_core_version)
255
+ for name in collections
256
+ ]
155
257
  result: list[CollectionData] = []
156
- with _update_collection_list() as updater:
258
+ with _update_collection_list(ansible_core_version=ansible_core_version) as updater:
157
259
  global_cache = updater.get_global_cache()
158
- install: list[str] = []
260
+ install: list[CollectionSource] = []
159
261
  download: list[CollectionSource] = []
160
- download_cache = _list_downloaded_dir(path=global_cache.download_cache)
262
+ download_cache = _COLLECTION_DOWNLOAD_CACHE.list_downloaded_dir(
263
+ path=global_cache.download_cache
264
+ )
161
265
  for source in sources:
162
266
  if cd := updater.find(source.name):
163
267
  result.append(cd)
164
268
  else:
165
- install.append(source.name)
166
- if not download_cache.get(source.name):
269
+ install.append(source)
270
+ if not download_cache.get((source.name, source.identifier())):
167
271
  download.append(source)
168
272
  if download:
169
- _download_collections(
273
+ _COLLECTION_DOWNLOAD_CACHE.download_collections(
170
274
  destination=global_cache.download_cache, sources=download, runner=runner
171
275
  )
172
- download_cache = _list_downloaded_dir(path=global_cache.download_cache)
276
+ download_cache = _COLLECTION_DOWNLOAD_CACHE.list_downloaded_dir(
277
+ path=global_cache.download_cache
278
+ )
173
279
  if install:
174
- for name in install:
175
- if name not in download_cache:
280
+ for source in install:
281
+ key = source.name, source.identifier()
282
+ if key not in download_cache:
176
283
  raise ValueError(
177
- f"Error: cannot find {name} in download cache"
178
- f" {global_cache.download_cache} after successful download!"
284
+ f"Error: cannot find {source.name} (source ID {source.identifier()})"
285
+ f" in download cache {global_cache.download_cache}"
286
+ " after successful download!"
179
287
  )
180
288
  c_dir = _install_from_download_cache(
181
- full_name=name,
182
- tarball=download_cache[name],
183
- destination=global_cache.extracted_cache,
289
+ full_name=source.name,
290
+ tarball=download_cache[key][0],
291
+ destination=global_cache.get_extracted_path(
292
+ ansible_core_version=ansible_core_version
293
+ ),
184
294
  )
185
- c_namespace, c_name = name.split(".", 1)
295
+ c_namespace, c_name = source.name.split(".", 1)
186
296
  result.append(
187
297
  updater.add_collection(
188
298
  directory=c_dir, namespace=c_namespace, name=c_name
@@ -414,6 +524,7 @@ def setup_collections(
414
524
  destination: str | os.PathLike,
415
525
  runner: Runner,
416
526
  *,
527
+ ansible_core_version: AnsibleCoreVersion,
417
528
  extra_collections: list[str] | None = None,
418
529
  extra_deps_files: list[str | os.PathLike] | None = None,
419
530
  global_cache_dir: Path,
@@ -423,7 +534,9 @@ def setup_collections(
423
534
  Setup all collections in a tree structure inside the destination directory.
424
535
  """
425
536
  all_collections = get_collection_list(
426
- runner=runner, global_cache_dir=global_cache_dir
537
+ runner=runner,
538
+ global_cache_dir=global_cache_dir,
539
+ ansible_core_version=ansible_core_version,
427
540
  )
428
541
  destination_root = Path(destination) / "ansible_collections"
429
542
  destination_root.mkdir(exist_ok=True)
@@ -451,7 +564,9 @@ def setup_collections(
451
564
  if missing.is_empty():
452
565
  break
453
566
  for collection_data in _install_missing(
454
- missing.get_missing_names(), runner=runner
567
+ missing.get_missing_names(),
568
+ ansible_core_version=ansible_core_version,
569
+ runner=runner,
455
570
  ):
456
571
  collections_to_install[collection_data.full_name] = collection_data
457
572
  missing.remove(collection_data.full_name)
@@ -21,6 +21,7 @@ from pathlib import Path
21
21
 
22
22
  from antsibull_fileutils.yaml import load_yaml_file
23
23
 
24
+ from ..ansible import AnsibleCoreVersion
24
25
  from .data import CollectionData
25
26
 
26
27
  # Function that runs a command (and fails on non-zero return code)
@@ -49,6 +50,12 @@ class _GlobalCache:
49
50
  extracted_cache=root / "extracted",
50
51
  )
51
52
 
53
+ def get_extracted_path(self, *, ansible_core_version: AnsibleCoreVersion) -> Path:
54
+ """
55
+ Given an ansible-core version, returns its extracted collection cache directory.
56
+ """
57
+ return self.extracted_cache / str(ansible_core_version)
58
+
52
59
 
53
60
  def _load_galaxy_yml(galaxy_yml: Path) -> dict[str, t.Any]:
54
61
  try:
@@ -279,9 +286,9 @@ class CollectionList:
279
286
  )
280
287
 
281
288
  @classmethod
282
- def collect(cls, *, runner: Runner, global_cache: _GlobalCache) -> CollectionList:
289
+ def collect_global(cls, *, runner: Runner) -> CollectionList:
283
290
  """
284
- Search for a list of collections. The result is not cached.
291
+ Search for a global list of collections. The result is not cached.
285
292
  """
286
293
  found_collections = {}
287
294
  for collection_data in _fs_list_local_collections():
@@ -291,7 +298,25 @@ class CollectionList:
291
298
  # Similar to Ansible, we use the first match
292
299
  if collection_data.full_name not in found_collections:
293
300
  found_collections[collection_data.full_name] = collection_data
294
- for collection_data in _fs_list_global_cache(global_cache.extracted_cache):
301
+ return cls.create(found_collections)
302
+
303
+ @classmethod
304
+ def collect_local(
305
+ cls,
306
+ *,
307
+ ansible_core_version: AnsibleCoreVersion,
308
+ global_cache: _GlobalCache,
309
+ current: CollectionData,
310
+ ) -> CollectionList:
311
+ """
312
+ Search for a list of collections from a local cache path. The result is not cached.
313
+ """
314
+ found_collections = {
315
+ current.full_name: current,
316
+ }
317
+ for collection_data in _fs_list_global_cache(
318
+ global_cache.get_extracted_path(ansible_core_version=ansible_core_version)
319
+ ):
295
320
  # Similar to Ansible, we use the first match
296
321
  if collection_data.full_name not in found_collections:
297
322
  found_collections[collection_data.full_name] = collection_data
@@ -313,6 +338,18 @@ class CollectionList:
313
338
  current=self.current,
314
339
  )
315
340
 
341
+ def merge_with(self, other: CollectionList) -> CollectionList:
342
+ """
343
+ Merge this collection list with another (local) one.
344
+ """
345
+ result = dict(self.collection_map)
346
+ for collection in other.collections:
347
+ # Similar to Ansible, we use the first match.
348
+ # For merge, this means we prefer self over other.
349
+ if collection.full_name not in result:
350
+ result[collection.full_name] = collection
351
+ return CollectionList.create(result)
352
+
316
353
  def _add(self, collection: CollectionData, *, force: bool = True) -> bool:
317
354
  if not force and collection.full_name in self.collection_map:
318
355
  return False
@@ -323,16 +360,23 @@ class CollectionList:
323
360
 
324
361
  class _CollectionListUpdater:
325
362
  def __init__(
326
- self, *, owner: "_CollectionListSingleton", collection_list: CollectionList
363
+ self,
364
+ *,
365
+ owner: "_CollectionListSingleton",
366
+ merged_collection_list: CollectionList,
367
+ local_collection_list: CollectionList,
368
+ ansible_core_version: AnsibleCoreVersion,
327
369
  ) -> None:
328
370
  self._owner = owner
329
- self._collection_list = collection_list
371
+ self._merged_collection_list = merged_collection_list
372
+ self._local_collection_list = local_collection_list
373
+ self._ansible_core_version = ansible_core_version
330
374
 
331
375
  def find(self, name: str) -> CollectionData | None:
332
376
  """
333
377
  Find a collection for a given name.
334
378
  """
335
- return self._collection_list.find(name)
379
+ return self._merged_collection_list.find(name)
336
380
 
337
381
  def add_collection(
338
382
  self, *, directory: Path, namespace: str, name: str
@@ -341,9 +385,15 @@ class _CollectionListUpdater:
341
385
  Add a new collection to the cache.
342
386
  """
343
387
  # pylint: disable-next=protected-access
344
- return self._owner._add_collection(
345
- directory=directory, namespace=namespace, name=name
388
+ result = self._owner._add_collection(
389
+ directory=directory,
390
+ namespace=namespace,
391
+ name=name,
392
+ ansible_core_version=self._ansible_core_version,
346
393
  )
394
+ self._merged_collection_list._add(result) # pylint: disable=protected-access
395
+ self._local_collection_list._add(result) # pylint: disable=protected-access
396
+ return result
347
397
 
348
398
  def get_global_cache(self) -> _GlobalCache:
349
399
  """
@@ -356,7 +406,10 @@ class _CollectionListSingleton:
356
406
  _lock = threading.Lock()
357
407
 
358
408
  _global_cache_dir: Path | None = None
359
- _collection_list: CollectionList | None = None
409
+ _global_collection_list: CollectionList | None = None
410
+ _global_collection_list_per_ansible_core_version: dict[
411
+ AnsibleCoreVersion, CollectionList
412
+ ] = {}
360
413
 
361
414
  def setup(self, *, global_cache_dir: Path) -> None:
362
415
  """
@@ -378,30 +431,48 @@ class _CollectionListSingleton:
378
431
  Clear collection cache.
379
432
  """
380
433
  with self._lock:
381
- self._collection_list = None
434
+ self._global_collection_list = None
435
+ self._global_collection_list_per_ansible_core_version.clear()
382
436
 
383
- def get_cached(self) -> CollectionList | None:
437
+ def get_cached(
438
+ self, *, ansible_core_version: AnsibleCoreVersion | None = None
439
+ ) -> CollectionList | None:
384
440
  """
385
441
  Return cached list of collections, if present.
386
442
  Do not modify the result!
387
443
  """
388
- return self._collection_list
444
+ if ansible_core_version is None:
445
+ return self._global_collection_list
446
+ return self._global_collection_list_per_ansible_core_version.get(
447
+ ansible_core_version
448
+ )
389
449
 
390
- def get(self, *, runner: Runner) -> CollectionList:
450
+ def get(
451
+ self, *, ansible_core_version: AnsibleCoreVersion, runner: Runner
452
+ ) -> CollectionList:
391
453
  """
392
454
  Search for a list of collections. The result is cached.
393
455
  """
394
456
  with self._lock:
395
457
  if self._global_cache_dir is None:
396
458
  raise ValueError("Internal error: global cache dir not setup")
397
- result = self._collection_list
398
- if result is None:
399
- result = CollectionList.collect(
400
- runner=runner,
459
+ global_list = self._global_collection_list
460
+ if global_list is None:
461
+ global_list = CollectionList.collect_global(runner=runner)
462
+ self._global_collection_list = global_list
463
+ local_list = self._global_collection_list_per_ansible_core_version.get(
464
+ ansible_core_version
465
+ )
466
+ if local_list is None:
467
+ local_list = CollectionList.collect_local(
401
468
  global_cache=_GlobalCache.create(root=self._global_cache_dir),
469
+ ansible_core_version=ansible_core_version,
470
+ current=global_list.current,
402
471
  )
403
- self._collection_list = result
404
- return result.clone()
472
+ self._global_collection_list_per_ansible_core_version[
473
+ ansible_core_version
474
+ ] = local_list
475
+ return global_list.merge_with(local_list)
405
476
 
406
477
  def _get_global_cache(self) -> _GlobalCache:
407
478
  """
@@ -412,26 +483,45 @@ class _CollectionListSingleton:
412
483
  return _GlobalCache.create(root=self._global_cache_dir)
413
484
 
414
485
  def _add_collection(
415
- self, *, directory: Path, namespace: str, name: str
486
+ self,
487
+ *,
488
+ directory: Path,
489
+ namespace: str,
490
+ name: str,
491
+ ansible_core_version: AnsibleCoreVersion,
416
492
  ) -> CollectionData:
417
493
  """
418
494
  Add collection in directory if the collection list has been cached.
419
495
  """
420
- if not self._collection_list:
421
- raise ValueError("Internal error: collections not listed")
496
+ local_list = self._global_collection_list_per_ansible_core_version.get(
497
+ ansible_core_version
498
+ )
499
+ if not local_list:
500
+ raise ValueError(
501
+ f"Internal error: collections not listed for {ansible_core_version}"
502
+ )
422
503
  data = load_collection_data_from_disk(directory, namespace=namespace, name=name)
423
- self._collection_list._add(data) # pylint: disable=protected-access
504
+ local_list._add(data) # pylint: disable=protected-access
424
505
  return data
425
506
 
426
507
  @contextmanager
427
- def _update_collection_list(self) -> t.Iterator[_CollectionListUpdater]:
508
+ def _update_collection_list(
509
+ self, *, ansible_core_version: AnsibleCoreVersion
510
+ ) -> t.Iterator[_CollectionListUpdater]:
428
511
  with self._lock:
429
- if not self._collection_list or self._global_cache_dir is None:
512
+ global_list = self._global_collection_list
513
+ local_list = self._global_collection_list_per_ansible_core_version.get(
514
+ ansible_core_version
515
+ )
516
+ if not global_list or self._global_cache_dir is None or local_list is None:
430
517
  raise ValueError(
431
518
  "Internal error: collections not listed or global cache not setup"
432
519
  )
433
520
  yield _CollectionListUpdater(
434
- owner=self, collection_list=self._collection_list
521
+ owner=self,
522
+ merged_collection_list=global_list.merge_with(local_list),
523
+ local_collection_list=local_list,
524
+ ansible_core_version=ansible_core_version,
435
525
  )
436
526
 
437
527
 
@@ -439,18 +529,26 @@ _COLLECTION_LIST = _CollectionListSingleton()
439
529
 
440
530
 
441
531
  @contextmanager
442
- def _update_collection_list() -> t.Iterator[_CollectionListUpdater]:
532
+ def _update_collection_list(
533
+ *, ansible_core_version: AnsibleCoreVersion
534
+ ) -> t.Iterator[_CollectionListUpdater]:
443
535
  # pylint: disable-next=protected-access
444
- with _COLLECTION_LIST._update_collection_list() as result:
536
+ with _COLLECTION_LIST._update_collection_list(
537
+ ansible_core_version=ansible_core_version
538
+ ) as result:
445
539
  yield result
446
540
 
447
541
 
448
- def get_collection_list(*, runner: Runner, global_cache_dir: Path) -> CollectionList:
542
+ def get_collection_list(
543
+ *, runner: Runner, global_cache_dir: Path, ansible_core_version: AnsibleCoreVersion
544
+ ) -> CollectionList:
449
545
  """
450
546
  Search for a list of collections. The result is cached.
451
547
  """
452
548
  _COLLECTION_LIST.setup(global_cache_dir=global_cache_dir)
453
- return _COLLECTION_LIST.get(runner=runner)
549
+ return _COLLECTION_LIST.get(
550
+ runner=runner, ansible_core_version=ansible_core_version
551
+ )
454
552
 
455
553
 
456
554
  __all__ = [