omlish 0.0.0.dev458__py3-none-any.whl → 0.0.0.dev460__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,7 @@ Possible alt impls:
15
15
  import builtins
16
16
  import contextlib
17
17
  import functools
18
+ import importlib.util
18
19
  import sys
19
20
  import threading
20
21
  import types
@@ -46,14 +47,27 @@ class ImportCaptureErrors:
46
47
  return f'{self.__class__.__qualname__}(module={self.module!r}, name={self.name!r})'
47
48
 
48
49
  class ImportError(ImportCaptureError): # noqa
49
- def __init__(self, module: str, from_list: ta.Sequence[str] | None) -> None:
50
+ def __init__(
51
+ self,
52
+ name: str,
53
+ *,
54
+ level: int | None = None,
55
+ from_list: ta.Sequence[str] | None,
56
+ ) -> None:
50
57
  super().__init__()
51
58
 
52
- self.module = module
59
+ self.name = name
60
+ self.level = level
53
61
  self.from_list = from_list
54
62
 
55
63
  def __repr__(self) -> str:
56
- return f'{self.__class__.__qualname__}(module={self.module!r}, from_list={self.from_list!r})'
64
+ return ''.join([
65
+ f'{self.__class__.__qualname__}(',
66
+ f'name={self.name!r}',
67
+ *([f', level={self.level!r}'] if self.level is not None else []),
68
+ *([f', from_list={self.from_list!r}'] if self.from_list is not None else []),
69
+ ')',
70
+ ])
57
71
 
58
72
  class ImportStarForbiddenError(ImportError):
59
73
  pass
@@ -62,7 +76,7 @@ class ImportCaptureErrors:
62
76
  pass
63
77
 
64
78
  class UnreferencedImportsError(ImportCaptureError):
65
- def __init__(self, unreferenced: ta.Mapping[str, ta.Sequence[str | None]]) -> None:
79
+ def __init__(self, unreferenced: ta.Sequence[str]) -> None:
66
80
  super().__init__()
67
81
 
68
82
  self.unreferenced = unreferenced
@@ -78,136 +92,138 @@ class ImportCaptureErrors:
78
92
 
79
93
 
80
94
  class _ImportCaptureHook:
81
- class ModuleSpec(ta.NamedTuple):
82
- name: str
83
- level: int
84
-
85
- def __str__(self) -> str:
86
- return f'{"." * self.level}{self.name}'
87
-
88
- def __repr__(self) -> str:
89
- return repr(str(self))
90
-
91
95
  def __init__(
92
96
  self,
93
97
  *,
98
+ package: str | None = None,
94
99
  forbid_uncaptured_imports: bool = False,
95
100
  ) -> None:
96
101
  super().__init__()
97
102
 
103
+ self._package = package
98
104
  self._forbid_uncaptured_imports = forbid_uncaptured_imports
99
105
 
100
- self._modules_by_spec: dict[_ImportCaptureHook.ModuleSpec, _ImportCaptureHook._Module] = {}
106
+ self._modules_by_name: dict[str, _ImportCaptureHook._Module] = {}
101
107
  self._modules_by_module_obj: dict[types.ModuleType, _ImportCaptureHook._Module] = {}
102
108
 
103
- self._attrs: dict[_ImportCaptureHook._ModuleAttr, tuple[_ImportCaptureHook._Module, str]] = {}
104
-
105
109
  #
106
110
 
107
- class _ModuleAttr:
111
+ class _Module:
108
112
  def __init__(
109
113
  self,
110
- module: '_ImportCaptureHook._Module',
111
114
  name: str,
115
+ getattr_handler: ta.Callable[['_ImportCaptureHook._Module', str], ta.Any],
116
+ *,
117
+ parent: ta.Optional['_ImportCaptureHook._Module'] = None,
112
118
  ) -> None:
113
119
  super().__init__()
114
120
 
115
- self.__module = module
116
- self.__name = name
121
+ if name.startswith('.'):
122
+ raise ImportCaptureError
117
123
 
118
- def __repr__(self) -> str:
119
- return f'<{self.__class__.__name__}: {f"{self.__module.spec}:{self.__name}"!r}>'
124
+ self.name = name
125
+ self.parent = parent
120
126
 
121
- class _Module:
122
- def __init__(
123
- self,
124
- spec: '_ImportCaptureHook.ModuleSpec',
125
- *,
126
- getattr_handler: ta.Callable[['_ImportCaptureHook._Module', str], ta.Any] | None = None,
127
- ) -> None:
128
- super().__init__()
127
+ self.base_name = name.rpartition('.')[2]
128
+ self.root: _ImportCaptureHook._Module = parent.root if parent is not None else self # noqa
129
129
 
130
- self.spec = spec
130
+ self.children: dict[str, _ImportCaptureHook._Module] = {}
131
+ self.descendants: set[_ImportCaptureHook._Module] = set()
131
132
 
132
- self.module_obj = types.ModuleType(f'<{self.__class__.__qualname__}: {spec!r}>')
133
- if getattr_handler is not None:
134
- self.module_obj.__getattr__ = functools.partial(getattr_handler, self) # type: ignore[method-assign] # noqa
133
+ self.module_obj = types.ModuleType(f'<{self.__class__.__qualname__}: {name}>')
134
+ self.module_obj.__file__ = None
135
+ self.module_obj.__getattr__ = functools.partial(getattr_handler, self) # type: ignore[method-assign] # noqa
135
136
  self.initial_module_dict = dict(self.module_obj.__dict__)
136
137
 
137
- self.contents: dict[str, _ImportCaptureHook._ModuleAttr | types.ModuleType] = {}
138
- self.imported_whole = False
138
+ self.explicit = False
139
+ self.immediate = False
139
140
 
140
141
  def __repr__(self) -> str:
141
- return f'{self.__class__.__name__}({self.spec!r})'
142
+ return f'{self.__class__.__name__}<{self.name}{"!" if self.immediate else "+" if self.explicit else ""}>'
142
143
 
143
- def _get_or_make_module(self, spec: ModuleSpec) -> _Module:
144
+ def set_explicit(self) -> None:
145
+ cur: _ImportCaptureHook._Module | None = self
146
+ while cur is not None and not cur.explicit:
147
+ cur.explicit = True
148
+ cur = cur.parent
149
+
150
+ #
151
+
152
+ @property
153
+ def _modules(self) -> ta.Sequence[_Module]:
154
+ return sorted(self._modules_by_name.values(), key=lambda m: m.name)
155
+
156
+ def _get_or_make_module(self, name: str) -> _Module:
144
157
  try:
145
- return self._modules_by_spec[spec]
158
+ return self._modules_by_name[name]
146
159
  except KeyError:
147
160
  pass
148
161
 
149
- module = self._Module(
150
- spec,
151
- getattr_handler=self._handle_module_getattr,
162
+ parent: _ImportCaptureHook._Module | None = None
163
+ if '.' in name:
164
+ rest, _, attr = name.rpartition('.')
165
+ parent = self._get_or_make_module(rest)
166
+ if attr in parent.children:
167
+ raise ImportCaptureErrors.AttrError(rest, attr)
168
+
169
+ module = _ImportCaptureHook._Module(
170
+ name,
171
+ self._handle_module_getattr,
172
+ parent=parent,
152
173
  )
153
- self._modules_by_spec[spec] = module
174
+ self._modules_by_name[name] = module
154
175
  self._modules_by_module_obj[module.module_obj] = module
176
+
177
+ if parent is not None:
178
+ parent.children[module.base_name] = module
179
+ setattr(parent.module_obj, module.base_name, module.module_obj)
180
+ parent.root.descendants.add(module)
181
+
155
182
  return module
156
183
 
157
- def _handle_module_getattr(self, module: _Module, attr: str) -> ta.Any:
158
- if attr in module.contents:
159
- raise ImportCaptureErrors.AttrError(str(module.spec), attr)
184
+ def _make_child_module(self, module: _Module, attr: str) -> _Module:
185
+ if attr in module.children:
186
+ raise ImportCaptureErrors.AttrError(module.name, attr)
160
187
 
161
- v: _ImportCaptureHook._ModuleAttr | types.ModuleType
162
- if not module.spec.name:
163
- if not module.spec.level:
164
- raise ImportCaptureError
165
- cs = _ImportCaptureHook.ModuleSpec(attr, module.spec.level)
166
- cm = self._get_or_make_module(cs)
167
- cm.imported_whole = True
168
- v = cm.module_obj
188
+ return self._get_or_make_module(f'{module.name}.{attr}')
169
189
 
170
- else:
171
- ma = _ImportCaptureHook._ModuleAttr(module, attr)
172
- self._attrs[ma] = (module, attr)
173
- v = ma
190
+ #
191
+
192
+ def _handle_module_getattr(self, module: _Module, attr: str) -> ta.Any:
193
+ if not module.explicit:
194
+ raise ImportCaptureErrors.AttrError(module.name, attr)
174
195
 
175
- module.contents[attr] = v
176
- setattr(module.module_obj, attr, v)
177
- return v
196
+ return self._make_child_module(module, attr).module_obj
178
197
 
179
198
  def _handle_import(
180
199
  self,
181
- module: _Module,
200
+ name: str,
182
201
  *,
183
202
  from_list: ta.Sequence[str] | None,
184
- ) -> None:
185
- if from_list is None:
186
- if module.spec.level or not module.spec.name:
187
- raise ImportCaptureError
203
+ ) -> types.ModuleType:
204
+ module = self._get_or_make_module(name)
188
205
 
189
- module.imported_whole = True
206
+ if from_list is None:
207
+ module.set_explicit()
208
+ module.root.immediate = True
209
+ return module.root.module_obj
190
210
 
191
211
  else:
192
212
  for attr in from_list:
193
213
  if attr == '*':
194
- raise ImportCaptureErrors.ImportStarForbiddenError(str(module.spec), from_list)
214
+ raise ImportCaptureErrors.ImportStarForbiddenError(module.name, from_list=from_list)
215
+
216
+ if (cm := module.children.get(attr)) is None:
217
+ cm = self._make_child_module(module, attr)
218
+ cm.set_explicit()
219
+ cm.immediate = True
220
+ continue
195
221
 
196
222
  x = getattr(module.module_obj, attr)
223
+ if x is not cm.module_obj or x not in self._modules_by_module_obj:
224
+ raise ImportCaptureErrors.AttrError(module.name, attr)
197
225
 
198
- bad = False
199
- if x is not module.contents.get(attr):
200
- bad = True
201
- if isinstance(x, _ImportCaptureHook._ModuleAttr):
202
- if self._attrs[x] != (module, attr):
203
- bad = True
204
- elif isinstance(x, types.ModuleType):
205
- if x not in self._modules_by_module_obj:
206
- bad = True
207
- else:
208
- bad = True
209
- if bad:
210
- raise ImportCaptureErrors.AttrError(str(module.spec), attr)
226
+ return module.module_obj
211
227
 
212
228
  #
213
229
 
@@ -227,16 +243,16 @@ class _ImportCaptureHook:
227
243
  ):
228
244
  return None
229
245
 
230
- spec = _ImportCaptureHook.ModuleSpec(name, level)
231
- module = self._get_or_make_module(spec)
246
+ if level:
247
+ if not self._package:
248
+ raise ImportCaptureError
249
+ name = importlib.util.resolve_name(('.' * level) + name, self._package)
232
250
 
233
- self._handle_import(
234
- module,
251
+ return self._handle_import(
252
+ name,
235
253
  from_list=from_list,
236
254
  )
237
255
 
238
- return module.module_obj
239
-
240
256
  @ta.final
241
257
  @contextlib.contextmanager
242
258
  def hook_context(
@@ -271,18 +287,24 @@ class _ImportCaptureHook:
271
287
  self,
272
288
  mod_globals: ta.MutableMapping[str, ta.Any], # noqa
273
289
  ) -> None:
274
- for m in self._modules_by_spec.values():
290
+ for m in self._modules_by_name.values():
291
+ if m.immediate and not m.explicit:
292
+ raise ImportCaptureError
293
+
294
+ if not m.explicit and m.children:
295
+ raise ImportCaptureError
296
+
275
297
  for a, o in m.module_obj.__dict__.items():
276
298
  try:
277
299
  i = m.initial_module_dict[a]
278
300
 
279
301
  except KeyError:
280
- if o is not m.contents[a]:
281
- raise ImportCaptureErrors.AttrError(str(m.spec), a) from None
302
+ if o is not m.children[a].module_obj:
303
+ raise ImportCaptureErrors.AttrError(m.name, a) from None
282
304
 
283
305
  else:
284
306
  if o != i:
285
- raise ImportCaptureErrors.AttrError(str(m.spec), a)
307
+ raise ImportCaptureErrors.AttrError(m.name, a)
286
308
 
287
309
  #
288
310
 
@@ -292,24 +314,20 @@ class _ImportCaptureHook:
292
314
  *,
293
315
  collect_unreferenced: bool = False,
294
316
  ) -> 'ImportCapture.Captured':
295
- dct: dict[_ImportCaptureHook._Module, list[tuple[str | None, str]]] = {}
296
-
297
- rem_whole_mods: set[_ImportCaptureHook._Module] = set()
298
- rem_mod_attrs: set[_ImportCaptureHook._ModuleAttr] = set()
317
+ rem_explicit_mods: set[_ImportCaptureHook._Module] = set()
299
318
  if collect_unreferenced:
300
- rem_whole_mods.update([m for m in self._modules_by_spec.values() if m.imported_whole])
301
- rem_mod_attrs.update(self._attrs)
319
+ rem_explicit_mods.update(
320
+ m for m in self._modules_by_name.values()
321
+ if m.immediate
322
+ and m.parent is not None # No good way to tell if user did `import a.b.c` or `import a.b.c as c`
323
+ )
302
324
 
303
- for attr, obj in mod_globals.items():
304
- if isinstance(obj, _ImportCaptureHook._ModuleAttr):
305
- try:
306
- m, a = self._attrs[obj]
307
- except KeyError:
308
- raise ImportCaptureErrors.AttrError(None, attr) from None
309
- dct.setdefault(m, []).append((a, attr))
310
- rem_mod_attrs.discard(obj)
325
+ #
326
+
327
+ dct: dict[_ImportCaptureHook._Module, list[tuple[str | None, str]]] = {}
311
328
 
312
- elif isinstance(obj, _ImportCaptureHook._Module):
329
+ for attr, obj in mod_globals.items():
330
+ if isinstance(obj, _ImportCaptureHook._Module):
313
331
  raise ImportCaptureErrors.AttrError(None, attr) from None
314
332
 
315
333
  elif isinstance(obj, types.ModuleType):
@@ -317,41 +335,80 @@ class _ImportCaptureHook:
317
335
  m = self._modules_by_module_obj[obj]
318
336
  except KeyError:
319
337
  continue
320
- if not m.imported_whole:
321
- raise RuntimeError(f'ImportCapture module {m.spec!r} not imported_whole')
322
- dct.setdefault(m, []).append((None, attr))
323
- rem_whole_mods.discard(m)
324
-
325
- lst: list[ImportCapture.Import] = []
326
- for m, ts in dct.items():
327
- if not m.spec.name:
328
- if not m.spec.level:
329
- raise ImportCaptureError
330
- for imp_attr, as_attr in ts:
331
- if not imp_attr:
332
- raise RuntimeError
333
- lst.append(ImportCapture.Import(
334
- '.' * m.spec.level + imp_attr,
335
- [(None, as_attr)],
336
- ))
337
338
 
338
- else:
339
- lst.append(ImportCapture.Import(
340
- str(m.spec),
341
- ts,
342
- ))
343
-
344
- unreferenced: dict[str, list[str | None]] | None = None
345
- if collect_unreferenced and (rem_whole_mods or rem_mod_attrs):
346
- unreferenced = {}
347
- for m in rem_whole_mods:
348
- unreferenced.setdefault(str(m.spec), []).append(None)
349
- for ma in rem_mod_attrs:
350
- m, a = self._attrs[ma]
351
- unreferenced.setdefault(str(m.spec), []).append(a)
339
+ if m.explicit:
340
+ dct.setdefault(m, []).append((None, attr))
341
+ if m in rem_explicit_mods:
342
+ # Remove everything reachable from this root *except* items imported immediately, such as
343
+ # `from x import y` - those still need to be immediately reachable.
344
+ rem_explicit_mods -= {dm for dm in m.descendants if not dm.immediate}
345
+ rem_explicit_mods.remove(m)
346
+
347
+ else:
348
+ p = m.parent
349
+ if p is None or not p.explicit:
350
+ raise ImportCaptureError
351
+ dct.setdefault(p, []).append((m.base_name, attr))
352
+
353
+ #
354
+
355
+ mods: dict[str, ImportCapture.Module] = {}
356
+
357
+ def build_import_module(m: _ImportCaptureHook._Module) -> ImportCapture.Module:
358
+ children: dict[str, ImportCapture.Module] = {}
359
+ attrs: list[str] = []
360
+ for cm in sorted(m.children.values(), key=lambda cm: cm.name):
361
+ if not cm.explicit:
362
+ attrs.append(cm.base_name)
363
+ else:
364
+ children[cm.base_name] = build_import_module(cm)
365
+
366
+ mod = ImportCapture.Module(
367
+ m.name,
368
+ children or None,
369
+ attrs or None,
370
+ )
371
+
372
+ if m.parent is None:
373
+ mod.parent = None
374
+ for c in children.values():
375
+ c.parent = mod
376
+
377
+ mods[mod.name] = mod
378
+ return mod
379
+
380
+ root_mods: dict[str, ImportCapture.Module] = {
381
+ m.base_name: build_import_module(m)
382
+ for m in self._modules_by_name.values()
383
+ if m.parent is None
384
+ }
385
+
386
+ mods = dict(sorted(mods.items(), key=lambda t: t[0]))
387
+ root_mods = dict(sorted(root_mods.items(), key=lambda t: t[0]))
388
+
389
+ #
390
+
391
+ imps: list[ImportCapture.Import] = []
392
+
393
+ for m, ts in sorted(dct.items(), key=lambda t: t[0].name):
394
+ imps.append(ImportCapture.Import(
395
+ mods[m.name],
396
+ [r for l, r in ts if l is None] or None,
397
+ [(l, r) for l, r in ts if l is not None] or None,
398
+ ))
399
+
400
+ #
401
+
402
+ unreferenced: list[str] | None = None
403
+ if collect_unreferenced and rem_explicit_mods:
404
+ unreferenced = sorted(m.name for m in rem_explicit_mods)
352
405
 
353
406
  return ImportCapture.Captured(
354
- lst,
407
+ {i.module.name: i for i in imps},
408
+
409
+ mods,
410
+ root_mods,
411
+
355
412
  unreferenced,
356
413
  )
357
414
 
@@ -360,6 +417,14 @@ class _ImportCaptureHook:
360
417
 
361
418
 
362
419
  class _AbstractBuiltinsImportCaptureHook(_ImportCaptureHook):
420
+ def __init__(
421
+ self,
422
+ *,
423
+ _frame: types.FrameType | None = None,
424
+ **kwargs: ta.Any,
425
+ ) -> None:
426
+ super().__init__(**kwargs)
427
+
363
428
  def _new_import(
364
429
  self,
365
430
  old_import,
@@ -379,8 +444,9 @@ class _AbstractBuiltinsImportCaptureHook(_ImportCaptureHook):
379
444
 
380
445
  if self._forbid_uncaptured_imports:
381
446
  raise ImportCaptureErrors.UncapturedImportForbiddenError(
382
- str(_ImportCaptureHook.ModuleSpec(name, level)),
383
- fromlist,
447
+ name,
448
+ level=level,
449
+ from_list=fromlist,
384
450
  )
385
451
 
386
452
  return old_import(
@@ -515,11 +581,24 @@ class _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook(_AbstractBuiltinsImport
515
581
  #
516
582
 
517
583
 
518
- _capture: ta.Any = None
519
- try:
520
- from . import _capture # type: ignore
521
- except ImportError:
522
- pass
584
+ _cext_: ta.Any
585
+
586
+
587
+ def _cext() -> ta.Any:
588
+ global _cext_
589
+ try:
590
+ return _cext_
591
+ except NameError:
592
+ pass
593
+
594
+ cext: ta.Any
595
+ try:
596
+ from . import _capture as cext # type: ignore
597
+ except ImportError:
598
+ cext = None
599
+
600
+ _cext_ = cext
601
+ return cext
523
602
 
524
603
 
525
604
  class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
@@ -539,7 +618,7 @@ class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
539
618
  frame: types.FrameType,
540
619
  new_builtins: dict[str, ta.Any],
541
620
  ) -> bool:
542
- return _capture._set_frame_builtins(frame, frame.f_builtins, new_builtins) # noqa
621
+ return _cext()._set_frame_builtins(frame, frame.f_builtins, new_builtins) # noqa
543
622
 
544
623
  @contextlib.contextmanager
545
624
  def _hook_context(
@@ -569,39 +648,155 @@ class _FrameBuiltinsImportCaptureHook(_AbstractBuiltinsImportCaptureHook):
569
648
  #
570
649
 
571
650
 
651
+ _CAPTURE_IMPLS: ta.Mapping[str, type[_AbstractBuiltinsImportCaptureHook]] = {
652
+ 'cext': _FrameBuiltinsImportCaptureHook,
653
+ 'somewhat_safe': _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook,
654
+ 'unsafe': _UnsafeGlobalBuiltinsImportCaptureHook,
655
+ }
656
+
657
+
572
658
  def _new_import_capture_hook(
573
659
  mod_globals: ta.MutableMapping[str, ta.Any], # noqa
574
660
  *,
575
661
  stack_offset: int = 0,
662
+ capture_impl: str | None = None,
576
663
  **kwargs: ta.Any,
577
664
  ) -> '_ImportCaptureHook':
578
- frame: types.FrameType | None = sys._getframe(1 + stack_offset) # noqa
579
- if frame is None or frame.f_globals is not mod_globals:
580
- raise ImportCaptureError("Can't find importing frame")
665
+ if '_frame' not in kwargs:
666
+ frame: types.FrameType | None = sys._getframe(1 + stack_offset) # noqa
667
+ if frame is None or frame.f_globals is not mod_globals:
668
+ raise ImportCaptureError("Can't find importing frame")
669
+ kwargs['_frame'] = frame
670
+
671
+ kwargs.setdefault('package', mod_globals.get('__package__'))
581
672
 
582
- if _capture is not None:
583
- return _FrameBuiltinsImportCaptureHook(_frame=frame, **kwargs)
673
+ cls: type[_AbstractBuiltinsImportCaptureHook]
674
+ if capture_impl is not None:
675
+ cls = _CAPTURE_IMPLS[capture_impl]
676
+ elif _cext() is not None:
677
+ cls = _FrameBuiltinsImportCaptureHook
678
+ else:
679
+ cls = _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook
584
680
 
585
- return _SomewhatThreadSafeGlobalBuiltinsImportCaptureHook(**kwargs)
681
+ return cls(**kwargs)
586
682
 
587
683
 
588
684
  ##
589
685
 
590
686
 
687
+ ImportCaptureModuleKind: ta.TypeAlias = ta.Literal[
688
+ 'parent',
689
+ 'terminal',
690
+ 'leaf',
691
+ ]
692
+
693
+
591
694
  class ImportCapture:
592
- class Import(ta.NamedTuple):
593
- spec: str
594
- attrs: ta.Sequence[tuple[str | None, str]]
695
+ @ta.final
696
+ class Module:
697
+ def __init__(
698
+ self,
699
+ name: str,
700
+ children: ta.Mapping[str, 'ImportCapture.Module'] | None = None,
701
+ attrs: ta.Sequence[str] | None = None,
702
+ ) -> None:
703
+ self.name = name
704
+ self.children = children
705
+ self.attrs = attrs
706
+
707
+ self.base_name = name.rpartition('.')[2]
708
+
709
+ if not self.children and not self.attrs:
710
+ self.kind = 'leaf'
711
+ elif not self.children or all(c.kind == 'leaf' for c in self.children.values()):
712
+ self.kind = 'terminal'
713
+ else:
714
+ self.kind = 'parent'
715
+
716
+ parent: ta.Optional['ImportCapture.Module']
717
+
718
+ kind: ImportCaptureModuleKind
719
+
720
+ def __repr__(self) -> str:
721
+ return ''.join([
722
+ f'{self.__class__.__name__}(',
723
+ f'{self.name!r}',
724
+ f', :{self.kind}',
725
+ *([f', children=[{", ".join(map(repr, self.children))}]'] if self.children else []),
726
+ *([f', attrs={self.attrs!r}'] if self.attrs else []),
727
+ ')',
728
+ ])
729
+
730
+ _root: 'ImportCapture.Module'
731
+
732
+ @property
733
+ def root(self) -> 'ImportCapture.Module':
734
+ try:
735
+ return self._root
736
+ except AttributeError:
737
+ pass
738
+
739
+ root = self
740
+ while root.parent is not None:
741
+ root = root.parent
742
+ self._root = root
743
+ return root
744
+
745
+ @ta.final
746
+ class Import:
747
+ def __init__(
748
+ self,
749
+ module: 'ImportCapture.Module',
750
+ as_: ta.Sequence[str] | None,
751
+ attrs: ta.Sequence[tuple[str, str]] | None, # ('foo', 'bar') -> `import foo as bar` - explicitly not a dict # noqa
752
+ ) -> None:
753
+ self.module = module
754
+ self.as_ = as_
755
+ self.attrs = attrs
595
756
 
596
- class Captured(ta.NamedTuple):
597
- imports: ta.Sequence['ImportCapture.Import']
598
- unreferenced: ta.Mapping[str, ta.Sequence[str | None]] | None
757
+ def __repr__(self) -> str:
758
+ return ''.join([
759
+ f'{self.__class__.__name__}(',
760
+ f'{self.module.name!r}',
761
+ *([f', as_={self.as_!r}'] if self.as_ else []),
762
+ *([f', attrs={self.attrs!r}'] if self.attrs else []),
763
+ ')',
764
+ ])
765
+
766
+ @ta.final
767
+ class Captured:
768
+ def __init__(
769
+ self,
770
+
771
+ imports: ta.Mapping[str, 'ImportCapture.Import'],
772
+
773
+ modules: ta.Mapping[str, 'ImportCapture.Module'],
774
+ root_modules: ta.Mapping[str, 'ImportCapture.Module'],
775
+
776
+ unreferenced: ta.Sequence[str] | None,
777
+ ) -> None:
778
+ self.imports = imports
779
+
780
+ self.modules = modules
781
+ self.root_modules = root_modules
782
+
783
+ self.unreferenced = unreferenced
599
784
 
600
785
  @property
601
786
  def attrs(self) -> ta.Iterator[str]:
602
- for pi in self.imports:
603
- for _, a in pi.attrs:
604
- yield a
787
+ for pi in self.imports.values():
788
+ if pi.as_:
789
+ yield from pi.as_
790
+ if pi.attrs:
791
+ for _, a in pi.attrs:
792
+ yield a
793
+
794
+ EMPTY_CAPTURED: ta.ClassVar[Captured] = Captured(
795
+ {},
796
+ {},
797
+ {},
798
+ None,
799
+ )
605
800
 
606
801
  #
607
802
 
@@ -651,7 +846,7 @@ class ImportCapture:
651
846
  def capture(
652
847
  self,
653
848
  *,
654
- unreferenced_callback: ta.Callable[[ta.Mapping[str, ta.Sequence[str | None]]], None] | None = None,
849
+ unreferenced_callback: ta.Callable[[ta.Sequence[str]], None] | None = None,
655
850
  raise_unreferenced: bool = False,
656
851
  ) -> ta.Iterator[ta.Self]:
657
852
  if self._result_ is not None:
@@ -659,10 +854,7 @@ class ImportCapture:
659
854
 
660
855
  if self._disabled:
661
856
  self._result_ = ImportCapture._Result(
662
- ImportCapture.Captured(
663
- [],
664
- None,
665
- ),
857
+ ImportCapture.EMPTY_CAPTURED,
666
858
  )
667
859
  yield self
668
860
  return
@@ -683,9 +875,8 @@ class ImportCapture:
683
875
  if raise_unreferenced:
684
876
  raise ImportCaptureErrors.UnreferencedImportsError(blt.unreferenced)
685
877
 
686
- for pi in blt.imports:
687
- for _, a in pi.attrs:
688
- del self._mod_globals[a]
878
+ for a in blt.attrs:
879
+ del self._mod_globals[a]
689
880
 
690
881
  self._result_ = ImportCapture._Result(
691
882
  blt,