Skip to content

Commit

Permalink
feat: add config home to config search path
Browse files Browse the repository at this point in the history
  • Loading branch information
tconbeer committed Feb 22, 2024
1 parent 2998b2a commit 8e06279
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 14 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,18 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

### Changes

- The default search path and priority for config files has changed, to better align with the standard defined by each operating system. Harlequin now loads config files from the following locations (and merges them, with items listed first taking priority):
1. The file located at the path provided by the `--config-path` CLI option.
2. Files named `harlequin.toml`, `.harlequin.toml`, or `pyproject.toml` in the current working directory.
3. Files named `harlequin.toml`, `.harlequin.toml`, or `config.toml` in the user's default config directory, in the `harlequin` subdirectory. For example:
- Linux: `$XDG_CONFIG_HOME/harlequin/config.toml` or `~/.config/harlequin/config.toml`
- Mac: `~/Library/Application Support/harlequin/config.toml`
- Windows: `~\AppData\Local\harlequin\config.toml`
4. Files named `harlequin.toml`, `.harlequin.toml`, or `pyproject.toml` in the user's home directory (`~`).
([#471](https://github.com/tconbeer/harlequin/issues/471))

### Features

- `harlequin --config` option now accepts the `--config-path` CLI option ([#466](https://github.com/tconbeer/harlequin/issues/466)).
Expand Down
39 changes: 26 additions & 13 deletions src/harlequin/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@
from pathlib import Path
from typing import Dict, List, Union

from platformdirs import user_config_path

from harlequin.exception import HarlequinConfigError

if sys.version_info < (3, 11):
import tomli as tomllib
else:
import tomllib

# these tuples define the search path; order matters: the latter items
# will have the highest priority and will override config in the
# former items
CONFIG_FILENAMES = ("pyproject.toml", ".harlequin.toml")
SEARCH_DIRS = (Path.home(), Path.cwd())

Profile = Dict[str, Union[bool, int, List[str], str, Path]]
Config = Dict[str, Union[str, Dict[str, Profile]]]
Expand Down Expand Up @@ -71,24 +68,40 @@ def sluggify_option_name(raw: str) -> str:
def _find_config_files(config_path: Path | None) -> list[Path]:
"""
Returns a list of candidate config file paths, to be read and
merged. Returns an empty list if none already exist.
merged. Returns an empty list if none already exist. Order matters:
the last item will have highest priority.
"""
found_files: list[Path] = []
if config_path is None:
for d in SEARCH_DIRS:
for p in [d / filename for filename in CONFIG_FILENAMES]:
if p.exists():
found_files.append(p)
elif config_path.exists():
for search in [_search_home, _search_config, _search_cwd]:
found_files.extend(search())
if config_path is not None and config_path.exists():
found_files.append(config_path)
else:
elif config_path is not None:
raise HarlequinConfigError(
f"Config file could not be found at specified path: {config_path}",
title="Harlequin couldn't load your config file.",
)
return found_files


def _search_cwd() -> list[Path]:
directory = Path.cwd()
filenames = ["pyproject.toml", ".harlequin.toml", "harlequin.toml"]
return [directory / f for f in filenames if (directory / f).exists()]


def _search_config() -> list[Path]:
directory = user_config_path(appname="harlequin", appauthor=False)
filenames = ["config.toml", ".harlequin.toml", "harlequin.toml"]
return [directory / f for f in filenames if (directory / f).exists()]


def _search_home() -> list[Path]:
directory = Path.home()
filenames = ["pyproject.toml", ".harlequin.toml", "harlequin.toml"]
return [directory / f for f in filenames if (directory / f).exists()]


def _read_config_file(path: Path) -> Config:
"""
Reads the relevant config section from a dedicated config file
Expand Down
43 changes: 42 additions & 1 deletion tests/unit_tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
from pathlib import Path

import pytest
from harlequin.config import get_config_for_profile, load_config
from harlequin.config import (
_find_config_files,
get_config_for_profile,
get_highest_priority_existing_config_file,
load_config,
)
from harlequin.exception import HarlequinConfigError


Expand Down Expand Up @@ -65,3 +70,39 @@ def test_bad_config_raises(
assert isinstance(err, HarlequinConfigError)
assert "config" in err.title
assert all([w in err.msg for w in key_words])


def test_config_file_discovery(
tmp_path_factory: pytest.TempPathFactory,
monkeypatch: pytest.MonkeyPatch,
) -> None:
# first, patch the real search paths with tmps
mock_home = tmp_path_factory.mktemp("home")
mock_config = tmp_path_factory.mktemp("config")
mock_cwd = tmp_path_factory.mktemp("cwd")
custom = tmp_path_factory.mktemp("custom") / "foo.toml"

# create empty config files in our mock dirs
expected_paths = [
mock_home / "pyproject.toml",
mock_home / ".harlequin.toml",
mock_config / "config.toml",
mock_config / "harlequin.toml",
mock_cwd / "pyproject.toml",
mock_cwd / ".harlequin.toml",
mock_cwd / "harlequin.toml",
custom,
]
for p in expected_paths:
p.parent.mkdir(parents=True, exist_ok=True)
p.open("w").close()

monkeypatch.setattr(Path, "cwd", lambda: mock_cwd)
monkeypatch.setattr(Path, "home", lambda: mock_home)
monkeypatch.setattr("harlequin.config.user_config_path", lambda **_: mock_config)

assert _find_config_files(config_path=custom) == expected_paths

expected_paths.pop()
assert _find_config_files(config_path=None) == expected_paths
assert get_highest_priority_existing_config_file() == expected_paths[-1]

0 comments on commit 8e06279

Please sign in to comment.