Skip to content

Commit

Permalink
Resolve circular dependency
Browse files Browse the repository at this point in the history
Signed-off-by: Cristian Le <[email protected]>
  • Loading branch information
LecrisUT committed Sep 5, 2024
1 parent 6ba0d36 commit ad6d1bc
Show file tree
Hide file tree
Showing 3 changed files with 184 additions and 180 deletions.
5 changes: 3 additions & 2 deletions docs/scripts/generate-plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from typing import Any

import tmt.checks
import tmt.dataclasses
import tmt.log
import tmt.plugins
import tmt.steps
Expand Down Expand Up @@ -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'):
Expand All @@ -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
Expand Down
191 changes: 168 additions & 23 deletions tmt/dataclasses.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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
Loading

0 comments on commit ad6d1bc

Please sign in to comment.