Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/hassfest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,5 @@ jobs:
validate:
runs-on: "ubuntu-latest"
steps:
- uses: "actions/checkout@v4"
- uses: "actions/checkout@v7"
- uses: home-assistant/actions/hassfest@master
6 changes: 3 additions & 3 deletions .github/workflows/pythonpackage.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ jobs:
strategy:
max-parallel: 4
matrix:
python-version: ["3.10"]
python-version: ["3.14"]

steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v7
with:
python-version: ${{ matrix.python-version }}

Expand Down
18 changes: 18 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Python bytecode
__pycache__/
*.py[cod]

# Test and coverage output
.coverage
.coverage.*
htmlcov/
coverage.xml
.pytest_cache/

# Tooling caches
.mypy_cache/

# Local environments
.python-version
.venv/
venv/
34 changes: 17 additions & 17 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,55 +1,55 @@
repos:
- repo: https://github.com/asottile/pyupgrade
rev: v2.7.4
rev: v3.22.0
hooks:
- id: pyupgrade
args: [--py37-plus]
args: [--py314-plus]
- repo: https://github.com/psf/black
rev: 22.3.0
rev: 26.10.1
hooks:
- id: black
args:
- --safe
- --quiet
files: ^((homeassistant|script|tests)/.+)?[^/]+\.py$
files: ^((custom_components|tests)/.+)?[^/]+\.py$
- repo: https://github.com/codespell-project/codespell
rev: v1.17.1
rev: v2.4.3
hooks:
- id: codespell
args:
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing
- --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,thirdparty,ue,uint,visability,wan,wanna,withing
- --skip="./.*,*.csv,*.json"
- --quiet-level=2
exclude_types: [csv, json]
- repo: https://github.com/pycqa/flake8.git
rev: 3.9.2
- repo: https://github.com/PyCQA/flake8
rev: 7.4.1
hooks:
- id: flake8
additional_dependencies:
- flake8-docstrings==1.5.0
- pydocstyle==5.0.2
files: ^(homeassistant|script|tests)/.+\.py$
- flake8-docstrings==1.7.0
- pydocstyle==6.3.0
files: ^(custom_components|tests)/.+\.py$
- repo: https://github.com/PyCQA/bandit
rev: 1.6.2
rev: 1.9.4
hooks:
- id: bandit
args:
- --quiet
- --format=custom
- --configfile=tests/bandit.yaml
files: ^(homeassistant|script|tests)/.+\.py$
- repo: https://github.com/pre-commit/mirrors-isort
rev: v5.6.4
files: ^(custom_components|tests)/.+\.py$
- repo: https://github.com/PyCQA/isort
rev: 9.0.2
hooks:
- id: isort
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.3.0
rev: v6.0.0
hooks:
- id: check-executables-have-shebangs
stages: [manual]
- id: check-json
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v0.790
rev: v2.4.0
hooks:
- id: mypy
args:
Expand Down
54 changes: 38 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,13 @@ The tutorial walks through the steps to create a custom component for use in Hom
An integration that monitors [GitHub](https://github.com/) repositories specified in a `configuration.yaml` file
or optionally via the Integrations UI.

Requires Home Assistant 2024.11.0 or newer.

## Running Tests

To run the test suite create a virtualenv (I recommend checking out [pyenv](https://github.com/pyenv/pyenv) and [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv) for this) and install the test requirements.
The test suite requires Python 3.14, the version required by current releases of Home Assistant. Create a virtualenv
(I recommend checking out [pyenv](https://github.com/pyenv/pyenv) and [pyenv-virtualenv](https://github.com/pyenv/pyenv-virtualenv)
for this) and install the test requirements.

```bash
$ pip install -r requirements.test.txt
Expand All @@ -34,26 +38,44 @@ the test suite.

```bash
$ pytest
Test session starts (platform: linux, Python 3.7.5, pytest 5.4.3, pytest-sugar 0.9.4)
rootdir: /home/aaron/projects/github-custom, inifile: setup.cfg, testpaths: tests
plugins: forked-1.3.0, timeout-1.4.2, cov-2.10.1, aiohttp-0.3.0, requests-mock-1.8.0, xdist-2.1.0, sugar-0.9.4, test-groups-1.0.3, homeassistant-custom-component-0.0.20
collecting ...
tests/test_config_flow.py ✓✓✓✓✓✓✓✓✓✓✓ 85% ████████▌
tests/test_sensor.py ✓✓ 100% ██████████

----------- coverage: platform linux, python 3.7.5-final-0 -----------
============================= test session starts ==============================
platform linux -- Python 3.14.8, pytest-9.0.3, pluggy-1.7.0
rootdir: /home/aaron/projects/github-custom-component-tutorial
configfile: setup.cfg
testpaths: tests
plugins: codspeed-5.0.3, aiohttp-1.1.1, homeassistant-custom-component-0.13.374, xdist-3.8.0, asyncio-1.4.0, cov-7.1.0, anyio-4.15.1, timeout-2.4.0, pytest_freezer-0.4.9, respx-0.23.1, github-actions-annotate-failures-0.4.2, picked-0.5.1, requests-mock-1.12.1, socket-0.8.1, unordered-0.8.0, syrupy-6.1.1
asyncio: mode=Mode.AUTO, debug=False, asyncio_default_fixture_loop_scope=function, asyncio_default_test_loop_scope=function
collected 14 items

tests/test_config_flow.py ............ [ 85%]
tests/test_sensor.py .. [100%]

================================ tests coverage ================================
_______________ coverage: platform linux, python 3.14.8-final-0 ________________

Name Stmts Miss Cover Missing
------------------------------------------------------------------------------
custom_components/__init__.py 0 0 100%
custom_components/github_custom/__init__.py 12 0 100%
custom_components/github_custom/config_flow.py 23 0 100%
custom_components/github_custom/__init__.py 24 1 96% 36
custom_components/github_custom/config_flow.py 60 19 68% 49-50, 148-186
custom_components/github_custom/const.py 18 0 100%
custom_components/github_custom/sensor.py 97 5 95% 86-89, 121
custom_components/github_custom/sensor.py 99 5 95% 72, 86-89
------------------------------------------------------------------------------
TOTAL 150 5 97%
TOTAL 201 25 88%
Required test coverage of 84.0% reached. Total coverage: 87.56%
============================== 14 passed in 0.41s ==============================
```

## Linting

Required test coverage of 93.0% reached. Total coverage: 96.67%
Linting and formatting checks are run with [pre-commit](https://pre-commit.com/). To run them against the whole repo:

Results (0.73s):
13 passed
```bash
$ pre-commit run --all-files
```

To have them run automatically on every commit:

```bash
$ pre-commit install
```
1 change: 1 addition & 0 deletions custom_components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Custom components."""
2 changes: 1 addition & 1 deletion custom_components/github_custom/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""GitHub Custom Component."""
import asyncio

import logging

from homeassistant import config_entries, core
Expand Down
31 changes: 15 additions & 16 deletions custom_components/github_custom/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Config flow for the GitHub Custom component."""

from copy import deepcopy
import logging
from typing import Any, Dict, Optional
from typing import Any

from gidgethub import BadRequest
from gidgethub.aiohttp import GitHubAPI
Expand Down Expand Up @@ -34,7 +36,7 @@


async def validate_path(path: str, access_token: str, hass: core.HassJob) -> None:
"""Validates a GitHub repo path.
"""Validate a GitHub repo path.

Raises a ValueError if the path is invalid.
"""
Expand All @@ -49,7 +51,7 @@ async def validate_path(path: str, access_token: str, hass: core.HassJob) -> Non


async def validate_auth(access_token: str, hass: core.HomeAssistant) -> None:
"""Validates a GitHub access token.
"""Validate a GitHub access token.

Raises a ValueError if the auth token is invalid.
"""
Expand All @@ -64,11 +66,11 @@ async def validate_auth(access_token: str, hass: core.HomeAssistant) -> None:
class GithubCustomConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Github Custom config flow."""

data: Optional[Dict[str, Any]]
data: dict[str, Any] | None

async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
"""Invoked when a user initiates a flow via the user interface."""
errors: Dict[str, str] = {}
async def async_step_user(self, user_input: dict[str, Any] | None = None):
"""Handle a flow initiated by the user via the user interface."""
errors: dict[str, str] = {}
if user_input is not None:
try:
await validate_auth(user_input[CONF_ACCESS_TOKEN], self.hass)
Expand All @@ -85,9 +87,9 @@ async def async_step_user(self, user_input: Optional[Dict[str, Any]] = None):
step_id="user", data_schema=AUTH_SCHEMA, errors=errors
)

async def async_step_repo(self, user_input: Optional[Dict[str, Any]] = None):
async def async_step_repo(self, user_input: dict[str, Any] | None = None):
"""Second step in config flow to add a repo to watch."""
errors: Dict[str, str] = {}
errors: dict[str, str] = {}
if user_input is not None:
# Validate the path.
try:
Expand Down Expand Up @@ -121,20 +123,17 @@ async def async_step_repo(self, user_input: Optional[Dict[str, Any]] = None):
@callback
def async_get_options_flow(config_entry):
"""Get the options flow for this handler."""
return OptionsFlowHandler(config_entry)
return OptionsFlowHandler()


class OptionsFlowHandler(config_entries.OptionsFlow):
"""Handles options flow for the component."""

def __init__(self, config_entry: config_entries.ConfigEntry) -> None:
self.config_entry = config_entry

async def async_step_init(
self, user_input: Dict[str, Any] = None
) -> Dict[str, Any]:
self, user_input: dict[str, Any] = None
) -> dict[str, Any]:
"""Manage the options for the custom component."""
errors: Dict[str, str] = {}
errors: dict[str, str] = {}
# Grab all configured repos from the entity registry so we can populate the
# multi-select dropdown that will allow a user to remove a repo.
entity_registry = async_get(self.hass)
Expand Down
2 changes: 2 additions & 0 deletions custom_components/github_custom/const.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"""Constants for the GitHub Custom component."""

DOMAIN = "github_custom"

ATTR_CLONES = "clones"
Expand Down
11 changes: 5 additions & 6 deletions custom_components/github_custom/sensor.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""GitHub sensor platform."""
from __future__ import annotations

from collections.abc import Callable
from datetime import timedelta
Expand All @@ -21,10 +20,7 @@
from homeassistant.helpers.aiohttp_client import async_get_clientsession
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.helpers.typing import (
ConfigType,
DiscoveryInfoType,
)
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
import voluptuous as vol

from .const import (
Expand Down Expand Up @@ -69,7 +65,7 @@ async def async_setup_entry(
config_entry: config_entries.ConfigEntry,
async_add_entities,
) -> None:
"""Setup sensors from a config entry created in the integrations UI."""
"""Set up sensors from a config entry created in the integrations UI."""
config = hass.data[DOMAIN][config_entry.entry_id]
# Update our config to include new repos and remove those that have been removed.
if config_entry.options:
Expand Down Expand Up @@ -97,6 +93,7 @@ class GitHubRepoSensor(Entity):
"""Representation of a GitHub Repo sensor."""

def __init__(self, github: GitHubAPI, repo: dict[str, str]):
"""Initialize the sensor."""
super().__init__()
self.github = github
self.repo = repo["path"]
Expand All @@ -122,10 +119,12 @@ def available(self) -> bool:

@property
def state(self) -> str | None:
"""Return the state of the sensor."""
return self._state

@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the sensor."""
return self.attrs

async def async_update(self) -> None:
Expand Down
3 changes: 2 additions & 1 deletion hacs.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"name": "Github Custom",
"render_readme": true,
"iot_class": "Cloud Polling"
"iot_class": "Cloud Polling",
"homeassistant": "2024.11.0"
}
16 changes: 8 additions & 8 deletions requirements.test.txt
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# From our manifest.json for our custom component
gidgethub[aiohttp]==5.2.1
cryptography==40.0.1
gidgethub[aiohttp]==6.0.0
# Pinned to match homeassistant's own pin
cryptography==50.0.1

# Strictly for tests
coverage==7.2.1
pytest==7.2.2
pytest-asyncio==0.20.3
pytest-cov==3.0.0
pytest-homeassistant-custom-component==0.13.20
# Strictly for tests. This also pins homeassistant, pytest, pytest-asyncio,
# pytest-cov and coverage to the versions HA itself tests with.
pytest-homeassistant-custom-component==0.13.374
# Ubuntu 26.04 runners no longer ship legacy zones (e.g. US/Pacific) used by the HA test harness
tzdata
4 changes: 3 additions & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ testpaths = tests
norecursedirs = .git
addopts =
--cov=custom_components
asyncio_mode = auto
asyncio_default_fixture_loop_scope = function

[flake8]
# https://github.com/ambv/black#line-length
Expand Down Expand Up @@ -51,7 +53,7 @@ forced_separate = tests
combine_as_imports = true

[mypy]
python_version = 3.7
python_version = 3.14
ignore_errors = true
follow_imports = silent
ignore_missing_imports = true
Expand Down
1 change: 1 addition & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""pytest fixtures."""

import pytest


Expand Down
Loading
Loading