Skip to content

Conversation

o-murphy
Copy link
Contributor

@o-murphy o-murphy commented Aug 21, 2025

This PR is solving #1813 issue and preparing project to resolve #1812

Changed


  • build_and_test.yml workflow_dispatch trigger added
  • Updated typing_extensions dependency platform flags
  • Simplified conditional imports from typing with alternatives from typing_extensions
    that resolves it under the hood, so we need no control it manually
  • Legacy collections.abc imports replaced with typing_extensions
    cause current required typing_extensions>=4.7 version already supports all them
  • Removed redundant (legacy) imports from __future__
  • Undefined annotations fixed

Added


  • Added _compat.py module to simplify future support for other python implementations, like pypy or circuitpython

@o-murphy o-murphy force-pushed the imports-refactoring branch from dfa1a49 to ad21cb6 Compare August 21, 2025 08:08
Changed
-------
- `build_and_test.yml` workflow_dispatch trigger added
- Updated `typing_extensions` dependency platform flags
- Simplified conditional imports from `typing` with alternatives from `typing_extensions`
  that resolves it under the hood, so we need no control it manually
- Legacy `collections.abc` imports replaced with `typing_extensions`
  cause current required `typing_extensions>=4.7` version already supports all them
- Removed redundant (legacy) imports from `__future__`
- Undefined annotations fixed

Added
-----
- Added `_compat.py` module to simplify future support for other python implementations, like `pypy` or `circuitpython`
@o-murphy o-murphy force-pushed the imports-refactoring branch from ad21cb6 to 952f579 Compare August 21, 2025 08:48
@o-murphy o-murphy marked this pull request as ready for review August 21, 2025 08:51
Copy link
Collaborator

@dlech dlech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there was some new typing feature that we would need typing_extensions for in more recent versions of Python, I could be convinced to not make it a conditional dependency. But as it is, I think this PR is too heavy-handed in undoing things that were intentionally done (see inline comments).

@@ -14,22 +12,24 @@
import os
import sys
import uuid
from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

collections.abc is the canonical place to import these from since Python 3.9, so this feels like a step backwards as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agree, but the problem this types are defined in different places for each python versions starting from 3.9, so if we know typing_extensions are duarantee reimport it as needed for each version why should we do it by ourself?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Till old python versions support will be done it looks quite good to use backward compatibility through typing_extensions instead of conditional imports.
Besides, I have great doubts that you will completely abandon typing_extension in the next few years.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have already dropped support for Python < 3.9, so I don't see why we would need to change this. typing_extensions just forwards typing declarations of these types and these types are deprecated in the typing module.

Copy link
Contributor Author

@o-murphy o-murphy Aug 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That’s why it’s logical. Instead of doing it manually in each individual module, you use a tool that does it under the hood, allowing you to focus on implementation rather than annotation compatibility. Why are you so sceptical to typing_extensions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rolled back

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you so sceptical to typing_extensions?

Not skeptical. One of the original core design decisions of Bleak was minimal dependencies. So that has been what has guided the current state of things.

pyproject.toml Outdated
@@ -27,7 +27,7 @@ classifiers = [

[tool.poetry.dependencies]
async-timeout = { version = ">=3.0.0", python = "<3.11" }
typing-extensions = { version = ">=4.7.0", python = "<3.12" }
typing-extensions = { version = ">=4.7.0" }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we would drop the typing-extensions dependency in a few years when we drop support for Python 3.11. This is why we have the extra conditionals in the code and here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is, but if you wanna conditional imports it would be a good practice then place it inside _compat.py module. You have such much this along the codebase

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Till 3.11 will be dropped I propose use typing_extensions directly, that is almost affects nothing except readability, but makes maintenance and extending simplier

@o-murphy
Copy link
Contributor Author

o-murphy commented Aug 21, 2025

If there was some new typing feature that we would need typing_extensions for in more recent versions of Python, I could be convinced to not make it a conditional dependency. But as it is, I think this PR is too heavy-handed in undoing things that were intentionally done (see inline comments).

If it's that fundamental, then the general practice is: "It's easier to ask for forgiveness than permission" so instead of if/else there should be try/except ImportError. I can refactor to this purpose, but take a look on my review replies

@o-murphy
Copy link
Contributor Author

@dlech

How can you explain that you mix your own version-based resolving with forwards from typing_extensions? Why don't you rely entirely on a self-written implementation then?

@o-murphy o-murphy force-pushed the imports-refactoring branch from 17192c7 to a744a60 Compare August 22, 2025 08:03
@dlech
Copy link
Collaborator

dlech commented Aug 22, 2025

Why don't you rely entirely on a self-written implementation then?

typing_extensions is handled as a special case by static analysis tools, so I don't think vendoring just the parts we need is an option.

@@ -60,7 +51,7 @@


# prevent tasks from being garbage collected
_background_tasks = set[asyncio.Task[None]]()
_background_tasks: set[asyncio.Task[None]] = set()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we apply at least changes like that? Won't it be more readable?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't say that one is objectively better than the other. So I'm not inclined to debate the readability since it is mostly a subjective measure.

If there was some existing linter check that had an opinion on this, it would make such a change more palatable.

Copy link
Contributor Author

@o-murphy o-murphy Aug 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reasoning for this change:

  • Canonical Syntax. This approach aligns with official recommendations from PEP 484 and PEP 585 (provided in python 3.9), making it the canonical way to write type annotations in Python.

  • Performance Overheads: Starting with Python 3.9, the expression set[T] is not a no-op, the interpreter resolves it at runtime into a types.GenericAlias object, and annotations would not be bypassed by the __future__.annotations
    You can check it running this simple code in REPL

    import asyncio
    type(set)  # <class 'type'>
    type(set[asyncio.Task[None]])  # <class 'types.GenericAlias'>
  • Improved Readability and Intent. The current syntax _background_tasks = set[asyncio.Task[None]]() can be confusing. It hides the true intent: the variable is being assigned a type object, not an empty set. This subtlety can be a source of confusion for other developers. In contrast, the refactored option clearly separates the type annotation from the initialization, making the code intuitive and easier to understand at a glance. Refactored option clearly shows the intent: we create a variable, annotate its type, and then initialize it with an empty value. This is intuitive.

  • Consistency and Robustness. While Bleak doesn't support Python versions below 3.9, it's worth noting that this syntax would raise a TypeError in older versions or language subsets. Adopting the standard approach ensures consistency and makes the code more robust against potential issues. Also it improves portability

  • You are already using : annotation style, so It will be good to have same style everywhere

  • You possibly can't check it with linter, but if you using annotations it's good practice to use some static type-checkers like mypy, and for now mypy fails for the whole project

@@ -11,7 +11,7 @@
from bleak.exc import BleakError

# prevent tasks from being garbage collected
_background_tasks = set[asyncio.Task[None]]()
_background_tasks: set[asyncio.Task[None]] = set()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in init

@@ -170,7 +161,7 @@ async def advertisement_data(

.. versionadded:: 0.21
"""
devices = asyncio.Queue[tuple[BLEDevice, AdvertisementData]]()
devices: asyncio.Queue[tuple[BLEDevice, AdvertisementData]] = asyncio.Queue()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as previous

@o-murphy
Copy link
Contributor Author

@dlech
Ok can we at least make annotation changes as I made in last commit?

@o-murphy
Copy link
Contributor Author

@dlech
I looked again at PEPs 649, 749 and 563, and it looks like PEP649 and PEP749 were not implemented until 3.14, i.e. for versions <3.14 from __future__ import annotations works according to PEP563. From the point of view of PEP563, converting all annotations to strings and reproducing types from string representation during type checking seemed more efficient than calculating annotations at runtime, but it allowed writing arbitrary expressions in annotations, which is also strange. Considering that most annotations are correctly written and only in 2 or 3 lines in the code I found unbound variables in annotations. From the point of view of preparing for PEP649 in 3.14, it would probably be more correct to wrap only these annotations in strings, or to introduce bounded typevar to have normal references in the annotation dictionary

pep-0749
In Python 3.14, from future import annotations will continue to work as it did before, converting annotations into strings.

@o-murphy
Copy link
Contributor Author

o-murphy commented Aug 23, 2025

So, regarding the drama surrounding PEP563 and Guido's decision at the time,
__future__.annoations will never be "future" and will remain here, in version 3.14, for backwards compatibility, but it would not be an expected sollution. The feature will be deprecated after so many years without ever getting the right to live.

@o-murphy o-murphy requested a review from dlech August 23, 2025 22:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Adafruit СircuitPython backend
2 participants