Repository: ploomber/jupysql (python)

Issue:
improve "found pyproject.toml..." message
when starting the extension (%load_ext sql), a message is displayed to show the location of the pyproject.toml file. However, this message is always displayed even if the `[tool.jupysql]` isn't defined.

context: https://github.com/ploomber/jupysql/pull/770#issuecomment-1657180203

I think we could improve this message

- if pyproject.toml is defined but there is no `[tool.jupysql]` section, then display a link to the docs so users know how to use it
- if `[tool.jupysql]` exists but it's empty, then mention that the section is empty and display a link to the docs




Pull request:
improve pyproject.toml load message
## Describe your changes


## Issue number

Closes #771 

Acceptance criteria:
1. if pyproject.toml is defined but there is no [tool.jupysql] section, then display a link to the docs so users know how to use it
2. if [tool.jupysql] exists but it's empty, then mention that the section is empty and display a link to the docs

## Checklist before requesting a review

- [ ] Performed a self-review of my code
- [ ] Formatted my code with [`pkgmt format`](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#linting-formatting)
- [ ] Added [tests](https://ploomber-contributing.readthedocs.io/en/latest/contributing/pr.html#testing) (when necessary).
- [ ] 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--823.org.readthedocs.build/en/823/

<!-- readthedocs-preview jupysql end -->

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b44d24..6738e0d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
 
 ## 0.10.2dev
 
+* [Feature] Improved messages when loading configurations from `pyproject.toml` file.
+
 ## 0.10.1 (2023-08-30)
 
 * [Feature] Automatically connect if the `dsn_filename` (defaults to `~/.jupysql/connections.ini`) contains a `default` section
@@ -431,4 +433,4 @@ Converted from an IPython Plugin to an Extension for 1.0 compatibility
 
 *Release date: 21-Mar-2013*
 
-* Initial release
+* Initial release
\ No newline at end of file
diff --git a/src/sql/display.py b/src/sql/display.py
index ae0df3d..8837171 100644
--- a/src/sql/display.py
+++ b/src/sql/display.py
@@ -94,7 +94,7 @@ def message_success(message):
 
 
 def message_warning(message):
-    """Display a success message"""
+    """Display a warning message"""
     display(
         Message(
             message,
diff --git a/src/sql/magic.py b/src/sql/magic.py
index 4734ed9..557b3ff 100644
--- a/src/sql/magic.py
+++ b/src/sql/magic.py
@@ -647,7 +647,7 @@ class SqlMagic(Magics, Configurable):
 def set_configs(ip, file_path):
     """Set user defined SqlMagic configuration settings"""
     sql = ip.find_cell_magic("sql").__self__
-    user_configs = util.get_user_configs(file_path, ["tool", "jupysql", "SqlMagic"])
+    user_configs = util.get_user_configs(file_path)
     default_configs = util.get_default_configs(sql)
     table_rows = []
     for config, value in user_configs.items():
diff --git a/src/sql/util.py b/src/sql/util.py
index 6ea59fc..7cdbfcf 100644
--- a/src/sql/util.py
+++ b/src/sql/util.py
@@ -18,6 +18,8 @@ except ModuleNotFoundError:
 SINGLE_QUOTE = "'"
 DOUBLE_QUOTE = '"'
 
+CONFIGURATION_DOCS_STR = "https://jupysql.ploomber.io/en/latest/api/configuration.html#loading-from-pyproject-toml"  # noqa
+
 
 def sanitize_identifier(identifier):
     if (identifier[0] == SINGLE_QUOTE and identifier[-1] == SINGLE_QUOTE) or (
@@ -164,7 +166,6 @@ def find_path_from_root(file_name):
             return None
 
         current = current.parent
-    display.message(f"Found {file_name} from '{current}'")
 
     return str(Path(current, file_name))
 
@@ -230,22 +231,24 @@ def load_toml(file_path):
 def parse_toml_error(e, file_path):
     eline, ekey, evalue = get_line_content_from_toml(file_path, e.lineno)
     if "Duplicate keys!" in str(e):
-        return exceptions.ConfigurationError(f"Duplicate key found : '{ekey}'")
+        return exceptions.ConfigurationError(
+            f"Duplicate key found: '{ekey}' in {file_path}"
+        )
     elif "Only all lowercase booleans" in str(e):
         return exceptions.ConfigurationError(
-            f"Invalid value '{evalue}' in '{eline}'. "
+            f"Invalid value '{evalue}' in '{eline}' in {file_path}. "
             "Valid boolean values: true, false"
         )
     elif "invalid literal for int()" in str(e):
         return exceptions.ConfigurationError(
-            f"Invalid value '{evalue}' in '{eline}'. "
+            f"Invalid value '{evalue}' in '{eline}' in {file_path}. "
             "To use str value, enclose it with ' or \"."
         )
     else:
         return e
 
 
-def get_user_configs(file_path, section_names):
+def get_user_configs(file_path):
     """
     Returns saved configuration settings in a toml file from given file_path
 
@@ -253,9 +256,6 @@ def get_user_configs(file_path, section_names):
     ----------
     file_path : str
         file path to a toml file
-    section_names : list
-        section names that contains the configuration settings
-        (e.g., ["tool", "jupysql", "SqlMagic"])
 
     Returns
     -------
@@ -263,18 +263,40 @@ def get_user_configs(file_path, section_names):
         saved configuration settings
     """
     data = load_toml(file_path)
+    section_names = ["tool", "jupysql", "SqlMagic"]
     while section_names:
         section_to_find, sections_from_user = section_names.pop(0), data.keys()
         if section_to_find not in sections_from_user:
             close_match = difflib.get_close_matches(section_to_find, sections_from_user)
             if not close_match:
+                MESSAGE_PREFIX = (
+                    f"Tip: You may define configurations in "
+                    f"{file_path}. Please review our "
+                )
+                display.message_html(
+                    f"{MESSAGE_PREFIX}<a href='{CONFIGURATION_DOCS_STR}'>"
+                    "configuration guideline</a>."
+                )
                 return {}
             else:
                 raise exceptions.ConfigurationError(
-                    f"{pretty_print(close_match)} is an invalid section name. "
+                    f"{pretty_print(close_match)} is an invalid section "
+                    f"name in {file_path}. "
                     f"Did you mean '{section_to_find}'?"
                 )
         data = data[section_to_find]
+    if not data:
+        if section_to_find == "SqlMagic":
+            MESSAGE_PREFIX = (
+                f"[tool.jupysql.SqlMagic] present in {file_path} but empty. "
+                f"Please review our "
+            )
+            display.message_html(
+                f"{MESSAGE_PREFIX}<a href='{CONFIGURATION_DOCS_STR}'>"
+                "configuration guideline</a>."
+            )
+    else:
+        display.message(f"Loading configurations from {file_path}")
     return data
 
 


Test patch:
diff --git a/src/tests/test_config.py b/src/tests/test_config.py
index 52312bb..ac07227 100644
--- a/src/tests/test_config.py
+++ b/src/tests/test_config.py
@@ -4,10 +4,12 @@ from pathlib import Path
 
 import pytest
 import sys
+from unittest.mock import Mock
 
 from sql.magic import load_ipython_extension
 from sql.connection import ConnectionManager
-from sql.util import get_default_configs
+from sql.util import get_default_configs, CONFIGURATION_DOCS_STR
+from sql import display
 from IPython.core.error import UsageError
 
 
@@ -146,87 +148,107 @@ dsn_filename = myconnections.ini
     assert "Could not load configuration file" in captured.out
 
 
-@pytest.mark.parametrize(
-    "file_content, expect, config_expected",
-    [
-        (
-            """
+def test_loading_valid_pyproject_toml_shows_feedback_and_modifies_config(
+    tmp_empty, ip_no_magics, capsys
+):
+    Path("pyproject.toml").write_text(
+        """
 [tool.jupysql.SqlMagic]
 autocommit = false
 autolimit = 1
 style = "RANDOM"
-""",
-            [
-                "Found pyproject.toml from '%s'",
-                "Settings changed:",
-                r"autocommit\s*\|\s*False",
-                r"autolimit\s*\|\s*1",
-                r"style\s*\|\s*RANDOM",
-            ],
-            {"autocommit": False, "autolimit": 1, "style": "RANDOM"},
-        ),
+"""
+    )
+
+    expect = [
+        "Loading configurations from {path}",
+        "Settings changed:",
+        r"autocommit\s*\|\s*False",
+        r"autolimit\s*\|\s*1",
+        r"style\s*\|\s*RANDOM",
+    ]
+
+    config_expected = {"autocommit": False, "autolimit": 1, "style": "RANDOM"}
+
+    toml_path = str(Path(os.getcwd()).joinpath("pyproject.toml"))
+
+    os.mkdir("sub")
+    os.chdir("sub")
+
+    load_ipython_extension(ip_no_magics)
+    magic = ip_no_magics.find_magic("sql").__self__
+    combined = {**get_default_testing_configs(magic), **config_expected}
+    out, _ = capsys.readouterr()
+    expect[0] = expect[0].format(path=re.escape(toml_path))
+    assert all(re.search(substring, out) for substring in expect)
+    assert get_current_configs(magic) == combined
+
+
+@pytest.mark.parametrize(
+    "file_content, param",
+    [
         (
             """
 [tool.jupysql.SqlMagic]
 """,
-            ["Found pyproject.toml from '%s'"],
-            {},
+            "[tool.jupysql.SqlMagic] present in {path} but empty.",
         ),
+        ("", "Tip: You may define configurations in {path}."),
+    ],
+    ids=["empty_sqlmagic_key", "missing_sqlmagic_key"],
+)
+def test_loading_pyproject_toml_display_configuration_docs_link(
+    tmp_empty, ip_no_magics, file_content, param, monkeypatch
+):
+    Path("pyproject.toml").write_text(file_content)
+    toml_path = str(Path(os.getcwd()).joinpath("pyproject.toml"))
+
+    os.mkdir("sub")
+    os.chdir("sub")
+    mock = Mock()
+    monkeypatch.setattr(display, "message_html", mock)
+
+    load_ipython_extension(ip_no_magics)
+    param = (
+        f"{param.format(path=toml_path)} Please review our "
+        f"<a href='{CONFIGURATION_DOCS_STR}'>configuration guideline</a>."
+    )
+    mock.assert_called_once_with(param)
+
+
+@pytest.mark.parametrize(
+    "file_content",
+    [
         (
             """
 [test]
 github = "ploomber/jupysql"
-""",
-            ["Found pyproject.toml from '%s'"],
-            {},
+"""
         ),
         (
             """
 [tool.pkgmt]
 github = "ploomber/jupysql"
-""",
-            ["Found pyproject.toml from '%s'"],
-            {},
+"""
         ),
         (
             """
 [tool.jupysql.test]
 github = "ploomber/jupysql"
-""",
-            ["Found pyproject.toml from '%s'"],
-            {},
-        ),
-        (
-            "",
-            ["Found pyproject.toml from '%s'"],
-            {},
+"""
         ),
     ],
 )
-def test_loading_valid_pyproject_toml_shows_feedback_and_modifies_config(
-    tmp_empty,
-    ip_no_magics,
-    capsys,
-    file_content,
-    expect,
-    config_expected,
+def test_load_pyproject_toml_user_configurations_not_specified(
+    tmp_empty, ip_no_magics, capsys, file_content
 ):
     Path("pyproject.toml").write_text(file_content)
-    toml_dir = os.getcwd()
-
     os.mkdir("sub")
     os.chdir("sub")
 
     load_ipython_extension(ip_no_magics)
-
-    magic = ip_no_magics.find_magic("sql").__self__
-
-    combined = {**get_default_testing_configs(magic), **config_expected}
     out, _ = capsys.readouterr()
-
-    expect[0] = expect[0] % (re.escape(toml_dir))
-    assert all(re.search(substring, out) for substring in expect)
-    assert get_current_configs(magic) == combined
+    assert "Loading configurations from" not in out
 
 
 @pytest.mark.parametrize(
@@ -238,14 +260,14 @@ def test_loading_valid_pyproject_toml_shows_feedback_and_modifies_config(
 autocommit = true
 autocommit = true
 """,
-            "Duplicate key found : 'autocommit'",
+            "Duplicate key found: 'autocommit' in {path}",
         ),
         (
             """
 [tool.jupySql.SqlMagic]
 autocommit = true
 """,
-            "'jupySql' is an invalid section name. Did you mean 'jupysql'?",
+            "'jupySql' is an invalid section name in {path}. Did you mean 'jupysql'?",
         ),
         (
             """
@@ -253,7 +275,7 @@ autocommit = true
 autocommit = True
 """,
             (
-                "Invalid value 'True' in 'autocommit = True'. "
+                "Invalid value 'True' in 'autocommit = True' in {path}. "
                 "Valid boolean values: true, false"
             ),
         ),
@@ -263,7 +285,7 @@ autocommit = True
 autocommit = invalid
 """,
             (
-                "Invalid value 'invalid' in 'autocommit = invalid'. "
+                "Invalid value 'invalid' in 'autocommit = invalid' in {path}. "
                 "To use str value, enclose it with ' or \"."
             ),
         ),
@@ -273,8 +295,7 @@ def test_error_on_toml_parsing(
     tmp_empty, ip_no_magics, capsys, file_content, error_msg
 ):
     Path("pyproject.toml").write_text(file_content)
-    toml_dir = os.getcwd()
-    found_statement = "Found pyproject.toml from '%s'" % (toml_dir)
+    toml_path = str(Path(os.getcwd()).joinpath("pyproject.toml"))
     os.makedirs("sub")
     os.chdir("sub")
 
@@ -283,9 +304,8 @@ def test_error_on_toml_parsing(
 
     out, _ = capsys.readouterr()
 
-    assert out.strip() == found_statement
     assert excinfo.value.error_type == "ConfigurationError"
-    assert str(excinfo.value) == error_msg
+    assert str(excinfo.value) == error_msg.format(path=toml_path)
 
 
 def test_valid_and_invalid_configs(tmp_empty, ip_no_magics, capsys):
@@ -299,14 +319,14 @@ invalid = false
 displaycon = false
 """
     )
-    toml_dir = os.getcwd()
+    toml_path = str(Path(os.getcwd()).joinpath("pyproject.toml"))
     os.makedirs("sub")
     os.chdir("sub")
 
     load_ipython_extension(ip_no_magics)
     out, _ = capsys.readouterr()
     expect = [
-        "Found pyproject.toml from '%s'" % (re.escape(toml_dir)),
+        f"Loading configurations from {re.escape(toml_path)}",
         "'autocomm' is an invalid configuration. Did you mean 'autocommit'?",
         (
             "'autop' is an invalid configuration. "