1from __future__ import annotations
2
3import warnings
4from typing import TYPE_CHECKING, cast
5
6import importlib_metadata
7
8from ..compat import py312
9from ..compat.py314 import lazy_importer
10
11__lazy_modules__ = {'importlib.metadata', 'importlib_metadata'}
12"""
13Imports to defer until first use (PEP 810).
14
15Only a foreign distribution reaches the code that reads these, so the
16cost belongs to that caller and not to every import of the package.
17"""
18
19with lazy_importer():
20 import importlib.metadata
21
22 from .. import _adapters
23
24if TYPE_CHECKING:
25 import email.message
26
27 from .._meta import SimplePath
28
29
30def dist(
31 dist: importlib_metadata.Distribution | importlib.metadata.Distribution,
32) -> importlib_metadata.Distribution:
33 """
34 Ensure dist is an :class:`importlib_metadata.Distribution`.
35
36 >>> import importlib.metadata
37 >>> stdlib = importlib.metadata.PathDistribution('foo')
38 >>> type(stdlib)
39 <class 'importlib.metadata.PathDistribution'>
40 >>> local = dist(stdlib)
41 >>> type(local)
42 <class 'importlib_metadata.PathDistribution'>
43
44 A provider of its own Distribution subclass is passed through, as
45 long as it implements the interface consumers expect.
46
47 >>> class CustomDist(importlib.metadata.Distribution):
48 ... def read_text(self, name):
49 ... return
50 ... def locate_file(self, name):
51 ... return
52 >>> custom = CustomDist()
53 >>> dist(custom) is custom
54 True
55
56 One that doesn't is passed through too, but names what it omits.
57
58 >>> class Incomplete:
59 ... def read_text(self, name):
60 ... return
61 >>> import pytest
62 >>> with pytest.warns(UserWarning, match="is missing at, discover"):
63 ... local = dist(Incomplete())
64 >>> local is not None
65 True
66 """
67 if isinstance(dist, importlib_metadata.Distribution):
68 return dist
69
70 if isinstance(dist, importlib.metadata.PathDistribution):
71 return importlib_metadata.PathDistribution(cast('SimplePath', dist._path))
72 # Warn on what the interface requires and the class omits, rather than
73 # on the class not being one of ours; a provider can act on the former.
74 # Also tolerates pytest having replaced importlib_metadata, as the
75 # replaced class still conforms.
76 if not py312.conforms(dist, importlib_metadata.IDistribution):
77 warnings.warn(
78 f"Distribution subclass {dist.__class__} is missing "
79 f"{', '.join(unimplemented(dist))}"
80 )
81 return cast('importlib_metadata.Distribution', dist)
82
83
84def unimplemented(dist) -> list[str]:
85 """
86 The members IDistribution requires that dist's class doesn't supply.
87 """
88 required = py312.required(importlib_metadata.IDistribution)
89 return sorted(attr for attr in required if not hasattr(type(dist), attr))
90
91
92def message(
93 input: _adapters.Message | email.message.Message,
94) -> _adapters.Message:
95 """
96 Ensure a message is adapted to an importlib_metadata.Message.
97
98 >>> import email.message
99 >>> stdlib = email.message.Message()
100 >>> local = message(stdlib)
101 >>> type(local)
102 <class 'importlib_metadata._adapters.Message'>
103 """
104 if isinstance(input, _adapters.Message):
105 return input
106 return _adapters.Message(input)
107
108
109def package_path(
110 input: importlib_metadata.PackagePath | importlib.metadata.PackagePath,
111) -> importlib_metadata.PackagePath:
112 """
113 Ensure a package path is adapted to an importlib_metadata.PackagePath.
114
115 >>> import importlib.metadata
116 >>> stdlib = importlib.metadata.PackagePath('foo')
117 >>> type(stdlib)
118 <class 'importlib.metadata.PackagePath'>
119 >>> local = package_path(stdlib)
120 >>> type(local)
121 <class 'importlib_metadata.PackagePath'>
122 """
123 if isinstance(input, importlib_metadata.PackagePath):
124 return input
125 replacement = importlib_metadata.PackagePath(input)
126 vars(replacement).update(vars(input))
127 return replacement