diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 95f6b9a865b..82398bf0d16 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -235,3 +235,12 @@ auth0-python<5.0 # Setuptools >=82.0.0 doesn't contain pkg_resources anymore setuptools<82.0.0 + +# Pin dependencies with '.pth' files to exact versions, only update manually! +# https://github.com/Azure/azure-kusto-python/ -> '.pth' files removed with >=5.0.5 +# https://github.com/xolox/python-coloredlogs -> unmaintained +# https://github.com/pypa/setuptools +azure-kusto-data==4.5.1 +azure-kusto-ingest==4.5.1 +coloredlogs==15.0.1 +setuptools==81.0.0 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 0c000426ed5..f54535e9278 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -225,6 +225,15 @@ auth0-python<5.0 # Setuptools >=82.0.0 doesn't contain pkg_resources anymore setuptools<82.0.0 + +# Pin dependencies with '.pth' files to exact versions, only update manually! +# https://github.com/Azure/azure-kusto-python/ -> '.pth' files removed with >=5.0.5 +# https://github.com/xolox/python-coloredlogs -> unmaintained +# https://github.com/pypa/setuptools +azure-kusto-data==4.5.1 +azure-kusto-ingest==4.5.1 +coloredlogs==15.0.1 +setuptools==81.0.0 """ GENERATED_MESSAGE = ( diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index 88bfba54010..39872330514 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -95,6 +95,8 @@ FORBIDDEN_PACKAGES = { "async-timeout": "be replaced by asyncio.timeout (Python 3.11+)", # Only needed for tests "codecov": "not be a runtime dependency", + # Coloredlogs is unmaintained and contains a '.pth' file + "coloredlogs": "be replaced with colorlog", # Only needed for docs "mkdocs": "not be a runtime dependency", # Does blocking I/O and should be replaced by pyserial-asyncio-fast @@ -149,11 +151,13 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { }, "flux_led": {"flux-led": {"async-timeout"}}, "foobot": {"foobot-async": {"async-timeout"}}, + "google_maps": {"locationsharinglib": {"coloredlogs"}}, "harmony": {"aioharmony": {"async-timeout"}}, "here_travel_time": { "here-routing": {"async-timeout"}, "here-transit": {"async-timeout"}, }, + "homeassistant_hardware": {"universal-silabs-flasher": {"coloredlogs"}}, "homewizard": {"python-homewizard-energy": {"async-timeout"}}, "imeon_inverter": {"imeon-inverter-api": {"async-timeout"}}, "izone": {"python-izone": {"async-timeout"}}, @@ -217,6 +221,7 @@ FORBIDDEN_PACKAGE_EXCEPTIONS: dict[str, dict[str, set[str]]] = { # https://github.com/waveform80/colorzero/issues/9 # zha > zigpy-zigate > gpiozero > colorzero > setuptools "colorzero": {"setuptools"}, + "zigpy-znp": {"coloredlogs"}, }, } @@ -236,16 +241,42 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = { # - reasonX should be the name of the invalid dependency # https://github.com/jaraco/jaraco.net "abode": {"jaraco-abode": {"jaraco-net"}}, + # https://github.com/Azure/azure-kusto-python/ + "azure_data_explorer": { + # Legacy namespace packages, resolved with >=5.0.5 + # azure_kusto_data-*-nspkg.pth + # azure_kusto_ingest-*-nspkg.pth + "homeassistant": {"azure-kusto-data", "azure-kusto-ingest"}, + "azure-kusto-ingest": {"azure-kusto-data"}, + }, # https://github.com/coinbase/coinbase-advanced-py + "cmus": { + # Setuptools - distutils-precedence.pth + "pbr": {"setuptools"} + }, "coinbase": {"homeassistant": {"coinbase-advanced-py"}}, # https://github.com/u9n/dlms-cosem "dsmr": {"dsmr-parser": {"dlms-cosem"}}, # https://github.com/ChrisMandich/PyFlume # Fixed with >=0.7.1 + "fitbit": { + # Setuptools - distutils-precedence.pth + "fitbit": {"setuptools"} + }, "flume": {"homeassistant": {"pyflume"}}, # https://github.com/fortinet-solutions-cse/fortiosapi "fortios": {"homeassistant": {"fortiosapi"}}, # https://github.com/manzanotti/geniushub-client "geniushub": {"homeassistant": {"geniushub-client"}}, + # https://github.com/costastf/locationsharinglib + "google_maps": { + # Coloredlogs, unmaintained - coloredlogs.pth + "locationsharinglib": {"coloredlogs"}, + }, + # https://github.com/NabuCasa/universal-silabs-flasher + "homeassistant_hardware": { + # Coloredlogs, unmaintained - coloredlogs.pth + "universal-silabs-flasher": {"coloredlogs"}, + }, # https://github.com/basnijholt/aiokef "kef": {"homeassistant": {"aiokef"}}, # https://github.com/danifus/pyzipper @@ -257,11 +288,26 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = { # https://github.com/timmo001/aiolyric "lyric": {"homeassistant": {"aiolyric"}}, # https://github.com/microBeesTech/pythonSDK/ - "microbees": {"homeassistant": {"microbeespy"}}, + "microbees": { + "homeassistant": {"microbeespy"}, + "microbeespy": {"setuptools"}, + }, + "mochad": { + # Setuptools - distutils-precedence.pth + "pbr": {"setuptools"} + }, # https://github.com/ejpenney/pyobihai "obihai": {"homeassistant": {"pyobihai"}}, + "opnsense": { + # Setuptools - distutils-precedence.pth + "pbr": {"setuptools"} + }, # https://github.com/iamkubi/pydactyl "pterodactyl": {"homeassistant": {"py-dactyl"}}, + "remote_rpi_gpio": { + # Setuptools - distutils-precedence.pth + "colorzero": {"setuptools"} + }, # https://github.com/sstallion/sensorpush-api "sensorpush_cloud": { "homeassistant": {"sensorpush-api"}, @@ -273,6 +319,14 @@ FORBIDDEN_PACKAGE_FILES_EXCEPTIONS = { "watergate": {"homeassistant": {"watergate-local-api"}}, # https://github.com/markusressel/xs1-api-client "xs1": {"homeassistant": {"xs1-api-client"}}, + # https://github.com/zigpy/zigpy-znp + "zha": { + # Setuptools - distutils-precedence.pth + "colorzero": {"setuptools"}, + # Coloredlogs, unmaintained - coloredlogs.pth + # https://github.com/xolox/python-coloredlogs/blob/15.0.1/coloredlogs.pth + "zigpy-znp": {"coloredlogs"}, + }, } PYTHON_VERSION_CHECK_EXCEPTIONS: dict[str, dict[str, set[str]]] = { @@ -670,8 +724,10 @@ def check_dependency_files( for file in files(pkg) or (): if not (top := file.parts[0].lower()).endswith((".dist-info", ".py")): top_level.add(top) - if (name := str(file)).lower() in FORBIDDEN_FILE_NAMES: - file_names.add(name) + if (name := str(file).lower()) in FORBIDDEN_FILE_NAMES or ( + name.endswith(".pth") and len(file.parts) == 1 + ): + file_names.add(str(file)) results = _PackageFilesCheckResult( top_level=FORBIDDEN_PACKAGE_NAMES & top_level, file_names=file_names, @@ -687,7 +743,8 @@ def check_dependency_files( f"Package {pkg} has a forbidden top level directory '{dir_name}' in {package}", ) for file_name in results["file_names"]: - integration.add_error( + integration.add_warning_or_error( + pkg in package_exceptions, "requirements", f"Package {pkg} has a forbidden file '{file_name}' in {package}", ) diff --git a/tests/hassfest/test_requirements.py b/tests/hassfest/test_requirements.py index 329357bfca4..ac95ce1e4d1 100644 --- a/tests/hassfest/test_requirements.py +++ b/tests/hassfest/test_requirements.py @@ -275,6 +275,7 @@ def test_check_dependency_file_names(integration: Integration) -> None: pkg_files = [ PackagePath("py.typed"), PackagePath("my_package.py"), + PackagePath("some_script.Pth"), PackagePath("my_package-1.0.0.dist-info/METADATA"), ] with ( @@ -285,17 +286,23 @@ def test_check_dependency_file_names(integration: Integration) -> None: ): assert not _packages_checked_files_cache assert check_dependency_files(integration, package, pkg, ()) is False - assert _packages_checked_files_cache[pkg]["file_names"] == {"py.typed"} - assert len(integration.errors) == 1 + assert _packages_checked_files_cache[pkg]["file_names"] == { + "py.typed", + "some_script.Pth", + } + assert len(integration.errors) == 2 assert f"Package {pkg} has a forbidden file 'py.typed' in {package}" in [ x.error for x in integration.errors ] + assert f"Package {pkg} has a forbidden file 'some_script.Pth' in {package}" in [ + x.error for x in integration.errors + ] integration.errors.clear() # Repeated call should use cache assert check_dependency_files(integration, package, pkg, ()) is False assert mock_files.call_count == 1 - assert len(integration.errors) == 1 + assert len(integration.errors) == 2 integration.errors.clear() # All good