Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend resource_files to general Traversables #2946

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ repos:
#
# For now, we simply copy & paste from pyproject.toml :(
additional_dependencies:
- "importlib-metadata>=3.6.0; python_version < '3.10'"
- "importlib-resources; python_version < '3.10'"
- "click>=8.0.3,!=8.1.4" # 8.1.3 / 8.1.6 TODO type annotations tmt.cli.Context -> click.core.Context click/issues/2558
- "docutils>=0.16" # 0.16 is the current one available for RHEL9
- "fmf>=1.3.0"
Expand Down Expand Up @@ -80,6 +82,8 @@ repos:
#
# For now, we simply copy & paste from pyproject.toml :(
additional_dependencies:
- "importlib-metadata>=3.6.0; python_version < '3.10'"
- "importlib-resources; python_version < '3.10'"
- "click>=8.0.3,!=8.1.4" # 8.1.3 / 8.1.6 TODO type annotations tmt.cli.Context -> click.core.Context click/issues/2558
- "docutils>=0.16" # 0.16 is the current one available for RHEL9
- "fmf>=1.3.0"
Expand Down
50 changes: 50 additions & 0 deletions docs/code/plugin-introduction.rst
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ provided in the ``TMT_PLUGINS`` environment variable and from
Consider adding a static type checker (e.g. ``mypy``) in your
plugin's CI using the ``main`` branch of ``tmt``.

.. versionadded:: 1.38
You can use ``tmt.resources`` entry point to inject resource
files to be used for tmt, e.g. schemas or templates. See
:ref:`additional-resources` for more details.


Inheritance
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Expand Down Expand Up @@ -62,3 +67,48 @@ features which cannot be covered by generic ssh implementation of
the ``Guest`` class.

__ https://github.com/teemtee/tmt/tree/main/examples/plugins

.. additional-resources:

Additional resource files
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

In order to make resource files available to the base ``tmt``
execution, you need to point a ``tmt.resources`` entry point to the
root python package where the resource files are located from, e.g.
with in the ``examples/plugins``:

.. code-block:: toml
:caption: pyproject.toml
:emphasize-lines: 5,6

[project.entry-points."tmt.plugin"]
ProvisionExample = "example.provision:ProvisionExample"
DiscoverExample = "example.discover:DiscoverExample"

[project.entry-points."tmt.resources"]
ResourcesExample = "example"

you can, for example, add a json schema file for the plugins
implemented above by including the schema files under a ``schemas``
folder:

.. code-block:: shell

$ tree ./example
./example
├── __init__.py
├── discover.py
├── provision.py
└── schemas
├── discover
│ └── example.yaml
└── provision
└── example.yaml

.. note::

Both the entry-point entries as well as any resource file
under the ``tmt.resources`` path **must** have unique names.
Consider namespacing all relevant entries with the name of the
project or an unambiguous derivative of it.
7 changes: 7 additions & 0 deletions docs/releases.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
Releases
======================

tmt-1.38.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

External plugins can inject additional resources to be used by tmt using
the ``tmt.resources`` entry-point, e.g. to extend the schema validation
or tmt templates.

tmt-1.37.0
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
Empty file.
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
import dataclasses

import tmt
import tmt.steps
import tmt.steps.discover
import tmt.utils

# See the online documentation for more details about writing plugins
# https://tmt.readthedocs.io/en/stable/plugins.html


@dataclasses.dataclass
class DiscoverExampleData(tmt.steps.discover.DiscoverStepData):
path: str = tmt.utils.field(
default=".",
option=('-p', '--path'),
metavar='ROOT',
help='Path to the metadata tree root.')


@tmt.steps.provides_method('example')
class DiscoverExample(tmt.steps.discover.DiscoverPlugin):
"""
Expand All @@ -16,6 +28,8 @@ class DiscoverExample(tmt.steps.discover.DiscoverPlugin):
of configuration examples as well.
"""

_data_class = DiscoverExampleData

def show(self):
""" Show plugin details for given or all available keys """
super().show([])
Expand Down Expand Up @@ -48,7 +62,8 @@ def go(self):
print("Code should prepare environment for tests.")

# Discover available tests
self._tests = tmt.Tree(logger=self._logger, path=".").tests()
self._tests = tmt.Tree(logger=self._logger,
path=self.data.path).tests()

def tests(self):
"""
Expand Down
File renamed without changes.
22 changes: 22 additions & 0 deletions examples/plugins/example/schemas/discover/example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
$id: /schemas/discover/example
$schema: https://json-schema.org/draft-07/schema

type: object
additionalProperties: false

properties:
how:
type: string
enum:
- example
path:
$ref: "/schemas/common#/definitions/fmf_id/properties/path"

# Other basic discover properties
name:
type: string
order:
$ref: "/schemas/core#/definitions/order"

required:
- how
20 changes: 20 additions & 0 deletions examples/plugins/example/schemas/provision/example.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
$id: /schemas/provision/example
$schema: https://json-schema.org/draft-07/schema

type: object
additionalProperties: false

properties:
how:
type: string
enum:
- example

# Other basic provision properties
name:
type: string
order:
$ref: "/schemas/core#/definitions/order"

required:
- how
10 changes: 6 additions & 4 deletions examples/plugins/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ dependencies = [

[tool.hatch.build.targets.wheel]
packages = [
"discover.py",
"provision.py",
"example",
]

[project.entry-points."tmt.plugin"]
ProvisionExample = "provision:ProvisionExample"
DiscoverExample = "discover:DiscoverExample"
ProvisionExample = "example.provision:ProvisionExample"
DiscoverExample = "example.discover:DiscoverExample"

[project.entry-points."tmt.resources"]
ResourcesExample = "example"
4 changes: 4 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ classifiers = [
"Operating System :: POSIX :: Linux",
]
dependencies = [ # F39 / PyPI
"importlib-metadata>=3.6.0; python_version < '3.10'", # Backporting selectable entry_points
"importlib-resources; python_version < '3.12'", # MultiplexedPath is broken on earlier versions
"click>=8.0.3,!=8.1.4", # 8.1.3 / 8.1.6 TODO type annotations tmt.cli.Context -> click.core.Context click/issues/2558
"docutils>=0.16", # 0.16 is the current one available for RHEL9
"fmf>=1.3.0",
Expand Down Expand Up @@ -413,6 +415,8 @@ builtins-ignorelist = ["help", "format", "input", "filter", "copyright", "max"]
"pathlib.PosixPath".msg = "Use tmt._compat.pathlib.Path instead."
"warnings.deprecated".msg = "Use tmt._compat.warnings.deprecated instead."
"os.path".msg = "Use tmt._compat.pathlib.Path and pathlib instead."
"importlib.metadata.entry_points".msg = "Use tmt._compat.importlib.metadata.entry_points instead."
"importlib.readers".msg = "Use tmt._compat.importlib.readers instead."

[tool.ruff.lint.isort]
known-first-party = ["tmt"]
Expand Down
2 changes: 1 addition & 1 deletion tests/core/env/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ rlJournalStart
rlRun -s "TMT_DEBUG=3 tmt plan show"
rlAssertGrep "Using the 'DiscoverFmf' plugin" $rlRun_LOG
rlRun -s "TMT_DEBUG=weird tmt plan show" 2
rlAssertGrep "Invalid debug level" $rlRun_LOG
rlAssertGrep "Invalid value.*'weird' is not a valid integer" $rlRun_LOG -E
rlPhaseEnd

for execute in 'tmt'; do
Expand Down
1 change: 1 addition & 0 deletions tests/plugins/data/.fmf/version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
8 changes: 8 additions & 0 deletions tests/plugins/data/main.fmf
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
summary: Check plugin schema validation
execute:
how: tmt
provision:
how: example
discover:
how: example
path: some/random/path
8 changes: 7 additions & 1 deletion tests/plugins/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ rlJournalStart
rlPhaseStartSetup
rlRun "tmp=\$(mktemp -d)" 0 "Create tmp directory"
rlRun "cp -r $(git rev-parse --show-toplevel)/examples/plugins $tmp"
rlRun "cp -a data $tmp"
rlRun "pushd $tmp"

# For local development this can run already in venv, do not use venv
Expand All @@ -27,11 +28,16 @@ rlJournalStart
rlAssertGrep "Unsupported discover method" "$rlRun_LOG"
rlRun -s "$tmt run provision -h example --help" "2"
rlAssertGrep "Unsupported provision method" "$rlRun_LOG"
rlRun -s "$tmt -r data lint --enable-check C000 --enforce-check C000" "1"
rlAssertGrep "fail C000 fmf node failed schema validation" "$rlRun_LOG"
rlAssertGrep "fail C000 key \"path\" not recognized" "$rlRun_LOG"
rlAssertGrep "fail C000 value of \"how\" is not" "$rlRun_LOG"

# Install them to entry_point and they work now
rlRun "pip install ./plugins"
rlRun "$tmt run discover -h example --help"
rlRun "$tmt run provision -h example --help"
rlRun -s "$tmt -r data lint --enable-check C000 --enforce-check C000"

# Uninstall them
rlRun "pip uninstall -y demo-plugins"
Expand All @@ -47,7 +53,7 @@ rlJournalStart
rlAssertGrep "Unsupported provision method" "$rlRun_LOG"

# Export variable and plugins work now
rlRun "export TMT_PLUGINS=./plugins"
rlRun "export TMT_PLUGINS=./plugins/example"
rlRun "$tmt run discover -h example --help"
rlRun "$tmt run provision -h example --help"
rlPhaseEnd
Expand Down
Empty file.
12 changes: 12 additions & 0 deletions tmt/_compat/importlib/metadata.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from __future__ import annotations

import sys

if sys.version_info < (3, 10):
from importlib_metadata import entry_points # pyright: ignore[reportUnknownVariableType]
else:
from importlib.metadata import entry_points

__all__ = [
"entry_points",
]
12 changes: 12 additions & 0 deletions tmt/_compat/importlib/readers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from __future__ import annotations

import sys

if sys.version_info < (3, 12):
from importlib_resources.readers import MultiplexedPath
else:
from importlib.readers import MultiplexedPath

__all__ = [
"MultiplexedPath",
]
3 changes: 2 additions & 1 deletion tmt/export/template.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from importlib.abc import Traversable
from typing import Any, Optional

import tmt.base
Expand All @@ -15,7 +16,7 @@ class TemplateExporter(tmt.export.ExportPlugin):
def render_template(
cls,
*,
template_filepath: Optional[Path] = None,
template_filepath: Optional[Traversable] = None,
default_template_filename: str,
keys: Optional[list[str]] = None,
**variables: Any
Expand Down
38 changes: 12 additions & 26 deletions tmt/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,21 +108,6 @@ def create_decolorizer(apply_colors: bool) -> Callable[[str], str]:
return tmt.utils.remove_color


def _debug_level_from_global_envvar() -> int:
import tmt.utils

raw_value = os.getenv('TMT_DEBUG', None)

if raw_value is None:
return 0

try:
return int(raw_value)

except ValueError:
raise tmt.utils.GeneralError(f"Invalid debug level '{raw_value}', use an integer.")


def decide_colorization(no_color: bool, force_color: bool) -> tuple[bool, bool]:
"""
Decide whether the output and logging should be colorized.
Expand Down Expand Up @@ -626,19 +611,13 @@ def apply_verbosity_options(
else:
self.verbosity_level = verbosity_level

debug_level_from_global_envvar = _debug_level_from_global_envvar()
debug_level_from_option = cast(Optional[int], actual_kwargs.get('debug', None))

if debug_level_from_global_envvar not in (None, 0):
self.debug_level = debug_level_from_global_envvar
if debug_level_from_option is None or debug_level_from_option == 0:
pass

else:
debug_level_from_option = cast(Optional[int], actual_kwargs.get('debug', None))

if debug_level_from_option is None or debug_level_from_option == 0:
pass

else:
self.debug_level = debug_level_from_option
self.debug_level = debug_level_from_option

quietness_level = actual_kwargs.get('quiet', False)

Expand Down Expand Up @@ -868,7 +847,14 @@ def get_bootstrap_logger(cls) -> 'Logger':
# Stay away of our future main logger
actual_logger = Logger._normalize_logger(logging.getLogger('_tmt_bootstrap'))

cls._bootstrap_logger = Logger.create(actual_logger=actual_logger)
# The environment variables are usually handled at the click cli stage
# Here we enable safe parsing of those variables.
try:
debug = int(os.getenv('TMT_DEBUG', 0))
except ValueError:
debug = None

cls._bootstrap_logger = Logger.create(actual_logger=actual_logger, debug=debug)
cls._bootstrap_logger.add_console_handler()

return cls._bootstrap_logger
2 changes: 1 addition & 1 deletion tmt/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ def option(
'-v', '--verbose', count=True, default=0,
help='Show more details. Use multiple times to raise verbosity.'),
option(
'-d', '--debug', count=True, default=0,
'-d', '--debug', count=True, default=0, envvar="TMT_DEBUG",
help='Provide debugging information. Repeat to see more details.'),
option(
'-q', '--quiet', is_flag=True,
Expand Down
Loading
Loading