diff --git a/.github/workflows/python-static-analysis-and-test.yml b/.github/workflows/python-static-analysis-and-test.yml index 512681c..b8ee105 100644 --- a/.github/workflows/python-static-analysis-and-test.yml +++ b/.github/workflows/python-static-analysis-and-test.yml @@ -52,15 +52,6 @@ jobs: json_ver: ['json', 'json5'] os: ['ubuntu-latest', 'windows-latest'] python: ['3.7', '3.8', '3.9', '3.10', '3.11'] - # Works around the depreciation of python 3.6 for ubuntu - # https://github.com/actions/setup-python/issues/544 - include: - - json_ver: 'json' - os: 'ubuntu-20.04' - python: '3.6' - - json_ver: 'json5' - os: 'ubuntu-20.04' - python: '3.6' runs-on: ${{ matrix.os }} diff --git a/README.md b/README.md index d47acbb..2ba91ac 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ home directory on other platforms. ## Installing -Hab is installed using pip. It requires python 3.6 or above. It's recommended +Hab is installed using pip. It requires python 3.7 or above. It's recommended that you add the path to your python's bin or Scripts folder to the `PATH` environment variable so you can simply run the `hab` command. diff --git a/hab/parsers/lazy_distro_version.py b/hab/parsers/lazy_distro_version.py index a1b0b17..4c13e75 100644 --- a/hab/parsers/lazy_distro_version.py +++ b/hab/parsers/lazy_distro_version.py @@ -9,7 +9,7 @@ logger = logging.getLogger(__name__) -class DistroPaths: +class DistroPath: __slots__ = ("distro", "hab_filename", "root", "site") def __init__(self, distro, root, relative=NotSet, site=None): @@ -83,8 +83,8 @@ def install(self, dest, replace=False, relative=NotSet): contains this distro this error is raised and it is not installed. Unless `replace` is set to True. """ - if not isinstance(dest, DistroPaths): - dest = DistroPaths(self, dest, relative=relative, site=self.resolver.site) + if not isinstance(dest, DistroPath): + dest = DistroPath(self, dest, relative=relative, site=self.resolver.site) installed = self.installed(dest, relative=relative) if installed: @@ -99,8 +99,8 @@ def install(self, dest, replace=False, relative=NotSet): self.resolver.clear_caches() def installed(self, dest, relative=NotSet): - if not isinstance(dest, DistroPaths): - dest = DistroPaths(self, dest, relative=relative, site=self.resolver.site) + if not isinstance(dest, DistroPath): + dest = DistroPath(self, dest, relative=relative, site=self.resolver.site) return dest.hab_filename.exists() def _ensure_loaded(self): diff --git a/setup.cfg b/setup.cfg index bdd8693..2392826 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,7 +38,7 @@ install_requires = importlib-metadata packaging>=20.0 setuptools-scm[toml]>=4 -python_requires = >=3.6 +python_requires = >=3.7 include_package_data = True scripts = bin/.hab-complete.bash diff --git a/tests/test_lazy_distro_version.py b/tests/test_lazy_distro_version.py new file mode 100644 index 0000000..0745591 --- /dev/null +++ b/tests/test_lazy_distro_version.py @@ -0,0 +1,151 @@ +import pytest +from packaging.requirements import Requirement +from packaging.version import Version + +from hab import DistroMode +from hab.errors import InstallDestinationExistsError +from hab.parsers.lazy_distro_version import DistroPath, LazyDistroVersion + + +def test_distro_path(zip_distro_sidecar, helpers, tmp_path): + resolver = helpers.render_resolver( + "site_distro_zip_sidecar.json", + tmp_path, + zip_root=zip_distro_sidecar.root.as_posix(), + ) + with resolver.distro_mode_override(DistroMode.Downloaded): + distro = resolver.find_distro("dist_a==0.2") + + # Passing root as a string converts it to a pathlib.Path object. + dpath = DistroPath( + distro, str(tmp_path), relative="{distro_name}-v{version}", site=resolver.site + ) + # Test that the custom relative string, it used to generate root + assert dpath.root == tmp_path / "dist_a-v0.2" + assert dpath.hab_filename == tmp_path / "dist_a-v0.2" / ".hab.json" + + # If site and relative are not passed the default is used + dpath = DistroPath(distro, tmp_path) + assert dpath.root == tmp_path / "dist_a" / "0.2" + assert dpath.hab_filename == tmp_path / "dist_a" / "0.2" / ".hab.json" + + # Test that site settings are respected when not passing relative + resolver.site.downloads["relative_path"] = "parent/{distro_name}/child/{version}" + dpath = DistroPath(distro, tmp_path, site=resolver.site) + assert dpath.root == tmp_path / "parent" / "dist_a" / "child" / "0.2" + assert ( + dpath.hab_filename + == tmp_path / "parent" / "dist_a" / "child" / "0.2" / ".hab.json" + ) + + +def test_is_lazy(zip_distro_sidecar, helpers, tmp_path): + """Check that a LazyDistroVersion doesn't automatically load all data.""" + resolver = helpers.render_resolver( + "site_distro_zip_sidecar.json", + tmp_path, + zip_root=zip_distro_sidecar.root.as_posix(), + ) + with resolver.distro_mode_override(DistroMode.Downloaded): + distro = resolver.find_distro("dist_a==0.1") + + frozen_data = dict( + context=["dist_a"], + name="dist_a==0.1", + version=Version("0.1"), + ) + filename = zip_distro_sidecar.root / "dist_a_v0.1.hab.json" + + # The find_distro call should have called load but does not actually load data + assert isinstance(distro, LazyDistroVersion) + assert distro._loaded is False + assert distro.context == ["dist_a"] + assert distro.filename == filename + assert distro.frozen_data == frozen_data + assert distro.name == "dist_a==0.1" + + # Calling _ensure_loaded actually loads the full distro from the finder's data + data = distro._ensure_loaded() + assert distro._loaded is True + assert isinstance(data, dict) + assert distro.name == "dist_a==0.1" + + # If called a second time, then nothing extra is done and no data is returned. + assert distro._ensure_loaded() is None + + +def test_bad_kwargs(): + """Test that the proper error is raised if you attempt to init with a filename.""" + match = "Passing filename to this class is not supported." + with pytest.raises(ValueError, match=match): + LazyDistroVersion(None, None, "filename") + + with pytest.raises(ValueError, match=match): + LazyDistroVersion(None, None, filename="a/filename") + + +@pytest.mark.parametrize( + "prop,check", + (("distros", {"dist_b": Requirement("dist_b")}),), +) +def test_lazy_hab_property(prop, check, zip_distro_sidecar, helpers, tmp_path): + """Check that a LazyDistroVersion doesn't automatically load all data.""" + resolver = helpers.render_resolver( + "site_distro_zip_sidecar.json", + tmp_path, + zip_root=zip_distro_sidecar.root.as_posix(), + ) + with resolver.distro_mode_override(DistroMode.Downloaded): + distro = resolver.find_distro("dist_a==0.2") + + # Calling a lazy getter ensures the data is loaded + assert distro._loaded is False + value = getattr(distro, prop) + assert distro._loaded is True + assert value == check + + # You can call the lazy getter repeatedly + value = getattr(distro, prop) + assert value == check + + +def test_install(zip_distro_sidecar, helpers, tmp_path): + """Check that a LazyDistroVersion doesn't automatically load all data.""" + resolver = helpers.render_resolver( + "site_distro_zip_sidecar.json", + tmp_path, + zip_root=zip_distro_sidecar.root.as_posix(), + ) + with resolver.distro_mode_override(DistroMode.Downloaded): + distro = resolver.find_distro("dist_a==0.2") + dest_root = resolver.site.downloads["install_root"] + distro_root = dest_root / "dist_a" / "0.2" + hab_json = distro_root / ".hab.json" + + # The distro is not currently installed. This also tests that it can + # auto-cast to DistroPath + assert not distro.installed(dest_root) + + # Install will clear the cache, ensure its populated + assert resolver._downloadable_distros is not None + # Install the distro using LazyDistroVersion + distro.install(dest_root) + assert distro.installed(dest_root) + assert hab_json.exists() + # Check that the cache was cleared by the install function + assert resolver._downloadable_distros is None + + # Test that if the distro is already installed, an error is raised + with pytest.raises(InstallDestinationExistsError) as excinfo: + distro.install(dest_root) + assert excinfo.value.filename == distro_root + + # Test forced replacement of an existing distro by creating an extra file + extra_file = distro_root / "extra_file.txt" + extra_file.touch() + # This won't raise the exception, but will remove the old distro + distro.install(dest_root, replace=True) + assert hab_json.exists() + assert distro.installed(dest_root) + + assert not extra_file.exists() diff --git a/tox.ini b/tox.ini index 2d086c7..a738dd3 100644 --- a/tox.ini +++ b/tox.ini @@ -1,5 +1,5 @@ [tox] -envlist = begin,py{36,37,38,39,310,311}-{json,json5},end,black,flake8 +envlist = begin,py{37,38,39,310,311}-{json,json5},end,black,flake8 skip_missing_interpreters = True skipsdist = True @@ -29,14 +29,14 @@ commands = coverage erase -[testenv:py{36,37,38,39,310,311}-{json,json5}] +[testenv:py{37,38,39,310,311}-{json,json5}] depends = begin [testenv:end] basepython = python3 depends = begin - py{36,37,38,39,310,311}-{json,json5} + py{37,38,39,310,311}-{json,json5} parallel_show_output = True deps = coverage