From b44afaff0efb8ef16d7875640d3462a1addf55f1 Mon Sep 17 00:00:00 2001 From: Snuffy2 Date: Wed, 1 Apr 2026 22:44:57 -0400 Subject: [PATCH] Migrate linting to prek, bump Python to 3.14 Migrate CI and local tooling from pre-commit to prek and update project tooling for Python 3.14. Changes include: - CI: simplified .github/workflows/linters.yml to checkout and run the prek action (removed detailed setup/cache/install steps). - Local tooling docs: AGENTS.md updated to instruct use of prek and prek run -a. - Pre-commit config: .pre-commit-config.yaml updated with additional shellcheck dependency, added a mypy hook and extra dependencies, removed pre-commit.ci config blocks. - pyproject.toml: requires-python set to >=3.14, lint tooling updated to use prek, mypy/ruff configuration adjusted (python_version, target-version, required-version, pydocstyle convention and property-decorators). - Code/tests: small refactors in tests to replace inline lambdas with named helper functions for is_attr_blank/set_attr; several exception handling lines edited in helpers.py, sensor.py and tests (exception clause formatting changed). These changes modernize the lint/test workflow and align tooling with Python 3.14 and the chosen prek-based tooling. --- .github/workflows/linters.yml | 46 ++++------------------------- .pre-commit-config.yaml | 43 ++++++++++++++++----------- AGENTS.md | 16 +++++----- custom_components/places/helpers.py | 2 +- custom_components/places/sensor.py | 2 +- pyproject.toml | 13 ++++---- tests/conftest.py | 2 +- tests/test_update_sensor.py | 40 ++++++++++++++++++++----- 8 files changed, 80 insertions(+), 84 deletions(-) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index a1fa47d6..bcba06c0 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -8,50 +8,16 @@ on: - master workflow_dispatch: +permissions: + contents: read + jobs: linters: name: Run Linters runs-on: ubuntu-latest steps: - name: Checkout Repository - uses: actions/checkout@v6.0.2 - - - name: Debug GitHub Variables - run: | - echo "github.event_name: ${{ github.event_name }}" - echo "github.ref_name: ${{ github.ref_name }}" - echo "github.event.repository.default_branch: ${{ github.event.repository.default_branch }}" - - - name: Setup Python 3 (with caching) - uses: actions/setup-python@v6.2.0 - id: setup-python - with: - python-version: 3.x - cache: 'pip' - cache-dependency-path: | - pyproject.toml - - - name: Install Linting Requirements - run: | - python -m pip install --upgrade pip - python -m pip install --group lint -e . - - - name: Cache pre-commit and mypy - uses: actions/cache@v5.0.4 - with: - path: | - ~/.cache/pre-commit - .mypy_cache - .ruff_cache - key: ${{ runner.os }}-lint-py${{ steps.setup-python.outputs.python-version || '3.x' }}-${{ hashFiles('**/.pre-commit-config.yaml', '**/pyproject.toml') }} - restore-keys: | - ${{ runner.os }}-lint- - - - name: Run pre-commit - uses: pre-commit/action@v3.0.1 - - - name: Run mypy - run: mypy "./custom_components/places/" --install-types --non-interactive --config-file pyproject.toml + uses: actions/checkout@v6 - - uses: pre-commit-ci/lite-action@v1.1.0 - if: always() + - name: Run prek + uses: j178/prek-action@v2 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 629706cd..1f6057be 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,21 +15,37 @@ repos: rev: v1.7.12 hooks: - id: actionlint - # Note: shellcheck cannot directly parse YAML; actionlint extracts workflow - # shell blocks and calls shellcheck when available. - - repo: https://github.com/shellcheck-py/shellcheck-py - rev: v0.11.0.1 - hooks: - - id: shellcheck - # Match by detected shell file type (extensions or shebang) - types: [shell] - args: ['-x'] + additional_dependencies: ['github.com/wasilibs/go-shellcheck/cmd/shellcheck@v0.11.1'] - repo: https://github.com/codespell-project/codespell rev: v2.4.2 hooks: - id: codespell + args: + - --ignore-words-list=hass additional_dependencies: - tomli + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.20.0 + hooks: + - id: mypy + language_version: python3.14 + args: [--config-file=pyproject.toml] + additional_dependencies: + - 'aiohttp' + - 'homeassistant-stubs' + - 'pytest-asyncio' + - 'pytest-cov' + - 'pytest-homeassistant-custom-component' + - 'pytest' + - 'types-cachetools' + - 'types-cffi' + - 'types-greenlet' + - 'types-PyMySQL' + - 'types-pyRFC3339' + - 'types-python-dateutil' + - 'types-PyYAML' + - 'types-requests' + - 'types-setuptools' - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.15.8 hooks: @@ -38,12 +54,3 @@ repos: args: [--fix] # Run the formatter. - id: ruff-format - -ci: - autofix_commit_msg: | - [pre-commit.ci] auto fixes from pre-commit hooks - autofix_prs: true - autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate' - autoupdate_schedule: weekly - skip: [] - submodules: false diff --git a/AGENTS.md b/AGENTS.md index 9b30e39f..9845887b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ - Be concise and explain coding steps briefly when making code changes; include code snippets and tests where relevant. - For non-trivial edits, provide a short plan. For small, low-risk edits, implement and include a one-line summary. - Focus on a single conceptual change at a time when public APIs or multiple modules are affected. -- Maintain project style and Python 3.13+ compatibility. Target latest Home Assistant core. +- Maintain project style and Python 3.14+ compatibility. Target latest Home Assistant core. - If deviating from these guidelines, explicitly state which guideline is deviated from and why. ## Agent permissions and venv policy @@ -39,7 +39,7 @@ ## Coding standards - Add typing annotations to all functions and classes (including return types). -- Add or update docstrings for all files, classes and methods, including private methods. Method docstrings must be in NumPy format. +- Add or update docstrings for all files, classes and methods, including private methods. Method docstrings must be in Google Style. - Preserve existing comments and keep imports at the top of files. - When editing code, prefer fixing root causes over surface patches. - Keep changes minimal and consistent with the codebase style. @@ -54,15 +54,13 @@ ## Local tooling (common commands) -- Use `pre-commit`, `mypy`, and `pytest` configured in the repo. You must run these inside `./.venv`. -- Prefer invoking tooling via `./.venv/bin/python -m ...` rather than relying on global/shell entry points (e.g., `pre-commit`). -- `ruff` is used for linting and formatting but should be called using `pre-commit`. +- Use `prek` and `pytest` configured in the repo. You must run these inside `./.venv`. +- Prefer invoking tooling via `./.venv/bin/python -m ...` rather than relying on global/shell entry points (e.g., `prek`). +- `ruff` is used for linting and formatting but should be called using `prek`. - Run tests: - `./.venv/bin/python -m pytest` -- Run pre-commit on all files (includes `ruff`): - - `./.venv/bin/python -m pre_commit run --all-files` -- Run mypy (use repo configuration): - - `./.venv/bin/python -m mypy` +- Run prek on all files (includes `ruff` and `mypy`): + - `./.venv/bin/python -m prek run -a` ## Testing diff --git a/custom_components/places/helpers.py b/custom_components/places/helpers.py index 57104bb9..c8bd935d 100644 --- a/custom_components/places/helpers.py +++ b/custom_components/places/helpers.py @@ -65,7 +65,7 @@ def is_float(value: Any) -> bool: return False try: float(value) - except (ValueError, TypeError): + except ValueError, TypeError: return False else: return True diff --git a/custom_components/places/sensor.py b/custom_components/places/sensor.py index 541f096b..8629ea5f 100644 --- a/custom_components/places/sensor.py +++ b/custom_components/places/sensor.py @@ -463,7 +463,7 @@ def get_attr_safe_float(self, attr: str | None, default: Any | None = None) -> f return 0.0 try: return float(value) - except (TypeError, ValueError): + except TypeError, ValueError: return 0.0 def get_attr_safe_list(self, attr: str | None, default: Any | None = None) -> list: diff --git a/pyproject.toml b/pyproject.toml index e3e1e189..9965b24f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ name = "places" dynamic = ["version"] description = "Places Home Assistant integration" readme = "README.md" -requires-python = ">=3.13" +requires-python = ">=3.14" dependencies = [ "aiohttp", "cachetools", @@ -20,7 +20,7 @@ include = ["custom_components*"] lint = [ "homeassistant-stubs", "mypy", - "pre-commit", + "prek", "ruff", "types-cachetools", "types-cffi", @@ -69,7 +69,7 @@ parallel = false show_missing = true [tool.mypy] -python_version = "3.13" +python_version = "3.14" platform = "linux" local_partial_types = true strict_equality = true @@ -105,8 +105,8 @@ line-length = 100 indent-width = 4 fix = true force-exclude = true -target-version = "py313" -required-version = ">=0.8.0" +target-version = "py314" +required-version = ">=0.15.0" [tool.ruff.format] quote-style = "double" @@ -351,4 +351,5 @@ split-on-trailing-comma = false max-complexity = 25 [tool.ruff.lint.pydocstyle] -property-decorators = ["propcache.cached_property"] +convention = "google" +property-decorators = ["propcache.api.cached_property"] diff --git a/tests/conftest.py b/tests/conftest.py index 7e976c20..deb287e3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -103,7 +103,7 @@ def _get_attr_safe_float_default(attr, default=None): return float(default) if default is not None else 0.0 try: return float(val) - except (TypeError, ValueError): + except TypeError, ValueError: if isinstance(default, MagicMock): return 0.0 return float(default) if default is not None else 0.0 diff --git a/tests/test_update_sensor.py b/tests/test_update_sensor.py index 60a106f7..de58d78c 100644 --- a/tests/test_update_sensor.py +++ b/tests/test_update_sensor.py @@ -1031,10 +1031,17 @@ async def test_calculate_distances_not_all_attrs_set( ): """Test calculate_distances does NOT set distance attributes if any required attribute is blank.""" updater = make_updater(mock_hass, mock_config_entry, sensor) + # Patch is_attr_blank to return True for the blank_attr, False otherwise - sensor.is_attr_blank = lambda k: k == blank_attr + def is_attr_blank(key: str) -> bool: + return key == blank_attr + + def set_attr(key: str, value: object) -> None: + sensor.attrs[key] = value + + sensor.is_attr_blank = is_attr_blank # Patch set_attr to update attrs - sensor.set_attr = lambda k, v: sensor.attrs.__setitem__(k, v) + sensor.set_attr = set_attr await updater.calculate_distances() # None of the distance attributes should be set assert ATTR_DISTANCE_FROM_HOME_M not in sensor.attrs @@ -1048,13 +1055,16 @@ async def test_calculate_distances_distance_from_home_m_blank(mock_hass, mock_co updater = make_updater(mock_hass, mock_config_entry, sensor) # Patch is_attr_blank so ATTR_DISTANCE_FROM_HOME_M is blank after calculation - def is_attr_blank(key): + def is_attr_blank(key: str) -> bool: # Only ATTR_DISTANCE_FROM_HOME_M is blank return key == ATTR_DISTANCE_FROM_HOME_M + def set_attr(key: str, value: object) -> None: + sensor.attrs[key] = value + sensor.is_attr_blank = is_attr_blank # Patch set_attr to update attrs - sensor.set_attr = lambda k, v: sensor.attrs.__setitem__(k, v) + sensor.set_attr = set_attr # Patch get_attr_safe_float to return valid floats sensor.get_attr_safe_float = lambda k: 1.0 # Patch all required attributes to not blank except ATTR_DISTANCE_FROM_HOME_M @@ -1086,9 +1096,16 @@ async def test_calculate_travel_distance_variants( sensor.get_attr_safe_float = lambda k: 1.0 if mode == "missing_old_coord": - sensor.is_attr_blank = lambda k: k == blank_attr + + def is_attr_blank(key: str) -> bool: + return key == blank_attr + + def set_attr(key: str, value: object) -> None: + sensor.attrs[key] = value + + sensor.is_attr_blank = is_attr_blank # Ensure set_attr updates attrs for this branch - sensor.set_attr = lambda k, v: sensor.attrs.__setitem__(k, v) + sensor.set_attr = set_attr await updater.calculate_travel_distance() assert sensor.attrs[ATTR_DIRECTION_OF_TRAVEL] == expected_direction assert sensor.attrs[ATTR_DISTANCE_TRAVELED_M] == 0 @@ -1096,12 +1113,19 @@ async def test_calculate_travel_distance_variants( return if mode == "blank_traveled_m": - sensor.is_attr_blank = lambda k: k == blank_attr + + def is_attr_blank(key: str) -> bool: + return key == blank_attr + + def set_attr(key: str, value: object) -> None: + sensor.attrs[key] = value + + sensor.is_attr_blank = is_attr_blank # Provide old coords so calculation proceeds for attr in [ATTR_LATITUDE_OLD, ATTR_LONGITUDE_OLD]: sensor.attrs[attr] = 1.0 # Ensure set_attr updates attrs for this branch - sensor.set_attr = lambda k, v: sensor.attrs.__setitem__(k, v) + sensor.set_attr = set_attr await updater.calculate_travel_distance() assert ATTR_DISTANCE_TRAVELED_M in sensor.attrs assert ATTR_DISTANCE_TRAVELED_MI not in sensor.attrs