Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

autoloading_settings #726

Merged
merged 7 commits into from
Jul 27, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# CHANGELOG

## 0.8.1dev
* [Feature] Allow loading configuration value from a `pyproject.toml` file upon magic initialization (#689)
* [Fix] Fix error that was incorrectly converted into a print message

* [Fix] Fixed vertical color breaks in histograms (#702)
Expand Down
10 changes: 10 additions & 0 deletions doc/api/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,3 +241,13 @@ print(res)
res = %sql SELECT * FROM languages LIMIT 2
print(res)
```

## Loading configuration settings

You can define configurations in a `pyproject.toml` file and automatically load the configurations when you run `%load_ext sql`. If the file is not found in the current or parent directories, default values will be used. A sample `pyproject.toml` could look like this:

```
[tool.jupysql.SqlMagic]
feedback = true
autopandas = true
```
12 changes: 11 additions & 1 deletion src/sql/display.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import html

from prettytable import PrettyTable
from IPython.display import display
from IPython.display import display, HTML


class Table:
Expand Down Expand Up @@ -90,3 +90,13 @@ def message(message):
def message_success(message):
"""Display a success message"""
display(Message(message, style="color: green"))


def message_html(message):
"""Display a message as HTML"""
display(HTML(str(Message(message))))


def table(headers, rows):
"""Display a table"""
display(Table(headers, rows))
3 changes: 3 additions & 0 deletions src/sql/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,6 @@ def _error(message):

# raised internally when the user chooses a table that doesn't exist
TableNotFoundError = exception_factory("TableNotFoundError")

# raise it when there is an error in parsing pyproject.toml file
ConfigurationError = exception_factory("ConfigurationError")
37 changes: 36 additions & 1 deletion src/sql/magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
from sql.magic_plot import SqlPlotMagic
from sql.magic_cmd import SqlCmdMagic
from sql._patch import patch_ipython_usage_error
from sql import query_util
from sql import query_util, util
from sql.util import get_suggestions_message, pretty_print
from sql.exceptions import RuntimeError
from sql.error_message import detail
Expand Down Expand Up @@ -621,6 +621,39 @@ def _persist_dataframe(
display.message_success(f"Success! Persisted {table_name} to the database.")


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"])
default_configs = util.get_default_configs(sql)
table_rows = []
for config, value in user_configs.items():
if config in default_configs.keys():
default_type = type(default_configs[config])
if isinstance(value, default_type):
setattr(sql, config, value)
table_rows.append([config, value])
else:
display.message(
f"'{value}' is an invalid value for '{config}'. "
f"Please use {default_type.__name__} value instead."
)
else:
util.find_close_match_config(config, default_configs.keys())

return table_rows


def load_SqlMagic_configs(ip):
"""Loads saved SqlMagic configs in pyproject.toml"""
file_path = util.find_path_from_root("pyproject.toml")
if file_path:
table_rows = set_configs(ip, file_path)
if table_rows:
display.message("Settings changed:")
display.table(["Config", "value"], table_rows)


def load_ipython_extension(ip):
"""Load the extension in IPython."""

Expand All @@ -635,3 +668,5 @@ def load_ipython_extension(ip):
ip.register_magics(SqlCmdMagic)

patch_ipython_usage_error(ip)

load_SqlMagic_configs(ip)
129 changes: 129 additions & 0 deletions src/sql/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@
from sql.store import store, _get_dependents_for_key
from sql import exceptions, display
import json
from pathlib import Path
from ploomber_core.dependencies import requires

try:
import toml
except ModuleNotFoundError:
toml = None

SINGLE_QUOTE = "'"
DOUBLE_QUOTE = '"'
Expand Down Expand Up @@ -366,3 +373,125 @@ def show_deprecation_warning():
"raise an exception in the next major release so please remove it.",
FutureWarning,
)


def find_path_from_root(file_name):
"""
Recursively finds an absolute path to file_name starting
from current to root directory
"""
current = Path().resolve()
while not (current / file_name).exists():
if current == current.parent:
return None

current = current.parent
display.message(f"Found {file_name} from '{current}'")

return str(Path(current, file_name))


def find_close_match_config(word, possibilities, n=3):
"""Finds closest matching configurations and displays message"""
closest_matches = difflib.get_close_matches(word, possibilities, n=n)
if not closest_matches:
display.message_html(
f"'{word}' is an invalid configuration. Please review our "
"<a href='https://jupysql.ploomber.io/en/latest/api/configuration.html#options'>" # noqa
"configuration guideline</a>."
)
else:
display.message(
f"'{word}' is an invalid configuration. Did you mean "
f"{pretty_print(closest_matches, last_delimiter='or')}?"
)


def get_line_content_from_toml(file_path, line_number):
"""
Locates a line that error occurs when loading a toml file
and returns the line, key, and value
"""
with open(file_path, "r") as file:
lines = file.readlines()
eline = lines[line_number - 1].strip()
ekey, evalue = None, None
if "=" in eline:
ekey, evalue = map(str.strip, eline.split("="))
return eline, ekey, evalue


@requires(["toml"])
def load_toml(file_path):
"""
Returns toml file content in a dictionary format
and raises error if it fails to load the toml file
"""
try:
with open(file_path, "r") as file:
content = file.read()
return toml.loads(content)
except toml.TomlDecodeError as e:
raise parse_toml_error(e, 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}'")
elif "Only all lowercase booleans" in str(e):
return exceptions.ConfigurationError(
f"Invalid value '{evalue}' in '{eline}'. "
"Valid boolean values: true, false"
)
elif "invalid literal for int()" in str(e):
return exceptions.ConfigurationError(
f"Invalid value '{evalue}' in '{eline}'. "
"To use str value, enclose it with ' or \"."
)
else:
return e


def get_user_configs(file_path, section_names):
"""
Returns saved configuration settings in a toml file from given file_path

Parameters
----------
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
-------
dict
saved configuration settings
"""
data = load_toml(file_path)
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:
return {}
else:
raise exceptions.ConfigurationError(
f"{pretty_print(close_match)} is an invalid section name. "
f"Did you mean '{section_to_find}'?"
)
data = data[section_to_find]
return data


def get_default_configs(sql):
"""
Returns a dictionary of SqlMagic configuration settings users can set
with their default values.
"""
default_configs = sql.trait_defaults()
del default_configs["parent"]
del default_configs["config"]
return default_configs
Loading