diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4b3f1f157..2642fb76e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -29,7 +29,7 @@ "files.trimTrailingWhitespace": true, "python.pythonPath": "/usr/local/bin/python3", "python.formatting.provider": "black", - "python.formatting.blackArgs": ["--target-version", "py311"], + "python.formatting.blackArgs": ["--target-version", "py312"], "python.formatting.blackPath": "/usr/local/bin/black" } } diff --git a/.github/workflows/builder.yml b/.github/workflows/builder.yml index cd7d59116..57adc6684 100644 --- a/.github/workflows/builder.yml +++ b/.github/workflows/builder.yml @@ -33,7 +33,7 @@ on: - setup.py env: - DEFAULT_PYTHON: "3.11" + DEFAULT_PYTHON: "3.12" BUILD_NAME: supervisor BUILD_TYPE: supervisor @@ -75,7 +75,7 @@ jobs: - name: Check if requirements files changed id: requirements run: | - if [[ "${{ steps.changed_files.outputs.all }}" =~ (requirements.txt|build.json) ]]; then + if [[ "${{ steps.changed_files.outputs.all }}" =~ (requirements.txt|build.yaml) ]]; then echo "changed=true" >> "$GITHUB_OUTPUT" fi @@ -108,7 +108,7 @@ jobs: if: needs.init.outputs.requirements == 'true' uses: home-assistant/wheels@2024.01.0 with: - abi: cp311 + abi: cp312 tag: musllinux_1_2 arch: ${{ matrix.arch }} wheels-key: ${{ secrets.WHEELS_KEY }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 5df02e68d..f28e26571 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -8,7 +8,7 @@ on: pull_request: ~ env: - DEFAULT_PYTHON: "3.11" + DEFAULT_PYTHON: "3.12" PRE_COMMIT_CACHE: ~/.cache/pre-commit concurrency: @@ -88,7 +88,7 @@ jobs: - name: Run black run: | . venv/bin/activate - black --target-version py311 --check supervisor tests setup.py + black --target-version py312 --check supervisor tests setup.py lint-dockerfile: name: Check Dockerfile diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b4cf85099..b55b7084b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,16 +1,16 @@ repos: - repo: https://github.com/psf/black - rev: 23.1.0 + rev: 23.12.1 hooks: - id: black args: - --safe - --quiet - --target-version - - py311 + - py312 files: ^((supervisor|tests)/.+)?[^/]+\.py$ - repo: https://github.com/PyCQA/flake8 - rev: 6.0.0 + rev: 7.0.0 hooks: - id: flake8 additional_dependencies: @@ -18,17 +18,17 @@ repos: - pydocstyle==6.3.0 files: ^(supervisor|script|tests)/.+\.py$ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.5.0 hooks: - id: check-executables-have-shebangs stages: [manual] - id: check-json - repo: https://github.com/PyCQA/isort - rev: 5.12.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/asottile/pyupgrade rev: v3.15.0 hooks: - id: pyupgrade - args: [--py311-plus] + args: [--py312-plus] diff --git a/build.yaml b/build.yaml index 2a16ba0a5..83433f80c 100644 --- a/build.yaml +++ b/build.yaml @@ -1,10 +1,10 @@ image: ghcr.io/home-assistant/{arch}-hassio-supervisor build_from: - aarch64: ghcr.io/home-assistant/aarch64-base-python:3.11-alpine3.18 - armhf: ghcr.io/home-assistant/armhf-base-python:3.11-alpine3.18 - armv7: ghcr.io/home-assistant/armv7-base-python:3.11-alpine3.18 - amd64: ghcr.io/home-assistant/amd64-base-python:3.11-alpine3.18 - i386: ghcr.io/home-assistant/i386-base-python:3.11-alpine3.18 + aarch64: ghcr.io/home-assistant/aarch64-base-python:3.12-alpine3.18 + armhf: ghcr.io/home-assistant/armhf-base-python:3.12-alpine3.18 + armv7: ghcr.io/home-assistant/armv7-base-python:3.12-alpine3.18 + amd64: ghcr.io/home-assistant/amd64-base-python:3.12-alpine3.18 + i386: ghcr.io/home-assistant/i386-base-python:3.12-alpine3.18 codenotary: signer: notary@home-assistant.io base_image: notary@home-assistant.io diff --git a/pyproject.toml b/pyproject.toml index 67b7c34c8..db39b5611 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ authors = [ { name = "The Home Assistant Authors", email = "hello@home-assistant.io" }, ] keywords = ["docker", "home-assistant", "api"] -requires-python = ">=3.11.0" +requires-python = ">=3.12.0" [project.urls] "Homepage" = "https://www.home-assistant.io/" diff --git a/requirements_tests.txt b/requirements_tests.txt index 6e61c02e5..866fd96fd 100644 --- a/requirements_tests.txt +++ b/requirements_tests.txt @@ -6,7 +6,7 @@ pre-commit==3.6.0 pydocstyle==6.3.0 pylint==3.0.3 pytest-aiohttp==1.0.5 -pytest-asyncio==0.18.3 +pytest-asyncio==0.23.3 pytest-cov==4.1.0 pytest-timeout==2.2.0 pytest==7.4.4 diff --git a/supervisor/addons/addon.py b/supervisor/addons/addon.py index f5de7f18a..3de78e1c2 100644 --- a/supervisor/addons/addon.py +++ b/supervisor/addons/addon.py @@ -1155,7 +1155,11 @@ class Addon(AddonModel): def _extract_tarfile(): """Extract tar backup.""" with tar_file as backup: - backup.extractall(path=Path(temp), members=secure_path(backup)) + backup.extractall( + path=Path(temp), + members=secure_path(backup), + filter="fully_trusted", + ) try: await self.sys_run_in_executor(_extract_tarfile) diff --git a/supervisor/backups/backup.py b/supervisor/backups/backup.py index b76f52593..e9919a5b9 100644 --- a/supervisor/backups/backup.py +++ b/supervisor/backups/backup.py @@ -315,7 +315,11 @@ class Backup(CoreSysAttributes): def _extract_backup(): """Extract a backup.""" with tarfile.open(self.tarfile, "r:") as tar: - tar.extractall(path=self._tmp.name, members=secure_path(tar)) + tar.extractall( + path=self._tmp.name, + members=secure_path(tar), + filter="fully_trusted", + ) await self.sys_run_in_executor(_extract_backup) @@ -535,7 +539,9 @@ class Backup(CoreSysAttributes): gzip=self.compressed, bufsize=BUF_SIZE, ) as tar_file: - tar_file.extractall(path=origin_dir, members=tar_file) + tar_file.extractall( + path=origin_dir, members=tar_file, filter="fully_trusted" + ) _LOGGER.info("Restore folder %s done", name) except (tarfile.TarError, OSError) as err: _LOGGER.warning("Can't restore folder %s: %s", name, err) diff --git a/supervisor/config.py b/supervisor/config.py index 63d469882..09cca0064 100644 --- a/supervisor/config.py +++ b/supervisor/config.py @@ -1,5 +1,5 @@ """Bootstrap Supervisor.""" -from datetime import datetime +from datetime import UTC, datetime import logging import os from pathlib import Path, PurePath @@ -50,7 +50,7 @@ MOUNTS_CREDENTIALS = PurePath(".mounts_credentials") EMERGENCY_DATA = PurePath("emergency") ADDON_CONFIGS = PurePath("addon_configs") -DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat() +DEFAULT_BOOT_TIME = datetime.fromtimestamp(0, UTC).isoformat() # We filter out UTC because it's the system default fallback # Core also not respect the cotnainer timezone and reset timezones @@ -164,7 +164,7 @@ class CoreConfig(FileConfiguration): boot_time = parse_datetime(boot_str) if not boot_time: - return datetime.utcfromtimestamp(1) + return datetime.fromtimestamp(1, UTC) return boot_time @last_boot.setter diff --git a/supervisor/hardware/helper.py b/supervisor/hardware/helper.py index 3ac3c88f6..8d499fb8c 100644 --- a/supervisor/hardware/helper.py +++ b/supervisor/hardware/helper.py @@ -1,5 +1,5 @@ """Read hardware info from system.""" -from datetime import datetime +from datetime import UTC, datetime import logging from pathlib import Path import re @@ -55,7 +55,7 @@ class HwHelper(CoreSysAttributes): _LOGGER.error("Can't found last boot time!") return None - return datetime.utcfromtimestamp(int(found.group(1))) + return datetime.fromtimestamp(int(found.group(1)), UTC) def hide_virtual_device(self, udev_device: pyudev.Device) -> bool: """Small helper to hide not needed Devices.""" diff --git a/supervisor/homeassistant/module.py b/supervisor/homeassistant/module.py index 36cc52b85..561fd5430 100644 --- a/supervisor/homeassistant/module.py +++ b/supervisor/homeassistant/module.py @@ -411,7 +411,11 @@ class HomeAssistant(FileConfiguration, CoreSysAttributes): def _extract_tarfile(): """Extract tar backup.""" with tar_file as backup: - backup.extractall(path=temp_path, members=secure_path(backup)) + backup.extractall( + path=temp_path, + members=secure_path(backup), + filter="fully_trusted", + ) try: await self.sys_run_in_executor(_extract_tarfile) diff --git a/supervisor/utils/dt.py b/supervisor/utils/dt.py index 47887f00e..c6a0c2667 100644 --- a/supervisor/utils/dt.py +++ b/supervisor/utils/dt.py @@ -1,15 +1,12 @@ """Tools file for Supervisor.""" from contextlib import suppress -from datetime import datetime, timedelta, timezone, tzinfo +from datetime import UTC, datetime, timedelta, timezone, tzinfo import re from typing import Any import zoneinfo import ciso8601 -UTC = timezone.utc - - # Copyright (c) Django Software Foundation and individual contributors. # All rights reserved. # https://github.com/django/django/blob/master/LICENSE @@ -67,7 +64,7 @@ def utcnow() -> datetime: def utc_from_timestamp(timestamp: float) -> datetime: """Return a UTC time from a timestamp.""" - return datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC) + return datetime.fromtimestamp(timestamp, UTC).replace(tzinfo=UTC) def get_time_zone(time_zone_str: str) -> tzinfo | None: diff --git a/tests/api/middleware/test_security.py b/tests/api/middleware/test_security.py index fbdb3ac72..7b44838ee 100644 --- a/tests/api/middleware/test_security.py +++ b/tests/api/middleware/test_security.py @@ -110,7 +110,6 @@ async def test_bad_requests( fail_on_query_string, api_system, caplog: pytest.LogCaptureFixture, - event_loop: asyncio.BaseEventLoop, ) -> None: """Test request paths that should be filtered.""" @@ -122,7 +121,7 @@ async def test_bad_requests( man_params = "" http = urllib3.PoolManager() - resp = await event_loop.run_in_executor( + resp = await asyncio.get_running_loop().run_in_executor( None, http.request, "GET", diff --git a/tests/conftest.py b/tests/conftest.py index 170cd6368..365b4e1e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -293,7 +293,6 @@ async def fixture_all_dbus_services( @pytest.fixture async def coresys( - event_loop, docker, dbus_session_bus, all_dbus_services, @@ -590,7 +589,7 @@ async def backups( ) -> list[Backup]: """Create and return mock backups.""" for i in range(request.param if hasattr(request, "param") else 5): - slug = f"sn{i+1}" + slug = f"sn{i + 1}" temp_tar = Path(tmp_path, f"{slug}.tar") with SecureTarFile(temp_tar, "w"): pass diff --git a/tests/jobs/test_job_decorator.py b/tests/jobs/test_job_decorator.py index 85d09c74b..d33451248 100644 --- a/tests/jobs/test_job_decorator.py +++ b/tests/jobs/test_job_decorator.py @@ -274,9 +274,7 @@ async def test_exception_conditions(coresys: CoreSys): await test.execute() -async def test_execution_limit_single_wait( - coresys: CoreSys, event_loop: asyncio.BaseEventLoop -): +async def test_execution_limit_single_wait(coresys: CoreSys): """Test the single wait job execution limit.""" class TestClass: @@ -302,9 +300,7 @@ async def test_execution_limit_single_wait( await asyncio.gather(*[test.execute(0.1), test.execute(0.1), test.execute(0.1)]) -async def test_execution_limit_throttle_wait( - coresys: CoreSys, event_loop: asyncio.BaseEventLoop -): +async def test_execution_limit_throttle_wait(coresys: CoreSys): """Test the throttle wait job execution limit.""" class TestClass: @@ -339,7 +335,7 @@ async def test_execution_limit_throttle_wait( @pytest.mark.parametrize("error", [None, PluginJobError]) async def test_execution_limit_throttle_rate_limit( - coresys: CoreSys, event_loop: asyncio.BaseEventLoop, error: JobException | None + coresys: CoreSys, error: JobException | None ): """Test the throttle wait job execution limit.""" @@ -379,9 +375,7 @@ async def test_execution_limit_throttle_rate_limit( assert test.call == 3 -async def test_execution_limit_throttle( - coresys: CoreSys, event_loop: asyncio.BaseEventLoop -): +async def test_execution_limit_throttle(coresys: CoreSys): """Test the ignore conditions decorator.""" class TestClass: @@ -414,9 +408,7 @@ async def test_execution_limit_throttle( assert test.call == 1 -async def test_execution_limit_once( - coresys: CoreSys, event_loop: asyncio.BaseEventLoop -): +async def test_execution_limit_once(coresys: CoreSys): """Test the ignore conditions decorator.""" class TestClass: @@ -439,7 +431,7 @@ async def test_execution_limit_once( await asyncio.sleep(sleep) test = TestClass(coresys) - run_task = event_loop.create_task(test.execute(0.3)) + run_task = asyncio.get_running_loop().create_task(test.execute(0.3)) await asyncio.sleep(0.1) with pytest.raises(JobException): @@ -595,7 +587,7 @@ async def test_host_network(coresys: CoreSys): assert await test.execute() -async def test_job_group_once(coresys: CoreSys, event_loop: asyncio.BaseEventLoop): +async def test_job_group_once(coresys: CoreSys): """Test job group once execution limitation.""" class TestClass(JobGroup): @@ -644,7 +636,7 @@ async def test_job_group_once(coresys: CoreSys, event_loop: asyncio.BaseEventLoo return True test = TestClass(coresys) - run_task = event_loop.create_task(test.execute()) + run_task = asyncio.get_running_loop().create_task(test.execute()) await asyncio.sleep(0) # All methods with group limits should be locked @@ -664,7 +656,7 @@ async def test_job_group_once(coresys: CoreSys, event_loop: asyncio.BaseEventLoo assert await run_task -async def test_job_group_wait(coresys: CoreSys, event_loop: asyncio.BaseEventLoop): +async def test_job_group_wait(coresys: CoreSys): """Test job group wait execution limitation.""" class TestClass(JobGroup): @@ -706,6 +698,7 @@ async def test_job_group_wait(coresys: CoreSys, event_loop: asyncio.BaseEventLoo self.other_count += 1 test = TestClass(coresys) + event_loop = asyncio.get_running_loop() run_task = event_loop.create_task(test.execute()) await asyncio.sleep(0) @@ -725,7 +718,7 @@ async def test_job_group_wait(coresys: CoreSys, event_loop: asyncio.BaseEventLoo assert test.other_count == 1 -async def test_job_cleanup(coresys: CoreSys, event_loop: asyncio.BaseEventLoop): +async def test_job_cleanup(coresys: CoreSys): """Test job is cleaned up.""" class TestClass: @@ -745,7 +738,7 @@ async def test_job_cleanup(coresys: CoreSys, event_loop: asyncio.BaseEventLoop): return True test = TestClass(coresys) - run_task = event_loop.create_task(test.execute()) + run_task = asyncio.get_running_loop().create_task(test.execute()) await asyncio.sleep(0) assert coresys.jobs.jobs == [test.job] @@ -758,7 +751,7 @@ async def test_job_cleanup(coresys: CoreSys, event_loop: asyncio.BaseEventLoop): assert test.job.done -async def test_job_skip_cleanup(coresys: CoreSys, event_loop: asyncio.BaseEventLoop): +async def test_job_skip_cleanup(coresys: CoreSys): """Test job is left in job manager when cleanup is false.""" class TestClass: @@ -782,7 +775,7 @@ async def test_job_skip_cleanup(coresys: CoreSys, event_loop: asyncio.BaseEventL return True test = TestClass(coresys) - run_task = event_loop.create_task(test.execute()) + run_task = asyncio.get_running_loop().create_task(test.execute()) await asyncio.sleep(0) assert coresys.jobs.jobs == [test.job] @@ -795,9 +788,7 @@ async def test_job_skip_cleanup(coresys: CoreSys, event_loop: asyncio.BaseEventL assert test.job.done -async def test_execution_limit_group_throttle( - coresys: CoreSys, event_loop: asyncio.BaseEventLoop -): +async def test_execution_limit_group_throttle(coresys: CoreSys): """Test the group throttle execution limit.""" class TestClass(JobGroup): @@ -844,9 +835,7 @@ async def test_execution_limit_group_throttle( assert test2.call == 2 -async def test_execution_limit_group_throttle_wait( - coresys: CoreSys, event_loop: asyncio.BaseEventLoop -): +async def test_execution_limit_group_throttle_wait(coresys: CoreSys): """Test the group throttle wait job execution limit.""" class TestClass(JobGroup): @@ -897,7 +886,7 @@ async def test_execution_limit_group_throttle_wait( @pytest.mark.parametrize("error", [None, PluginJobError]) async def test_execution_limit_group_throttle_rate_limit( - coresys: CoreSys, event_loop: asyncio.BaseEventLoop, error: JobException | None + coresys: CoreSys, error: JobException | None ): """Test the group throttle rate limit job execution limit.""" diff --git a/tox.ini b/tox.ini index 23a28f734..537a4e4bc 100644 --- a/tox.ini +++ b/tox.ini @@ -21,4 +21,4 @@ commands = [testenv:black] basepython = python3 commands = - black --target-version py311 --check supervisor tests setup.py + black --target-version py312 --check supervisor tests setup.py