Skip to content

Commit

Permalink
add support for pyproject.toml configuration and deprecated previous …
Browse files Browse the repository at this point in the history
…config
  • Loading branch information
Constantin Gahr authored and Constantin Gahr committed Sep 12, 2024
1 parent 99e6d7b commit e440229
Show file tree
Hide file tree
Showing 2 changed files with 70 additions and 199 deletions.
108 changes: 65 additions & 43 deletions src/latexplotlib/_config.py
Original file line number Diff line number Diff line change
@@ -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]

Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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)
161 changes: 5 additions & 156 deletions tests/test_10_config.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -150,41 +21,19 @@ 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

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)

Expand Down

0 comments on commit e440229

Please sign in to comment.