Repository: tox-dev/tox

Issue:
tox4: Prefer f-strings instead of the str.format method.
In https://tox.readthedocs.io/en/rewrite/development.html#code-style-guide it says:

- Prefer f-strings instead of the `str.format` method.

It would be nice to have more f-strings in the code.


Discussion:
eumiro: > Some files still need to be python 2 compatible, to allow and test for running python 2 envs from tox.

What would be the best way to tag these files compatible with both 2/3? Something in the header?
gaborbernat: That can be, currently they're tagged in my head and hopefully by the test suite.
eumiro: Reverted changes in Py2/3 files, added changelog entry.

And, @gaborbernat, I'd like to find a standard way of marking files to be Py2/3 compatible to avoid contributors from keeping fixing the Py2 relics from them.
gaborbernat: > Reverted changes in Py2/3 files, added changelog entry.
> 
> And, @gaborbernat, I'd like to find a standard way of marking files to be Py2/3 compatible to avoid contributors from keeping fixing the Py2 relics from them.

Can't think of any better than mention this in the module docstring...
eumiro: There's one formatting test failing, can have a look at it later.

And created #2013 to discuss the Py2/3 problem. Module docstring is a good idea.
eumiro: The failed tests come from the usage of `textwrap.shorten`. Now the question is, how do you want to handle the words in the output? Should it cut after X characters like before or should it show whole words and possibly cut earlier? 
gaborbernat: > The failed tests come from the usage of `textwrap.shorten`. Now the question is, how do you want to handle the words in the output? Should it cut after X characters like before or should it show whole words and possibly cut earlier?

As long as the ... are added I'm happy to cut it at the word boundary.

Patch:
diff --git a/docs/changelog/2012.bugfix.rst b/docs/changelog/2012.bugfix.rst
new file mode 100644
index 00000000..5f2a80f0
--- /dev/null
+++ b/docs/changelog/2012.bugfix.rst
@@ -0,0 +1,1 @@
+Prefer f-strings instead of the str.format method - by :user:`eumiro`.
diff --git a/docs/development.rst b/docs/development.rst
index 48d1cda5..ce0f78d1 100644
--- a/docs/development.rst
+++ b/docs/development.rst
@@ -96,6 +96,11 @@ Code style guide
 
 - First and foremost, the linters configured for the project must pass; this generally means following PEP-8 rules,
   as codified by: ``flake8``, ``black``, ``isort``, ``pyupgrade``.
+- The supported Python versions (and the code syntax to use) are listed in the ``setup.cfg`` file
+  in the ``options/python_requires`` entry. However, there are some files that have to be kept compatible
+  with Python 2.7 to allow and test for running Python 2 envs from tox. They are listed in ``.pre-commit-config.yaml``
+  under ``repo: https://github.com/asottile/pyupgrade`` under ``hooks/exclude``.
+  Please do not attempt to modernize them to Python 3.x.
 - Packaging options should be specified within ``setup.cfg``; ``setup.py`` is only kept for editable installs.
 - All code (tests too) must be type annotated as much as required by ``mypy``.
 - We use a line length of 120.
diff --git a/src/tox/config/cli/parser.py b/src/tox/config/cli/parser.py
index df8d6163..2be63258 100644
--- a/src/tox/config/cli/parser.py
+++ b/src/tox/config/cli/parser.py
@@ -236,7 +236,7 @@ class ToxParser(ArgumentParserWithEnvAndConfig):
 def add_verbosity_flags(parser: ArgumentParser) -> None:
     from tox.report import LEVELS
 
-    level_map = "|".join("{} - {}".format(c, logging.getLevelName(l)) for c, l in sorted(LEVELS.items()))
+    level_map = "|".join(f"{c} - {logging.getLevelName(l)}" for c, l in sorted(LEVELS.items()))
     verbosity_group = parser.add_argument_group("verbosity")
     verbosity_group.description = f"verbosity=verbose-quiet, default {logging.getLevelName(LEVELS[3])}, map {level_map}"
     verbosity = verbosity_group.add_mutually_exclusive_group()
diff --git a/src/tox/config/of_type.py b/src/tox/config/of_type.py
index b555e887..c53964be 100644
--- a/src/tox/config/of_type.py
+++ b/src/tox/config/of_type.py
@@ -109,7 +109,7 @@ class ConfigDynamicDefinition(ConfigDefinition[T]):
 
     def __repr__(self) -> str:
         values = ((k, v) for k, v in vars(self).items() if k != "post_process" and v is not None)
-        return f"{type(self).__name__}({', '.join('{}={}'.format(k, v) for k,v in values)})"
+        return f"{type(self).__name__}({', '.join(f'{k}={v}' for k, v in values)})"
 
     def __eq__(self, o: Any) -> bool:
         return (
diff --git a/src/tox/util/pep517/backend.py b/src/tox/util/pep517/backend.py
index a4f2db70..d7ae6c87 100644
--- a/src/tox/util/pep517/backend.py
+++ b/src/tox/util/pep517/backend.py
@@ -1,4 +1,8 @@
-"""Handles communication on the backend side between frontend and backend"""
+"""Handles communication on the backend side between frontend and backend
+
+Please keep this file Python 2.7 compatible.
+See https://tox.readthedocs.io/en/rewrite/development.html#code-style-guide
+"""
 from __future__ import print_function, unicode_literals
 
 import json
diff --git a/src/tox/util/spinner.py b/src/tox/util/spinner.py
index 19cb0e33..78edc8fa 100644
--- a/src/tox/util/spinner.py
+++ b/src/tox/util/spinner.py
@@ -1,6 +1,7 @@
 """A minimal non-colored version of https://pypi.org/project/halo, to track list progress"""
 import os
 import sys
+import textwrap
 import threading
 import time
 from collections import OrderedDict
@@ -83,9 +84,8 @@ class Spinner:
         self._frame_index %= len(self.frames)
         total = f"/{self.total}" if self.total is not None else ""
         text_frame = f"[{len(self._envs)}{total}] {' | '.join(self._envs)}"
-        if len(text_frame) > self.max_width - 1:
-            text_frame = "{}...".format(text_frame[: self.max_width - 1 - 3])
-        return "{} {}".format(*[(frame, text_frame)][0])
+        text_frame = textwrap.shorten(text_frame, width=self.max_width - 1, placeholder="...")
+        return f"{frame} {text_frame}"
 
     def __enter__(self: T) -> T:
         if self.enabled:


Test patch:
diff --git a/src/tox/pytest.py b/src/tox/pytest.py
index 8ce123f6..43fb49f0 100644
--- a/src/tox/pytest.py
+++ b/src/tox/pytest.py
@@ -151,7 +151,7 @@ class ToxProject:
             elif isinstance(value, str):
                 at_path.write_text(textwrap.dedent(value))
             else:
-                msg = "could not handle {} with content {!r}".format(at_path / key, value)  # pragma: no cover
+                msg = f"could not handle {at_path / key} with content {value!r}"  # pragma: no cover
                 raise TypeError(msg)  # pragma: no cover
 
     def patch_execute(self, handle: Callable[[ExecuteRequest], Optional[int]]) -> MagicMock:
diff --git a/tests/demo_pkg_inline/build.py b/tests/demo_pkg_inline/build.py
index ae51dc5c..9885cc24 100644
--- a/tests/demo_pkg_inline/build.py
+++ b/tests/demo_pkg_inline/build.py
@@ -1,3 +1,8 @@
+"""
+Please keep this file Python 2.7 compatible.
+See https://tox.readthedocs.io/en/rewrite/development.html#code-style-guide
+"""
+
 import os
 import sys
 import tarfile
diff --git a/tests/session/cmd/test_show_config.py b/tests/session/cmd/test_show_config.py
index b44b43c8..52269fd4 100644
--- a/tests/session/cmd/test_show_config.py
+++ b/tests/session/cmd/test_show_config.py
@@ -12,7 +12,7 @@ from tox.pytest import MonkeyPatch, ToxProjectCreator
 
 def test_show_config_default_run_env(tox_project: ToxProjectCreator, monkeypatch: MonkeyPatch) -> None:
     py_ver = sys.version_info[0:2]
-    name = "py{}{}".format(*py_ver) if platform.python_implementation() == "CPython" else "pypy3"
+    name = f"py{py_ver[0]}{py_ver[1]}" if platform.python_implementation() == "CPython" else "pypy3"
     project = tox_project({"tox.ini": f"[tox]\nenv_list = {name}\n[testenv:{name}]\ncommands={{posargs}}"})
     result = project.run("c", "-e", name, "--core", "--", "magic")
     state = result.state
diff --git a/tests/util/test_spinner.py b/tests/util/test_spinner.py
index d9f87d7d..0ef4e304 100644
--- a/tests/util/test_spinner.py
+++ b/tests/util/test_spinner.py
@@ -21,8 +21,8 @@ def test_spinner(capfd: CaptureFixture, monkeypatch: MonkeyPatch) -> None:
         spin.stream.write("\n")
     out, err = capfd.readouterr()
     lines = out.split("\n")
-    expected = [f"\r{spin.CLEAR_LINE}\r{i} [0] " for i in spin.frames] + [
-        f"\r{spin.CLEAR_LINE}\r{spin.frames[0]} [0] ",
+    expected = [f"\r{spin.CLEAR_LINE}\r{i} [0]" for i in spin.frames] + [
+        f"\r{spin.CLEAR_LINE}\r{spin.frames[0]} [0]",
         f"\r{spin.CLEAR_LINE}",
     ]
     assert lines == expected
@@ -63,7 +63,7 @@ def test_spinner_atty(capfd: CaptureFixture, monkeypatch: MonkeyPatch) -> None:
     lines = out.split("\n")
     posix = os.name == "posix"
     expected = [
-        "{}\r{}\r{} [0] ".format("\x1b[?25l" if posix else "", spin.CLEAR_LINE, spin.frames[0]),
+        "{}\r{}\r{} [0]".format("\x1b[?25l" if posix else "", spin.CLEAR_LINE, spin.frames[0]),
         "\r\x1b[K{}".format("\x1b[?25h" if posix else ""),
     ]
     assert lines == expected
@@ -104,7 +104,7 @@ def test_spinner_long_text(capfd: CaptureFixture, monkeypatch: MonkeyPatch) -> N
     out, err = capfd.readouterr()
     assert not err
     expected = [
-        f"\r{spin.CLEAR_LINE}\r{spin.frames[1]} [2] {'a' * 60} | {'b' * 49}...",
+        f"\r{spin.CLEAR_LINE}\r{spin.frames[1]} [2] {'a' * 60} |...",
         f"\r{spin.CLEAR_LINE}",
     ]
     lines = out.split("\n")