Skip to content

Commit eb8c550

Browse files
authored
#319: Add restore_package_file method to class ScriptLanguagesContainer (#322)
fixes #319
1 parent 93a8cb3 commit eb8c550

File tree

8 files changed

+183
-69
lines changed

8 files changed

+183
-69
lines changed

doc/changes/unreleased.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,8 @@
99
## Refactoring
1010

1111
* #314: Added more robust for class ScriptLanguageContainer
12-
* #316: Implemented consistency check for workspace in ScriptLanguageContainer.create()
12+
* #316: Implemented consistency check for workspace in ScriptLanguageContainer.create()
13+
14+
## Features
15+
16+
* #319: Add restore_package_file method to class ScriptLanguagesContainer
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
from exasol.nb_connector.slc.script_language_container import (
1+
from exasol.nb_connector.slc.package_types import (
22
CondaPackageDefinition,
33
PipPackageDefinition,
4-
ScriptLanguageContainer,
54
)
5+
from exasol.nb_connector.slc.script_language_container import ScriptLanguageContainer
66
from exasol.nb_connector.slc.slc_error import SlcError

exasol/nb_connector/slc/git_access.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,8 @@ def clone_from_recursively(url: str, path: Path, branch: str) -> None:
2525
def checkout_recursively(path: Path) -> None:
2626
repo = Repo(path)
2727
repo.git.checkout("--recurse-submodules", ".")
28+
29+
@staticmethod
30+
def checkout_file(repo_path: Path, file: Path) -> None:
31+
repo = Repo(repo_path)
32+
repo.git.checkout("HEAD", "--", str(file))
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import logging
2+
from pathlib import Path
3+
4+
from exasol.nb_connector.slc.package_types import (
5+
CondaPackageDefinition,
6+
PipPackageDefinition,
7+
)
8+
from exasol.nb_connector.slc.slc_error import SlcError
9+
10+
11+
def _read_packages(file_path: Path, package_definition: type) -> list:
12+
packages = []
13+
with file_path.open("r", encoding="utf-8") as f:
14+
for line in f:
15+
line = line.strip()
16+
if not line or line.startswith("#"):
17+
continue # skip empty or commented lines
18+
# Take everything before the '|' as package name
19+
package, version = line.split("|", 1)
20+
packages.append(package_definition(package, version))
21+
return packages
22+
23+
24+
def _ends_with_newline(file_path: Path) -> bool:
25+
content = file_path.read_text()
26+
return content.endswith("\n")
27+
28+
29+
def _filter_packages(
30+
original_packages: list[PipPackageDefinition] | list[CondaPackageDefinition],
31+
new_packages: list[PipPackageDefinition] | list[CondaPackageDefinition],
32+
) -> list[PipPackageDefinition | CondaPackageDefinition]:
33+
filtered_packages = []
34+
for package in new_packages:
35+
add_package = True
36+
for original_package in original_packages:
37+
if package.pkg == original_package.pkg:
38+
add_package = False
39+
if package.version == original_package.version:
40+
logging.warning("Package already exists: %s", original_package)
41+
else:
42+
raise SlcError(
43+
"Package already exists: %s but with different version",
44+
original_package,
45+
)
46+
if add_package:
47+
filtered_packages.append(package)
48+
return filtered_packages
49+
50+
51+
def append_packages(
52+
file_path: Path,
53+
package_definition: type,
54+
packages: list[PipPackageDefinition] | list[CondaPackageDefinition],
55+
):
56+
"""
57+
Appends packages to the custom packages file.
58+
"""
59+
original_packages = _read_packages(file_path, package_definition)
60+
filtered_packages = _filter_packages(original_packages, packages)
61+
ends_with_newline = _ends_with_newline(file_path)
62+
if filtered_packages:
63+
with open(file_path, "a") as f:
64+
if not ends_with_newline:
65+
f.write("\n")
66+
for p in filtered_packages:
67+
print(f"{p.pkg}|{p.version}", file=f)
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
from collections import namedtuple
2+
3+
PipPackageDefinition = namedtuple("PipPackageDefinition", ["pkg", "version"])
4+
CondaPackageDefinition = namedtuple("CondaPackageDefinition", ["pkg", "version"])

exasol/nb_connector/slc/script_language_container.py

Lines changed: 26 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import logging
44
import re
5-
from collections import namedtuple
65
from pathlib import (
76
Path,
87
)
@@ -20,6 +19,12 @@
2019
from exasol.nb_connector.ai_lab_config import AILabConfig as CKey
2120
from exasol.nb_connector.secret_store import Secrets
2221
from exasol.nb_connector.slc import constants
22+
from exasol.nb_connector.slc.git_access import GitAccess
23+
from exasol.nb_connector.slc.package_file_editor import append_packages
24+
from exasol.nb_connector.slc.package_types import (
25+
CondaPackageDefinition,
26+
PipPackageDefinition,
27+
)
2328
from exasol.nb_connector.slc.slc_compression_strategy import SlcCompressionStrategy
2429
from exasol.nb_connector.slc.slc_flavor import (
2530
SlcError,
@@ -31,9 +36,6 @@
3136
)
3237
from exasol.nb_connector.utils import optional_str_to_bool
3338

34-
PipPackageDefinition = namedtuple("PipPackageDefinition", ["pkg", "version"])
35-
CondaPackageDefinition = namedtuple("CondaPackageDefinition", ["pkg", "version"])
36-
3739
NAME_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$", flags=re.IGNORECASE)
3840

3941

@@ -45,65 +47,6 @@ def _verify_name(slc_name: str) -> None:
4547
)
4648

4749

48-
def _read_packages(file_path: Path, package_definition: type) -> list:
49-
packages = []
50-
with file_path.open("r", encoding="utf-8") as f:
51-
for line in f:
52-
line = line.strip()
53-
if not line or line.startswith("#"):
54-
continue # skip empty or commented lines
55-
# Take everything before the '|' as package name
56-
package, version = line.split("|", 1)
57-
packages.append(package_definition(package, version))
58-
return packages
59-
60-
61-
def _ends_with_newline(file_path: Path) -> bool:
62-
content = file_path.read_text()
63-
return content.endswith("\n")
64-
65-
66-
def _filter_packages(
67-
original_packages: list[PipPackageDefinition] | list[CondaPackageDefinition],
68-
new_packages: list[PipPackageDefinition] | list[CondaPackageDefinition],
69-
) -> list[PipPackageDefinition | CondaPackageDefinition]:
70-
filtered_packages = []
71-
for package in new_packages:
72-
add_package = True
73-
for original_package in original_packages:
74-
if package.pkg == original_package.pkg:
75-
add_package = False
76-
if package.version == original_package.version:
77-
logging.warning("Package already exists: %s", original_package)
78-
else:
79-
raise SlcError(
80-
"Package already exists: %s but with different version",
81-
original_package,
82-
)
83-
if add_package:
84-
filtered_packages.append(package)
85-
return filtered_packages
86-
87-
88-
def _append_packages(
89-
file_path: Path,
90-
package_definition: type,
91-
packages: list[PipPackageDefinition] | list[CondaPackageDefinition],
92-
):
93-
"""
94-
Appends packages to the custom packages file.
95-
"""
96-
original_packages = _read_packages(file_path, package_definition)
97-
filtered_packages = _filter_packages(original_packages, packages)
98-
ends_with_newline = _ends_with_newline(file_path)
99-
if filtered_packages:
100-
with open(file_path, "a") as f:
101-
if not ends_with_newline:
102-
f.write("\n")
103-
for p in filtered_packages:
104-
print(f"{p.pkg}|{p.version}", file=f)
105-
106-
10750
class ScriptLanguageContainer:
10851
"""
10952
Support building different flavors of Exasol Script Language
@@ -242,6 +185,24 @@ def custom_conda_file(self) -> Path:
242185
"""
243186
return self.custom_packages_dir / "conda_packages"
244187

188+
def restore_custom_pip_file(self):
189+
"""
190+
Restores the custom pip packages file from Git. All changes will be overwritten.
191+
"""
192+
GitAccess.checkout_file(
193+
self.checkout_dir / "script-languages",
194+
self.custom_pip_file.relative_to(self.checkout_dir),
195+
)
196+
197+
def restore_custom_conda_file(self):
198+
"""
199+
Restores the custom conda packages file from Git. All changes will be overwritten.
200+
"""
201+
GitAccess.checkout_file(
202+
self.checkout_dir / "script-languages",
203+
self.custom_conda_file.relative_to(self.checkout_dir),
204+
)
205+
245206
def export(self) -> None:
246207
"""
247208
Exports the current SLC to the export directory.
@@ -333,7 +294,7 @@ def append_custom_pip_packages(self, pip_packages: list[PipPackageDefinition]):
333294
Note: This method is not idempotent: Multiple calls with the same
334295
package definitions will result in duplicated entries.
335296
"""
336-
_append_packages(self.custom_pip_file, PipPackageDefinition, pip_packages)
297+
append_packages(self.custom_pip_file, PipPackageDefinition, pip_packages)
337298

338299
def append_custom_conda_packages(
339300
self, conda_packages: list[CondaPackageDefinition]
@@ -343,7 +304,7 @@ def append_custom_conda_packages(
343304
Note: This method is not idempotent: Multiple calls with the same
344305
package definitions will result in duplicated entries.
345306
"""
346-
_append_packages(self.custom_conda_file, CondaPackageDefinition, conda_packages)
307+
append_packages(self.custom_conda_file, CondaPackageDefinition, conda_packages)
347308

348309
@property
349310
def docker_image_tags(self) -> list[str]:

test/integration/ordinary/itest_slc.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,3 +356,39 @@ def test_fresh_clone_if_repo_is_corrupt(
356356
assert not marker_file.exists()
357357
expected_error = f"Git repository is inconsistent: {temp_cwd_func}/slc_workspace/{slc_name}/git-clone. Doing a fresh clone..."
358358
assert expected_error in caplog.text
359+
360+
361+
def test_restore_pip_custom_file(
362+
temp_cwd_func,
363+
secrets_module: Secrets,
364+
default_flavor: str,
365+
compression_strategy: CompressionStrategy,
366+
):
367+
slc_name = "slc_restore_pip_custom_file"
368+
slc = create_slc(secrets_module, slc_name, default_flavor, compression_strategy)
369+
370+
slc.append_custom_pip_packages([PipPackageDefinition("my_test_package", "1.2.3")])
371+
custom_pip_file_content = slc.custom_pip_file.read_text()
372+
assert "my_test_package" in custom_pip_file_content
373+
slc.restore_custom_pip_file()
374+
custom_pip_file_content = slc.custom_pip_file.read_text()
375+
assert "my_test_package" not in custom_pip_file_content
376+
377+
378+
def test_restore_conda_custom_file(
379+
temp_cwd_func,
380+
secrets_module: Secrets,
381+
compression_strategy: CompressionStrategy,
382+
):
383+
slc_name = "slc_restore_conda_custom_file"
384+
flavor = DEFAULT_FLAVORS[PackageManager.CONDA]
385+
slc = create_slc(secrets_module, slc_name, flavor, compression_strategy)
386+
387+
slc.append_custom_conda_packages(
388+
[CondaPackageDefinition("my_test_package", "1.2.3")]
389+
)
390+
custom_conda_file_content = slc.custom_conda_file.read_text()
391+
assert "my_test_package" in custom_conda_file_content
392+
slc.restore_custom_conda_file()
393+
custom_conda_file_content = slc.custom_conda_file.read_text()
394+
assert "my_test_package" not in custom_conda_file_content

test/unit/slc/utest_slc.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import contextlib
22
import logging
3-
import shutil
43
import textwrap
54
from collections.abc import Generator
65
from pathlib import Path
@@ -11,6 +10,7 @@
1110
from typing import (
1211
Callable,
1312
)
13+
from unittest import mock
1414
from unittest.mock import (
1515
Mock,
1616
create_autospec,
@@ -53,6 +53,7 @@ def create_dir(url: str, dir: Path, branch: str):
5353

5454
mock = create_autospec(GitAccess)
5555
monkeypatch.setattr(workspace, "GitAccess", mock)
56+
monkeypatch.setattr(script_language_container, "GitAccess", mock)
5657
mock.clone_from_recursively.side_effect = create_dir
5758
yield mock
5859

@@ -613,3 +614,39 @@ def test_make_fresh_clone_if_repo_is_corrupt(
613614
f"Git repository is inconsistent: something went wrong. Doing a fresh clone..."
614615
in caplog.text
615616
)
617+
618+
619+
def test_restore_pip_package_file(sample_slc_name, slc_factory_create, git_access_mock):
620+
flavor = "Strawberry"
621+
with slc_factory_create.context(slc_name=sample_slc_name, flavor=flavor) as slc:
622+
with git_access_mock(flavor) as git_access:
623+
slc.restore_custom_pip_file()
624+
assert git_access.checkout_file.mock_calls == [
625+
mock.call(
626+
slc.workspace.git_clone_path / "script-languages",
627+
Path("flavors")
628+
/ flavor
629+
/ "flavor_customization"
630+
/ "packages"
631+
/ "python3_pip_packages",
632+
)
633+
]
634+
635+
636+
def test_restore_conda_package_file(
637+
sample_slc_name, slc_factory_create, git_access_mock
638+
):
639+
flavor = "Strawberry"
640+
with slc_factory_create.context(slc_name=sample_slc_name, flavor=flavor) as slc:
641+
with git_access_mock(flavor) as git_access:
642+
slc.restore_custom_conda_file()
643+
assert git_access.checkout_file.mock_calls == [
644+
mock.call(
645+
slc.workspace.git_clone_path / "script-languages",
646+
Path("flavors")
647+
/ flavor
648+
/ "flavor_customization"
649+
/ "packages"
650+
/ "conda_packages",
651+
)
652+
]

0 commit comments

Comments
 (0)