Skip to content

Commit

Permalink
allow configuration in the home directory (#899)
Browse files Browse the repository at this point in the history
* #880 User-level config

* 880 changelog

* 880 works if .jupysql dir doesn't exist yet

* #890 debugging windows

* #892 fix link

* Revert "#892 fix link"

This reverts commit 3354a8e.

* #890

* #880 fix link

* Revert "#880 fix link"

This reverts commit 815b05b.

* #880 PR feedback: tests and docs

* #880 formatting

* #880 patch -> monkeypatch
  • Loading branch information
marshallwhiteorg authored Oct 10, 2023
1 parent 433178c commit c5f27df
Show file tree
Hide file tree
Showing 7 changed files with 123 additions and 14 deletions.
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)
* [Fix] Fixed bug passing :NUMBER while string slicing in query (#901)
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

0 comments on commit c5f27df

Please sign in to comment.