Source code for tests.test_hygiene
# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program is Free Software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
"""Conventions the whole code base follows, checked file by file."""
from __future__ import annotations
import ast
from pathlib import Path
import pytest
ROOT = Path(__file__).parent.parent
"""Root of the repository."""
PYTHON_FILES = tuple(
sorted(
path
for folder in ("click_extra", "tests")
for path in (ROOT / folder).rglob("*.py")
)
)
"""Python sources of the package and its test suite."""
[docs]
def codec_names(tree: ast.AST) -> list[tuple[int, str]]:
"""Each codec name an `encoding=` argument, `encode()` or `decode()` receives.
:return: the line and the literal of every codec spelled as a string
constant, in the order of the source.
"""
found: list[tuple[int, str]] = []
for node in ast.walk(tree):
if not isinstance(node, ast.Call):
continue
values = [
keyword.value for keyword in node.keywords if keyword.arg == "encoding"
]
if (
isinstance(node.func, ast.Attribute)
and node.func.attr in {"encode", "decode"}
and node.args
):
values.append(node.args[0])
found.extend(
(value.lineno, value.value)
for value in values
if isinstance(value, ast.Constant) and isinstance(value.value, str)
)
return sorted(found)
[docs]
def test_codec_names_detected() -> None:
"""The scan sees every way the suite spells a codec name."""
source = (
'open(path, encoding="utf-8")\n'
"text.encode('utf8')\n"
'data.decode("UTF-8")\n'
'subprocess.run(args, text=True, encoding="ascii")\n'
)
assert codec_names(ast.parse(source)) == [
(1, "utf-8"),
(2, "utf8"),
(3, "UTF-8"),
(4, "ascii"),
]
[docs]
@pytest.mark.once
def test_utf8_spelled_uppercase() -> None:
"""Every UTF-8 codec name reads `UTF-8`, the one spelling of this code base.
Python normalizes `utf-8`, `utf8` and `UTF-8` alike, so the difference carries
no meaning, and a mixed code base only makes a reader stop to work that out.
"""
wrong = [
f"{path.relative_to(ROOT)}:{line}: {name!r}"
for path in PYTHON_FILES
for line, name in codec_names(ast.parse(path.read_text(encoding="UTF-8")))
if name.replace("_", "-").lower() in {"utf-8", "utf8"} and name != "UTF-8"
]
assert not wrong, "\n".join(wrong)