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

Prepare Loader and MemoryLoader for TOML support #3344

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/changelog/3343.misc.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Prepare ``Loader`` and ``MemoryLoader`` for TOML support - by :user:`ziima`
2 changes: 1 addition & 1 deletion src/tox/config/loader/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def found_keys(self) -> set[str]:
raise NotImplementedError

def __repr__(self) -> str:
return f"{type(self).__name__}"
return f"{self.__class__.__name__}(section={self._section.key}, overrides={self.overrides!r})"

def __contains__(self, item: str) -> bool:
return item in self.found_keys()
Expand Down
3 changes: 0 additions & 3 deletions src/tox/config/loader/ini/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,3 @@ def get_section(self, name: str) -> SectionProxy | None:
if self._parser.has_section(name):
return self._parser[name]
return None

def __repr__(self) -> str:
return f"{self.__class__.__name__}(section={self._section.key}, overrides={self.overrides!r})"
19 changes: 14 additions & 5 deletions src/tox/config/loader/memory.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
from __future__ import annotations

from pathlib import Path
from typing import TYPE_CHECKING, Any, Iterator
from typing import TYPE_CHECKING, Any, Iterator, Sequence

from tox.config.types import Command, EnvList

from .api import Loader
from .api import Loader, Override
from .section import Section
from .str_convert import StrConvert

Expand All @@ -14,9 +14,16 @@


class MemoryLoader(Loader[Any]):
def __init__(self, **kwargs: Any) -> None:
super().__init__(Section(prefix="<memory>", name=str(id(self))), [])
self.raw: dict[str, Any] = {**kwargs}
def __init__(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a backwards incompatible change and cannot happen.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, we could have cleared this in #3309. Anyway, he are reasons why I did it:

  • MemoryLoader is not listed in API docs: https://tox.wiki/en/latest/plugins_api.html Hence, I assumed it may have incompatible changes.
  • MemoryLoader.__init__ isn't currently compatible with the documented API for Loader.__init__.

I can add some compatible layer and/or deprecation warnings, but I need to know how to handle it correctly. More incompatible changes will be needed for TOML support.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oversight.

  • MemoryLoader.__init__ isn't currently compatible with the documented API for Loader.__init__.

By design. It is meant to be instantiated with the appropriate values. Why would a child class be compatible with its parent constructor? The substitution principle works in the other way around, I believe.

Copy link
Contributor Author

@ziima ziima Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compatibility makes the child class more flexible, especially when some of the omitted arguments become needed such as in this case.

Now, I'm not quite sure which way use to make it compatible. It seems to me it would be best to create new MemoryLoader class with modified constructor and deprecate current one. Is that OK?

Copy link
Member

@gaborbernat gaborbernat Sep 23, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I disagree. Why does a memory loader need to know about its section? Or overrides. These are concepts that do not make sense in this context, so I am not sure why you are trying to force it into?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the MemoryLoader is part of public API it should we able to handle various sorts of configs, such as complete config of tox. In such case, it needs to know which section it should extract the key from and whether to apply any overrides.

Or do I not get the purpose of MemoryLoader correctly?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hope you reach an agreement here. I see the reasoning for having to access these bits, we use it in tox-ansible plugin. At the same time, I would try to avoid breaking changes (changing signature does not always count as breaking in my eyes).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like overrides should be their own loader 🤔 and not depend on the config source. 🤔

Copy link
Member

@gaborbernat gaborbernat Sep 27, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I looked into this, overrides are intended to be not enabled for a memory loader. And I guess what I am not understanding is why you want to change that? The TOML loader should not be a memory loader.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is an implementation demonstrating there is no need for this #3353, will continue over the next week to get it in a ready to ship state 👍

self,
raw: dict[str, Any],
*,
section: Section | None = None,
overrides: list[Override] | None = None,
) -> None:
section = section or Section(prefix="<memory>", name=str(id(self)))
super().__init__(section, overrides or [])
self.raw = raw

def load_raw(self, key: Any, conf: Config | None, env_name: str | None) -> Any: # noqa: ARG002
return self.raw[key]
Expand Down Expand Up @@ -62,4 +69,6 @@ def to_env_list(value: Any) -> EnvList:
return value
if isinstance(value, str):
return StrConvert.to_env_list(value)
if isinstance(value, Sequence):
return EnvList(value)
raise TypeError(value)
14 changes: 8 additions & 6 deletions src/tox/provision.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,14 @@ def add_tox_requires_min_version(reqs: list[Requirement]) -> list[Requirement]:

deps = ", ".join(f"{p}{'' if v is None else f' ({v})'}" for p, v in missing)
loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf)
base=[], # disable inheritance for provision environments
package="skip", # no packaging for this please
# use our own dependency specification
deps=PythonDeps("\n".join(str(r) for r in requires), root=state.conf.core["tox_root"]),
pass_env=["*"], # do not filter environment variables, will be handled by provisioned tox
recreate=state.conf.options.recreate and not state.conf.options.no_recreate_provision,
{
"base": [], # disable inheritance for provision environments
"package": "skip", # no packaging for this please
# use our own dependency specification
"deps": PythonDeps("\n".join(str(r) for r in requires), root=state.conf.core["tox_root"]),
"pass_env": ["*"], # do not filter environment variables, will be handled by provisioned tox
"recreate": state.conf.options.recreate and not state.conf.options.no_recreate_provision,
}
)
provision_tox_env: str = state.conf.core["provision_tox_env"]
state.conf.memory_seed_loaders[provision_tox_env].append(loader)
Expand Down
6 changes: 4 additions & 2 deletions src/tox/session/cmd/devenv.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ def devenv(state: State) -> int:
opt.skip_pkg_install = False # always install a package in this case
opt.no_test = True # do not run the test phase
loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf)
usedevelop=True, # dev environments must be of type dev
env_dir=opt.devenv_path, # move it in source
{
"usedevelop": True, # dev environments must be of type dev
"env_dir": opt.devenv_path, # move it in source
}
)
state.conf.memory_seed_loaders[next(iter(opt.env))].append(loader)

Expand Down
8 changes: 5 additions & 3 deletions src/tox/session/cmd/exec_.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,11 @@ def exec_(state: State) -> int:
msg = f"exactly one target environment allowed in exec mode but found {', '.join(envs)}"
raise HandledError(msg)
loader = MemoryLoader( # these configuration values are loaded from in-memory always (no file conf)
commands_pre=[],
commands=[],
commands_post=[],
{
"commands_pre": [],
"commands": [],
"commands_post": [],
}
)
conf = state.envs[envs[0]].conf
conf.loaders.insert(0, loader)
Expand Down
2 changes: 1 addition & 1 deletion src/tox/session/cmd/legacy.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def _handle_legacy_only_flags(option: Parsed, envs: EnvSelector) -> None: # noq
for env in envs.iter(only_active=True, package=False):
env_conf = envs[env].conf
if override:
env_conf.loaders.insert(0, MemoryLoader(**override))
env_conf.loaders.insert(0, MemoryLoader(override))
if set_env:
cast(SetEnv, env_conf["set_env"]).update(set_env, override=True)
if forced:
Expand Down
8 changes: 1 addition & 7 deletions tests/config/loader/ini/test_ini_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import pytest

from tox.config.loader.api import ConfigLoadArgs, Override
from tox.config.loader.api import ConfigLoadArgs
from tox.config.loader.ini import IniLoader
from tox.config.source.ini_section import IniSection

Expand All @@ -18,12 +18,6 @@ def test_ini_loader_keys(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
assert loader.found_keys() == {"a", "c"}


def test_ini_loader_repr(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
core = IniSection(None, "tox")
loader = IniLoader(core, mk_ini_conf("\n[tox]\n\na=b\nc=d\n\n"), [Override("tox.a=1")], core_section=core)
assert repr(loader) == "IniLoader(section=tox, overrides={'a': [Override('tox.a=1')]})"


def test_ini_loader_has_section(mk_ini_conf: Callable[[str], ConfigParser]) -> None:
core = IniSection(None, "tox")
loader = IniLoader(core, mk_ini_conf("[magic]\n[tox]\n\na=b\nc=d\n\n"), [], core_section=core)
Expand Down
21 changes: 20 additions & 1 deletion tests/config/loader/test_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
import pytest

from tox.config.cli.parse import get_options
from tox.config.loader.api import Override
from tox.config.loader.api import Loader, Override
from tox.config.loader.section import Section
from tox.config.loader.str_convert import StrConvert

if TYPE_CHECKING:
from tox.config.main import Config
from tox.pytest import CaptureFixture


Expand Down Expand Up @@ -62,3 +65,19 @@ def test_override_not_equals_different_type() -> None:

def test_override_repr() -> None:
assert repr(Override("b.a=c")) == "Override('b.a=c')"


class SimpleLoader(StrConvert, Loader[str]):
"""Simple loader for tests."""

def load_raw(self, key: str, conf: Config | None, env_name: str | None) -> str: # type: ignore[empty-body]
pass

def found_keys(self) -> set[str]: # type: ignore[empty-body]
pass


def test_loader_repr() -> None:
core = Section(None, "tox")
loader = SimpleLoader(core, [Override("tox.a=1")])
assert repr(loader) == "SimpleLoader(section=tox, overrides={'a': [Override('tox.a=1')]})"
16 changes: 6 additions & 10 deletions tests/config/loader/test_memory_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,8 @@
from tox.config.types import Command, EnvList


def test_memory_loader_repr() -> None:
loader = MemoryLoader(a=1)
assert repr(loader) == "MemoryLoader"


def test_memory_loader_override() -> None:
loader = MemoryLoader(a=1)
loader = MemoryLoader({"a": 1})
loader.overrides["a"] = [Override("a=2")]
args = ConfigLoadArgs([], "name", None)
loaded = loader.load("a", of_type=int, conf=None, factory=None, args=args)
Expand Down Expand Up @@ -48,11 +43,12 @@ def test_memory_loader_override() -> None:
(os.getcwd(), Path, Path.cwd()), # noqa: PTH109
("pip list", Command, Command(["pip", "list"])),
("a\nb", EnvList, EnvList(["a", "b"])),
(["a", "b"], EnvList, EnvList(["a", "b"])),
("1", Optional[int], 1),
],
)
def test_memory_loader(value: Any, of_type: type[Any], outcome: Any) -> None:
loader = MemoryLoader(a=value, kwargs={})
loader = MemoryLoader({"a": value, "kwargs": {}})
args = ConfigLoadArgs([], "name", None)
loaded = loader.load("a", of_type=of_type, conf=None, factory=None, args=args)
assert loaded == outcome
Expand All @@ -72,18 +68,18 @@ def test_memory_loader(value: Any, of_type: type[Any], outcome: Any) -> None:
],
)
def test_memory_loader_fails_invalid(value: Any, of_type: type[Any], exception: Exception, msg: str) -> None:
loader = MemoryLoader(a=value, kwargs={})
loader = MemoryLoader({"a": value, "kwargs": {}})
args = ConfigLoadArgs([], "name", None)
with pytest.raises(exception, match=msg): # type: ignore[call-overload]
loader.load("a", of_type=of_type, conf=None, factory=None, args=args)


def test_memory_found_keys() -> None:
loader = MemoryLoader(a=1, c=2)
loader = MemoryLoader({"a": 1, "c": 2})
assert loader.found_keys() == {"a", "c"}


def test_memory_loader_contains() -> None:
loader = MemoryLoader(a=1)
loader = MemoryLoader({"a": 1})
assert "a" in loader
assert "b" not in loader
2 changes: 1 addition & 1 deletion tests/config/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def test_config_overrides(tox_ini_conf: ToxIniCreator) -> None:

def test_config_override_wins_memory_loader(tox_ini_conf: ToxIniCreator) -> None:
main_conf = tox_ini_conf("[testenv]", override=[Override("testenv.c=ok")])
conf = main_conf.get_env("py", loaders=[MemoryLoader(c="something_else")])
conf = main_conf.get_env("py", loaders=[MemoryLoader({"c": "something_else"})])
conf.add_config("c", of_type=str, default="d", desc="desc")
assert conf["c"] == "ok"

Expand Down
2 changes: 1 addition & 1 deletion tests/config/test_sets.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ def test_do_not_allow_create_config_set(mocker: MockerFixture) -> None:

def test_set_env_raises_on_non_str(mocker: MockerFixture) -> None:
env_set = EnvConfigSet(mocker.create_autospec(Config), Section("a", "b"), "b")
env_set.loaders.insert(0, MemoryLoader(set_env=1))
env_set.loaders.insert(0, MemoryLoader({"set_env": 1}))
with pytest.raises(TypeError, match="1"):
assert env_set["set_env"]

Expand Down
4 changes: 2 additions & 2 deletions tests/plugin/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ def test_plugin_can_set_core_conf(
) -> None:
@impl
def tox_add_core_config(core_conf: CoreConfigSet, state: State) -> None: # noqa: ARG001
core_conf.loaders.insert(0, MemoryLoader(**{dir_name: tmp_path}))
core_conf.loaders.insert(0, MemoryLoader({dir_name: tmp_path}))

register_inline_plugin(mocker, tox_add_core_config)

Expand Down Expand Up @@ -186,7 +186,7 @@ def tox_add_core_config(core_conf: CoreConfigSet, state: State) -> None: # noqa
def test_plugin_injects_invalid_python_run(tox_project: ToxProjectCreator, mocker: MockerFixture) -> None:
@impl
def tox_add_env_config(env_conf: EnvConfigSet, state: State) -> None: # noqa: ARG001
env_conf.loaders.insert(0, MemoryLoader(deps=[1]))
env_conf.loaders.insert(0, MemoryLoader({"deps": [1]}))
with pytest.raises(TypeError, match="1"):
assert env_conf["deps"]

Expand Down
Loading