lifx-async 5.0.0__tar.gz → 5.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lifx_async-5.0.0 → lifx_async-5.0.1}/PKG-INFO +1 -1
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/changelog.md +8 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/pyproject.toml +1 -1
- lifx_async-5.0.1/scripts/test_multiversion.py +227 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/const.py +18 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/effects/colorloop.py +2 -2
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/connection.py +6 -5
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/mdns/transport.py +2 -2
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/transport.py +8 -3
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_api/test_api_apply_theme.py +2 -1
- {lifx_async-5.0.0 → lifx_async-5.0.1}/uv.lock +1 -1
- {lifx_async-5.0.0 → lifx_async-5.0.1}/.claude/settings.json +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/.github/dependabot.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/.github/labeler.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/.github/workflows/ci.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/.github/workflows/docs.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/.github/workflows/pr-automation.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/.gitignore +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/.pre-commit-config.yaml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/CLAUDE.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/LICENSE +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/README.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/context7.json +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/colors.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/devices.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/effects.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/exceptions.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/high-level.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/index.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/network.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/protocol.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/api/themes.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/architecture/effects-architecture.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/architecture/overview.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/faq.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/getting-started/effects.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/getting-started/installation.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/getting-started/quickstart.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/getting-started/themes.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/index.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/migration/effect-api-changes.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/stylesheets/extra.css +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/user-guide/advanced-usage.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/user-guide/ceiling-lights.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/user-guide/effects-custom.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/user-guide/effects-troubleshooting.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/user-guide/protocol-deep-dive.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/user-guide/themes.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/docs/user-guide/troubleshooting.md +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/01_simple_discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/02_simple_control.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/03_waveforms.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/04_logging.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/06_pulse_effect.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/07_colorloop_effect.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/08_custom_effect.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/09_background_effect.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/10_find_specific_devices.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/11_matrix_basic.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/12_matrix_effects.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/13_matrix_large.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/examples/14_mdns_discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/mkdocs.yml +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/renovate.json +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/scripts/mdns_probe.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/api.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/color.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/devices/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/devices/base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/devices/ceiling.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/devices/hev.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/devices/infrared.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/devices/light.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/devices/matrix.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/devices/multizone.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/effects/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/effects/base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/effects/conductor.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/effects/const.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/effects/models.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/effects/pulse.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/effects/state_manager.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/exceptions.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/mdns/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/mdns/discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/mdns/dns.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/mdns/types.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/network/message.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/products/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/products/generator.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/products/quirks.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/products/registry.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/protocol/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/protocol/base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/protocol/generator.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/protocol/header.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/protocol/models.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/protocol/packets.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/protocol/protocol_types.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/protocol/serializer.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/py.typed +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/theme/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/theme/canvas.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/theme/generators.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/theme/library.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/src/lifx/theme/theme.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/conftest.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_api/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_api/test_api_batch_errors.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_api/test_api_batch_operations.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_api/test_api_discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_api/test_api_organization.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_color.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/conftest.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_ceiling.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_hev.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_infrared.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_light.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_mac_address.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_matrix.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_multizone.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_state_ceiling.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_state_hev.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_state_infrared.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_state_light.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_state_management.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_state_matrix.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_devices/test_state_multizone.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_effects/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_effects/test_base.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_effects/test_capability_filtering.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_effects/test_colorloop.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_effects/test_integration.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_effects/test_models.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_effects/test_pulse.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_effects/test_state_manager.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_concurrent_requests.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_connection.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_discovery_devices.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_discovery_errors.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_mdns/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_mdns/conftest.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_mdns/test_discovery.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_mdns/test_dns.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_mdns/test_transport.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_message.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_message_advanced.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_network/test_transport.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_products/test_product_generator.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_products/test_registry.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_protocol/test_generated.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_protocol/test_header.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_protocol/test_protocol_generator.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_protocol/test_serializer.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_theme/__init__.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_theme/conftest.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_theme/test_apply_theme.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_theme/test_canvas.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_theme/test_generators.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_theme/test_library.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_theme/test_theme.py +0 -0
- {lifx_async-5.0.0 → lifx_async-5.0.1}/tests/test_utils.py +0 -0
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<!-- version list -->
|
|
4
4
|
|
|
5
|
+
## v5.0.1 (2026-01-14)
|
|
6
|
+
|
|
7
|
+
### Bug Fixes
|
|
8
|
+
|
|
9
|
+
- Handle asyncio.TimeoutError on Python 3.10
|
|
10
|
+
([`4438bc4`](https://github.com/Djelibeybi/lifx-async/commit/4438bc45f19f477b585c6af8cf8cbaf5e9341d14))
|
|
11
|
+
|
|
12
|
+
|
|
5
13
|
## v5.0.0 (2026-01-12)
|
|
6
14
|
|
|
7
15
|
### Features
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Multi-version Python test runner.
|
|
3
|
+
|
|
4
|
+
This script runs the test suite against multiple Python versions to catch
|
|
5
|
+
version-specific issues (like the asyncio.TimeoutError vs TimeoutError
|
|
6
|
+
difference between Python 3.10 and 3.11+) before pushing to GitHub.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
# Run tests on all supported versions (default)
|
|
10
|
+
uv run scripts/test_multiversion.py
|
|
11
|
+
|
|
12
|
+
# Run tests on specific versions only
|
|
13
|
+
uv run scripts/test_multiversion.py --versions 3.10 3.14
|
|
14
|
+
|
|
15
|
+
# Run with verbose pytest output
|
|
16
|
+
uv run scripts/test_multiversion.py -v
|
|
17
|
+
|
|
18
|
+
# Run specific test file/pattern
|
|
19
|
+
uv run scripts/test_multiversion.py -- tests/test_network/
|
|
20
|
+
|
|
21
|
+
# Quick mode: skip coverage
|
|
22
|
+
uv run scripts/test_multiversion.py --quick
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import subprocess # nosec B404 - subprocess needed for running uv/pytest
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
|
|
33
|
+
# Python versions supported by this project (from pyproject.toml)
|
|
34
|
+
SUPPORTED_VERSIONS = ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
35
|
+
|
|
36
|
+
# Default: test all supported versions
|
|
37
|
+
DEFAULT_VERSIONS = SUPPORTED_VERSIONS
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class TestResult:
|
|
42
|
+
"""Result of running tests on a specific Python version."""
|
|
43
|
+
|
|
44
|
+
version: str
|
|
45
|
+
success: bool
|
|
46
|
+
duration: float
|
|
47
|
+
output: str
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check_python_available(version: str) -> bool:
|
|
51
|
+
"""Check if a Python version is available via uv."""
|
|
52
|
+
result = subprocess.run( # nosec B603 B607 - trusted uv command
|
|
53
|
+
["uv", "python", "find", version],
|
|
54
|
+
capture_output=True,
|
|
55
|
+
text=True,
|
|
56
|
+
)
|
|
57
|
+
return result.returncode == 0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def run_tests(
|
|
61
|
+
version: str,
|
|
62
|
+
pytest_args: list[str],
|
|
63
|
+
verbose: bool = False,
|
|
64
|
+
quick: bool = False,
|
|
65
|
+
) -> TestResult:
|
|
66
|
+
"""Run pytest on a specific Python version using uv."""
|
|
67
|
+
start_time = time.time()
|
|
68
|
+
|
|
69
|
+
cmd = [
|
|
70
|
+
"uv",
|
|
71
|
+
"run",
|
|
72
|
+
"--python",
|
|
73
|
+
version,
|
|
74
|
+
"--isolated", # Use isolated environment to avoid conflicts
|
|
75
|
+
"pytest",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# Add coverage unless in quick mode
|
|
79
|
+
if not quick:
|
|
80
|
+
cmd.extend(["--cov=lifx", "--cov-report=term-missing:skip-covered"])
|
|
81
|
+
|
|
82
|
+
# Add verbosity
|
|
83
|
+
if verbose:
|
|
84
|
+
cmd.append("-v")
|
|
85
|
+
else:
|
|
86
|
+
cmd.append("-q")
|
|
87
|
+
|
|
88
|
+
# Add any additional pytest arguments
|
|
89
|
+
cmd.extend(pytest_args)
|
|
90
|
+
|
|
91
|
+
# Ignore animation tests (uncommitted module with known issues)
|
|
92
|
+
cmd.extend(["--ignore=tests/test_animation"])
|
|
93
|
+
|
|
94
|
+
print(f"\n{'=' * 60}")
|
|
95
|
+
print(f"Running tests on Python {version}")
|
|
96
|
+
print(f"{'=' * 60}")
|
|
97
|
+
print(f"Command: {' '.join(cmd)}\n")
|
|
98
|
+
|
|
99
|
+
result = subprocess.run( # nosec B603 - trusted uv/pytest command
|
|
100
|
+
cmd,
|
|
101
|
+
capture_output=not verbose, # Show output in real-time if verbose
|
|
102
|
+
text=True,
|
|
103
|
+
cwd=subprocess.run( # nosec B603 B607 - trusted git command
|
|
104
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
105
|
+
capture_output=True,
|
|
106
|
+
text=True,
|
|
107
|
+
).stdout.strip(),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
duration = time.time() - start_time
|
|
111
|
+
|
|
112
|
+
output = ""
|
|
113
|
+
if not verbose:
|
|
114
|
+
output = result.stdout + result.stderr
|
|
115
|
+
|
|
116
|
+
return TestResult(
|
|
117
|
+
version=version,
|
|
118
|
+
success=result.returncode == 0,
|
|
119
|
+
duration=duration,
|
|
120
|
+
output=output,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def main() -> int:
|
|
125
|
+
"""Run tests across multiple Python versions."""
|
|
126
|
+
parser = argparse.ArgumentParser(
|
|
127
|
+
description="Run tests across multiple Python versions",
|
|
128
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
129
|
+
epilog=__doc__,
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--versions",
|
|
133
|
+
nargs="+",
|
|
134
|
+
default=None,
|
|
135
|
+
help=f"Python versions to test (default: all supported: {SUPPORTED_VERSIONS})",
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"-v",
|
|
139
|
+
"--verbose",
|
|
140
|
+
action="store_true",
|
|
141
|
+
help="Verbose pytest output",
|
|
142
|
+
)
|
|
143
|
+
parser.add_argument(
|
|
144
|
+
"--quick",
|
|
145
|
+
action="store_true",
|
|
146
|
+
help="Quick mode: skip coverage for faster execution",
|
|
147
|
+
)
|
|
148
|
+
parser.add_argument(
|
|
149
|
+
"pytest_args",
|
|
150
|
+
nargs="*",
|
|
151
|
+
help="Additional arguments to pass to pytest",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
args = parser.parse_args()
|
|
155
|
+
|
|
156
|
+
# Determine which versions to test
|
|
157
|
+
if args.versions:
|
|
158
|
+
versions = args.versions
|
|
159
|
+
else:
|
|
160
|
+
versions = DEFAULT_VERSIONS
|
|
161
|
+
|
|
162
|
+
# Validate versions
|
|
163
|
+
for version in versions:
|
|
164
|
+
if version not in SUPPORTED_VERSIONS:
|
|
165
|
+
print(
|
|
166
|
+
f"Warning: {version} is not in supported versions {SUPPORTED_VERSIONS}"
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Check availability
|
|
170
|
+
available_versions = []
|
|
171
|
+
missing_versions = []
|
|
172
|
+
for version in versions:
|
|
173
|
+
if check_python_available(version):
|
|
174
|
+
available_versions.append(version)
|
|
175
|
+
else:
|
|
176
|
+
missing_versions.append(version)
|
|
177
|
+
|
|
178
|
+
if missing_versions:
|
|
179
|
+
print(f"Missing Python versions: {missing_versions}")
|
|
180
|
+
print("Install with: uv python install " + " ".join(missing_versions))
|
|
181
|
+
if not available_versions:
|
|
182
|
+
return 1
|
|
183
|
+
|
|
184
|
+
print(f"Testing on Python versions: {available_versions}")
|
|
185
|
+
|
|
186
|
+
# Run tests
|
|
187
|
+
results: list[TestResult] = []
|
|
188
|
+
for version in available_versions:
|
|
189
|
+
result = run_tests(
|
|
190
|
+
version,
|
|
191
|
+
args.pytest_args,
|
|
192
|
+
verbose=args.verbose,
|
|
193
|
+
quick=args.quick,
|
|
194
|
+
)
|
|
195
|
+
results.append(result)
|
|
196
|
+
|
|
197
|
+
if not args.verbose:
|
|
198
|
+
# Show last few lines of output for quick feedback
|
|
199
|
+
lines = result.output.strip().split("\n")
|
|
200
|
+
for line in lines[-10:]:
|
|
201
|
+
print(line)
|
|
202
|
+
|
|
203
|
+
# Summary
|
|
204
|
+
print(f"\n{'=' * 60}")
|
|
205
|
+
print("SUMMARY")
|
|
206
|
+
print(f"{'=' * 60}")
|
|
207
|
+
|
|
208
|
+
all_passed = True
|
|
209
|
+
for result in results:
|
|
210
|
+
status = "✓ PASSED" if result.success else "✗ FAILED"
|
|
211
|
+
print(f"Python {result.version}: {status} ({result.duration:.1f}s)")
|
|
212
|
+
if not result.success:
|
|
213
|
+
all_passed = False
|
|
214
|
+
|
|
215
|
+
if missing_versions:
|
|
216
|
+
print(f"\nSkipped (not installed): {missing_versions}")
|
|
217
|
+
|
|
218
|
+
if all_passed:
|
|
219
|
+
print("\n✓ All tests passed across all Python versions!")
|
|
220
|
+
return 0
|
|
221
|
+
else:
|
|
222
|
+
print("\n✗ Some tests failed. Check output above for details.")
|
|
223
|
+
return 1
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
if __name__ == "__main__":
|
|
227
|
+
sys.exit(main())
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# lifx-async constants
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import sys
|
|
3
5
|
import uuid
|
|
4
6
|
from typing import Final
|
|
5
7
|
|
|
@@ -109,3 +111,19 @@ PROTOCOL_URL: Final[str] = (
|
|
|
109
111
|
PRODUCTS_URL: Final[str] = (
|
|
110
112
|
"https://raw.githubusercontent.com/LIFX/products/refs/heads/master/products.json"
|
|
111
113
|
)
|
|
114
|
+
|
|
115
|
+
# ============================================================================
|
|
116
|
+
# Python Version Compatibility
|
|
117
|
+
# ============================================================================
|
|
118
|
+
|
|
119
|
+
# On Python 3.10, asyncio.wait_for() raises asyncio.TimeoutError which is NOT
|
|
120
|
+
# a subclass of the built-in TimeoutError. In Python 3.11+, they are unified.
|
|
121
|
+
# Use this tuple with `except TIMEOUT_ERRORS:` to catch timeouts from asyncio
|
|
122
|
+
# operations on all supported Python versions.
|
|
123
|
+
if sys.version_info < (3, 11):
|
|
124
|
+
TIMEOUT_ERRORS: Final[tuple[type[BaseException], ...]] = (
|
|
125
|
+
TimeoutError,
|
|
126
|
+
asyncio.TimeoutError,
|
|
127
|
+
)
|
|
128
|
+
else:
|
|
129
|
+
TIMEOUT_ERRORS: Final[tuple[type[BaseException], ...]] = (TimeoutError,)
|
|
@@ -11,7 +11,7 @@ import random
|
|
|
11
11
|
from typing import TYPE_CHECKING
|
|
12
12
|
|
|
13
13
|
from lifx.color import HSBK
|
|
14
|
-
from lifx.const import KELVIN_NEUTRAL
|
|
14
|
+
from lifx.const import KELVIN_NEUTRAL, TIMEOUT_ERRORS
|
|
15
15
|
from lifx.effects.base import LIFXEffect
|
|
16
16
|
|
|
17
17
|
if TYPE_CHECKING:
|
|
@@ -172,7 +172,7 @@ class EffectColorloop(LIFXEffect):
|
|
|
172
172
|
self._stop_event.wait(), timeout=iteration_period
|
|
173
173
|
)
|
|
174
174
|
break # Stop event was set
|
|
175
|
-
except
|
|
175
|
+
except TIMEOUT_ERRORS:
|
|
176
176
|
pass # Normal - continue to next iteration
|
|
177
177
|
|
|
178
178
|
iteration += 1
|
|
@@ -17,6 +17,7 @@ from lifx.const import (
|
|
|
17
17
|
DEFAULT_MAX_RETRIES,
|
|
18
18
|
DEFAULT_REQUEST_TIMEOUT,
|
|
19
19
|
LIFX_UDP_PORT,
|
|
20
|
+
TIMEOUT_ERRORS,
|
|
20
21
|
)
|
|
21
22
|
from lifx.exceptions import (
|
|
22
23
|
LifxConnectionError,
|
|
@@ -193,7 +194,7 @@ class DeviceConnection:
|
|
|
193
194
|
await asyncio.wait_for(
|
|
194
195
|
self._receiver_task, timeout=_RECEIVER_SHUTDOWN_TIMEOUT
|
|
195
196
|
)
|
|
196
|
-
except
|
|
197
|
+
except TIMEOUT_ERRORS:
|
|
197
198
|
self._receiver_task.cancel()
|
|
198
199
|
try:
|
|
199
200
|
await self._receiver_task
|
|
@@ -559,7 +560,7 @@ class DeviceConnection:
|
|
|
559
560
|
header, payload = await asyncio.wait_for(
|
|
560
561
|
response_queue.get(), timeout=remaining_time
|
|
561
562
|
)
|
|
562
|
-
except
|
|
563
|
+
except TIMEOUT_ERRORS:
|
|
563
564
|
if not has_yielded:
|
|
564
565
|
# No response this attempt, retry
|
|
565
566
|
raise TimeoutError(
|
|
@@ -603,7 +604,7 @@ class DeviceConnection:
|
|
|
603
604
|
|
|
604
605
|
# Continue loop to wait for more responses
|
|
605
606
|
|
|
606
|
-
except
|
|
607
|
+
except TIMEOUT_ERRORS as e:
|
|
607
608
|
last_error = LifxTimeoutError(str(e))
|
|
608
609
|
if attempt < max_retries:
|
|
609
610
|
# Sleep with jitter before retry
|
|
@@ -702,7 +703,7 @@ class DeviceConnection:
|
|
|
702
703
|
header, _payload = await asyncio.wait_for(
|
|
703
704
|
response_queue.get(), timeout=current_timeout
|
|
704
705
|
)
|
|
705
|
-
except
|
|
706
|
+
except TIMEOUT_ERRORS:
|
|
706
707
|
raise TimeoutError(
|
|
707
708
|
f"No acknowledgement within {current_timeout:.3f}s "
|
|
708
709
|
f"(attempt {attempt + 1}/{max_retries + 1})"
|
|
@@ -731,7 +732,7 @@ class DeviceConnection:
|
|
|
731
732
|
yield True
|
|
732
733
|
return
|
|
733
734
|
|
|
734
|
-
except
|
|
735
|
+
except TIMEOUT_ERRORS as e:
|
|
735
736
|
last_error = LifxTimeoutError(str(e))
|
|
736
737
|
if attempt < max_retries:
|
|
737
738
|
# Sleep with jitter before retry
|
|
@@ -13,7 +13,7 @@ import struct
|
|
|
13
13
|
from asyncio import DatagramTransport
|
|
14
14
|
from typing import TYPE_CHECKING
|
|
15
15
|
|
|
16
|
-
from lifx.const import MDNS_ADDRESS, MDNS_PORT
|
|
16
|
+
from lifx.const import MDNS_ADDRESS, MDNS_PORT, TIMEOUT_ERRORS
|
|
17
17
|
from lifx.exceptions import LifxNetworkError, LifxTimeoutError
|
|
18
18
|
|
|
19
19
|
if TYPE_CHECKING:
|
|
@@ -259,7 +259,7 @@ class MdnsTransport:
|
|
|
259
259
|
self._protocol.queue.get(), timeout=timeout
|
|
260
260
|
)
|
|
261
261
|
return data, addr
|
|
262
|
-
except
|
|
262
|
+
except TIMEOUT_ERRORS as e:
|
|
263
263
|
raise LifxTimeoutError(f"No mDNS data received within {timeout}s") from e
|
|
264
264
|
except OSError as e:
|
|
265
265
|
_LOGGER.debug(
|
|
@@ -6,7 +6,12 @@ import asyncio
|
|
|
6
6
|
import logging
|
|
7
7
|
from typing import TYPE_CHECKING
|
|
8
8
|
|
|
9
|
-
from lifx.const import
|
|
9
|
+
from lifx.const import (
|
|
10
|
+
DEFAULT_IP_ADDRESS,
|
|
11
|
+
MAX_PACKET_SIZE,
|
|
12
|
+
MIN_PACKET_SIZE,
|
|
13
|
+
TIMEOUT_ERRORS,
|
|
14
|
+
)
|
|
10
15
|
from lifx.exceptions import LifxNetworkError
|
|
11
16
|
from lifx.exceptions import LifxTimeoutError as LifxTimeoutError
|
|
12
17
|
|
|
@@ -208,7 +213,7 @@ class UdpTransport:
|
|
|
208
213
|
data, addr = await asyncio.wait_for(
|
|
209
214
|
self._protocol.queue.get(), timeout=timeout
|
|
210
215
|
)
|
|
211
|
-
except
|
|
216
|
+
except TIMEOUT_ERRORS as e:
|
|
212
217
|
raise LifxTimeoutError(f"No data received within {timeout}s") from e
|
|
213
218
|
except OSError as e:
|
|
214
219
|
_LOGGER.error(
|
|
@@ -302,7 +307,7 @@ class UdpTransport:
|
|
|
302
307
|
continue
|
|
303
308
|
|
|
304
309
|
packets.append((data, addr))
|
|
305
|
-
except
|
|
310
|
+
except TIMEOUT_ERRORS:
|
|
306
311
|
# Timeout is expected - return what we collected
|
|
307
312
|
break
|
|
308
313
|
except OSError:
|
|
@@ -11,6 +11,7 @@ from __future__ import annotations
|
|
|
11
11
|
import asyncio
|
|
12
12
|
|
|
13
13
|
from lifx.api import DeviceGroup
|
|
14
|
+
from lifx.const import TIMEOUT_ERRORS
|
|
14
15
|
from lifx.theme import ThemeLibrary
|
|
15
16
|
|
|
16
17
|
|
|
@@ -251,7 +252,7 @@ class TestDeviceGroupApplyTheme:
|
|
|
251
252
|
# (will fail at connection level, but our method works)
|
|
252
253
|
try:
|
|
253
254
|
await asyncio.wait_for(group.apply_theme(theme), timeout=0.1)
|
|
254
|
-
except
|
|
255
|
+
except TIMEOUT_ERRORS:
|
|
255
256
|
# Expected - no real devices
|
|
256
257
|
pass
|
|
257
258
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|