core/script/gen_requirements_all.py

408 lines
11 KiB
Python
Raw Normal View History

2015-11-17 08:18:42 +00:00
#!/usr/bin/env python3
2016-03-09 10:15:04 +00:00
"""Generate an updated requirements_all.txt."""
import difflib
2015-11-17 08:18:42 +00:00
import importlib
import os
from pathlib import Path
2015-11-17 08:18:42 +00:00
import pkgutil
import re
2015-12-18 07:51:34 +00:00
import sys
2015-11-17 08:18:42 +00:00
from homeassistant.util.yaml.loader import load_yaml
2020-08-29 06:23:55 +00:00
from script.hassfest.model import Integration
COMMENT_REQUIREMENTS = (
2019-07-31 19:25:30 +00:00
"Adafruit_BBIO",
"avea", # depends on bluepy
2019-07-31 19:25:30 +00:00
"avion",
"beacontools",
"beewi_smartclim", # depends on bluepy
2019-07-31 19:25:30 +00:00
"blinkt",
"bluepy",
"bme280spi",
2019-07-31 19:25:30 +00:00
"bme680",
"decora",
"decora_wifi",
2019-07-31 19:25:30 +00:00
"envirophat",
"evdev",
"face_recognition",
"i2csense",
"opencv-python-headless",
"pybluez",
"pycups",
"PySwitchbot",
"pySwitchmate",
"python-eq3bt",
"python-gammu",
2019-07-31 19:25:30 +00:00
"python-lirc",
"pyuserinput",
"raspihats",
"rpi-rf",
"RPi.GPIO",
"smbus-cffi",
"tensorflow",
"tf-models-official",
"VL53L1X2",
)
IGNORE_PIN = ("colorlog>2.1,<3", "urllib3")
2019-07-31 19:25:30 +00:00
URL_PIN = (
"https://developers.home-assistant.io/docs/"
"creating_platform_code_review.html#1-requirements"
)
2017-01-22 16:34:00 +00:00
2015-11-17 08:18:42 +00:00
2019-07-31 19:25:30 +00:00
CONSTRAINT_PATH = os.path.join(
os.path.dirname(__file__), "../homeassistant/package_constraints.txt"
)
CONSTRAINT_BASE = """
2018-08-28 10:49:50 +00:00
pycryptodome>=3.6.6
# Constrain urllib3 to ensure we deal with CVE-2019-11236 & CVE-2019-11324
urllib3>=1.24.3
# Constrain H11 to ensure we get a new enough version to support non-rfc line endings
h11>=0.12.0
# Constrain httplib2 to protect against GHSA-93xj-8mrv-444m
# https://github.com/advisories/GHSA-93xj-8mrv-444m
httplib2>=0.19.0
2020-06-01 07:44:18 +00:00
# gRPC 1.32+ currently causes issues on ARMv7, see:
# https://github.com/home-assistant/core/issues/40148
# Newer versions of some other libraries pin a higher version of grpcio,
# so those also need to be kept at an old version until the grpcio pin
# is reverted, see:
# https://github.com/home-assistant/core/issues/53427
grpcio==1.31.0
google-cloud-pubsub==2.1.0
google-api-core<=1.31.2
# This is a old unmaintained library and is replaced with pycryptodome
pycrypto==1000000000.0.0
2020-08-26 14:53:22 +00:00
# To remove reliance on typing
btlewrap>=0.0.10
# This overrides a built-in Python package
enum34==1000000000.0.0
typing==1000000000.0.0
uuid==1000000000.0.0
# Temporary constraint on pandas, to unblock 2021.7 releases
# until we have fixed the wheels builds for newer versions.
pandas==1.3.0
2021-08-28 13:00:14 +00:00
# regex causes segfault with version 2021.8.27
# https://bitbucket.org/mrabarnett/mrab-regex/issues/421/2021827-results-in-fatal-python-error
# This is fixed in 2021.8.28
regex==2021.8.28
"""
IGNORE_PRE_COMMIT_HOOK_ID = (
"check-executables-have-shebangs",
"check-json",
"no-commit-to-branch",
"prettier",
"python-typing-update",
)
def has_tests(module: str):
"""Test if a module has tests.
Module format: homeassistant.components.hue
Test if exists: tests/components/hue
"""
path = Path(module.replace(".", "/").replace("homeassistant", "tests"))
if not path.exists():
return False
if not path.is_dir():
return True
# Dev environments might have stale directories around
# from removed tests. Check for that.
content = [f.name for f in path.glob("*")]
# Directories need to contain more than `__pycache__`
# to exist in Git and so be seen by CI.
return content != ["__pycache__"]
2015-11-17 08:18:42 +00:00
def explore_module(package, explore_children):
2016-03-09 10:15:04 +00:00
"""Explore the modules."""
2015-11-17 08:18:42 +00:00
module = importlib.import_module(package)
found = []
2019-07-31 19:25:30 +00:00
if not hasattr(module, "__path__"):
2015-11-17 08:18:42 +00:00
return found
for _, name, _ in pkgutil.iter_modules(module.__path__, f"{package}."):
2015-11-17 08:18:42 +00:00
found.append(name)
if explore_children:
found.extend(explore_module(name, False))
return found
def core_requirements():
2016-03-09 10:15:04 +00:00
"""Gather core requirements out of setup.py."""
reqs_raw = re.search(
r"REQUIRES = \[(.*?)\]", Path("setup.py").read_text(), re.S
).group(1)
return [x[1] for x in re.findall(r"(['\"])(.*?)\1", reqs_raw)]
2015-11-17 08:18:42 +00:00
def gather_recursive_requirements(domain, seen=None):
"""Recursively gather requirements from a module."""
if seen is None:
seen = set()
seen.add(domain)
integration = Integration(Path(f"homeassistant/components/{domain}"))
integration.load_manifest()
2020-04-03 19:58:19 +00:00
reqs = set(integration.requirements)
for dep_domain in integration.dependencies:
reqs.update(gather_recursive_requirements(dep_domain, seen))
return reqs
def comment_requirement(req):
"""Comment out requirement. Some don't install on all systems."""
return any(ign.lower() in req.lower() for ign in COMMENT_REQUIREMENTS)
2015-11-25 22:31:04 +00:00
def gather_modules():
"""Collect the information."""
2016-02-01 07:52:42 +00:00
reqs = {}
2015-11-17 08:18:42 +00:00
errors = []
2015-11-25 22:31:04 +00:00
gather_requirements_from_manifests(errors, reqs)
gather_requirements_from_modules(errors, reqs)
for key in reqs:
2019-07-31 19:25:30 +00:00
reqs[key] = sorted(reqs[key], key=lambda name: (len(name.split(".")), name))
if errors:
print("******* ERROR")
2019-07-31 19:25:30 +00:00
print("Errors while importing: ", ", ".join(errors))
return None
return reqs
def gather_requirements_from_manifests(errors, reqs):
"""Gather all of the requirements from manifests."""
integrations = Integration.load_dir(Path("homeassistant/components"))
for domain in sorted(integrations):
integration = integrations[domain]
if not integration.manifest:
errors.append(f"The manifest for integration {domain} is invalid.")
continue
if integration.disabled:
continue
process_requirements(
errors, integration.requirements, f"homeassistant.components.{domain}", reqs
)
def gather_requirements_from_modules(errors, reqs):
"""Collect the requirements from the modules directly."""
for package in sorted(
2019-07-31 19:25:30 +00:00
explore_module("homeassistant.scripts", True)
+ explore_module("homeassistant.auth", True)
):
2015-11-17 08:18:42 +00:00
try:
module = importlib.import_module(package)
except ImportError as err:
print(f"{package.replace('.', '/')}.py: {err}")
errors.append(package)
2015-11-17 08:18:42 +00:00
continue
2019-07-31 19:25:30 +00:00
if getattr(module, "REQUIREMENTS", None):
process_requirements(errors, module.REQUIREMENTS, package, reqs)
2016-02-01 07:52:42 +00:00
2015-11-17 08:18:42 +00:00
def process_requirements(errors, module_requirements, package, reqs):
"""Process all of the requirements."""
for req in module_requirements:
2019-07-31 19:25:30 +00:00
if "://" in req:
errors.append(f"{package}[Only pypi dependencies are allowed: {req}]")
2019-07-31 19:25:30 +00:00
if req.partition("==")[1] == "" and req not in IGNORE_PIN:
errors.append(f"{package}[Please pin requirement {req}, see {URL_PIN}]")
reqs.setdefault(req, []).append(package)
def generate_requirements_list(reqs):
"""Generate a pip file based on requirements."""
output = []
2016-02-01 07:52:42 +00:00
for pkg, requirements in sorted(reqs.items(), key=lambda item: item[0]):
for req in sorted(requirements):
output.append(f"\n# {req}")
if comment_requirement(pkg):
output.append(f"\n# {pkg}\n")
else:
output.append(f"\n{pkg}\n")
2019-07-31 19:25:30 +00:00
return "".join(output)
def requirements_output(reqs):
"""Generate output for requirements."""
output = [
"-c homeassistant/package_constraints.txt\n",
"\n",
"# Home Assistant Core\n",
]
2019-07-31 19:25:30 +00:00
output.append("\n".join(core_requirements()))
output.append("\n")
return "".join(output)
def requirements_all_output(reqs):
"""Generate output for requirements_all."""
output = [
2020-08-27 14:56:53 +00:00
"# Home Assistant Core, full dependency set\n",
"-r requirements.txt\n",
]
output.append(generate_requirements_list(reqs))
2019-07-31 19:25:30 +00:00
return "".join(output)
2020-08-27 14:56:53 +00:00
def requirements_test_all_output(reqs):
"""Generate output for test_requirements."""
output = [
"# Home Assistant tests, full dependency set\n",
f"# Automatically generated by {Path(__file__).name}, do not edit\n",
"\n",
2020-08-27 14:56:53 +00:00
"-r requirements_test.txt\n",
]
2019-07-31 19:25:30 +00:00
filtered = {
requirement: modules
for requirement, modules in reqs.items()
2019-07-31 19:25:30 +00:00
if any(
# Always install requirements that are not part of integrations
not mdl.startswith("homeassistant.components.") or
# Install tests for integrations that have tests
has_tests(mdl)
for mdl in modules
2019-07-31 19:25:30 +00:00
)
}
output.append(generate_requirements_list(filtered))
2015-11-25 22:31:04 +00:00
2019-07-31 19:25:30 +00:00
return "".join(output)
2015-11-25 22:31:04 +00:00
def requirements_pre_commit_output():
"""Generate output for pre-commit dependencies."""
source = ".pre-commit-config.yaml"
pre_commit_conf = load_yaml(source)
reqs = []
for repo in (x for x in pre_commit_conf["repos"] if x.get("rev")):
for hook in repo["hooks"]:
if hook["id"] not in IGNORE_PRE_COMMIT_HOOK_ID:
reqs.append(f"{hook['id']}=={repo['rev'].lstrip('v')}")
reqs.extend(x for x in hook.get("additional_dependencies", ()))
output = [
f"# Automatically generated "
f"from {source} by {Path(__file__).name}, do not edit",
"",
]
output.extend(sorted(reqs))
return "\n".join(output) + "\n"
def gather_constraints():
"""Construct output for constraint file."""
return (
"\n".join(
sorted(
2020-07-22 02:19:32 +00:00
{
*core_requirements(),
*gather_recursive_requirements("default_config"),
*gather_recursive_requirements("mqtt"),
}
)
+ [""]
2019-07-31 19:25:30 +00:00
)
+ CONSTRAINT_BASE
2019-07-31 19:25:30 +00:00
)
def diff_file(filename, content):
"""Diff a file."""
return list(
difflib.context_diff(
[f"{line}\n" for line in Path(filename).read_text().split("\n")],
[f"{line}\n" for line in content.split("\n")],
filename,
"generated",
)
)
2018-03-09 20:27:39 +00:00
def main(validate):
"""Run the script."""
2019-07-31 19:25:30 +00:00
if not os.path.isfile("requirements_all.txt"):
print("Run this from HA root dir")
2018-03-09 20:27:39 +00:00
return 1
2015-11-25 22:31:04 +00:00
data = gather_modules()
if data is None:
2018-03-09 20:27:39 +00:00
return 1
2015-12-18 07:51:34 +00:00
reqs_file = requirements_output(data)
reqs_all_file = requirements_all_output(data)
2020-08-27 14:56:53 +00:00
reqs_test_all_file = requirements_test_all_output(data)
reqs_pre_commit_file = requirements_pre_commit_output()
constraints = gather_constraints()
files = (
("requirements.txt", reqs_file),
("requirements_all.txt", reqs_all_file),
("requirements_test_pre_commit.txt", reqs_pre_commit_file),
2020-08-27 14:56:53 +00:00
("requirements_test_all.txt", reqs_test_all_file),
("homeassistant/package_constraints.txt", constraints),
)
2018-03-09 20:27:39 +00:00
if validate:
errors = []
for filename, content in files:
diff = diff_file(filename, content)
if diff:
errors.append("".join(diff))
if errors:
print("ERROR - FOUND THE FOLLOWING DIFFERENCES")
print()
print()
print("\n\n".join(errors))
print()
print("Please run python3 -m script.gen_requirements_all")
2018-03-09 20:27:39 +00:00
return 1
2018-03-09 20:27:39 +00:00
return 0
for filename, content in files:
Path(filename).write_text(content)
2018-03-09 20:27:39 +00:00
return 0
2015-11-17 08:18:42 +00:00
2016-11-19 05:47:59 +00:00
2019-07-31 19:25:30 +00:00
if __name__ == "__main__":
_VAL = sys.argv[-1] == "validate"
2018-03-09 20:27:39 +00:00
sys.exit(main(_VAL))