Source code for mcdreforged.plugin.meta.metadata
"""
Information of a plugin
"""
import dataclasses
import re
from typing import List, Dict, TYPE_CHECKING, Optional, Union, ClassVar, TypeVar, Any, cast, Type
from typing_extensions import override
from mcdreforged.minecraft.rtext.text import RTextBase, RText
from mcdreforged.plugin.meta.schema import PluginMetadataJsonModel
from mcdreforged.plugin.meta.version import Version, VersionParsingError, VersionRequirement
from mcdreforged.translation.translation_text import RTextMCDRTranslation
from mcdreforged.utils import translation_utils, class_utils
from mcdreforged.utils.types.message import TranslationLanguageDict
if TYPE_CHECKING:
from mcdreforged.plugin.type.plugin import AbstractPlugin
_T = TypeVar('_T')
def _none_or(value: Optional[_T], default: _T) -> _T:
return value if value is not None else default
class __MetadataMeta(type):
@override
def __call__(cls: type[_T], *args: Any, **kwargs: Any) -> _T:
if cls is Metadata and len(args) == 1 and len(kwargs) == 0 and isinstance(args[0], dict):
# compat old usage (before v2.16.0) that pass a single dict into the constructor
# XXX: drop this compactibility hack in v3?
return cast(_T, cast(Type[Metadata], cls).create(args[0]))
else:
return type.__call__(cls, *args, **kwargs)
[docs]
@dataclasses.dataclass(frozen=True)
class Metadata(metaclass=__MetadataMeta):
"""
The metadata of a MCDR plugin
"""
id: str
"""
The id of the plugin. Should match regexp ``[a-z][a-z0-9_]{0,63}``
.. versionchanged:: v2.11.0
Plugin id starts with non-alphabet character is no longer allowed
"""
version: Version
"""The version of the plugin, in a less restrictive semver format"""
name: str
"""The name of the plugin"""
description: Optional[Union[str, TranslationLanguageDict]] # translation: lang -> description
"""
The description of the plugin
It can be a regular str or a ``Dict[str, str]`` indicating a mapping from language to description
"""
author: Optional[List[str]]
"""The authors of the plugin"""
link: Optional[str]
"""The url to the plugin, e.g. link to a github repository"""
dependencies: Dict[str, VersionRequirement]
"""
A dict of dependencies the plugin relies on
:Key: The id of the dependent plugin
:Value: The version requirement of the dependent plugin
"""
entrypoint: str
"""
The entrypoint module of the plugin
The entrypoint should be import-able
"""
archive_name: Optional[str] # used in MCDR CLI only
resources: Optional[List[str]] # used in MCDR CLI only
PLUGIN_ID_REGEX_OLD: ClassVar[re.Pattern] = re.compile(r'[a-z0-9_]{1,64}')
PLUGIN_ID_REGEX: ClassVar[re.Pattern] = re.compile(r'[a-z][a-z0-9_]{0,63}')
FALLBACK_VERSION: ClassVar[str] = '0.0.0'
[docs]
@classmethod
def create(cls, data: Union[dict, PluginMetadataJsonModel], *, plugin: Optional['AbstractPlugin'] = None) -> 'Metadata':
"""
:param AbstractPlugin plugin: the plugin which this metadata is belonged to
:param dict or None data: a dict with information of the plugin
.. versionadded:: v2.16.0
"""
class_utils.check_type(data, (dict, PluginMetadataJsonModel))
plugin_name_text = repr(plugin)
def warn(*args, **kwargs):
if plugin is not None:
plugin.mcdr_server.logger.warning(*args, **kwargs, stacklevel=3)
def create_id() -> str:
if isinstance(data, PluginMetadataJsonModel):
return data.id
if (plugin_id := data.get('id')) is not None:
if isinstance(plugin_id, str) and cls.PLUGIN_ID_REGEX.fullmatch(plugin_id):
return plugin_id
use_fallback_id_reason = 'Plugin ID {!r} of {} is invalid'.format(plugin_id, plugin_name_text)
else:
use_fallback_id_reason = 'Plugin ID of {} not found'.format(plugin_name_text)
if plugin is not None:
fallback_id = plugin.get_fallback_metadata_id()
warn('{}, use fallback id {} instead'.format(use_fallback_id_reason, fallback_id))
return fallback_id
else:
raise ValueError('Plugin id not found in metadata')
meta_id = create_id()
def create_name() -> str:
if isinstance(data, PluginMetadataJsonModel):
return _none_or(data.name, meta_id)
meta_name = data.get('name', meta_id)
if isinstance(meta_name, RTextBase):
meta_name = meta_name.to_plain_text()
class_utils.check_type(meta_name, str)
return meta_name
def create_description() -> Optional[Union[str, TranslationLanguageDict]]:
if isinstance(data, PluginMetadataJsonModel):
return data.description
description = data.get('description')
if isinstance(description, RTextBase):
description = description.to_plain_text()
meta_description = description
class_utils.check_type(meta_description, (None, str, dict))
return meta_description
def create_author() -> Optional[List[str]]:
if isinstance(data, PluginMetadataJsonModel):
return [data.author] if isinstance(data.author, str) else data.author
meta_author = data.get('author')
if isinstance(meta_author, str):
meta_author = [meta_author]
if isinstance(meta_author, list):
for i in range(len(meta_author)):
meta_author[i] = str(meta_author[i])
if len(meta_author) == 0:
meta_author = None
class_utils.check_type(meta_author, (None, list))
return meta_author
def create_link() -> Optional[str]:
if isinstance(data, PluginMetadataJsonModel):
return data.link
meta_link = data.get('link')
class_utils.check_type(meta_link, (None, str))
return meta_link
def create_version() -> Version:
if (version_str := data.version if isinstance(data, PluginMetadataJsonModel) else data.get('version')) is not None:
try:
return Version(version_str, allow_wildcard=False)
except VersionParsingError as e:
warn('Version {!r} of {} is invalid ({}), ignore and use fallback version instead {}'.format(version_str, plugin_name_text, e, cls.FALLBACK_VERSION))
else:
warn("{} doesn't specific a version, use fallback version {}".format(plugin_name_text, cls.FALLBACK_VERSION))
return Version(cls.FALLBACK_VERSION)
def create_dependencies() -> Dict[str, VersionRequirement]:
meta_dependencies = {}
raw_dependencies: Dict[str, str] = data.dependencies if isinstance(data, PluginMetadataJsonModel) else data.get('dependencies', {})
for plugin_id, requirement in raw_dependencies.items():
try:
meta_dependencies[plugin_id] = VersionRequirement(requirement)
except VersionParsingError as e:
warn('Dependency "{}: {}" of {} is invalid ({}), ignore'.format(
plugin_id, requirement, plugin_name_text, e
))
return meta_dependencies
def create_entrypoint() -> str:
if isinstance(data, PluginMetadataJsonModel):
meta_entrypoint = _none_or(data.entrypoint, meta_id)
else:
meta_entrypoint = data.get('entrypoint', meta_id)
class_utils.check_type(meta_entrypoint, str)
# entrypoint module should be inside the plugin module
if meta_entrypoint != meta_id and not meta_entrypoint.startswith(meta_id + '.'):
raise ValueError('Invalid entry point {!r} for plugin id {!r}'.format(meta_entrypoint, meta_id))
return meta_entrypoint
def create_archive_name() -> Optional[str]:
if isinstance(data, PluginMetadataJsonModel):
return data.archive_name
else:
return class_utils.check_type(data.get('archive_name'), (None, str))
def create_resources() -> List[str]:
if isinstance(data, PluginMetadataJsonModel):
return _none_or(data.resources, [])
else:
return class_utils.check_type(data.get('resources', []), list)
return cls(
id=meta_id,
version=create_version(),
name=create_name(),
description=create_description(),
author=create_author(),
link=create_link(),
dependencies=create_dependencies(),
entrypoint=create_entrypoint(),
archive_name=create_archive_name(),
resources=create_resources(),
)
[docs]
def get_description(self, lang: Optional[str] = None) -> Optional[str]:
"""
Return a translated plugin description in str
:param lang: Optional, the language to translate to. When not specified it will use the language of MCDR
:return: Translated plugin description
"""
if self.description is None:
return None
if isinstance(self.description, str):
return self.description
if lang is None:
lang = translation_utils.get_mcdr_language()
result = translation_utils.translate_from_dict(self.description, lang, default=None)
return class_utils.check_type(result, (str, None))
[docs]
def get_description_rtext(self) -> RTextBase:
"""
Return a translated plugin description in :class:`RText <mcdreforged.minecraft.rtext.text.RTextBase>`
.. versionadded:: v2.1.2
"""
if self.description is None:
raise ValueError('description is None')
if isinstance(self.description, str):
return RText(self.description)
return RTextMCDRTranslation.from_translation_dict(self.description)
[docs]
def to_dict(self) -> dict:
"""
Create a dict present of this metadata object
.. versionadded:: v2.13.0
"""
def copy(obj):
return obj.copy() if isinstance(obj, (list, dict)) else obj
return {
# Fields for all plugins
'id': self.id,
'version': str(self.version),
'name': self.name,
'description': copy(self.description),
'author': copy(self.author),
'link': self.link,
'dependencies': {k: str(v) for k, v in self.dependencies.items()},
# Fields for packed plugins
'entrypoint': self.entrypoint,
'archive_name': self.archive_name,
'resources': copy(self.resources),
}
def __sample_test():
sample_metadata = {
'id': 'example_plugin', # If missing it will be the file name without .py suffix
'version': '1.0.0', # If missing it will be '0.0.0'
'name': 'Sample Plugin',
# single string description is also supported
# 'description': 'Sample plugin for MCDR',
'description': {
'en_us': 'Sample plugin for MCDR'
},
'author': [
'Fallen_Breath'
],
'link': 'https://github.com/MCDReforged/MCDReforged',
'dependencies': {
'mcdreforged': '>=1.0.0'
},
# Fields for packed plugins
'entrypoint': 'example_plugin.entry',
'archive_name': 'MyExamplePlugin-v{version}',
'resources': [
'my_resource_folder',
'another_resource_file',
]
}
# there's should be no exception on both new method and old method
meta1 = Metadata.create(sample_metadata)
meta2 = Metadata(sample_metadata) # type: ignore
if meta1 != meta2:
raise AssertionError(f'{meta1} != {meta2}')
__sample_test()