Repository: XKNX/xknx
Pull request #1424 (2024-03-04 10:27:18)

Bump ruff from 0.2.2 to 0.3.0
Bumps [ruff](https://github.com/astral-sh/ruff) from 0.2.2 to 0.3.0.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/astral-sh/ruff/releases">ruff's releases</a>.</em></p>
<blockquote>
<h2>v0.3.0</h2>
<p>This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to
detect invalid formatter suppression comments.</p>
<h2>Changes</h2>
<h3>Preview features</h3>
<ul>
<li>[<code>flake8-bandit</code>] Remove suspicious-lxml-import (<code>S410</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10154">#10154</a>)</li>
<li>[<code>pycodestyle</code>] Allow <code>os.environ</code> modifications between imports (<code>E402</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10066">#10066</a>)</li>
<li>[<code>pycodestyle</code>] Don't warn about a single whitespace character before a comma in a tuple (<code>E203</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10094">#10094</a>)</li>
</ul>
<h3>Rule changes</h3>
<ul>
<li>[<code>eradicate</code>] Detect commented out <code>case</code> statements (<code>ERA001</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10055">#10055</a>)</li>
<li>[<code>eradicate</code>] Detect single-line code for <code>try:</code>, <code>except:</code>, etc. (<code>ERA001</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10057">#10057</a>)</li>
<li>[<code>flake8-boolean-trap</code>] Allow boolean positionals in <code>__post_init__</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/10027">#10027</a>)</li>
<li>[<code>flake8-copyright</code>] Allow © in copyright notices (<a href="https://redirect.github.com/astral-sh/ruff/pull/10065">#10065</a>)</li>
<li>[<code>isort</code>]: Use one blank line after imports in typing stub files (<a href="https://redirect.github.com/astral-sh/ruff/pull/9971">#9971</a>)</li>
<li>[<code>pylint</code>] New Rule <code>dict-iter-missing-items</code> (<code>PLE1141</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/9845">#9845</a>)</li>
<li>[<code>pylint</code>] Ignore <code>sys.version</code> and <code>sys.platform</code> (<code>PLR1714</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10054">#10054</a>)</li>
<li>[<code>pyupgrade</code>] Detect literals with unary operators (<code>UP018</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10060">#10060</a>)</li>
<li>[<code>ruff</code>] Expand rule for <code>list(iterable).pop(0)</code> idiom (<code>RUF015</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10148">#10148</a>)</li>
</ul>
<h3>Formatter</h3>
<p>This release introduces the Ruff 2024.2 style, stabilizing the following changes:</p>
<ul>
<li>Prefer splitting the assignment's value over the target or type annotation (<a href="https://redirect.github.com/astral-sh/ruff/pull/8943">#8943</a>)</li>
<li>Remove blank lines before class docstrings (<a href="https://redirect.github.com/astral-sh/ruff/pull/9154">#9154</a>)</li>
<li>Wrap multiple context managers in <code>with</code> parentheses when targeting Python 3.9 or newer (<a href="https://redirect.github.com/astral-sh/ruff/pull/9222">#9222</a>)</li>
<li>Add a blank line after nested classes with a dummy body (<code>...</code>) in typing stub files (<a href="https://redirect.github.com/astral-sh/ruff/pull/9155">#9155</a>)</li>
<li>Reduce vertical spacing for classes and functions with a dummy (<code>...</code>) body (<a href="https://redirect.github.com/astral-sh/ruff/issues/7440">#7440</a>, <a href="https://redirect.github.com/astral-sh/ruff/pull/9240">#9240</a>)</li>
<li>Add a blank line after the module docstring (<a href="https://redirect.github.com/astral-sh/ruff/pull/8283">#8283</a>)</li>
<li>Parenthesize long type hints in assignments (<a href="https://redirect.github.com/astral-sh/ruff/pull/9210">#9210</a>)</li>
<li>Preserve indent for single multiline-string call-expressions (<a href="https://redirect.github.com/astral-sh/ruff/pull/9637">#9673</a>)</li>
<li>Normalize hex escape and unicode escape sequences (<a href="https://redirect.github.com/astral-sh/ruff/pull/9280">#9280</a>)</li>
<li>Format module docstrings (<a href="https://redirect.github.com/astral-sh/ruff/pull/9725">#9725</a>)</li>
</ul>
<h3>CLI</h3>
<ul>
<li>Explicitly disallow <code>extend</code> as part of a <code>--config</code> flag (<a href="https://redirect.github.com/astral-sh/ruff/pull/10135">#10135</a>)</li>
<li>Remove <code>build</code> from the default exclusion list (<a href="https://redirect.github.com/astral-sh/ruff/pull/10093">#10093</a>)</li>
<li>Deprecate <code>ruff &lt;path&gt;</code>, <code>ruff --explain</code>, <code>ruff --clean</code>, and <code>ruff --generate-shell-completion</code> in favor of <code>ruff check &lt;path&gt;</code>, <code>ruff rule</code>, <code>ruff clean</code>, and <code>ruff generate-shell-completion</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/10169">#10169</a>)</li>
<li>Remove the deprecated CLI option <code>--format</code> from <code>ruff rule</code> and <code>ruff linter</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/10170">#10170</a>)</li>
</ul>
<h3>Bug fixes</h3>
<ul>
<li>[<code>flake8-bugbear</code>] Avoid adding default initializers to stubs (<code>B006</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10152">#10152</a>)</li>
<li>[<code>flake8-type-checking</code>] Respect runtime-required decorators for function signatures (<a href="https://redirect.github.com/astral-sh/ruff/pull/10091">#10091</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/astral-sh/ruff/blob/main/CHANGELOG.md">ruff's changelog</a>.</em></p>
<blockquote>
<h2>0.3.0</h2>
<p>This release introduces the new Ruff formatter 2024.2 style and adds a new lint rule to
detect invalid formatter suppression comments.</p>
<h3>Preview features</h3>
<ul>
<li>[<code>flake8-bandit</code>] Remove suspicious-lxml-import (<code>S410</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10154">#10154</a>)</li>
<li>[<code>pycodestyle</code>] Allow <code>os.environ</code> modifications between imports (<code>E402</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10066">#10066</a>)</li>
<li>[<code>pycodestyle</code>] Don't warn about a single whitespace character before a comma in a tuple (<code>E203</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10094">#10094</a>)</li>
</ul>
<h3>Rule changes</h3>
<ul>
<li>[<code>eradicate</code>] Detect commented out <code>case</code> statements (<code>ERA001</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10055">#10055</a>)</li>
<li>[<code>eradicate</code>] Detect single-line code for <code>try:</code>, <code>except:</code>, etc. (<code>ERA001</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10057">#10057</a>)</li>
<li>[<code>flake8-boolean-trap</code>] Allow boolean positionals in <code>__post_init__</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/10027">#10027</a>)</li>
<li>[<code>flake8-copyright</code>] Allow © in copyright notices (<a href="https://redirect.github.com/astral-sh/ruff/pull/10065">#10065</a>)</li>
<li>[<code>isort</code>]: Use one blank line after imports in typing stub files (<a href="https://redirect.github.com/astral-sh/ruff/pull/9971">#9971</a>)</li>
<li>[<code>pylint</code>] New Rule <code>dict-iter-missing-items</code> (<code>PLE1141</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/9845">#9845</a>)</li>
<li>[<code>pylint</code>] Ignore <code>sys.version</code> and <code>sys.platform</code> (<code>PLR1714</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10054">#10054</a>)</li>
<li>[<code>pyupgrade</code>] Detect literals with unary operators (<code>UP018</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10060">#10060</a>)</li>
<li>[<code>ruff</code>] Expand rule for <code>list(iterable).pop(0)</code> idiom (<code>RUF015</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10148">#10148</a>)</li>
</ul>
<h3>Formatter</h3>
<p>This release introduces the Ruff 2024.2 style, stabilizing the following changes:</p>
<ul>
<li>Prefer splitting the assignment's value over the target or type annotation (<a href="https://redirect.github.com/astral-sh/ruff/pull/8943">#8943</a>)</li>
<li>Remove blank lines before class docstrings (<a href="https://redirect.github.com/astral-sh/ruff/pull/9154">#9154</a>)</li>
<li>Wrap multiple context managers in <code>with</code> parentheses when targeting Python 3.9 or newer (<a href="https://redirect.github.com/astral-sh/ruff/pull/9222">#9222</a>)</li>
<li>Add a blank line after nested classes with a dummy body (<code>...</code>) in typing stub files (<a href="https://redirect.github.com/astral-sh/ruff/pull/9155">#9155</a>)</li>
<li>Reduce vertical spacing for classes and functions with a dummy (<code>...</code>) body (<a href="https://redirect.github.com/astral-sh/ruff/issues/7440">#7440</a>, <a href="https://redirect.github.com/astral-sh/ruff/pull/9240">#9240</a>)</li>
<li>Add a blank line after the module docstring (<a href="https://redirect.github.com/astral-sh/ruff/pull/8283">#8283</a>)</li>
<li>Parenthesize long type hints in assignments (<a href="https://redirect.github.com/astral-sh/ruff/pull/9210">#9210</a>)</li>
<li>Preserve indent for single multiline-string call-expressions (<a href="https://redirect.github.com/astral-sh/ruff/pull/9637">#9673</a>)</li>
<li>Normalize hex escape and unicode escape sequences (<a href="https://redirect.github.com/astral-sh/ruff/pull/9280">#9280</a>)</li>
<li>Format module docstrings (<a href="https://redirect.github.com/astral-sh/ruff/pull/9725">#9725</a>)</li>
</ul>
<h3>CLI</h3>
<ul>
<li>Explicitly disallow <code>extend</code> as part of a <code>--config</code> flag (<a href="https://redirect.github.com/astral-sh/ruff/pull/10135">#10135</a>)</li>
<li>Remove <code>build</code> from the default exclusion list (<a href="https://redirect.github.com/astral-sh/ruff/pull/10093">#10093</a>)</li>
<li>Deprecate <code>ruff &lt;path&gt;</code>, <code>ruff --explain</code>, <code>ruff --clean</code>, and <code>ruff --generate-shell-completion</code> in favor of <code>ruff check &lt;path&gt;</code>, <code>ruff rule</code>, <code>ruff clean</code>, and <code>ruff generate-shell-completion</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/10169">#10169</a>)</li>
<li>Remove the deprecated CLI option <code>--format</code> from <code>ruff rule</code> and <code>ruff linter</code> (<a href="https://redirect.github.com/astral-sh/ruff/pull/10170">#10170</a>)</li>
</ul>
<h3>Bug fixes</h3>
<ul>
<li>[<code>flake8-bugbear</code>] Avoid adding default initializers to stubs (<code>B006</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10152">#10152</a>)</li>
<li>[<code>flake8-type-checking</code>] Respect runtime-required decorators for function signatures (<a href="https://redirect.github.com/astral-sh/ruff/pull/10091">#10091</a>)</li>
<li>[<code>pycodestyle</code>] Mark fixes overlapping with a multiline string as unsafe (<code>W293</code>) (<a href="https://redirect.github.com/astral-sh/ruff/pull/10049">#10049</a>)</li>
</ul>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/astral-sh/ruff/commit/b53118ed0016ac37233d3dadbcea9ed3ac1f538e"><code>b53118e</code></a> Bump version to v0.3.0 (<a href="https://redirect.github.com/astral-sh/ruff/issues/10151">#10151</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/52f4c1e41be0ce09181c220b1e4918d5bb5ee596"><code>52f4c1e</code></a> Remove deprecated CLI option <code>--format</code> (<a href="https://redirect.github.com/astral-sh/ruff/issues/10170">#10170</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/eceffe74a06f0b1163491624c6c696ebdf3648e9"><code>eceffe7</code></a> Deprecate <code>ruff \&lt;path&gt;</code> <code>ruff --explain</code>, <code>ruff --clean</code> and `ruff --generate...</li>
<li><a href="https://github.com/astral-sh/ruff/commit/c73c497477f5abd0d4133c0508d233ebcd78aeba"><code>c73c497</code></a> [<code>pydocstyle</code>] Trim whitespace when removing blank lines after section (`D413...</li>
<li><a href="https://github.com/astral-sh/ruff/commit/c9c98c4fe350fc66d442737ac3b978387fb436e7"><code>c9c98c4</code></a> Fix mkdocs local link (<a href="https://redirect.github.com/astral-sh/ruff/issues/10167">#10167</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/72ccb34ba69969a15da3328248cdaebeccb82b38"><code>72ccb34</code></a> Fix ecosystem check for indico (<a href="https://redirect.github.com/astral-sh/ruff/issues/10164">#10164</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/dcc92f50cf3786fd3cc5a90d9c465fc80209aad6"><code>dcc92f5</code></a> Update black tests (<a href="https://redirect.github.com/astral-sh/ruff/issues/10166">#10166</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/a6f32ddc5e9582112e76ad25039e9b080e281dce"><code>a6f32dd</code></a> Ruff 2024.2 style (<a href="https://redirect.github.com/astral-sh/ruff/issues/9639">#9639</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/0293908b71eeaf610d8ce77d2950f0602fd88dc5"><code>0293908</code></a> Implement RUF028 to detect useless formatter suppression comments (<a href="https://redirect.github.com/astral-sh/ruff/issues/9899">#9899</a>)</li>
<li><a href="https://github.com/astral-sh/ruff/commit/36bc725eaa8d0decb4ad880ddb42094a967feb20"><code>36bc725</code></a> [<code>flake8-bugbear</code>] Avoid adding default initializers to stubs (<code>B006</code>) (<a href="https://redirect.github.com/astral-sh/ruff/issues/10152">#10152</a>)</li>
<li>Additional commits viewable in <a href="https://github.com/astral-sh/ruff/compare/v0.2.2...v0.3.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=ruff&package-manager=pip&previous-version=0.2.2&new-version=0.3.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index d088f5df..c79c94f3 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -6,6 +6,12 @@ nav_order: 2
 
 # Changelog
 
+# 2.12.2 Fix thread leak 2024-03-05
+
+### Bugfixes
+
+- Fix thread leak when initial connection attempt fails (on threaded connection mode).
+
 # 2.12.1 Address error messages 2024-02-26
 
 ### Internal
diff --git a/xknx/__version__.py b/xknx/__version__.py
index 0943f6cc..8a19238c 100644
--- a/xknx/__version__.py
+++ b/xknx/__version__.py
@@ -1,3 +1,3 @@
 """XKNX version."""
 
-__version__ = "2.12.1"
+__version__ = "2.12.2"
diff --git a/xknx/io/knxip_interface.py b/xknx/io/knxip_interface.py
index d07167ff..a66ee6b4 100644
--- a/xknx/io/knxip_interface.py
+++ b/xknx/io/knxip_interface.py
@@ -499,16 +499,19 @@ class KNXIPInterfaceThreaded(KNXIPInterface):
         """Initialize KNXIPInterface class."""
         super().__init__(xknx, connection_config)
         self._main_loop: asyncio.AbstractEventLoop = asyncio.get_running_loop()
-        self._thread_loop: asyncio.AbstractEventLoop
+        self._connection_thread: threading.Thread | None = None
+        self._thread_loop: asyncio.AbstractEventLoop | None = None
 
+    def _init_connection_thread(self) -> None:
+        """Start KNX/IP interface in its own thread."""
         loop_loaded = threading.Event()
-        self.connection_thread = threading.Thread(
+        self._connection_thread = threading.Thread(
             target=self._init_connection_loop,
             args=[loop_loaded],
             name="KNX Interface",
             daemon=True,
         )
-        self.connection_thread.start()
+        self._connection_thread.start()
         loop_loaded.wait()  # wait for the thread to initialize its loop
 
     def _init_connection_loop(self, loop_loaded: threading.Event) -> None:
@@ -520,6 +523,9 @@ class KNXIPInterfaceThreaded(KNXIPInterface):
 
     async def _await_from_connection_thread(self, coro: Awaitable[T]) -> T:
         """Await coroutine in different thread."""
+        if self._thread_loop is None:
+            raise CommunicationError("KNX connection thread not initialized.")
+
         fut = asyncio.run_coroutine_threadsafe(coro, self._thread_loop)
         finished = threading.Event()
 
@@ -534,19 +540,26 @@ class KNXIPInterfaceThreaded(KNXIPInterface):
 
     async def start(self) -> None:
         """Start KNX/IP interface."""
-        return await self._await_from_connection_thread(self._start())
+        if self._connection_thread is not None or self._thread_loop is not None:
+            raise CommunicationError("KNX threaded interface already initialized.")
+        await self._main_loop.run_in_executor(None, self._init_connection_thread)
+        try:
+            return await self._await_from_connection_thread(self._start())
+        except CommunicationError:
+            await self.stop()
+            raise
 
     async def stop(self) -> None:
-        """
-        Stop connected interface (either Tunneling or Routing).
-
-        Can not be restarted, create a new instance instead.
-        """
+        """Stop connected interface (either Tunneling or Routing)."""
         if self._interface is not None:
             await self._await_from_connection_thread(self._interface.disconnect())
             self._interface = None
-        self._thread_loop.call_soon_threadsafe(self._thread_loop.stop)
-        self.connection_thread.join()
+        if self._thread_loop is not None:
+            self._thread_loop.call_soon_threadsafe(self._thread_loop.stop)
+            self._thread_loop = None
+        if self._connection_thread is not None:
+            self._connection_thread.join()
+            self._connection_thread = None
 
     def cemi_received(self, raw_cemi: bytes) -> None:
         """Pass CEMIFrame to CEMIHandler. Callback for having received CEMIFrames."""


Test patch:
diff --git a/requirements/testing.txt b/requirements/testing.txt
index bf31b336..8243f844 100644
--- a/requirements/testing.txt
+++ b/requirements/testing.txt
@@ -1,11 +1,11 @@
 -r production.txt
 pre-commit==3.6.2
 pylint==3.1.0
-pytest==8.0.2
+pytest==8.1.0
 pytest-asyncio==0.21.1
 pytest-cov==4.1.0
 pytest-icdiff==0.9
-ruff==0.2.2
+ruff==0.3.0
 setuptools==69.1.1
 tox==4.13.0
 tox-gh-actions==3.2.0
diff --git a/test/io_tests/knxip_interface_test.py b/test/io_tests/knxip_interface_test.py
index 6d47b0fc..6a3b0049 100644
--- a/test/io_tests/knxip_interface_test.py
+++ b/test/io_tests/knxip_interface_test.py
@@ -312,6 +312,10 @@ class TestKNXIPInterface:
             await interface.start()
             start_automatic_mock.assert_called_once_with(local_ip=None, keyring=None)
 
+            with pytest.raises(CommunicationError):
+                # interface can only be started once
+                await interface.start()
+
     async def test_threaded_send_cemi(self):
         """Test sending cemi with threaded connection."""
         # pylint: disable=attribute-defined-outside-init
@@ -346,6 +350,20 @@ class TestKNXIPInterface:
             disconnect_routing_mock.assert_called_once_with()
             assert interface._interface is None
 
+    async def test_threaded_connection_unsuccessful_start(self):
+        """Test cleanup when unsuccessful initial connection."""
+        connection_config = ConnectionConfig(threaded=True)
+        assert connection_config.connection_type == ConnectionType.AUTOMATIC
+        with patch(
+            "xknx.io.KNXIPInterface._start", side_effect=CommunicationError("Error")
+        ):
+            interface = knx_interface_factory(self.xknx, connection_config)
+            with pytest.raises(CommunicationError):
+                await interface.start()
+            # thread and loop are cleaned up after unsuccessful start
+            assert interface._connection_thread is None
+            assert interface._thread_loop is None
+
     async def test_start_secure_connection_knx_keys_user_id(self):
         """Test starting a secure connection from a knxkeys file with user_id."""
         gateway_ip = "192.168.1.1"