Skip to content

Commit b6f4e4d

Browse files
authored
Merge pull request #78 from raman325/handle_unpinned_versions
For package installations, handle version mismatches with higher version winning, and store installed packages to handle downgrades
2 parents f7169ac + 144930d commit b6f4e4d

File tree

15 files changed

+708
-182
lines changed

15 files changed

+708
-182
lines changed

custom_components/pyscript/__init__.py

Lines changed: 3 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
import json
55
import logging
66
import os
7-
import sys
87

9-
import pkg_resources
108
import voluptuous as vol
119

1210
from homeassistant.config import async_hass_config_yaml
@@ -21,7 +19,6 @@
2119
import homeassistant.helpers.config_validation as cv
2220
from homeassistant.helpers.restore_state import RestoreStateData
2321
from homeassistant.loader import bind_hass
24-
from homeassistant.requirements import async_process_requirements
2522

2623
from .const import (
2724
CONF_ALLOW_ALL_IMPORTS,
@@ -30,8 +27,6 @@
3027
DOMAIN,
3128
FOLDER,
3229
LOGGER_PATH,
33-
REQUIREMENTS_FILE,
34-
REQUIREMENTS_PATHS,
3530
SERVICE_JUPYTER_KERNEL_START,
3631
UNSUB_LISTENERS,
3732
)
@@ -40,20 +35,10 @@
4035
from .function import Function
4136
from .global_ctx import GlobalContext, GlobalContextMgr
4237
from .jupyter_kernel import Kernel
38+
from .requirements import install_requirements
4339
from .state import State
4440
from .trigger import TrigTime
4541

46-
if sys.version_info[:2] >= (3, 8):
47-
from importlib.metadata import ( # pylint: disable=no-name-in-module,import-error
48-
PackageNotFoundError,
49-
version as installed_version,
50-
)
51-
else:
52-
from importlib_metadata import ( # pylint: disable=import-error
53-
PackageNotFoundError,
54-
version as installed_version,
55-
)
56-
5742
_LOGGER = logging.getLogger(LOGGER_PATH)
5843

5944
PYSCRIPT_SCHEMA = vol.Schema(
@@ -149,7 +134,7 @@ async def async_setup_entry(hass, config_entry):
149134

150135
State.set_pyscript_config(config_entry.data)
151136

152-
await install_requirements(hass)
137+
await install_requirements(hass, config_entry, pyscript_folder)
153138
await load_scripts(hass, config_entry.data)
154139

155140
async def reload_scripts_handler(call):
@@ -169,7 +154,7 @@ async def reload_scripts_handler(call):
169154

170155
await unload_scripts(global_ctx_only=global_ctx_only)
171156

172-
await install_requirements(hass)
157+
await install_requirements(hass, config_entry, pyscript_folder)
173158
await load_scripts(hass, config_entry.data, global_ctx_only=global_ctx_only)
174159

175160
start_global_contexts(global_ctx_only=global_ctx_only)
@@ -271,81 +256,6 @@ async def unload_scripts(global_ctx_only=None, unload_all=False):
271256
await GlobalContextMgr.delete(global_ctx_name)
272257

273258

274-
@bind_hass
275-
def process_all_requirements(hass, requirements_paths, requirements_file):
276-
"""
277-
Load all lines from requirements_file located in requirements_paths.
278-
279-
Returns files and a list of packages, if any, that need to be installed.
280-
"""
281-
all_requirements_to_process = {}
282-
for root in requirements_paths:
283-
for requirements_path in glob.glob(os.path.join(hass.config.path(FOLDER), root, requirements_file)):
284-
with open(requirements_path, "r") as requirements_fp:
285-
all_requirements_to_process[requirements_path] = requirements_fp.readlines()
286-
287-
all_requirements_to_install = {}
288-
for requirements_path, pkg_lines in all_requirements_to_process.items():
289-
all_requirements_to_install[requirements_path] = []
290-
for pkg in pkg_lines:
291-
# Remove inline comments which are accepted by pip but not by Home
292-
# Assistant's installation method.
293-
# https://rosettacode.org/wiki/Strip_comments_from_a_string#Python
294-
i = pkg.find("#")
295-
if i >= 0:
296-
pkg = pkg[:i]
297-
pkg = pkg.strip()
298-
299-
if not pkg:
300-
continue
301-
302-
try:
303-
# Attempt to get version of package. Do nothing if it's found since
304-
# we want to use the version that's already installed to be safe
305-
requirement = pkg_resources.Requirement.parse(pkg)
306-
requirement_installed_version = installed_version(requirement.project_name)
307-
308-
if requirement_installed_version in requirement:
309-
_LOGGER.debug("`%s` already found", requirement.project_name)
310-
else:
311-
_LOGGER.warning(
312-
(
313-
"`%s` already found but found version `%s` does not"
314-
" match requirement. Keeping found version."
315-
),
316-
requirement.project_name,
317-
requirement_installed_version,
318-
)
319-
except PackageNotFoundError:
320-
# Since package wasn't found, add it to installation list
321-
_LOGGER.debug("%s not found, adding it to package installation list", pkg)
322-
all_requirements_to_install[requirements_path].append(pkg)
323-
except ValueError:
324-
# Not valid requirements line so it can be skipped
325-
_LOGGER.debug("Ignoring `%s` because it is not a valid package", pkg)
326-
327-
return all_requirements_to_install
328-
329-
330-
@bind_hass
331-
async def install_requirements(hass):
332-
"""Install missing requirements from requirements.txt."""
333-
all_requirements = await hass.async_add_executor_job(
334-
process_all_requirements, hass, REQUIREMENTS_PATHS, REQUIREMENTS_FILE
335-
)
336-
337-
for requirements_path, requirements_to_install in all_requirements.items():
338-
if requirements_to_install:
339-
_LOGGER.info(
340-
"Installing the following packages from %s: %s",
341-
requirements_path,
342-
", ".join(requirements_to_install),
343-
)
344-
await async_process_requirements(hass, DOMAIN, requirements_to_install)
345-
else:
346-
_LOGGER.debug("All packages in %s are already available", requirements_path)
347-
348-
349259
@bind_hass
350260
async def load_scripts(hass, data, global_ctx_only=None):
351261
"""Load all python scripts in FOLDER."""

custom_components/pyscript/config_flow.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
99
from homeassistant.core import callback
1010

11-
from .const import CONF_ALLOW_ALL_IMPORTS, CONF_HASS_IS_GLOBAL, DOMAIN
11+
from .const import CONF_ALLOW_ALL_IMPORTS, CONF_HASS_IS_GLOBAL, CONF_INSTALLED_PACKAGES, DOMAIN
1212

1313
CONF_BOOL_ALL = {CONF_ALLOW_ALL_IMPORTS, CONF_HASS_IS_GLOBAL}
1414

@@ -122,7 +122,11 @@ async def async_step_import(self, import_config: Dict[str, Any] = None) -> Dict[
122122
# Remove values for all keys in entry.data that are not in the imported config,
123123
# excluding `allow_all_imports` for entries set up through the UI.
124124
for key in entry.data:
125-
if (entry.source == SOURCE_IMPORT or key not in CONF_BOOL_ALL) and key not in import_config:
125+
if (
126+
(entry.source == SOURCE_IMPORT or key not in CONF_BOOL_ALL)
127+
and key != CONF_INSTALLED_PACKAGES
128+
and key not in import_config
129+
):
126130
updated_data.pop(key)
127131

128132
# Update and reload entry if data needs to be updated

custom_components/pyscript/const.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@
77

88
FOLDER = "pyscript"
99

10+
UNPINNED_VERSION = "_unpinned_version"
11+
12+
ATTR_INSTALLED_VERSION = "installed_version"
13+
ATTR_SOURCES = "sources"
14+
ATTR_VERSION = "version"
15+
1016
CONF_ALLOW_ALL_IMPORTS = "allow_all_imports"
1117
CONF_HASS_IS_GLOBAL = "hass_is_global"
18+
CONF_INSTALLED_PACKAGES = "_installed_packages"
1219

1320
SERVICE_JUPYTER_KERNEL_START = "jupyter_kernel_start"
1421

0 commit comments

Comments
 (0)