소스 검색

Close the scandir handle in available_configs (#6091)

`available_configs()` yields from inside a bare `scandir()` loop.
`scandir()` holds an open directory fd until the iterator is exhausted,
closed or collected, so a consumer that stops iterating early leaves it
to the garbage collector. This wraps the scan in a `with` block, which
is the form the `os.scandir` docs recommend for exactly this case.

This is a hygiene fix rather than a user-facing bug. CPython closes the
fd as soon as the abandoned generator is collected, so nothing
accumulates in practice.

Notes:

- Reproduction: take one entry, drop the generator, collect. On current
main that emits `ResourceWarning: unclosed scandir iterator`; with the
fix, nothing.
- The early exit happens in `test_can_find_config_files`, which breaks
after the first entry. `config_for_legacy_use` also returns from inside
the loop, but it is only reachable from `get_config`, which the
integration itself never calls.
- What it buys: no ResourceWarning where warnings are escalated to
errors, and a deterministic close rather than one that depends on
refcounting.
- Added a regression test, which fails without the fix.
- ruff, yamllint and the full pytest run pass locally.
Leesh 15 시간 전
부모
커밋
38347807cf
2개의 변경된 파일19개의 추가작업 그리고 3개의 파일을 삭제
  1. 4 3
      custom_components/tuya_local/helpers/device_config.py
  2. 15 0
      tests/test_device_config.py

+ 4 - 3
custom_components/tuya_local/helpers/device_config.py

@@ -1180,9 +1180,10 @@ def available_configs():
     """List the available config files."""
     _CONFIG_DIR = dirname(config_dir.__file__)
 
-    for direntry in scandir(_CONFIG_DIR):
-        if direntry.is_file() and fnmatch(direntry.name, "*.yaml"):
-            yield direntry.name
+    with scandir(_CONFIG_DIR) as entries:
+        for direntry in entries:
+            if direntry.is_file() and fnmatch(direntry.name, "*.yaml"):
+                yield direntry.name
 
 
 def possible_matches(dps, product_ids=None):

+ 15 - 0
tests/test_device_config.py

@@ -1,5 +1,8 @@
 """Test the config parser"""
 
+import gc
+import warnings
+
 import pytest
 import voluptuous as vol
 from fuzzywuzzy import fuzz
@@ -340,6 +343,18 @@ def test_can_find_config_files():
     assert found
 
 
+def test_available_configs_closes_scandir_handle():
+    """Test that the scandir handle is closed when the generator is dropped."""
+    with warnings.catch_warnings(record=True) as caught:
+        warnings.simplefilter("always")
+        configs = available_configs()
+        next(configs)
+        del configs
+        gc.collect()
+
+    assert not [w for w in caught if issubclass(w.category, ResourceWarning)]
+
+
 def dp_match(condition, accounted, unaccounted, known, required=False):
     if isinstance(condition, str):
         known.add(condition)