From ad6d1bc4bd7d00af06f2e205031d2fba467bf3d7 Mon Sep 17 00:00:00 2001 From: Cristian Le Date: Thu, 5 Sep 2024 18:24:55 +0200 Subject: [PATCH] Resolve circular dependency Signed-off-by: Cristian Le --- docs/scripts/generate-plugins.py | 5 +- tmt/dataclasses.py | 191 +++++++++++++++++++++++++++---- tmt/utils/__init__.py | 168 +++------------------------ 3 files changed, 184 insertions(+), 180 deletions(-) diff --git a/docs/scripts/generate-plugins.py b/docs/scripts/generate-plugins.py index f44b03a5f9..1592013149 100755 --- a/docs/scripts/generate-plugins.py +++ b/docs/scripts/generate-plugins.py @@ -6,6 +6,7 @@ from typing import Any import tmt.checks +import tmt.dataclasses import tmt.log import tmt.plugins import tmt.steps @@ -36,7 +37,7 @@ def _is_ignored( container: ContainerClass, field: dataclasses.Field[Any], - metadata: tmt.utils.FieldMetadata) -> bool: + metadata: tmt.dataclasses.FieldMetadata) -> bool: """ Check whether a given field is to be ignored in documentation """ if field.name in ('how', '_OPTIONLESS_FIELDS'): @@ -51,7 +52,7 @@ def _is_ignored( def _is_inherited( container: ContainerClass, field: dataclasses.Field[Any], - metadata: tmt.utils.FieldMetadata) -> bool: + metadata: tmt.dataclasses.FieldMetadata) -> bool: """ Check whether a given field is inherited from step data base class """ # TODO: for now, it's a list, but inspecting the actual tree of classes diff --git a/tmt/dataclasses.py b/tmt/dataclasses.py index 325e93513e..6fd9a3066c 100644 --- a/tmt/dataclasses.py +++ b/tmt/dataclasses.py @@ -1,20 +1,22 @@ """ Tmt dataclass wrappers """ +from __future__ import annotations + import dataclasses +import functools import textwrap from collections.abc import Sequence from dataclasses import _MISSING_TYPE, MISSING, asdict, dataclass, fields, is_dataclass, replace -from typing import TYPE_CHECKING, Any, Callable, Optional, TypeVar, Union, overload - -import tmt.utils +from typing import TYPE_CHECKING, Any, Callable, Generic, Optional, TypeVar, Union, overload if TYPE_CHECKING: import tmt.log import tmt.options __all__ = [ - "_MISSING_TYPE", + "FieldMetadata", "MISSING", + "_MISSING_TYPE", "asdict", "dataclass", "field", @@ -23,27 +25,26 @@ "replace", ] -if TYPE_CHECKING: - Field = dataclasses.Field - - # A stand-in variable for generic use. T = TypeVar('T') -#: Type of field's normalization callback. -NormalizeCallback = Callable[[str, Any, tmt.log.Logger], T] +if TYPE_CHECKING: + Field = dataclasses.Field -#: Type of field's exporter callback. -FieldExporter = Callable[[T], Any] + #: Type of field's normalization callback. + NormalizeCallback = Callable[[str, Any, tmt.log.Logger], T] -#: Type of field's CLI option specification. -FieldCLIOption = Union[str, Sequence[str]] + #: Type of field's exporter callback. + FieldExporter = Callable[[T], Any] -#: Type of field's serialization callback. -SerializeCallback = Callable[[T], Any] + #: Type of field's CLI option specification. + FieldCLIOption = Union[str, Sequence[str]] -#: Type of field's unserialization callback. -UnserializeCallback = Callable[[Any], T] + #: Type of field's serialization callback. + SerializeCallback = Callable[[T], Any] + + #: Type of field's unserialization callback. + UnserializeCallback = Callable[[Any], T] # Override field definition @@ -86,7 +87,7 @@ def field( multiple: bool = False, metavar: Optional[str] = None, envvar: Optional[str] = None, - deprecated: Optional['tmt.options.Deprecated'] = None, + deprecated: Optional[tmt.options.Deprecated] = None, help: Optional[str] = None, show_default: bool = False, internal: bool = False, @@ -112,7 +113,7 @@ def field( multiple: bool = False, metavar: Optional[str] = None, envvar: Optional[str] = None, - deprecated: Optional['tmt.options.Deprecated'] = None, + deprecated: Optional[tmt.options.Deprecated] = None, help: Optional[str] = None, show_default: bool = False, internal: bool = False, @@ -137,7 +138,7 @@ def field( multiple: bool = False, metavar: Optional[str] = None, envvar: Optional[str] = None, - deprecated: Optional['tmt.options.Deprecated'] = None, + deprecated: Optional[tmt.options.Deprecated] = None, help: Optional[str] = None, show_default: bool = False, internal: bool = False, @@ -163,7 +164,7 @@ def field( multiple: bool = False, metavar: Optional[str] = None, envvar: Optional[str] = None, - deprecated: Optional['tmt.options.Deprecated'] = None, + deprecated: Optional[tmt.options.Deprecated] = None, help: Optional[str] = None, show_default: bool = False, internal: bool = False, @@ -232,7 +233,7 @@ def field( raise tmt.utils.GeneralError( "Container field must have a boolean default value when it is a flag.") - metadata: tmt.utils.FieldMetadata[T] = tmt.utils.FieldMetadata( + metadata: FieldMetadata[T] = FieldMetadata( internal=internal, help=textwrap.dedent(help).strip() if help else None, _metavar=metavar, @@ -259,3 +260,147 @@ def field( default_factory=default_factory or dataclasses.MISSING, metadata={'tmt': metadata} ) + + +@dataclasses.dataclass +class FieldMetadata(Generic[T]): + """ + A dataclass metadata container used by our custom dataclass field management. + + Attached to fields defined with :py:func:`field` + """ + + internal: bool = False + + #: Help text documenting the field. + help: Optional[str] = None + + #: If field accepts a value, this string would represent it in documentation. + #: This stores the metavar provided when field was created - it may be unset. + #: py:attr:`metavar` provides the actual metavar to be used. + _metavar: Optional[str] = None + + #: The default value for the field. + default: Optional[T] = None + + #: A zero-argument callable that will be called when a default value is + #: needed for the field. + default_factory: Optional[Callable[[], T]] = None + + #: Marks the fields as a flag. + is_flag: bool = False + + #: Marks the field as accepting multiple values. When used on command line, + #: the option could be used multiple times, accumulating values. + multiple: bool = False + + #: If set, show the default value in command line help. + show_default: bool = False + + #: Either a list of allowed values the field can take, or a zero-argument + #: callable that would return such a list. + _choices: Union[None, Sequence[str], Callable[[], Sequence[str]]] = None + + #: Environment variable providing value for the field. + envvar: Optional[str] = None + + #: Mark the option as deprecated. Instance of :py:class:`Deprecated` + #: describes the version in which the field was deprecated plus an optional + #: hint with the recommended alternative. Documentation and help texts would + #: contain this info. + deprecated: Optional[tmt.options.Deprecated] = None + + #: One or more command-line option names. + cli_option: Optional[FieldCLIOption] = None + + #: A normalization callback to call when loading the value from key source + #: (performed by :py:class:`NormalizeKeysMixin`). + normalize_callback: Optional[NormalizeCallback[T]] = None + + # Callbacks for custom serialize/unserialize operations (performed by + # :py:class:`SerializableContainer`). + serialize_callback: Optional[SerializeCallback[T]] = None + unserialize_callback: Optional[SerializeCallback[T]] = None + + #: An export callback to call when exporting the field (performed by + #: :py:class:`tmt.export.Exportable`). + export_callback: Optional[FieldExporter[T]] = None + + #: CLI option parameters, for lazy option creation. + _option_args: Optional[FieldCLIOption] = None + _option_kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) + + #: A :py:func:`click.option` decorator defining a corresponding CLI option. + _option: Optional[tmt.options.ClickOptionDecoratorType] = None + + @functools.cached_property + def choices(self) -> Optional[Sequence[str]]: + """ A list of allowed values the field can take """ + + if isinstance(self._choices, (list, tuple)): + return list(self._choices) + + if callable(self._choices): + return self._choices() + + return None + + @functools.cached_property + def metavar(self) -> Optional[str]: + """ Placeholder for field's value in documentation and help """ + + if self._metavar: + return self._metavar + + if self.choices: + return '|'.join(self.choices) + + return None + + @property + def has_default(self) -> bool: + """ Whether the field has a default value """ + + return self.default_factory is not None \ + or self.default is not dataclasses.MISSING + + @property + def materialized_default(self) -> Optional[T]: + """ Returns the actual default value of the field """ + + if self.default_factory is not None: + return self.default_factory() + + if self.default is not dataclasses.MISSING: + return self.default + + return None + + @property + def option(self) -> Optional[tmt.options.ClickOptionDecoratorType]: + if self._option is None and self.cli_option: + from tmt.options import option + + self._option_args = (self.cli_option,) if isinstance(self.cli_option, str) \ + else self.cli_option + + self._option_kwargs.update({ + 'is_flag': self.is_flag, + 'multiple': self.multiple, + 'envvar': self.envvar, + 'metavar': self.metavar, + 'choices': self.choices, + 'show_default': self.show_default, + 'help': self.help, + 'deprecated': self.deprecated + }) + + if self.default is not dataclasses.MISSING and not self.is_flag: + self._option_kwargs['default'] = self.default + + self._option = option( + *self._option_args, + **self._option_kwargs + ) + + return self._option diff --git a/tmt/utils/__init__.py b/tmt/utils/__init__.py index 5d249cd690..72640b150c 100644 --- a/tmt/utils/__init__.py +++ b/tmt/utils/__init__.py @@ -67,7 +67,6 @@ from tmt import dataclasses from tmt._compat.pathlib import Path from tmt._compat.warnings import deprecated -from tmt.log import LoggableValue, Logger if TYPE_CHECKING: from _typeshed import DataclassInstance @@ -1874,7 +1873,7 @@ def print( def info( self, key: str, - value: Optional[LoggableValue] = None, + value: Optional[tmt.log.LoggableValue] = None, color: Optional[str] = None, shift: int = 0) -> None: """ Show a message unless in quiet mode """ @@ -1883,7 +1882,7 @@ def info( def verbose( self, key: str, - value: Optional[LoggableValue] = None, + value: Optional[tmt.log.LoggableValue] = None, color: Optional[str] = None, shift: int = 0, level: int = 1, @@ -1898,7 +1897,7 @@ def verbose( def debug( self, key: str, - value: Optional[LoggableValue] = None, + value: Optional[tmt.log.LoggableValue] = None, color: Optional[str] = None, shift: int = 0, level: int = 1, @@ -2743,151 +2742,7 @@ def option_to_key(option: str) -> str: return option.replace('-', '_') -@dataclasses.dataclass -class FieldMetadata(Generic[T]): - """ - A dataclass metadata container used by our custom dataclass field management. - - Attached to fields defined with :py:func:`field` - """ - - internal: bool = False - - #: Help text documenting the field. - help: Optional[str] = None - - #: If field accepts a value, this string would represent it in documentation. - #: This stores the metavar provided when field was created - it may be unset. - #: py:attr:`metavar` provides the actual metavar to be used. - _metavar: Optional[str] = None - - #: The default value for the field. - default: Optional[T] = None - - #: A zero-argument callable that will be called when a default value is - #: needed for the field. - default_factory: Optional[Callable[[], T]] = None - - #: Marks the fields as a flag. - is_flag: bool = False - - #: Marks the field as accepting multiple values. When used on command line, - #: the option could be used multiple times, accumulating values. - multiple: bool = False - - #: If set, show the default value in command line help. - show_default: bool = False - - #: Either a list of allowed values the field can take, or a zero-argument - #: callable that would return such a list. - _choices: Union[None, Sequence[str], Callable[[], Sequence[str]]] = None - - #: Environment variable providing value for the field. - envvar: Optional[str] = None - - #: Mark the option as deprecated. Instance of :py:class:`Deprecated` - #: describes the version in which the field was deprecated plus an optional - #: hint with the recommended alternative. Documentation and help texts would - #: contain this info. - deprecated: Optional['tmt.options.Deprecated'] = None - - #: One or more command-line option names. - cli_option: Optional[tmt.dataclasses.FieldCLIOption] = None - - #: A normalization callback to call when loading the value from key source - #: (performed by :py:class:`NormalizeKeysMixin`). - normalize_callback: Optional['tmt.dataclasses.NormalizeCallback[T]'] = None - - # Callbacks for custom serialize/unserialize operations (performed by - # :py:class:`SerializableContainer`). - serialize_callback: Optional['tmt.dataclasses.SerializeCallback[T]'] = None - unserialize_callback: Optional['tmt.dataclasses.SerializeCallback[T]'] = None - - #: An export callback to call when exporting the field (performed by - #: :py:class:`tmt.export.Exportable`). - export_callback: Optional['tmt.dataclasses.FieldExporter[T]'] = None - - #: CLI option parameters, for lazy option creation. - _option_args: Optional['tmt.dataclasses.FieldCLIOption'] = None - _option_kwargs: dict[str, Any] = dataclasses.field(default_factory=dict) - - #: A :py:func:`click.option` decorator defining a corresponding CLI option. - _option: Optional['tmt.options.ClickOptionDecoratorType'] = None - - @functools.cached_property - def choices(self) -> Optional[Sequence[str]]: - """ A list of allowed values the field can take """ - - if isinstance(self._choices, (list, tuple)): - return list(self._choices) - - if callable(self._choices): - return self._choices() - - return None - - @functools.cached_property - def metavar(self) -> Optional[str]: - """ Placeholder for field's value in documentation and help """ - - if self._metavar: - return self._metavar - - if self.choices: - return '|'.join(self.choices) - - return None - - @property - def has_default(self) -> bool: - """ Whether the field has a default value """ - - return self.default_factory is not None \ - or self.default is not dataclasses.MISSING - - @property - def materialized_default(self) -> Optional[T]: - """ Returns the actual default value of the field """ - - if self.default_factory is not None: - return self.default_factory() - - if self.default is not dataclasses.MISSING: - return self.default - - return None - - @property - def option(self) -> Optional['tmt.options.ClickOptionDecoratorType']: - if self._option is None and self.cli_option: - from tmt.options import option - - self._option_args = (self.cli_option,) if isinstance(self.cli_option, str) \ - else self.cli_option - - self._option_kwargs.update({ - 'is_flag': self.is_flag, - 'multiple': self.multiple, - 'envvar': self.envvar, - 'metavar': self.metavar, - 'choices': self.choices, - 'show_default': self.show_default, - 'help': self.help, - 'deprecated': self.deprecated - }) - - if self.default is not dataclasses.MISSING and not self.is_flag: - self._option_kwargs['default'] = self.default - - self._option = option( - *self._option_args, - **self._option_kwargs - ) - - return self._option - - -def container_fields(container: Container) -> Iterator[dataclasses.Field[Any]]: +def container_fields(container: Container) -> Iterator['dataclasses.Field[Any]']: yield from dataclasses.fields(container) @@ -2912,9 +2767,12 @@ def container_items(container: ContainerInstance) -> Iterator[tuple[str, Any]]: yield field.name, container.__dict__[field.name] -def container_field( - container: Container, - key: str) -> tuple[str, str, Any, dataclasses.Field[Any], 'FieldMetadata[Any]']: +def container_field(container: Container, + key: str) -> tuple[str, + str, + Any, + 'dataclasses.Field[Any]', + 'dataclasses.FieldMetadata[Any]']: """ Return a dataclass/data container field info by the field's name. @@ -2931,7 +2789,7 @@ def container_field( if field.name != key: continue - metadata = field.metadata.get('tmt', FieldMetadata()) + metadata = field.metadata.get('tmt', dataclasses.FieldMetadata()) return ( field.name, key_to_option(field.name), @@ -6241,7 +6099,7 @@ class RestVisitor(docutils.nodes.NodeVisitor): node, filling "rendered paragraphs" list with rendered strings. """ - def __init__(self, document: docutils.nodes.document, logger: Logger) -> None: + def __init__(self, document: docutils.nodes.document, logger: tmt.log.Logger) -> None: super().__init__(document) self.logger = logger @@ -6467,7 +6325,7 @@ def parse_rst(text: str) -> docutils.nodes.document: return document -def render_rst(text: str, logger: Logger) -> str: +def render_rst(text: str, logger: tmt.log.Logger) -> str: """ Render a ReST document """ document = parse_rst(text)