- re.escape() the whole filter string first to escape _all_ regex
metacharacters in it, not just $. (# and + are both regex metacharacters,
so their replace expressions now need a leading \\ to replace the
escaping, too.)
- # matches topics both with and without a trailing /, so the replace
expressions adds a '?' before the '.*'. The .lstrip('?') at the end removes
this in case the # was the first character in the filter.
- + should only match a single level, but it should _also_ match empty levels,
so use '[^/]*' to replace it.
- Use Regex.fullmatch() to match against the whole topic string, not just
its start.
Also add two unit tests to test this matching, and fix an incorrect match
against + in test_client_subscribe_publish_dollar_topic_2.
WebSocketsWriter.get_peer_info() naively returns just
self._protocol.remote_address, but this is a 4-tuple in IPv6,
which will raise ValueErrors when client code tries to unpack
it into two elements. Fortunately the first two elements of the
tuple are the same as in IPv4 and should be all that we need
(get_peer_info() is currently only used for logging), so just
return a 2-prefix of the value instead.
The plain `except BaseException` statement used in the two coroutines
also matches asyncio.CancelledError and thus prevents them from
being cancelled. Prepend a simple re-raising except block for
asyncio.CancelledError to get them to work with task cancellation.
If `loop` is passed to `MQTTClient`, previously it was not passed down
to `PluginManager`, which could potentially grab a different loop from
`asyncio.get_event_loop()`, resulting in a separate event loop being
created or wrong one being used. Now the `loop` argument is passed down
as expected.