[PATCH] hgdemandimport: avoid Python 3.15 errors for heapq and copy

Manuel Jacob me at manueljacob.de
Mon Mar 30 19:27:59 UTC 2026


On 28/03/2026 17.13, Mads Kiilerich wrote:
> # HG changeset patch
> # User Mads Kiilerich <mads at kiilerich.com>
> # Date 1774706053 -3600
> #      Sat Mar 28 14:54:13 2026 +0100
> # Branch stable
> # Node ID 4ef356e1dd1f573af87bc4ab5aa3d1a0be925346
> # Parent  1afb8f260d18a38a94e81398a778f9430f103ba8
> hgdemandimport: avoid Python 3.15 errors for heapq and copy
> 
> Got:
> 
>      ImportError: cannot import name 'nlargest' from 'heapq'
> 
> The new PEP 810 lazy imports might enable new ways of doing things. In the
> future.
> 
> diff --git a/hgdemandimport/__init__.py b/hgdemandimport/__init__.py
> --- a/hgdemandimport/__init__.py
> +++ b/hgdemandimport/__init__.py
> @@ -65,6 +65,8 @@ IGNORES = {
>       'warnings',
>       'threading',
>       'collections.abc',
> +    'heapq',
> +    'copy',

When looking at the full traceback, it looked suspicious. Why is an 
attribute missing from a module when there is no import cycle? At the 
point where `difflib` can’t import `nlargest` from `heapq`, `heapq` has 
all the other attributes (including `nsmallest`, which is defined right 
before `nlargest`). `heapq.__dict__['nsmallest']` is an ordinary 
function, while `heapq.__dict__['nlargest']` is an object of type 
`lazy_import`, which is a new type introduced to Python 3.15 by the 
implementation of PEP 810 lazy imports. The attribute was set at [1] (I 
found this place with rr by setting a watchpoint on the dict entry for 
`nlargest` in `heapq.__dict__` and going backwards). `attrs_updated` 
contains only `nlargest`. The difference between `nlargest` and the 
other attributes of `heapq` is that the former is PEP 810-lazily 
imported by standard library modules (e.g. `collections`).

It turns out that our way of doing lazy imports (using LazyLoader) and 
the PEP 810 way of doing lazy imports don’t work well together. Here’s a 
minimized reproducer for the problem:

```
import importlib.util
import sys

class LazyFinder:
     def find_spec(self, fullname, path, target=None):
         if fullname in ["heapq"]:
             for finder in sys.meta_path:
                 if finder is not self:
                     spec = finder.find_spec(fullname, path, target)
                     if spec is not None:
                         spec.loader = 
importlib.util.LazyLoader(spec.loader)
                         return spec

sys.meta_path.insert(0, LazyFinder())

import collections
import heapq

heapq.nsmallest
heapq.nlargest
```

It is possible to turn lazy imports off or on completely (the default is 
that only imports explicitly prefixed with `lazy` are lazy) [2]. There 
is ongoing discussion on whether the standard library should rely on 
lazy imports or not (e.g. in case of circular imports) [3].

I suggest the following:
- In the stable branch, we turn PEP 810 lazy imports off, at least when 
hgdemandimport is enabled. I see no reason to disable PEP 810 lazy 
imports if hgdemandimport is disabled.
- In the default branch, we set the global lazy imports mode to "all" if 
available and disable hgdemandimport in that case. In a quick test, most 
things seemed to work and there was a small startup speedup over 
hgdemandimport (around 5%).

[1] 
https://github.com/python/cpython/blob/4d0e8ee649ceff96b130e1676a73c20c469624a9/Lib/importlib/util.py#L218
[2] https://docs.python.org/3.15/library/sys.html#sys.set_lazy_imports
[3] https://lwn.net/Articles/1061112/


More information about the Mercurial-devel mailing list