Skip to content

Commit

Permalink
WIP: Remove python 3.6 support code.
Browse files Browse the repository at this point in the history
CentOS is rapidly being dropped so no longer a need to support it.
Identical pathlib requirements don't compare equal in 3.6 and below.
  • Loading branch information
MHendricks committed Dec 23, 2024
1 parent 106fea5 commit a9705f6
Show file tree
Hide file tree
Showing 10 changed files with 164 additions and 83 deletions.
9 changes: 0 additions & 9 deletions .github/workflows/python-static-analysis-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ repos:
rev: v2.5.0
hooks:
- id: setup-cfg-fmt
args: [--min-py3-version=3.6]
args: [--min-py3-version=3.7]

- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.6.0
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
9 changes: 1 addition & 8 deletions hab/launcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,6 @@

from . import utils

try:
CREATE_NO_WINDOW = subprocess.CREATE_NO_WINDOW
except AttributeError:
# This constant comes from the WindowsAPI, but is not
# defined in subprocess until python 3.7
CREATE_NO_WINDOW = 0x08000000


class Launcher(subprocess.Popen):
"""Runs cmd using subprocess.Popen enabling stdout/err/in redirection.
Expand Down Expand Up @@ -42,6 +35,6 @@ def __init__(self, args, **kwargs):

# If this is a pythonw process, because there is no current window
# for stdout, any subprocesses will try to create a new window
kwargs.setdefault("creationflags", CREATE_NO_WINDOW)
kwargs.setdefault("creationflags", subprocess.CREATE_NO_WINDOW)

super().__init__(args, **kwargs)
10 changes: 2 additions & 8 deletions hab/user_prefs.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,16 +131,10 @@ def save(self):

@classmethod
def _fromisoformat(cls, value):
"""Calls `datetime.fromisoforamt` if possible otherwise replicates
its basic requirements (for python 3.6 support).
"""
"""Calls `datetime.fromisoformat` unless a datetime is passed."""
if isinstance(value, datetime.datetime):
return value
try:
return datetime.datetime.fromisoformat(value)
except AttributeError:
iso_format = r"%Y-%m-%dT%H:%M:%S.%f"
return datetime.datetime.strptime(value, iso_format)
return datetime.datetime.fromisoformat(value)

def uri_check(self):
"""Returns the uri saved in preferences. It will only do that if enabled
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
47 changes: 3 additions & 44 deletions tests/test_launch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import functools
import os
import re
import site
import subprocess
import sys

Expand All @@ -15,40 +13,6 @@ class Topen(subprocess.Popen):
"""A custom subclass of Popen."""


def missing_annotations_hack(function):
"""Decorator that works around a missing annotations module in python 3.6.
Allows for calling subprocess calls in python 3.6 that would normally fail
with a `SyntaxError: future feature annotations is not defined` exception.
Works by temporarily removing the `_virtualenv.pth` file in the venv's
site-packages.
TODO: Figure out a better method until we can drop CentOS requirement.
"""

@functools.wraps(function)
def new_function(*args, **kwargs):
if sys.version_info.minor != 6:
return function(*args, **kwargs)

site_packages = site.getsitepackages()
for path in site_packages:
pth = os.path.join(path, "_virtualenv.pth")
if os.path.exists(pth):
os.rename(pth, f"{pth}.bak")
try:
ret = function(*args, **kwargs)
finally:
for path in site_packages:
pth = os.path.join(path, "_virtualenv.pth.bak")
if os.path.exists(pth):
os.rename(pth, pth[:-3])
return ret

return new_function


def test_launch(resolver):
"""Check the Config.launch method."""
cfg = resolver.resolve("app/aliased/mod")
Expand All @@ -72,7 +36,6 @@ def test_launch(resolver):
assert "\n".join(check) in proc.output_stdout


@missing_annotations_hack
def test_launch_str(resolver):
cfg = resolver.resolve("app/aliased/mod")

Expand Down Expand Up @@ -212,14 +175,11 @@ class TestCliExitCodes:
# but is used to ensure that exit-codes are returned to the calling process.
py_cmd = "print('Running...');import sys;print(sys);sys.exit({code})"
output_text = "Running...\n<module 'sys' (built-in)>\n"
run_kwargs = dict(stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5)
if sys.version_info.minor >= 7:
run_kwargs["text"] = True
else:
run_kwargs["universal_newlines"] = True
run_kwargs = dict(
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5, text=True
)

@pytest.mark.skipif(sys.platform != "win32", reason="only applies on windows")
@missing_annotations_hack
def test_bat(self, config_root, exit_code, tmp_path):
hab_bin = (config_root / ".." / "bin" / "hab.bat").resolve()
# fmt: off
Expand Down Expand Up @@ -247,7 +207,6 @@ def test_bat(self, config_root, exit_code, tmp_path):
os.getenv("GITHUB_ACTIONS") == "true",
reason="PowerShell tests timeout when run in a github action",
)
@missing_annotations_hack
def test_ps1(self, config_root, exit_code):
# -File is needed to get the exit-code from powershell, and requires a full path
script = (config_root / ".." / "bin" / "hab.ps1").resolve()
Expand Down
151 changes: 151 additions & 0 deletions tests/test_lazy_distro_version.py
Original file line number Diff line number Diff line change
@@ -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()
9 changes: 1 addition & 8 deletions tests/test_resolver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import os
import pathlib
import sys
from collections import OrderedDict
from pathlib import Path
from zipfile import ZipFile
Expand Down Expand Up @@ -624,13 +623,7 @@ def test_forced_requirements(
# Ensure this is a deepcopy of forced and ensure the values are equal
assert resolver_forced.__forced_requirements__ is not forced
for k, v in resolver_forced.__forced_requirements__.items():
if sys.version_info.minor == 6:
# NOTE: packaging>22.0 doesn't support equal checks for Requirement
# objects. Python 3.6 only has a 21 release, so we have to compare str
# TODO: Once we drop py3.6 support drop this if statement
assert str(forced[k]) == str(v)
else:
assert forced[k] == v
assert forced[k] == v
assert forced[k] is not v

# Check that forced_requirements work if the config defines zero distros
Expand Down
6 changes: 3 additions & 3 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down

0 comments on commit a9705f6

Please sign in to comment.