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

Add optional tox configuration #2469

Merged
merged 2 commits into from
Jul 14, 2023
Merged
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
11 changes: 0 additions & 11 deletions contrib/README

This file was deleted.

36 changes: 36 additions & 0 deletions contrib/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This folder contains contributed files for use with Sopel.

## Service configuration

`sopel.service` and `sopel.cfg` are designed to be distributed by third parties
such as Fedora Project or Arch Linux.

`sopel.cfg` is a default configuration file for Sopel, that assumes the OS is
new enough to have `/run` and `/usr/lib/tmpfiles.d`

`sopel.service` is a systemd service file, and `sopel@.service` is a
multi-instance template. Both assume you are using a rather recent Sopel; that
the system has a special user named `sopel` designated for running the bot; and
that this user has access to `/run/sopel` (should be setup by `sopel.conf` in
`/usr/lib/tmpfiles.d`), `/var/log/sopel` and `/var/lib/sopel`

Default installation paths:

```
sopel.cfg /etc
sopel.conf /usr/lib/tmpfiles.d
sopel.service /usr/lib/systemd/system
sopel@.service /usr/lib/systemd/system
```

## tox

`tox.ini` and `toxfile.py` provide support for running Sopel's QA automation
against multiple Python versions locally. To run QA for all configured versions
in parallel, run `tox --conf contrib/ -p` from the repository root. You may
also want to set `TOX_CONFIG_FILE=path/to/contrib` to shorten that invocation
to just `tox -p`.

## githooks

Git hooks for development use
47 changes: 47 additions & 0 deletions contrib/tox.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
[tox]
package_root = .
min_version = 4.3.3
envlist =
py{37,38,39,310,311}-qa
skip_missing_interpreters = true
ignore_base_python_conflict = true
labels =
mypy = py{37,38,39,310,311}-mypy
quality = py{37,38,39,310,311}-quality
test = py{37,38,39,310,311}-test


[testenv]
runner = ignore_env_name_mismatch
package = sdist
allowlist_externals =
make
envname =
py37: py37
py38: py38
py39: py39
py310: py310
py311: py311
envdir =
py37: {toxinidir}/.tox/py37
py38: {toxinidir}/.tox/py38
py39: {toxinidir}/.tox/py39
py310: {toxinidir}/.tox/py310
py311: {toxinidir}/.tox/py311
dgw marked this conversation as resolved.
Show resolved Hide resolved
depends =
base
deps =
-r ../dev-requirements.txt
passenv =
PYTEST_ADDOPTS
setenv =
COVERAGE_FILE = {envdir}/.coverage

commands =
qa: make -C.. qa
mypy: make -C.. mypy
quality: make -C.. quality
test: make -C.. test
# NOTE:there's currently no way to specify separate output directories for
# the HTML coverage report, but the CLI report is probably fine anyway
test: make -C.. coverage_report
108 changes: 108 additions & 0 deletions contrib/toxfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from pathlib import Path

from tox.execute.request import StdinSource
from tox.plugin import impl


CWD = Path().resolve()
HERE = Path(__file__).parent.resolve()

if CWD != HERE.parent:
raise RuntimeError("Tox must be run from the sopel repository root")


@impl
def tox_on_install(tox_env, arguments, section, of_type):
EXCLUDED_DEPS = ("sopel-help",)

# NOTE: this hook is a workaround for the cyclic dependency between sopel and sopel-help
if of_type == "deps":
cmd = ["python", "-m", "pip", "install", "--no-deps", "sopel-help"]
tox_env.execute(cmd, stdin=StdinSource.OFF)
elif of_type == "package":
sopel_sdist = arguments[0]
deps = sopel_sdist.deps
deps[:] = [d for d in deps if d.name not in EXCLUDED_DEPS]


# NOTE:The following is included verbatim from upstream (v0.2.0.post2 @ ebed159)
# to allow re-use of tox environments for each Python version. We *can* depend
# on the plugin from tox.ini but it makes sense to use the recommended vendoring
# approach since we have a toxfile.py anyway.
"""
https://github.com/masenf/tox-ignore-env-name-mismatch

MIT License
Copyright (c) 2023 Masen Furer
"""
from contextlib import contextmanager
from typing import Any, Iterator, Optional, Sequence, Tuple

from tox.plugin import impl
from tox.tox_env.api import ToxEnv
from tox.tox_env.info import Info
from tox.tox_env.python.virtual_env.runner import VirtualEnvRunner
from tox.tox_env.register import ToxEnvRegister


class FilteredInfo(Info):
"""Subclass of Info that optionally filters specific keys during compare()."""

def __init__(
self,
*args: Any,
filter_keys: Optional[Sequence[str]] = None,
filter_section: Optional[str] = None,
**kwargs: Any,
):
"""
:param filter_keys: key names to pop from value
:param filter_section: if specified, only pop filter_keys when the compared section matches

All other args and kwargs are passed to super().__init__
"""
self.filter_keys = filter_keys
self.filter_section = filter_section
super().__init__(*args, **kwargs)

@contextmanager
def compare(
self,
value: Any,
section: str,
sub_section: Optional[str] = None,
) -> Iterator[Tuple[bool, Optional[Any]]]:
"""Perform comparison and update cached info after filtering `value`."""
if self.filter_section is None or section == self.filter_section:
try:
value = value.copy()
except AttributeError: # pragma: no cover
pass
else:
for fkey in self.filter_keys or []:
value.pop(fkey, None)
with super().compare(value, section, sub_section) as rv:
yield rv


class IgnoreEnvNameMismatchVirtualEnvRunner(VirtualEnvRunner):
"""EnvRunner that does NOT save the env name as part of the cached info."""

@staticmethod
def id() -> str:
return "ignore_env_name_mismatch"

@property
def cache(self) -> Info:
"""Return a modified Info class that does NOT pass "name" key to `Info.compare`."""
return FilteredInfo(
self.env_dir,
filter_keys=["name"],
filter_section=ToxEnv.__name__,
)


@impl
def tox_register_tox_env(register: ToxEnvRegister) -> None:
"""tox4 entry point: add IgnoreEnvNameMismatchVirtualEnvRunner to registry."""
register.add_run_env(IgnoreEnvNameMismatchVirtualEnvRunner)