Repository: hikari-py/hikari (python)

Issue:
Allow custom args in `hikari.internal.ux.print_banner`
### Summary

Allow any `args` dict to be used in `hikari.internal.ux.print_banner`

### Why is this needed?

It is difficult to add extra information that changes to the banner. For example, a version number. Allowing custom args to be passed in would make this extremely easy,

### Ideal implementation

I think the a kwarg should be added.
```python
async def print_banner(package, allow_color, force_color, args=None):
    if args is None:
       # Set args to the current version
```
Updating args may also work
```python
async def print_banner(package, allow_color, force_color, custom_args=None):
    args = {...}    
    if custom_args:
        args.update(custom_args)
```


### Checklist

- [X] I have searched the issue tracker and have made sure it's not a duplicate. If it is a follow up of another issue, I have specified it.

Pull request description:
Allow for custom $-substitutions in `hikari.internal.ux.print_banner`
### Summary
<!-- Small summary of the merge request -->
closes #1006 
Allows custom $-substitutions to be used when printing a banner.

### Checklist
<!-- Make sure to tick all the following boxes by putting an `x` in between (like this `[x]`) -->
- [x] I have run `nox` and all the pipelines have passed.
- [x] I have made unittests according to the code I have added/modified/deleted.

### Related issues
<!--
To mention an issue use `#issue-id` and to mention a merge request use `!merge-request-id`
To close/fix an issue use `Close #issue-id` or `Fix #issue-id` (depending on the merge request)
-->


Patch:
diff --git a/changes/1022.feature.md b/changes/1022.feature.md
new file mode 100644
index 00000000..4c98a0de
--- /dev/null
+++ b/changes/1022.feature.md
@@ -0,0 +1,1 @@
+Custom substitutions can now be used in `hikari.internal.ux.print_banner`.
diff --git a/hikari/impl/bot.py b/hikari/impl/bot.py
index 67b1b3f7..afda60c7 100644
--- a/hikari/impl/bot.py
+++ b/hikari/impl/bot.py
@@ -649,7 +649,12 @@ def listen(
         return self._event_manager.listen(event_type)
 
     @staticmethod
-    def print_banner(banner: typing.Optional[str], allow_color: bool, force_color: bool) -> None:
+    def print_banner(
+        banner: typing.Optional[str],
+        allow_color: bool,
+        force_color: bool,
+        extra_args: typing.Optional[typing.Dict[str, str]] = None,
+    ) -> None:
         """Print the banner.
 
         This allows library vendors to override this behaviour, or choose to
@@ -674,8 +679,16 @@ def print_banner(banner: typing.Optional[str], allow_color: bool, force_color: b
 
         !!! note
             `force_color` will always take precedence over `allow_color`.
+        extra_args : typing.Optional[typing.Dict[builtins.str, builtins.str]]
+            If provided, extra $-substitutions to use when printing the banner.
+            Default substitutions can not be overwritten.
+
+        Raises
+        ------
+        builtins.ValueError
+            If `extra_args` contains a default $-substitution.
         """
-        ux.print_banner(banner, allow_color, force_color)
+        ux.print_banner(banner, allow_color, force_color, extra_args=extra_args)
 
     def run(
         self,
diff --git a/hikari/impl/rest_bot.py b/hikari/impl/rest_bot.py
index a531b195..fadfc01a 100644
--- a/hikari/impl/rest_bot.py
+++ b/hikari/impl/rest_bot.py
@@ -316,7 +316,12 @@ def executor(self) -> typing.Optional[concurrent.futures.Executor]:
         return self._executor
 
     @staticmethod
-    def print_banner(banner: typing.Optional[str], allow_color: bool, force_color: bool) -> None:
+    def print_banner(
+        banner: typing.Optional[str],
+        allow_color: bool,
+        force_color: bool,
+        extra_args: typing.Optional[typing.Dict[str, str]] = None,
+    ) -> None:
         """Print the banner.
 
         This allows library vendors to override this behaviour, or choose to
@@ -341,8 +346,16 @@ def print_banner(banner: typing.Optional[str], allow_color: bool, force_color: b
 
         !!! note
             `force_color` will always take precedence over `allow_color`.
+        extra_args : typing.Optional[typing.Dict[builtins.str, builtins.str]]
+            If provided, extra $-substitutions to use when printing the banner.
+            Default substitutions can not be overwritten.
+
+        Raises
+        ------
+        builtins.ValueError
+            If `extra_args` contains a default $-substitution.
         """
-        ux.print_banner(banner, allow_color, force_color)
+        ux.print_banner(banner, allow_color, force_color, extra_args=extra_args)
 
     async def close(self) -> None:
         if not self._close_event:
diff --git a/hikari/internal/ux.py b/hikari/internal/ux.py
index e3a30690..b3a9f695 100644
--- a/hikari/internal/ux.py
+++ b/hikari/internal/ux.py
@@ -148,7 +148,12 @@ def init_logging(
 """Set of env variables which always indicate that ANSI flags should be included."""
 
 
-def print_banner(package: typing.Optional[str], allow_color: bool, force_color: bool) -> None:
+def print_banner(
+    package: typing.Optional[str],
+    allow_color: bool,
+    force_color: bool,
+    extra_args: typing.Optional[typing.Dict[str, str]] = None,
+) -> None:
     """Print a banner of choice to `sys.stdout`.
 
     Inspired by Spring Boot, we display an ASCII logo on startup. This is styled
@@ -172,6 +177,14 @@ def print_banner(package: typing.Optional[str], allow_color: bool, force_color:
         If `builtins.True`, return `builtins.True` always, otherwise only
         return `builtins.True` if the device supports colour output and the
         `allow_color` flag is not `builtins.False`.
+    extra_args : typing.Optional[typing.Dict[builtins.str, builtins.str]]
+        If provided, extra $-substitutions to use when printing the banner.
+        Default substitutions can not be overwritten.
+
+    Raises
+    ------
+    builtins.ValueError
+        If `extra_args` contains a default $-substitution.
     """
     if package is None:
         return
@@ -198,6 +211,12 @@ def print_banner(package: typing.Optional[str], allow_color: bool, force_color:
         "system_description": " ".join(filtered_system_bits),
     }
 
+    if extra_args:
+        for key in extra_args:
+            if key in args:
+                raise ValueError(f"Cannot overwrite $-substitution `{key}`. Please use a different key.")
+        args.update(extra_args)
+
     if supports_color(allow_color, force_color):
         args.update(colorlog.escape_codes.escape_codes)
     else:


Test patch:
diff --git a/tests/hikari/impl/test_bot.py b/tests/hikari/impl/test_bot.py
index 8d1cc85e..5e05c0c7 100644
--- a/tests/hikari/impl/test_bot.py
+++ b/tests/hikari/impl/test_bot.py
@@ -528,9 +528,9 @@ def test_listen(self, bot, event_manager):
 
     def test_print_banner(self, bot):
         with mock.patch.object(ux, "print_banner") as print_banner:
-            bot.print_banner("testing", False, True)
+            bot.print_banner("testing", False, True, extra_args={"test_key": "test_value"})
 
-        print_banner.assert_called_once_with("testing", False, True)
+        print_banner.assert_called_once_with("testing", False, True, extra_args={"test_key": "test_value"})
 
     def test_run_when_already_running(self, bot):
         bot._is_alive = True
diff --git a/tests/hikari/impl/test_rest_bot.py b/tests/hikari/impl/test_rest_bot.py
index b825389f..da8e0fb5 100644
--- a/tests/hikari/impl/test_rest_bot.py
+++ b/tests/hikari/impl/test_rest_bot.py
@@ -232,9 +232,9 @@ def test_is_alive_property(self, mock_rest_bot, close_event, expected):
 
     def test_print_banner(self, mock_rest_bot):
         with mock.patch.object(ux, "print_banner") as print_banner:
-            mock_rest_bot.print_banner("okokok", True, False)
+            mock_rest_bot.print_banner("okokok", True, False, {"test_key": "test_value"})
 
-            print_banner.assert_called_once_with("okokok", True, False)
+            print_banner.assert_called_once_with("okokok", True, False, extra_args={"test_key": "test_value"})
 
     @pytest.mark.asyncio()
     async def test_close(self, mock_rest_bot, mock_interaction_server, mock_rest_client):
diff --git a/tests/hikari/internal/test_ux.py b/tests/hikari/internal/test_ux.py
index f3ff235a..401bebeb 100644
--- a/tests/hikari/internal/test_ux.py
+++ b/tests/hikari/internal/test_ux.py
@@ -250,6 +250,61 @@ def test_when_doesnt_supports_color(self, mock_args):
         abspath.assert_called_once_with(dirname())
         supports_color.assert_called_once_with(True, False)
 
+    def test_use_extra_args(self, mock_args):
+        stack = contextlib.ExitStack()
+        stack.enter_context(mock.patch.object(colorlog.escape_codes, "escape_codes", new={}))
+        read_text = stack.enter_context(mock.patch.object(importlib.resources, "read_text"))
+        template = stack.enter_context(mock.patch.object(string, "Template"))
+        write = stack.enter_context(mock.patch.object(sys.stdout, "write"))
+        stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path"))
+
+        extra_args = {
+            "extra_argument_1": "one",
+            "extra_argument_2": "two",
+        }
+
+        with stack:
+            ux.print_banner("hikari", True, False, extra_args=extra_args)
+
+        args = {
+            # Hikari stuff.
+            "hikari_version": "2.2.2",
+            "hikari_git_sha1": "12345678",
+            "hikari_copyright": "© 2020 Nekokatt",
+            "hikari_license": "MIT",
+            "hikari_install_location": "some path",
+            "hikari_documentation_url": "https://nekokatt.github.io/hikari/docs",
+            "hikari_discord_invite": "https://discord.gg/Jx4cNGG",
+            "hikari_source_url": "https://nekokatt.github.io/hikari",
+            "python_implementation": "CPython",
+            "python_version": "4.0.0",
+            "system_description": "Machine Potato 1.0.0",
+        }
+
+        args.update(extra_args)
+
+        template.assert_called_once_with(read_text())
+        template().safe_substitute.assert_called_once_with(args)
+        write.assert_called_once_with(template().safe_substitute())
+
+    def test_overwrite_args_raises_error(self, mock_args):
+        stack = contextlib.ExitStack()
+        stack.enter_context(mock.patch.object(colorlog.escape_codes, "escape_codes", new={}))
+        stack.enter_context(mock.patch.object(importlib.resources, "read_text"))
+        stack.enter_context(mock.patch.object(string, "Template"))
+        stack.enter_context(mock.patch.object(sys.stdout, "write"))
+        stack.enter_context(mock.patch.object(os.path, "abspath", return_value="some path"))
+
+        extra_args = {
+            "hikari_version": "overwrite",
+        }
+
+        with stack:
+            with pytest.raises(
+                ValueError, match=r"Cannot overwrite \$-substitution `hikari_version`. Please use a different key."
+            ):
+                ux.print_banner("hikari", True, False, extra_args=extra_args)
+
 
 class TestSupportsColor:
     def test_when_not_allow_color(self):