import inspect
import sys
from functools import lru_cache
from typing import Any, Dict, Optional, overload
if sys.version_info >= (3, 8):
from importlib import metadata as importlib_metadata
else:
import importlib_metadata
@lru_cache(maxsize=1)
def _top_level_module_to_dist() -> Dict[str, importlib_metadata.Distribution]:
mapping = {}
for dist in importlib_metadata.distributions():
modules = dist.read_text('top_level.txt')
if modules:
for mod in modules.split('\n'):
if not mod:
continue
mapping[mod] = dist
return mapping
def _object_to_top_level_module(obj: Any) -> Optional[str]:
module = inspect.getmodule(obj)
name = getattr(module, '__name__', None)
return name.split('.')[0] if name else None
[docs]def get_dist(obj) -> Optional[importlib_metadata.Distribution]:
"""Return a :class:`importlib.metadata.Distribution` for any python object.
Parameters
----------
obj : Any
A python object. If a string, will be interpreted as a dist name.
Returns
-------
dist: Distribution
The distribution object for the corresponding package, if found.
"""
if isinstance(obj, str):
try:
return importlib_metadata.distribution(obj)
except importlib_metadata.PackageNotFoundError:
return None
top_level = _object_to_top_level_module(obj)
return _top_level_module_to_dist().get(top_level or '')
def get_version(plugin) -> str:
dist = get_dist(plugin)
version = dist.metadata.get('version') if dist else ''
if not version and inspect.ismodule(plugin):
version = getattr(plugin, '__version__', '')
if not version:
top_module = _object_to_top_level_module(plugin)
if top_module in sys.modules:
version = getattr(sys.modules[top_module], '__version__', '')
return str(version) if version else ''
@overload
def get_metadata(plugin, arg: str, *args: None) -> Optional[str]:
...
@overload # noqa: F811
def get_metadata( # noqa: F811
plugin, arg: str, *args: str
) -> Dict[str, Optional[str]]:
...
def get_metadata(plugin, *args): # noqa: F811
"""Get metadata for this plugin.
Valid arguments are any keys from the Core metadata specifications:
https://packaging.python.org/specifications/core-metadata/
Parameters
----------
*args : str
(Case insensitive) names of metadata entries to retrieve.
Returns
-------
str or dict, optional
If a single argument is provided, the value for that entry is
returned (which may be ``None``).
If multiple arguments are provided, a dict of {arg: value} is
returned.
"""
dist = get_dist(plugin)
dct = {}
if dist:
for a in args:
if a == 'version':
dct[a] = get_version(plugin)
else:
dct[a] = dist.metadata.get(a)
if len(args) == 1:
return dct[args[0]] if dct else None
return dct
def standard_metadata(plugin: Any) -> Dict[str, Optional[str]]:
"""Return a standard metadata dict for ``plugin``.
Parameters
----------
plugin : Any
A python object. If a string, will be interpreted as a dist name.
Returns
-------
metadata : dict
A dicts with plugin object metadata. The dict is guaranteed to have
the following keys:
- **package**: The name of the package
- **version**: The version of the plugin package
- **summary**: A one-line summary of what the distribution does
- **author**: The author’s name
- **email**: The author’s (or maintainer's) e-mail address.
- **license**: The license covering the distribution
- **url**: The home page for the package, or dowload url if N/A.
Raises
------
ValueError
If no distribution can be found for ``plugin``.
"""
meta = {}
if not get_dist(plugin):
_top_level_module_to_dist.cache_clear()
if not get_dist(plugin):
return {}
# TODO: decide appropriate behavior here.
# raise ValueError(f"could not find metadata for {plugin}")
meta = get_metadata(
plugin,
'name',
'version',
'summary',
'author',
'license',
'Author-Email',
'Home-page',
)
meta['package'] = meta.pop('name')
meta['email'] = meta.pop('Author-Email') or get_metadata(
plugin, 'Maintainer-Email'
)
meta['url'] = meta.pop('Home-page') or get_metadata(plugin, 'Download-Url')
if meta['url'] == 'UNKNOWN':
meta['url'] = None
return meta