antsibull-nox 0.2.0__py3-none-any.whl → 0.4.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.
Files changed (34) hide show
  1. antsibull_nox/__init__.py +7 -51
  2. antsibull_nox/_pydantic.py +98 -0
  3. antsibull_nox/ansible.py +15 -0
  4. antsibull_nox/cli.py +132 -0
  5. antsibull_nox/collection/__init__.py +2 -2
  6. antsibull_nox/collection/data.py +12 -0
  7. antsibull_nox/collection/install.py +194 -79
  8. antsibull_nox/collection/search.py +136 -34
  9. antsibull_nox/config.py +51 -2
  10. antsibull_nox/data/action-groups.py +2 -2
  11. antsibull_nox/data/antsibull-nox-lint-config.py +29 -0
  12. antsibull_nox/data/file-yamllint.py +138 -0
  13. antsibull_nox/data/license-check.py +5 -1
  14. antsibull_nox/data/plugin-yamllint.py +54 -24
  15. antsibull_nox/init.py +83 -0
  16. antsibull_nox/interpret_config.py +29 -8
  17. antsibull_nox/lint_config.py +113 -0
  18. antsibull_nox/sessions/__init__.py +70 -0
  19. antsibull_nox/sessions/ansible_lint.py +60 -0
  20. antsibull_nox/sessions/ansible_test.py +559 -0
  21. antsibull_nox/sessions/build_import_check.py +147 -0
  22. antsibull_nox/sessions/collections.py +145 -0
  23. antsibull_nox/sessions/docs_check.py +78 -0
  24. antsibull_nox/sessions/extra_checks.py +127 -0
  25. antsibull_nox/sessions/license_check.py +73 -0
  26. antsibull_nox/sessions/lint.py +694 -0
  27. antsibull_nox/sessions/utils.py +206 -0
  28. {antsibull_nox-0.2.0.dist-info → antsibull_nox-0.4.0.dist-info}/METADATA +2 -2
  29. antsibull_nox-0.4.0.dist-info/RECORD +41 -0
  30. antsibull_nox-0.4.0.dist-info/entry_points.txt +2 -0
  31. antsibull_nox/sessions.py +0 -1712
  32. antsibull_nox-0.2.0.dist-info/RECORD +0 -25
  33. {antsibull_nox-0.2.0.dist-info → antsibull_nox-0.4.0.dist-info}/WHEEL +0 -0
  34. {antsibull_nox-0.2.0.dist-info → antsibull_nox-0.4.0.dist-info}/licenses/LICENSES/GPL-3.0-or-later.txt +0 -0
@@ -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)
@@ -28,6 +29,10 @@ from .data import CollectionData
28
29
  Runner = t.Callable[[list[str]], tuple[bytes, bytes]]
29
30
 
30
31
 
32
+ GALAXY_YML = "galaxy.yml"
33
+ MANIFEST_JSON = "MANIFEST.json"
34
+
35
+
31
36
  @dataclass(frozen=True)
32
37
  class _GlobalCache:
33
38
  root: Path
@@ -45,6 +50,12 @@ class _GlobalCache:
45
50
  extracted_cache=root / "extracted",
46
51
  )
47
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
+
48
59
 
49
60
  def _load_galaxy_yml(galaxy_yml: Path) -> dict[str, t.Any]:
50
61
  try:
@@ -80,19 +91,19 @@ def load_collection_data_from_disk(
80
91
  """
81
92
  Load collection data from disk.
82
93
  """
83
- galaxy_yml = path / "galaxy.yml"
84
- manifest_json = path / "MANIFEST.json"
94
+ galaxy_yml = path / GALAXY_YML
95
+ manifest_json = path / MANIFEST_JSON
85
96
  found: Path
86
97
  if galaxy_yml.is_file():
87
98
  found = galaxy_yml
88
99
  data = _load_galaxy_yml(galaxy_yml)
89
100
  elif not accept_manifest:
90
- raise ValueError(f"Cannot find galaxy.yml in {path}")
101
+ raise ValueError(f"Cannot find {GALAXY_YML} in {path}")
91
102
  elif manifest_json.is_file():
92
103
  found = manifest_json
93
104
  data = _load_manifest_json_collection_info(manifest_json)
94
105
  else:
95
- raise ValueError(f"Cannot find galaxy.yml or MANIFEST.json in {path}")
106
+ raise ValueError(f"Cannot find {GALAXY_YML} or {MANIFEST_JSON} in {path}")
96
107
 
97
108
  ns = data.get("namespace")
98
109
  if not isinstance(ns, str):
@@ -275,9 +286,9 @@ class CollectionList:
275
286
  )
276
287
 
277
288
  @classmethod
278
- def collect(cls, *, runner: Runner, global_cache: _GlobalCache) -> CollectionList:
289
+ def collect_global(cls, *, runner: Runner) -> CollectionList:
279
290
  """
280
- Search for a list of collections. The result is not cached.
291
+ Search for a global list of collections. The result is not cached.
281
292
  """
282
293
  found_collections = {}
283
294
  for collection_data in _fs_list_local_collections():
@@ -287,7 +298,25 @@ class CollectionList:
287
298
  # Similar to Ansible, we use the first match
288
299
  if collection_data.full_name not in found_collections:
289
300
  found_collections[collection_data.full_name] = collection_data
290
- 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
+ ):
291
320
  # Similar to Ansible, we use the first match
292
321
  if collection_data.full_name not in found_collections:
293
322
  found_collections[collection_data.full_name] = collection_data
@@ -309,6 +338,18 @@ class CollectionList:
309
338
  current=self.current,
310
339
  )
311
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
+
312
353
  def _add(self, collection: CollectionData, *, force: bool = True) -> bool:
313
354
  if not force and collection.full_name in self.collection_map:
314
355
  return False
@@ -319,16 +360,23 @@ class CollectionList:
319
360
 
320
361
  class _CollectionListUpdater:
321
362
  def __init__(
322
- 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,
323
369
  ) -> None:
324
370
  self._owner = owner
325
- 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
326
374
 
327
375
  def find(self, name: str) -> CollectionData | None:
328
376
  """
329
377
  Find a collection for a given name.
330
378
  """
331
- return self._collection_list.find(name)
379
+ return self._merged_collection_list.find(name)
332
380
 
333
381
  def add_collection(
334
382
  self, *, directory: Path, namespace: str, name: str
@@ -337,9 +385,15 @@ class _CollectionListUpdater:
337
385
  Add a new collection to the cache.
338
386
  """
339
387
  # pylint: disable-next=protected-access
340
- return self._owner._add_collection(
341
- 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,
342
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
343
397
 
344
398
  def get_global_cache(self) -> _GlobalCache:
345
399
  """
@@ -352,7 +406,10 @@ class _CollectionListSingleton:
352
406
  _lock = threading.Lock()
353
407
 
354
408
  _global_cache_dir: Path | None = None
355
- _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
+ ] = {}
356
413
 
357
414
  def setup(self, *, global_cache_dir: Path) -> None:
358
415
  """
@@ -374,30 +431,48 @@ class _CollectionListSingleton:
374
431
  Clear collection cache.
375
432
  """
376
433
  with self._lock:
377
- self._collection_list = None
434
+ self._global_collection_list = None
435
+ self._global_collection_list_per_ansible_core_version.clear()
378
436
 
379
- def get_cached(self) -> CollectionList | None:
437
+ def get_cached(
438
+ self, *, ansible_core_version: AnsibleCoreVersion | None = None
439
+ ) -> CollectionList | None:
380
440
  """
381
441
  Return cached list of collections, if present.
382
442
  Do not modify the result!
383
443
  """
384
- 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
+ )
385
449
 
386
- def get(self, *, runner: Runner) -> CollectionList:
450
+ def get(
451
+ self, *, ansible_core_version: AnsibleCoreVersion, runner: Runner
452
+ ) -> CollectionList:
387
453
  """
388
454
  Search for a list of collections. The result is cached.
389
455
  """
390
456
  with self._lock:
391
457
  if self._global_cache_dir is None:
392
458
  raise ValueError("Internal error: global cache dir not setup")
393
- result = self._collection_list
394
- if result is None:
395
- result = CollectionList.collect(
396
- 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(
397
468
  global_cache=_GlobalCache.create(root=self._global_cache_dir),
469
+ ansible_core_version=ansible_core_version,
470
+ current=global_list.current,
398
471
  )
399
- self._collection_list = result
400
- 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)
401
476
 
402
477
  def _get_global_cache(self) -> _GlobalCache:
403
478
  """
@@ -408,26 +483,45 @@ class _CollectionListSingleton:
408
483
  return _GlobalCache.create(root=self._global_cache_dir)
409
484
 
410
485
  def _add_collection(
411
- self, *, directory: Path, namespace: str, name: str
486
+ self,
487
+ *,
488
+ directory: Path,
489
+ namespace: str,
490
+ name: str,
491
+ ansible_core_version: AnsibleCoreVersion,
412
492
  ) -> CollectionData:
413
493
  """
414
494
  Add collection in directory if the collection list has been cached.
415
495
  """
416
- if not self._collection_list:
417
- 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
+ )
418
503
  data = load_collection_data_from_disk(directory, namespace=namespace, name=name)
419
- self._collection_list._add(data) # pylint: disable=protected-access
504
+ local_list._add(data) # pylint: disable=protected-access
420
505
  return data
421
506
 
422
507
  @contextmanager
423
- def _update_collection_list(self) -> t.Iterator[_CollectionListUpdater]:
508
+ def _update_collection_list(
509
+ self, *, ansible_core_version: AnsibleCoreVersion
510
+ ) -> t.Iterator[_CollectionListUpdater]:
424
511
  with self._lock:
425
- 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:
426
517
  raise ValueError(
427
518
  "Internal error: collections not listed or global cache not setup"
428
519
  )
429
520
  yield _CollectionListUpdater(
430
- 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,
431
525
  )
432
526
 
433
527
 
@@ -435,18 +529,26 @@ _COLLECTION_LIST = _CollectionListSingleton()
435
529
 
436
530
 
437
531
  @contextmanager
438
- def _update_collection_list() -> t.Iterator[_CollectionListUpdater]:
532
+ def _update_collection_list(
533
+ *, ansible_core_version: AnsibleCoreVersion
534
+ ) -> t.Iterator[_CollectionListUpdater]:
439
535
  # pylint: disable-next=protected-access
440
- 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:
441
539
  yield result
442
540
 
443
541
 
444
- 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:
445
545
  """
446
546
  Search for a list of collections. The result is cached.
447
547
  """
448
548
  _COLLECTION_LIST.setup(global_cache_dir=global_cache_dir)
449
- return _COLLECTION_LIST.get(runner=runner)
549
+ return _COLLECTION_LIST.get(
550
+ runner=runner, ansible_core_version=ansible_core_version
551
+ )
450
552
 
451
553
 
452
554
  __all__ = [