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

allow configuration in the home directory #899

Merged
merged 13 commits into from
Oct 10, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 0.10.3dev

* [Feature] Allow user-level config using ~/.jupysql/config (#880)
* [Fix] Remove force deleted snippets from dependent snippet's `with` (#717)
* [Fix] Comments added in SQL query to be stripped before saved as snippet (#886)

Expand Down
10 changes: 7 additions & 3 deletions doc/api/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -317,17 +317,21 @@ res = %sql SELECT * FROM languages LIMIT 2
print(res)
```

## Loading from `pyproject.toml`
## Loading from a file

```{versionadded} 0.9
```

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:
```{versionchanged} 0.10.3
Look for `~/.jupysql/config` if `pyproject.toml` doesn't exist.
```

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, jupysql then looks for configurations in `~/.jupysql/config`. If no configuration file is found, default values will be used. A sample configuration file could look like this:

```
[tool.jupysql.SqlMagic]
feedback = true
autopandas = true
```

Note that `pyproject.toml` is only for setting configurations. To store connection details, please use [`connections.ini`](../user-guide/connection-file.md) file.
Note that these files are only for setting configurations. To store connection details, please use [`connections.ini`](../user-guide/connection-file.md) file.
2 changes: 1 addition & 1 deletion doc/user-guide/connection-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ However, you can change this:
```

```{tip}
For configuration settings other than connections, you can use a [`pyproject.toml`](../api/configuration.md#loading-from-pyprojecttoml) file.
For configuration settings other than connections, you can use a [`pyproject.toml` or `~/.jupysql/config`](../api/configuration.md#loading-from-a-file) file.
```

The `.ini` format defines sections and you can define key-value pairs within each section. For example:
Expand Down
2 changes: 1 addition & 1 deletion src/sql/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ 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
# raise it when there is an error in parsing the configuration file
ConfigurationError = exception_factory("ConfigurationError")


Expand Down
9 changes: 7 additions & 2 deletions src/sql/magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -664,8 +664,12 @@ def set_configs(ip, file_path):


def load_SqlMagic_configs(ip):
"""Loads saved SqlMagic configs in pyproject.toml"""
"""Loads saved SqlMagic configs in pyproject.toml or ~/.jupysql/config"""
file_path = util.find_path_from_root("pyproject.toml")
if not file_path:
alternate_path = Path("~/.jupysql/config").expanduser()
if alternate_path.exists():
file_path = str(alternate_path)
if file_path:
try:
table_rows = set_configs(ip, file_path)
Expand All @@ -680,7 +684,8 @@ def load_SqlMagic_configs(ip):
if type(e).__name__ == "ModuleNotFoundError":
display.message(
"The 'toml' package isn't installed. To load settings from "
"the pyproject.toml file, install with: pip install toml"
"pyproject.toml or ~/.jupysql/config, install with: "
"pip install toml"
)
return
else:
Expand Down
2 changes: 1 addition & 1 deletion src/sql/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
SINGLE_QUOTE = "'"
DOUBLE_QUOTE = '"'

CONFIGURATION_DOCS_STR = "https://jupysql.ploomber.io/en/latest/api/configuration.html#loading-from-pyproject-toml" # noqa
CONFIGURATION_DOCS_STR = "https://jupysql.ploomber.io/en/latest/api/configuration.html#loading-from-a-file" # noqa


def sanitize_identifier(identifier):
Expand Down
111 changes: 105 additions & 6 deletions src/tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,44 @@ def test_start_ini_default_connection_if_any(tmp_empty, ip_no_magics):
assert ConnectionManager.current.dialect == "sqlite"


def test_start_ini_default_connection_using_pyproject_if_any(tmp_empty, ip_no_magics):
def test_load_home_toml_if_no_pyproject_toml(
tmp_empty, ip_no_magics, capsys, monkeypatch
):
monkeypatch.setattr(
Path, "expanduser", lambda path: Path(str(path).replace("~", tmp_empty))
)
home_toml = Path("~/.jupysql/config").expanduser()
home_toml.parent.mkdir(exist_ok=True)
home_toml.write_text(
"""
[tool.jupysql.SqlMagic]
autocommit = false
autolimit = 1
style = "RANDOM"
"""
)

expect = [
"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"}

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()
assert all(re.search(substring, out) for substring in expect)
assert get_current_configs(magic) == combined


def test_start_ini_default_connection_using_toml_if_any(tmp_empty, ip_no_magics):
Path("pyproject.toml").write_text(
"""
[tool.jupysql.SqlMagic]
Expand Down Expand Up @@ -130,11 +167,13 @@ def test_magic_initialization_when_default_connection_fails(
assert "Cannot start default connection" in captured.out


def test_magic_initialization_with_no_pyproject(tmp_empty, ip_no_magics):
def test_magic_initialization_with_no_toml(tmp_empty, ip_no_magics):
load_ipython_extension(ip_no_magics)


def test_magic_initialization_with_corrupted_pyproject(tmp_empty, ip_no_magics, capsys):
def test_magic_initialization_with_corrupted_pyproject_toml(
tmp_empty, ip_no_magics, capsys
):
Path("pyproject.toml").write_text(
"""
[tool.jupysql.SqlMagic]
Expand All @@ -148,6 +187,27 @@ def test_magic_initialization_with_corrupted_pyproject(tmp_empty, ip_no_magics,
assert "Could not load configuration file" in captured.out


def test_magic_initialization_with_corrupted_home_toml(
tmp_empty, ip_no_magics, capsys, monkeypatch
):
monkeypatch.setattr(
Path, "expanduser", lambda path: Path(str(path).replace("~", tmp_empty))
)
home_toml = Path("~/.jupysql/config").expanduser()
home_toml.parent.mkdir(exist_ok=True)
home_toml.write_text(
"""
[tool.jupysql.SqlMagic]
dsn_filename = myconnections.ini
"""
)

load_ipython_extension(ip_no_magics)

captured = capsys.readouterr()
assert "Could not load configuration file" in captured.out


def test_loading_valid_pyproject_toml_shows_feedback_and_modifies_config(
tmp_empty, ip_no_magics, capsys
):
Expand Down Expand Up @@ -184,6 +244,45 @@ def test_loading_valid_pyproject_toml_shows_feedback_and_modifies_config(
assert get_current_configs(magic) == combined


def test_loading_valid_home_toml_shows_feedback_and_modifies_config(
tmp_empty, ip_no_magics, capsys, monkeypatch
):
monkeypatch.setattr(
Path, "expanduser", lambda path: Path(str(path).replace("~", tmp_empty))
)
home_toml = Path("~/.jupysql/config").expanduser()
home_toml.parent.mkdir(exist_ok=True)
home_toml.write_text(
"""
[tool.jupysql.SqlMagic]
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"}

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(str(home_toml)))
assert all(re.search(substring, out) for substring in expect)
assert get_current_configs(magic) == combined


@pytest.mark.parametrize(
"file_content, param",
[
Expand All @@ -197,7 +296,7 @@ def test_loading_valid_pyproject_toml_shows_feedback_and_modifies_config(
],
ids=["empty_sqlmagic_key", "missing_sqlmagic_key"],
)
def test_loading_pyproject_toml_display_configuration_docs_link(
def test_loading_toml_display_configuration_docs_link(
tmp_empty, ip_no_magics, file_content, param, monkeypatch
):
Path("pyproject.toml").write_text(file_content)
Expand Down Expand Up @@ -239,7 +338,7 @@ def test_loading_pyproject_toml_display_configuration_docs_link(
),
],
)
def test_load_pyproject_toml_user_configurations_not_specified(
def test_load_toml_user_configurations_not_specified(
tmp_empty, ip_no_magics, capsys, file_content
):
Path("pyproject.toml").write_text(file_content)
Expand Down Expand Up @@ -359,6 +458,6 @@ def test_toml_optional_message(tmp_empty, monkeypatch, ip, capsys):
out, _ = capsys.readouterr()
assert (
"The 'toml' package isn't installed. "
"To load settings from the pyproject.toml file, "
"To load settings from pyproject.toml or ~/.jupysql/config, "
"install with: pip install toml"
) in out