diff --git a/src/latexplotlib/_config.py b/src/latexplotlib/_config.py index c0d98f1..f17041c 100644 --- a/src/latexplotlib/_config.py +++ b/src/latexplotlib/_config.py @@ -1,10 +1,17 @@ import json +import sys +import warnings from contextlib import contextmanager from pathlib import Path -from typing import Dict, Iterator, Mapping, Tuple, Union +from typing import Dict, Iterator, Tuple, Union from appdirs import user_config_dir +if sys.version_info >= (3, 11): + import tomllib +else: + import tomli as tomllib + Number = Union[int, float] ConfigData = Union[Number, bool] @@ -15,60 +22,60 @@ CONFIGFILE: str = "config.ini" CONFIGDIR: Path = Path(user_config_dir(NAME)) CONFIGPATH: Path = CONFIGDIR / CONFIGFILE -DEFAULT_CONFIG: Dict[str, Number] = {"width": 630, "height": 412, _PURGED_OLD: False} - -class Config: - def __init__(self, path: Path) -> None: - self.path = path +DEFAULT_WIDTH = 630 +DEFAULT_HEIGHT = 412 - if not self.path.exists(): - self.reset() - self._config = self._open(path) +def find_pyproject_toml() -> Path: + pyproject = "pyproject.toml" + path = Path().absolute() + while path != Path("/"): + if (path / pyproject).exists(): + return path / pyproject + path = path.parent - def _open(self, path: Path) -> Dict[str, ConfigData]: - with path.open(encoding="utf-8") as fh: - config: Dict[str, ConfigData] = json.load(fh) - return config + msg = "Could not find 'pyproject.toml'" + raise FileNotFoundError(msg) - def _write(self, cfg: Mapping[str, ConfigData]) -> None: - if not self.path.parent.exists(): - self.path.parent.mkdir(parents=True) - with self.path.open("w", encoding="utf-8") as fh: - json.dump(cfg, fh, indent=4) +def find_config_ini() -> Path: + if CONFIGPATH.exists(): + return CONFIGPATH - def reset(self) -> None: - if self.path.exists(): - self.path.unlink() + msg = f"No such file: '{CONFIGPATH}'" + raise FileNotFoundError(msg) - self._write(DEFAULT_CONFIG) - def reload(self) -> None: - self._config = self._open(self.path) +class Size: + _width: Number + _height: Number - def __getitem__(self, name: str) -> ConfigData: - return self._config.get(name, DEFAULT_CONFIG[name]) + def __init__(self, width: Number, height: Number) -> None: + self._width, self._height = width, height - def __setitem__(self, name: str, value: ConfigData) -> None: - self._config[name] = value - self._write(self._config) + @classmethod + def from_pyproject_toml(cls, path: Path) -> "Size": # noqa: ANN102 + with path.open("rb") as fh: + cfg = tomllib.load(fh) + config = cfg["tool"].get("latexplotlib", {}) -config = Config(CONFIGPATH) + if config == {}: + return cls(DEFAULT_WIDTH, DEFAULT_HEIGHT) + return cls( + config.get("width", DEFAULT_WIDTH), config.get("height", DEFAULT_HEIGHT) + ) -class Size: - _width: Number - _height: Number - - def __init__(self) -> None: - self._width, self._height = config["width"], config["height"] + @classmethod + def from_config_ini(cls, path: Path) -> "Size": # noqa: ANN102 + with path.open(encoding="utf-8") as fh: + config: Dict[str, Number] = json.load(fh) - def reload(self) -> None: - config.reload() - self._width, self._height = config["width"], config["height"] + return cls( + config.get("width", DEFAULT_WIDTH), config.get("height", DEFAULT_HEIGHT) + ) def get(self) -> Tuple[Number, Number]: """Returns the current size of the figure in pts. @@ -95,7 +102,6 @@ def set(self, width: Number, height: Number) -> None: height : int The height of the latex page in pts. """ - config["width"], config["height"] = width, height self._width, self._height = width, height @contextmanager @@ -116,10 +122,26 @@ def context(self, width: Number, height: Number) -> Iterator[None]: self._width, self._height = _width, _height def __repr__(self) -> str: - return repr(f"{self._width}pt, {self._height}pt") + return repr(f"{self._width}pt, {self._height }pt") def __str__(self) -> str: - return str(f"{self._width}pt, {self._height}pt") + return str(f"{self._width}pt, {self._height }pt") -size = Size() +try: + path = find_pyproject_toml() + size = Size.from_pyproject_toml(path) +except FileNotFoundError: + try: + path = find_config_ini() + + msg = f""" + Configuring latexplotlib via {CONFIGPATH} is being deprecated. Please use + the [tool.latexplotlib] section of the 'pyproject.toml' file instead. + + To silence this warning, please delete the config file {CONFIGPATH} + """ + warnings.warn(msg, DeprecationWarning, stacklevel=2) + size = Size.from_config_ini(path) + except FileNotFoundError: + size = Size(DEFAULT_WIDTH, DEFAULT_HEIGHT) diff --git a/tests/test_10_config.py b/tests/test_10_config.py index 5ae8f39..ceb08b8 100644 --- a/tests/test_10_config.py +++ b/tests/test_10_config.py @@ -1,144 +1,15 @@ -import json - import pytest from latexplotlib import _config as cfg GOLDEN_RATIO = (5**0.5 + 1) / 2 - - CONFIGFILE = "config.ini" - NAME = "latexplotlib" def test_constants(): - assert cfg.CONFIGFILE - assert cfg.NAME - assert cfg.CONFIGDIR - assert cfg.CONFIGPATH - assert cfg.DEFAULT_CONFIG - - -class TestConfig: - @pytest.fixture - def default(self, monkeypatch): - default = {"apple": 10, "egg": 1, "skyscraper": "a"} - monkeypatch.setattr(cfg, "DEFAULT_CONFIG", default) - return default - - @pytest.fixture - def path(self, tmp_path, monkeypatch): - path = tmp_path / "directory" / "dir2" / "tmp.ini" - path.parent.mkdir(parents=True) - return path - - @pytest.fixture - def config(self, default, path): - with path.open("w", encoding="utf-8") as fh: - json.dump(default, fh) - - return cfg.Config(path) - - @pytest.fixture - def mock_open(self, default, mocker): - return mocker.patch("latexplotlib._config.Config._open", return_value=default) - - def test___init___path_exists(self, default, mock_open, mocker, path): - path.touch() - mocker.patch( - "latexplotlib._config.Config.reset", - side_effect=ValueError("Should not happen"), - ) - - config = cfg.Config(path) - - assert config.path == path - assert config._config == default - mock_open.assert_called_once_with(path) - - def test___init___path_not_exists(self, default, mock_open, mocker, path): - reset = mocker.patch( - "latexplotlib._config.Config.reset", side_effect=path.touch - ) - - config = cfg.Config(path) - - assert config.path == path - assert config._config == default - reset.assert_called_once() - mock_open.assert_called_once_with(path) - - def test__open(self, config, default, path): - config._config = None - assert config._open(path) == default - - def test__write(self, config, default, path): - config.path.unlink() - config._write(default) - - cfg = config._open(path) - assert cfg == default - - def test__write_no_parent(self, config, default, path): - config.path.unlink() - config.path.parent.rmdir() - - config._write(default) - - cfg = config._open(path) - assert cfg == default - - def test__write_no_parents_2(self, config, default, path): - config.path.unlink() - config.path.parent.rmdir() - config.path.parent.parent.rmdir() - - config._write(default) - - cfg = config._open(path) - assert cfg == default - - def test_reset_path_exists(self, config, default, mocker): - config._write(default) - assert config.path.exists() - - mocker.patch.object(config, "_write") - - config.reset() - - assert not config.path.exists() - config._write.assert_called_once_with(default) - - def test_reset_path_not_exists(self, config, default, mocker): - config.path.unlink() - assert not config.path.exists() - mocker.patch.object(config, "_write") - - config.reset() - - assert not config.path.exists() - config._write.assert_called_once_with(default) - - def test_reload(self, config, default, mock_open): - config._config = None - config.reload() - - assert config._config == default - mock_open.assert_called_once() - - def test___getitem__(self, config, default): - for key, item in default.items(): - assert config[key] == item - - def test___setitem__(self, config, default): - assert config["skyscraper"] != "apple" - config["skyscraper"] = "apple" - assert config["skyscraper"] == "apple" - - -def test_config_path(): - assert cfg.config.path == cfg.CONFIGPATH + assert cfg.DEFAULT_HEIGHT + assert cfg.DEFAULT_WIDTH class TestSize: @@ -150,20 +21,12 @@ def height(self): def width(self): return 10 - @pytest.fixture(autouse=True) - def _patch_config(self, height, width, monkeypatch, mocker): - d = {"width": width, "height": height} - config = mocker.MagicMock( - __getitem__=lambda _, v: d.__getitem__(v), reload=mocker.MagicMock() - ) - monkeypatch.setattr(cfg, "config", config) - @pytest.fixture - def size(self): - return cfg.Size() + def size(self, width, height): + return cfg.Size(width, height) def test___init__(self, width, height): - size = cfg.Size() + size = cfg.Size(width, height) assert size._width == width assert size._height == height @@ -171,20 +34,6 @@ def test___init__(self, width, height): def test_get(self, width, height, size): assert size.get() == (width, height) - def test_set(self, size): - size.set(43, 44) - assert size.get() == (43, 44) - - def test_reload(self, size): - cur = size.get() - - size.set(0, 0) - assert size.get() != cur - - size.reload() - cfg.config.reload.assert_called_once() - assert size.get() == cur - def test_context(self, size): assert size.get() == (10, 20)