`Layer.current_leaflet` keeps the last `ui.leaflet` element alive after its client is gone
Description
Layer.current_leaflet is a ClassVar on Layer (leaflet_layer.py:16) that is written in two places and cleared in none:
Leaflet.__enter__(leaflet.py:88), andLeaflet.__getattribute__(leaflet.py:91-95), which sets it whenever an attribute that is aLayersubclass is read — this is howm.marker(...)tells the newMarkerwhich map it belongs to, sinceLayer.__post_init__reads the class variable rather than an argument.
So after the last ui.leaflet on a page has been used, the class variable still points at it, and the reference outlives the page: deleting the client removes the element from client.elements, but the ClassVar keeps the element object itself — and its layers list, plus anything those layers close over — reachable until the next ui.leaflet anywhere in the process replaces it.
Measured with a User test, with a ui.label on the same page as the control:
import gc
import weakref
from nicegui import ui
from nicegui.testing import User
async def test_leaflet_survives_client_deletion(user: User) -> None:
refs = {}
@ui.page('/')
def page():
refs['label'] = weakref.ref(ui.label('hello'))
refs['leaflet'] = weakref.ref(ui.leaflet(center=(51.5, -0.09)))
await user.open('/')
user.client.delete()
gc.collect()
assert refs['label']() is None # collected
assert refs['leaflet']() is None # fails: still aliveThe label is collected, the map is not. Element._client is a weakref, so the client itself is not held — what leaks is the element plus its layer graph, per process, indefinitely.
Beyond the leak
The same class variable is also the only channel that binds a layer to its map, which makes the binding depend on global state rather than on the call that creates the layer. A __getattribute__ override that writes global state on attribute access is a lot of machinery for passing one argument, and AGENTS.md rules out global mutable state in library code.
So there are two levels of fix:
- Small: clear
Layer.current_leafletinLeaflet._handle_deletewhen it points at the element being deleted. Closes the leak, keeps the mechanism. - Proper: pass the map explicitly when constructing a layer and drop both the
ClassVarand the__getattribute__override. That changes how layer classes are reached, so it wants 4.0.
Found while reviewing #6294, where it is out of scope.
Source: zauberzeug/nicegui