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.
- antsibull_nox/__init__.py +7 -51
- antsibull_nox/_pydantic.py +98 -0
- antsibull_nox/ansible.py +15 -0
- antsibull_nox/cli.py +132 -0
- antsibull_nox/collection/__init__.py +2 -2
- antsibull_nox/collection/data.py +12 -0
- antsibull_nox/collection/install.py +194 -79
- antsibull_nox/collection/search.py +136 -34
- antsibull_nox/config.py +51 -2
- antsibull_nox/data/action-groups.py +2 -2
- antsibull_nox/data/antsibull-nox-lint-config.py +29 -0
- antsibull_nox/data/file-yamllint.py +138 -0
- antsibull_nox/data/license-check.py +5 -1
- antsibull_nox/data/plugin-yamllint.py +54 -24
- antsibull_nox/init.py +83 -0
- antsibull_nox/interpret_config.py +29 -8
- antsibull_nox/lint_config.py +113 -0
- antsibull_nox/sessions/__init__.py +70 -0
- antsibull_nox/sessions/ansible_lint.py +60 -0
- antsibull_nox/sessions/ansible_test.py +559 -0
- antsibull_nox/sessions/build_import_check.py +147 -0
- antsibull_nox/sessions/collections.py +145 -0
- antsibull_nox/sessions/docs_check.py +78 -0
- antsibull_nox/sessions/extra_checks.py +127 -0
- antsibull_nox/sessions/license_check.py +73 -0
- antsibull_nox/sessions/lint.py +694 -0
- antsibull_nox/sessions/utils.py +206 -0
- {antsibull_nox-0.2.0.dist-info → antsibull_nox-0.4.0.dist-info}/METADATA +2 -2
- antsibull_nox-0.4.0.dist-info/RECORD +41 -0
- antsibull_nox-0.4.0.dist-info/entry_points.txt +2 -0
- antsibull_nox/sessions.py +0 -1712
- antsibull_nox-0.2.0.dist-info/RECORD +0 -25
- {antsibull_nox-0.2.0.dist-info → antsibull_nox-0.4.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
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(
|
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
|
-
|
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 = [
|
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[
|
260
|
+
install: list[CollectionSource] = []
|
159
261
|
download: list[CollectionSource] = []
|
160
|
-
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
|
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
|
-
|
273
|
+
_COLLECTION_DOWNLOAD_CACHE.download_collections(
|
170
274
|
destination=global_cache.download_cache, sources=download, runner=runner
|
171
275
|
)
|
172
|
-
download_cache =
|
276
|
+
download_cache = _COLLECTION_DOWNLOAD_CACHE.list_downloaded_dir(
|
277
|
+
path=global_cache.download_cache
|
278
|
+
)
|
173
279
|
if install:
|
174
|
-
for
|
175
|
-
|
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}
|
178
|
-
f" {global_cache.download_cache}
|
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[
|
183
|
-
destination=global_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,
|
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(),
|
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 /
|
84
|
-
manifest_json = path /
|
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
|
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
|
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
|
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
|
-
|
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,
|
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.
|
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.
|
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
|
-
|
341
|
-
directory=directory,
|
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
|
-
|
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.
|
434
|
+
self._global_collection_list = None
|
435
|
+
self._global_collection_list_per_ansible_core_version.clear()
|
378
436
|
|
379
|
-
def get_cached(
|
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
|
-
|
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(
|
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
|
-
|
394
|
-
if
|
395
|
-
|
396
|
-
|
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.
|
400
|
-
|
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,
|
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
|
-
|
417
|
-
|
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
|
-
|
504
|
+
local_list._add(data) # pylint: disable=protected-access
|
420
505
|
return data
|
421
506
|
|
422
507
|
@contextmanager
|
423
|
-
def _update_collection_list(
|
508
|
+
def _update_collection_list(
|
509
|
+
self, *, ansible_core_version: AnsibleCoreVersion
|
510
|
+
) -> t.Iterator[_CollectionListUpdater]:
|
424
511
|
with self._lock:
|
425
|
-
|
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,
|
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(
|
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(
|
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(
|
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(
|
549
|
+
return _COLLECTION_LIST.get(
|
550
|
+
runner=runner, ansible_core_version=ansible_core_version
|
551
|
+
)
|
450
552
|
|
451
553
|
|
452
554
|
__all__ = [
|