Repository: ploomber/jupysql (python)

Issue:
improvements to the `display.py` module
we added a `display.py` module to centralize the way to show feedback to the user because we have to support two frontends: Jupyter (which supports displaying HTML) and the terminal (only supports plain text)

the `Message` class allows creating an object that correctly displays its value in HTML if it's running in Jupyter or in plain text if running in the terminal, and it allows adding an optional CSS style (which is only visible in Jupyter).

The limitation happens when we want to display a link in the message. this happens when we want to point users to certain sections in our docs: if we add the HTML link to a Message instance, it'll display correctly in Jupyter but it won't in the terminal.

We can do something like this:

```python
# to keep backward compatibility
Message("some plain text message")

# new API
Message(["go to our", Link("home", "https://ploomber.io"), "page"])
```

then, internally, the plain text representation can resolve to:

```
go to our home (https://ploomber.io) page
```

and the HTML one to:

```
go to our <a href="[url](https://ploomber.io)">home</a> page
```






Pull request description:
improve display.py module
## Describe your changes

- Determine whether a code is run in a jupypter notebook or terminal and decide how to embed link in `Message`
- Expose link in feedback when it is shown in a terminal
- Allow `Message` to take a list as input

## Issue number

Closes #846 

## Checklist before requesting a review

- [x] Performed a self-review of my code
- [x] Formatted my code with [`pkgmt format`](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#linting-formatting)
- [x] Added [tests](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#testing) (when necessary).
- [x] Added [docstring](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#documenting-changes-and-new-features) documentation and update the [changelog](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#changelog) (when needed)



<!-- readthedocs-preview jupysql start -->
----
:books: Documentation preview :books:: https://jupysql--861.org.readthedocs.build/en/861/

<!-- readthedocs-preview jupysql end -->

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5d72697..d10bfc8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 * [Doc] Fixed typo in the `./doc/integrations/postgres-connect.ipynb` file (Line 180) (#845)
 * [Feature] Improved messages when loading configurations from `pyproject.toml` file.
 * [Feature] Add `--schema/-s` for `%sqlcmd` commands that support `--table/-t` and ensure `--table schema.table` works (#519)
+* [Feature] Expose link in feedback when it is shown in a terminal (#846)
 * [Feature] Show feedback when starting a new connection (#807)
 * [Doc] Add chDB integration tutorial
 * [Doc] Clarify the use of `pyproject.toml` and `connections.ini` in documentations (#850)
diff --git a/doc/community/developer-guide.md b/doc/community/developer-guide.md
index 41d4e0b..a9ca174 100644
--- a/doc/community/developer-guide.md
+++ b/doc/community/developer-guide.md
@@ -5,7 +5,7 @@ jupytext:
     extension: .md
     format_name: myst
     format_version: 0.13
-    jupytext_version: 1.14.5
+    jupytext_version: 1.15.1
 kernelspec:
   display_name: Python 3 (ipykernel)
   language: python
@@ -89,6 +89,20 @@ message("Some information")
 message_success("Some operation finished successfully!")
 ```
 
+You can use `message_html` to embed a link in a message and point users to certain sections in our docs. Here's an example:
+
+```{code-cell} ipython3
+from sql.display import message_html, Link
+```
+
+```{code-cell} ipython3
+message_html(["Go to our", Link("home", "https://ploomber.io"), "page"])
+```
+
+`message_html` will detect the running environment and display `Go to our home (https://ploomber.io) page` message instead if feedback is shown through a terminal.
+
++++
+
 ## Throwing errors
 
 When writing Python libraries, we often throw errors (and display error tracebacks) to let users know that something went wrong. However, JupySQL is an abstraction for executing SQL queries; hence, Python tracebacks a useless to end-users since they expose JupySQL's internals.
diff --git a/src/sql/display.py b/src/sql/display.py
index 8837171..12d4d41 100644
--- a/src/sql/display.py
+++ b/src/sql/display.py
@@ -5,6 +5,7 @@ import html
 
 from prettytable import PrettyTable
 from IPython.display import display, HTML
+from IPython import get_ipython
 
 
 class Table:
@@ -71,9 +72,12 @@ class Message:
     """Message for the user"""
 
     def __init__(self, message, style=None) -> None:
-        self._message = message
+        if isinstance(message, str):
+            self._message = message
+        elif isinstance(message, list):
+            self._message = " ".join([str(msg) for msg in message])
         # escape html and replace newlines with <br> tags so newlines are displayed
-        self._message_html = html.escape(message).replace("\n\n", "<br>")
+        self._message_html = html.escape(self._message).replace("\n\n", "<br>")
         self._style = "" or style
 
     def _repr_html_(self):
@@ -83,6 +87,20 @@ class Message:
         return self._message
 
 
+class Link:
+    """Formatting of link depending on the running environment"""
+
+    def __init__(self, text, url):
+        self.text = text
+        self.url = url
+
+    def __str__(self):
+        if get_ipython():
+            return f'<a href="{self.url}">{self.text}</a>'
+        else:
+            return f"{self.text} ({self.url})"
+
+
 def message(message):
     """Display a generic message"""
     display(Message(message))
@@ -104,8 +122,11 @@ def message_warning(message):
 
 
 def message_html(message):
-    """Display a message as HTML"""
-    display(HTML(str(Message(message))))
+    """Display a message with link"""
+    if get_ipython():
+        display(HTML(str(Message(message))))
+    else:
+        display(Message(message))
 
 
 def table(headers, rows):


Test patch:
diff --git a/src/tests/test_display.py b/src/tests/test_display.py
index 08651f6..2c52caa 100644
--- a/src/tests/test_display.py
+++ b/src/tests/test_display.py
@@ -1,4 +1,5 @@
 from sql import display
+from sql.display import Message, Link, message_html
 
 
 def test_html_escaping():
@@ -6,3 +7,15 @@ def test_html_escaping():
 
     assert "<>" in str(message)
     assert "&lt;&gt;" in message._repr_html_()
+
+
+def test_message_html_with_list_input(capsys):
+    message_html(["go to our", Link("home", "https://ploomber.io"), "page"])
+    out, _ = capsys.readouterr()
+    assert "go to our home (https://ploomber.io) page" in out
+
+
+def test_message_with_link_object():
+    assert "go to our home (https://ploomber.io) page" == str(
+        Message(["go to our", Link("home", "https://ploomber.io"), "page"])
+    )