Repository: MatthewFlamm/pytest-homeassistant-custom-component (language: python)

Issue:
Make generate_package more reproducible
Make it easier to use same or chosen commit to generate the package.
Also include homeassistant package
By including homeassistant package, we enable a consistent testing version.


Discussion / hints:
For this, we should use a specific version or commit to generate the package from.  If we specify a commit, we will have to install homeassistant/core from github rather than PyPI.
In #46 it would be useful to allow extracting from a different repo than `homeassistant/core`
I wrote this in #47, but it fits better here:

It is possible to install homeassistant directly from github using a commit ref, so it is possible to pin it this way. The big advantage here is being able to test your custom component with the most up to date version possible.

The other option is to instead extract from the rc or master branch which would have the latest releases available on PyPI. Distinguishing which is the most recent between the branches is a hurdle here, but probably doable. Another downside is that I then need to synchronize updates with homeassistant, which is too much work for me. The big advantage here is you could image setting up a battery of tests in CI to test your component against versions X-Y of homeassistant.

I'm leaning towards simply using the commit ref from dev branch as this is the most straightforward to implement. I'm not sure how useful being able to test backwards to a very specific version is. You would still be able to test backwards using dev branch commits, it just isn't as precise.
I think supporting a local version of home assistant to generate the package is out of scope for now.
It is possible to retrieve the release commits using `git describe --tags` but it has to be done separately for `rc` and `master` branch to get both beta version or stable version.  Generate based on latest tag version of either?

This approach will be brittle to the home-assistant/core releases if there is a missing tag.  It looks like they recently all have tags, but it may not always occur?


Fix patch:
diff --git a/.github/workflows/generate_package.yml b/.github/workflows/generate_package.yml
index 70d9af0..b82e96b 100644
--- a/.github/workflows/generate_package.yml
+++ b/.github/workflows/generate_package.yml
@@ -17,7 +17,7 @@ jobs:
         with:
           python-version: 3.8
       - name: execute generate package
-        run: python generate_package.py
+        run: python generate_phacc/generate_phacc.py
       - name: Create Pull Request
         uses: peter-evans/create-pull-request@v3
         with:
diff --git a/.gitignore b/.gitignore
index b6e4761..49560ef 100644
--- a/.gitignore
+++ b/.gitignore
@@ -127,3 +127,6 @@ dmypy.json
 
 # Pyre type checker
 .pyre/
+
+# this packages temporary folder
+tmp_dir/
\ No newline at end of file
diff --git a/README.md b/README.md
index c59da45..aea4505 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
 # pytest-homeassistant-custom-component
 
-![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2021.4.0.dev0&labelColor=blue)
+![HA core version](https://img.shields.io/static/v1?label=HA+core+version&message=2021.4.0b4&labelColor=blue)
 
 Package to automatically extract testing plugins from Home Assistant for custom component testing.
 The goal is to provide the same functionality as the tests in home-assistant/core.
diff --git a/generate_phacc/__init__.py b/generate_phacc/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/generate_phacc/const.py b/generate_phacc/const.py
new file mode 100644
index 0000000..27a43b4
--- /dev/null
+++ b/generate_phacc/const.py
@@ -0,0 +1,31 @@
+"""Constants for phacc."""
+TMP_DIR = "tmp_dir"
+PACKAGE_DIR = "pytest_homeassistant_custom_component"
+REQUIREMENTS_FILE = "requirements_test.txt"
+CONST_FILE = "const.py"
+
+
+REQUIREMENTS_FILE_DEV = "requirements_dev.txt"
+
+
+path = "."
+clone = "git clone https://github.com/home-assistant/core.git tmp_dir"
+diff = "git diff --exit-code"
+
+
+files = [
+    "__init__.py",
+    "common.py",
+    "conftest.py",
+    "ignore_uncaught_exceptions.py",
+]
+
+
+# remove rrquirements for development only, i.e not related to homeassistant tests
+requirements_remove = [
+    "codecov",
+    "mypy",
+    "pre-commit",
+    "pylint",
+    "astroid",
+]
diff --git a/generate_package.py b/generate_phacc/generate_phacc.py
similarity index 83%
rename from generate_package.py
rename to generate_phacc/generate_phacc.py
index 6d3ec2f..7232639 100755
--- a/generate_package.py
+++ b/generate_phacc/generate_phacc.py
@@ -4,41 +4,18 @@
 import shutil
 import os
 
-TMP_DIR = "tmp_dir"
-PACKAGE_DIR = "pytest_homeassistant_custom_component"
-REQUIREMENTS_FILE = "requirements_test.txt"
-CONST_FILE = "const.py"
-
-REQUIREMENTS_FILE_DEV = "requirements_dev.txt"
-
-path = "."
-clone = "git clone --depth=1 https://github.com/home-assistant/core.git tmp_dir"
-diff = "git diff --exit-code"
-
-files = [
-    "__init__.py",
-    "common.py",
-    "conftest.py",
-    "ignore_uncaught_exceptions.py",
-]
-
-# remove rrquirements for development only, i.e not related to homeassistant tests
-requirements_remove = [
-    "codecov",
-    "mypy",
-    "pre-commit",
-    "pylint",
-    "astroid",
-]
-
-if os.path.isdir(TMP_DIR):
-    shutil.rmtree(TMP_DIR)
+import git
+
+from ha import prepare_homeassistant
+from const import TMP_DIR, PACKAGE_DIR, REQUIREMENTS_FILE, CONST_FILE, REQUIREMENTS_FILE_DEV, path, files, requirements_remove
+
 if os.path.isdir(PACKAGE_DIR):
     shutil.rmtree(PACKAGE_DIR)
 if os.path.isfile(REQUIREMENTS_FILE):
     os.remove(REQUIREMENTS_FILE)
 
-os.system(clone)  # Cloning
+ha_version = prepare_homeassistant()
+
 os.mkdir(PACKAGE_DIR)
 os.mkdir(os.path.join(PACKAGE_DIR, "test_util"))
 shutil.copy2(os.path.join(TMP_DIR, REQUIREMENTS_FILE), REQUIREMENTS_FILE)
@@ -68,7 +45,6 @@
     with open(filename, "w") as file:
         file.write(filedata)
 
-shutil.rmtree(TMP_DIR)
 os.rename(
     os.path.join(PACKAGE_DIR, "conftest.py"), os.path.join(PACKAGE_DIR, "plugins.py")
 )
@@ -104,7 +80,7 @@
         new_data.append(d)
     else:
         removed_data.append(d)
-
+new_data.append(f"homeassistant=={ha_version}\n")
 new_data.insert(0, added_text)
 removed_data.insert(0, added_text)
 
@@ -114,8 +90,6 @@
 with open(REQUIREMENTS_FILE_DEV, "w") as new_file:
     new_file.writelines(removed_data)
 
-diff_files = os.system(diff)
-
 from pytest_homeassistant_custom_component.const import __version__
 
 with open("README.md", "r") as original_file:
diff --git a/generate_phacc/ha.py b/generate_phacc/ha.py
new file mode 100644
index 0000000..322e53b
--- /dev/null
+++ b/generate_phacc/ha.py
@@ -0,0 +1,86 @@
+import os
+
+import git
+
+from const import clone, TMP_DIR
+
+
+class HAVersion:
+    def __init__(self, version):
+        self._version = version
+        split_version = version.split(".")
+        try:
+            self.major = int(split_version[0])
+        except ValueError:
+            self.major = 0
+        self.minor = 0
+        self.patch = 0
+        self.beta = None
+        if self.major < 2021:
+            return
+        
+        if len(split_version)>=2:
+            self.minor = int(split_version[1])
+        if len(split_version)>=3:
+            patch = split_version[2].split("b")
+            self.patch = int(patch[0])
+            if len(patch) == 2:
+                self.beta = int(patch[1])
+
+
+    def __eq__(self, other):
+        if (
+                self.major==other.major
+                and self.minor==self.minor
+                and self.patch==self.patch
+                and self.beta==self.beta
+        ):
+            return True
+        return False
+
+
+    def __gt__(self, other):
+        if self.major > other.major:
+            return True
+        elif self.major < other.major:
+            return False
+
+
+        if self.minor > other.minor:
+            return True
+        elif self.minor < other.minor:
+            return False
+
+
+        if self.patch > other.patch:
+            return True
+        elif self.patch < other.patch:
+            return False
+
+
+        if self.beta is not None and other.beta is None:
+            return False
+        elif self.beta is None and other.beta is not None:
+            return True
+        elif self.beta is None and other.beta is None:
+            return False
+        elif self.beta > other.beta:
+            return True
+        return False
+
+def prepare_homeassistant(ref=None):
+    if not os.path.isdir(TMP_DIR):
+        os.system(clone)  # Cloning
+
+    if ref is None:
+        repo = git.Repo(TMP_DIR)
+        versions = {str(tag): HAVersion(str(tag)) for tag in repo.tags}
+        latest_version = HAVersion("0.0.0")
+        for key, version in versions.items():
+            if version > latest_version:
+                latest_version = version
+                ref = key
+
+    repo.head.reference = repo.refs[ref]
+    repo.head.reset(index=True, working_tree=True)
+    return ref
diff --git a/requirements_dev.txt b/requirements_dev.txt
index d24558d..6038c64 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -1,6 +1,6 @@
 # This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
 codecov==2.1.10
 mypy==0.812
-pre-commit==2.10.1
-pylint==2.7.2
-astroid==2.5.1
+pre-commit==2.11.1
+pylint==2.7.4
+astroid==2.5.2
diff --git a/setup.py b/setup.py
index 6b2b892..257631d 100644
--- a/setup.py
+++ b/setup.py
@@ -3,7 +3,6 @@
 from setuptools import setup, find_packages
 
 requirements = [
-    "homeassistant",
     "sqlalchemy",
 ]
 with open("requirements_test.txt","r") as f:


Test patch:
diff --git a/pytest_homeassistant_custom_component/common.py b/pytest_homeassistant_custom_component/common.py
index 8568296..3b40435 100644
--- a/pytest_homeassistant_custom_component/common.py
+++ b/pytest_homeassistant_custom_component/common.py
@@ -3,6 +3,8 @@
 
 This file is originally from homeassistant/core and modified by pytest-homeassistant-custom-component.
 """
+from __future__ import annotations
+
 import asyncio
 import collections
 from collections import OrderedDict
@@ -19,9 +21,8 @@
 from time import monotonic
 import traceback
 import types
-from typing import Any, Awaitable, Collection, Optional
+from typing import Any, Awaitable, Collection
 from unittest.mock import AsyncMock, Mock, patch
-import uuid
 
 from aiohttp.test_utils import unused_port as get_test_instance_port  # noqa: F401
 
@@ -64,6 +65,7 @@
 from homeassistant.util.async_ import run_callback_threadsafe
 import homeassistant.util.dt as date_util
 from homeassistant.util.unit_system import METRIC_SYSTEM
+import homeassistant.util.uuid as uuid_util
 import homeassistant.util.yaml.loader as yaml_loader
 
 _LOGGER = logging.getLogger(__name__)
@@ -202,12 +204,12 @@ async def async_wait_for_task_count(self, max_remaining_tasks: int = 0) -> None:
         """
         # To flush out any call_soon_threadsafe
         await asyncio.sleep(0)
-        start_time: Optional[float] = None
+        start_time: float | None = None
 
         while len(self._pending_tasks) > max_remaining_tasks:
-            pending = [
+            pending: Collection[Awaitable[Any]] = [
                 task for task in self._pending_tasks if not task.done()
-            ]  # type: Collection[Awaitable[Any]]
+            ]
             self._pending_tasks.clear()
             if len(pending) > max_remaining_tasks:
                 remaining_pending = await self._await_count_and_log_pending(
@@ -279,7 +281,7 @@ async def _await_count_and_log_pending(
     hass.config.skip_pip = True
 
     hass.config_entries = config_entries.ConfigEntries(hass, {})
-    hass.config_entries._entries = []
+    hass.config_entries._entries = {}
     hass.config_entries._store._async_ensure_stop_listener = lambda: None
 
     # Load the registries
@@ -570,7 +572,7 @@ def __init__(
         if platform_schema_base is not None:
             self.PLATFORM_SCHEMA_BASE = platform_schema_base
 
-        if setup is not None:
+        if setup:
             # We run this in executor, wrap it in function
             self.setup = lambda *args: setup(*args)
 
@@ -741,7 +743,7 @@ def __init__(
     ):
         """Initialize a mock config entry."""
         kwargs = {
-            "entry_id": entry_id or uuid.uuid4().hex,
+            "entry_id": entry_id or uuid_util.random_uuid_hex(),
             "domain": domain,
             "data": data or {},
             "system_options": system_options,
@@ -760,17 +762,17 @@ def __init__(
 
     def add_to_hass(self, hass):
         """Test helper to add entry to hass."""
-        hass.config_entries._entries.append(self)
+        hass.config_entries._entries[self.entry_id] = self
 
     def add_to_manager(self, manager):
         """Test helper to add entry to entry manager."""
-        manager._entries.append(self)
+        manager._entries[self.entry_id] = self
 
 
 def patch_yaml_files(files_dict, endswith=True):
     """Patch load_yaml with a dictionary of yaml files."""
     # match using endswith, start search with longest string
-    matchlist = sorted(list(files_dict.keys()), key=len) if endswith else []
+    matchlist = sorted(files_dict.keys(), key=len) if endswith else []
 
     def mock_open_f(fname, **_):
         """Mock open() in the yaml module, used by load_yaml."""
diff --git a/pytest_homeassistant_custom_component/const.py b/pytest_homeassistant_custom_component/const.py
index 1a30d0d..11f465d 100644
--- a/pytest_homeassistant_custom_component/const.py
+++ b/pytest_homeassistant_custom_component/const.py
@@ -5,6 +5,6 @@
 """
 MAJOR_VERSION = 2021
 MINOR_VERSION = 4
-PATCH_VERSION = "0.dev0"
+PATCH_VERSION = "0b4"
 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}"
 __version__ = f"{__short_version__}.{PATCH_VERSION}"
diff --git a/requirements_test.txt b/requirements_test.txt
index 3fc9ae0..99fcbe8 100644
--- a/requirements_test.txt
+++ b/requirements_test.txt
@@ -22,3 +22,4 @@ responses==0.12.0
 respx==0.16.2
 stdlib-list==0.7.0
 tqdm==4.49.0
+homeassistant==2021.4.0b4