mirror of
https://github.com/ChristianLight/tutor.git
synced 2024-12-12 14:17:46 +00:00
feat: add cli for listing available patches
This commit is contained in:
parent
a373e24b2c
commit
f13627a32a
@ -0,0 +1,12 @@
|
|||||||
|
<!--
|
||||||
|
Create a changelog entry for every new user-facing change. Please respect the following instructions:
|
||||||
|
- Indicate breaking changes by prepending an explosion 💥 character.
|
||||||
|
- Prefix your changes with either [Bugfix], [Improvement], [Feature], [Security], [Deprecation].
|
||||||
|
- You may optionally append "(by @<author>)" at the end of the line, where "<author>" is either one (just one)
|
||||||
|
of your GitHub username, real name or affiliated organization. These affiliations will be displayed in
|
||||||
|
the release notes for every release.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<!-- - 💥[Feature] Foobarize the blorginator. This breaks plugins by renaming the `FOO_DO` filter to `BAR_DO`. (by @regisb) -->
|
||||||
|
<!-- - [Improvement] This is a non-breaking change. Life is good. (by @billgates) -->
|
||||||
|
[Feature] Add `tutor config patches list` CLI for listing available patches. (by @mafermazu)
|
@ -10,6 +10,10 @@ This is the list of all patches used across Tutor (outside of any plugin). Alter
|
|||||||
cd tutor
|
cd tutor
|
||||||
git grep "{{ patch" -- tutor/templates
|
git grep "{{ patch" -- tutor/templates
|
||||||
|
|
||||||
|
Or you can list all available patches with the following command::
|
||||||
|
|
||||||
|
tutor config patches list
|
||||||
|
|
||||||
See also `this GitHub search <https://github.com/search?utf8=✓&q={{+patch+repo%3Aoverhangio%2Ftutor+path%3A%2Ftutor%2Ftemplates&type=Code&ref=advsearch&l=&l= 8>`__.
|
See also `this GitHub search <https://github.com/search?utf8=✓&q={{+patch+repo%3Aoverhangio%2Ftutor+path%3A%2Ftutor%2Ftemplates&type=Code&ref=advsearch&l=&l= 8>`__.
|
||||||
|
|
||||||
.. patch:: caddyfile
|
.. patch:: caddyfile
|
||||||
|
@ -58,3 +58,12 @@ class ConfigTests(unittest.TestCase, TestCommandMixin):
|
|||||||
self.assertFalse(result.exception)
|
self.assertFalse(result.exception)
|
||||||
self.assertEqual(0, result.exit_code)
|
self.assertEqual(0, result.exit_code)
|
||||||
self.assertTrue(result.output)
|
self.assertTrue(result.output)
|
||||||
|
|
||||||
|
|
||||||
|
class PatchesTests(unittest.TestCase, TestCommandMixin):
|
||||||
|
def test_config_patches_list(self) -> None:
|
||||||
|
with temporary_root() as root:
|
||||||
|
self.invoke_in_root(root, ["config", "save"])
|
||||||
|
result = self.invoke_in_root(root, ["config", "patches", "list"])
|
||||||
|
self.assertFalse(result.exception)
|
||||||
|
self.assertEqual(0, result.exit_code)
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import unittest
|
import unittest
|
||||||
|
from io import StringIO
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
from tests.helpers import PluginsTestCase, temporary_root
|
from tests.helpers import PluginsTestCase, temporary_root
|
||||||
@ -261,3 +262,138 @@ class CurrentVersionTests(unittest.TestCase):
|
|||||||
self.assertEqual("olive", env.get_env_release(root))
|
self.assertEqual("olive", env.get_env_release(root))
|
||||||
self.assertIsNone(env.should_upgrade_from_release(root))
|
self.assertIsNone(env.should_upgrade_from_release(root))
|
||||||
self.assertTrue(env.is_up_to_date(root))
|
self.assertTrue(env.is_up_to_date(root))
|
||||||
|
|
||||||
|
|
||||||
|
class PatchRendererTests(unittest.TestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.render = env.PatchRenderer()
|
||||||
|
self.render.current_template = "current_template"
|
||||||
|
return super().setUp()
|
||||||
|
|
||||||
|
@patch("tutor.env.Renderer.render_template")
|
||||||
|
def test_render_template(self, render_template_mock: Mock) -> None:
|
||||||
|
"""Test that render_template changes the current template and
|
||||||
|
calls once render_template from Renderer with the current template."""
|
||||||
|
self.render.render_template("new_template")
|
||||||
|
|
||||||
|
self.assertEqual(self.render.current_template, "new_template")
|
||||||
|
render_template_mock.assert_called_once_with("new_template")
|
||||||
|
|
||||||
|
@patch("tutor.env.Renderer.patch")
|
||||||
|
def test_patch_with_first_patch(self, patch_mock: Mock) -> None:
|
||||||
|
"""Test that patch is called from Renderer and adds patches_locations
|
||||||
|
when we didn't have that patch."""
|
||||||
|
self.render.patches_locations = {}
|
||||||
|
|
||||||
|
self.render.patch("first_patch")
|
||||||
|
|
||||||
|
patch_mock.assert_called_once_with("first_patch", separator="\n", suffix="")
|
||||||
|
self.assertEqual(
|
||||||
|
self.render.patches_locations,
|
||||||
|
{"first_patch": [self.render.current_template]},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_patch_with_patch_multiple_locations(self) -> None:
|
||||||
|
"""Test add more locations to a patch."""
|
||||||
|
self.render.patches_locations = {"first_patch": ["template_1"]}
|
||||||
|
|
||||||
|
self.render.patch("first_patch")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.render.patches_locations,
|
||||||
|
{"first_patch": ["template_1", "current_template"]},
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("tutor.env.plugins.iter_patches")
|
||||||
|
def test_patch_with_custom_patch_in_a_plugin_patch(
|
||||||
|
self, iter_patches_mock: Mock
|
||||||
|
) -> None:
|
||||||
|
"""Test the patch function with a plugin with a custom patch.
|
||||||
|
Examples:
|
||||||
|
- When first_patch is in a plugin patches and has a 'custom_patch',
|
||||||
|
the patches_locations will reflect that 'custom_patch' is from
|
||||||
|
first_patch location.
|
||||||
|
- If in tutor-mfe/tutormfe/patches/caddyfile you add a custom patch
|
||||||
|
inside the caddyfile patch, the patches_locations will reflect that.
|
||||||
|
|
||||||
|
Expected behavior:
|
||||||
|
- Process the first_patch and find the custom_patch in a plugin with
|
||||||
|
first_patch patch.
|
||||||
|
- Process the custom_patch and add "within patch: first_patch" in the
|
||||||
|
patches_locations."""
|
||||||
|
iter_patches_mock.side_effect = [
|
||||||
|
["""{{ patch('custom_patch')|indent(4) }}"""],
|
||||||
|
[],
|
||||||
|
]
|
||||||
|
self.render.patches_locations = {}
|
||||||
|
calls = [unittest.mock.call("first_patch"), unittest.mock.call("custom_patch")]
|
||||||
|
|
||||||
|
self.render.patch("first_patch")
|
||||||
|
|
||||||
|
iter_patches_mock.assert_has_calls(calls)
|
||||||
|
self.assertEqual(
|
||||||
|
self.render.patches_locations,
|
||||||
|
{
|
||||||
|
"first_patch": ["current_template"],
|
||||||
|
"custom_patch": ["within patch: first_patch"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("tutor.env.plugins.iter_patches")
|
||||||
|
def test_patch_with_processed_patch_in_a_plugin_patch(
|
||||||
|
self, iter_patches_mock: Mock
|
||||||
|
) -> None:
|
||||||
|
"""Test the patch function with a plugin with a processed patch.
|
||||||
|
Example:
|
||||||
|
- When first_patch was processed and the second_patch is used in a
|
||||||
|
plugin and call the first_patch again. Then the patches_locations will
|
||||||
|
reflect that first_patch also have a location from second_patch."""
|
||||||
|
iter_patches_mock.side_effect = [
|
||||||
|
["""{{ patch('first_patch')|indent(4) }}"""],
|
||||||
|
[],
|
||||||
|
]
|
||||||
|
self.render.patches_locations = {"first_patch": ["current_template"]}
|
||||||
|
|
||||||
|
self.render.patch("second_patch")
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
self.render.patches_locations,
|
||||||
|
{
|
||||||
|
"first_patch": ["current_template", "within patch: second_patch"],
|
||||||
|
"second_patch": ["current_template"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
@patch("tutor.env.Renderer.iter_templates_in")
|
||||||
|
@patch("tutor.env.PatchRenderer.render_template")
|
||||||
|
def test_render_all(
|
||||||
|
self, render_template_mock: Mock, iter_templates_in_mock: Mock
|
||||||
|
) -> None:
|
||||||
|
"""Test render_template was called for templates in iter_templates_in."""
|
||||||
|
iter_templates_in_mock.return_value = ["template_1", "template_2"]
|
||||||
|
calls = [unittest.mock.call("template_1"), unittest.mock.call("template_2")]
|
||||||
|
|
||||||
|
self.render.render_all()
|
||||||
|
|
||||||
|
iter_templates_in_mock.assert_called_once()
|
||||||
|
render_template_mock.assert_has_calls(calls)
|
||||||
|
|
||||||
|
@patch("sys.stdout", new_callable=StringIO)
|
||||||
|
@patch("tutor.env.PatchRenderer.render_all")
|
||||||
|
def test_print_patches_locations(
|
||||||
|
self, render_all_mock: Mock, stdout_mock: Mock
|
||||||
|
) -> None:
|
||||||
|
"""Test render_all was called and the output of print_patches_locations."""
|
||||||
|
self.render.patches_locations = {"first_patch": ["template_1", "template_2"]}
|
||||||
|
|
||||||
|
self.render.print_patches_locations()
|
||||||
|
|
||||||
|
render_all_mock.assert_called_once()
|
||||||
|
self.assertEqual(
|
||||||
|
"""
|
||||||
|
PATCH LOCATIONS
|
||||||
|
first_patch template_1
|
||||||
|
template_2
|
||||||
|
""".strip(),
|
||||||
|
stdout_mock.getvalue().strip(),
|
||||||
|
)
|
||||||
|
@ -155,6 +155,21 @@ def printvalue(context: Context, key: str) -> None:
|
|||||||
raise exceptions.TutorError(f"Missing configuration value: {key}") from e
|
raise exceptions.TutorError(f"Missing configuration value: {key}") from e
|
||||||
|
|
||||||
|
|
||||||
|
@click.group(name="patches", help="Commands related to patches in configurations")
|
||||||
|
def patches_command() -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@click.command(name="list", help="Print all available patches")
|
||||||
|
@click.pass_obj
|
||||||
|
def patches_list(context: Context) -> None:
|
||||||
|
config = tutor_config.load(context.root)
|
||||||
|
renderer = env.PatchRenderer(config)
|
||||||
|
renderer.print_patches_locations()
|
||||||
|
|
||||||
|
|
||||||
config_command.add_command(save)
|
config_command.add_command(save)
|
||||||
config_command.add_command(printroot)
|
config_command.add_command(printroot)
|
||||||
config_command.add_command(printvalue)
|
config_command.add_command(printvalue)
|
||||||
|
config_command.add_command(patches_command)
|
||||||
|
patches_command.add_command(patches_list)
|
||||||
|
63
tutor/env.py
63
tutor/env.py
@ -215,6 +215,69 @@ class Renderer:
|
|||||||
raise exceptions.TutorError(f"Missing configuration value: {e.args[0]}")
|
raise exceptions.TutorError(f"Missing configuration value: {e.args[0]}")
|
||||||
|
|
||||||
|
|
||||||
|
class PatchRenderer(Renderer):
|
||||||
|
"""
|
||||||
|
Render patches for print it.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config: t.Optional[Config] = None):
|
||||||
|
self.patches_locations: t.Dict[str, t.List[str]] = {}
|
||||||
|
self.current_template: str = ""
|
||||||
|
super().__init__(config)
|
||||||
|
|
||||||
|
def render_template(self, template_name: str) -> t.Union[str, bytes]:
|
||||||
|
"""
|
||||||
|
Set the current template and render template from Renderer.
|
||||||
|
"""
|
||||||
|
self.current_template = template_name
|
||||||
|
return super().render_template(self.current_template)
|
||||||
|
|
||||||
|
def patch(self, name: str, separator: str = "\n", suffix: str = "") -> str:
|
||||||
|
"""
|
||||||
|
Set the patches locations and render calls to {{ patch("...") }} from Renderer.
|
||||||
|
"""
|
||||||
|
if not self.patches_locations.get(name):
|
||||||
|
self.patches_locations.update({name: [self.current_template]})
|
||||||
|
else:
|
||||||
|
if self.current_template not in self.patches_locations[name]:
|
||||||
|
self.patches_locations[name].append(self.current_template)
|
||||||
|
|
||||||
|
# Store the template's name, and replace it with the name of this patch.
|
||||||
|
# This handles the case where patches themselves include patches.
|
||||||
|
original_template = self.current_template
|
||||||
|
self.current_template = f"within patch: {name}"
|
||||||
|
|
||||||
|
rendered_patch = super().patch(name, separator=separator, suffix=suffix)
|
||||||
|
self.current_template = (
|
||||||
|
original_template # Restore the template's name from before.
|
||||||
|
)
|
||||||
|
return rendered_patch
|
||||||
|
|
||||||
|
def render_all(self, *prefix: str) -> None:
|
||||||
|
"""
|
||||||
|
Render all templates.
|
||||||
|
"""
|
||||||
|
for template_name in self.iter_templates_in(*prefix):
|
||||||
|
self.render_template(template_name)
|
||||||
|
|
||||||
|
def print_patches_locations(self) -> None:
|
||||||
|
"""
|
||||||
|
Print patches locations.
|
||||||
|
"""
|
||||||
|
plugins_table: list[tuple[str, ...]] = [("PATCH", "LOCATIONS")]
|
||||||
|
self.render_all()
|
||||||
|
for patch, locations in sorted(self.patches_locations.items()):
|
||||||
|
n_locations = 0
|
||||||
|
for location in locations:
|
||||||
|
if n_locations < 1:
|
||||||
|
plugins_table.append((patch, location))
|
||||||
|
n_locations += 1
|
||||||
|
else:
|
||||||
|
plugins_table.append(("", location))
|
||||||
|
|
||||||
|
fmt.echo(utils.format_table(plugins_table))
|
||||||
|
|
||||||
|
|
||||||
def is_rendered(path: str) -> bool:
|
def is_rendered(path: str) -> bool:
|
||||||
"""
|
"""
|
||||||
Return whether the template should be rendered or not.
|
Return whether the template should be rendered or not.
|
||||||
|
Loading…
Reference in New Issue
Block a user