6
0
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:
Maria Fernanda Magallanes Zubillaga 2023-02-07 23:35:25 -05:00 committed by Régis Behmo
parent a373e24b2c
commit f13627a32a
6 changed files with 239 additions and 0 deletions

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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(),
)

View File

@ -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)

View File

@ -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.