chore: add PyLint and fix most issues (#999)

* chore: add PyLint and fix most issues

* fix: minor fixes after first push

* chore: remove reference to old toml lib

* chore: minor options cleanup

* ci: only fail with many pylint issues, annotate in GHA

* refactor: address review

* ci: force nox to report in CI

* refactor: use map instead of if chain

* refactor: simpler shell_with_arch

* refactor: even simpler shell_with_arch

* Restore if...elif...else blocks where guard-style is not intended

Co-authored-by: Joe Rickerby <joerick@mac.com>
This commit is contained in:
Henry Schreiner
2022-02-27 14:16:37 -05:00
committed by GitHub
co-authored by Joe Rickerby
parent 6d120e7a6b
commit d6dc3b7cd9
16 changed files with 240 additions and 85 deletions
+32
View File
@@ -0,0 +1,32 @@
{
"problemMatcher": [
{
"severity": "warning",
"pattern": [
{
"regexp": "^([^:]+):(\\d+):(\\d+): ([A-DF-Z]\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
"file": 1,
"line": 2,
"column": 3,
"code": 4,
"message": 5
}
],
"owner": "pylint-warning"
},
{
"severity": "error",
"pattern": [
{
"regexp": "^([^:]+):(\\d+):(\\d+): (E\\d+): \\033\\[[\\d;]+m([^\\033]+).*$",
"file": 1,
"line": 2,
"column": 3,
"code": 4,
"message": 5
}
],
"owner": "pylint-error"
}
]
}
+10 -5
View File
@@ -15,18 +15,23 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
jobs: jobs:
pre-commit: lint:
name: Pre-commit checks (mypy, flake8, etc.) name: Linters (mypy, flake8, etc.)
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2 - uses: actions/setup-python@v2
- uses: pre-commit/action@v2.0.3 - uses: pre-commit/action@v2.0.3
- run: pipx run nox -s check_manifest - name: Check manifest
run: pipx run nox -s check_manifest
- name: PyLint checks
run: |
echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json"
pipx run nox -s pylint
test: test:
name: Test cibuildwheel on ${{ matrix.os }} name: Test cibuildwheel on ${{ matrix.os }}
needs: pre-commit needs: lint
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
@@ -67,7 +72,7 @@ jobs:
test-emulated: test-emulated:
name: Test emulated cibuildwheel using qemu name: Test emulated cibuildwheel using qemu
needs: pre-commit needs: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 180 timeout-minutes: 180
steps: steps:
+2 -2
View File
@@ -237,7 +237,7 @@ def print_preamble(platform: str, options: Options, identifiers: List[str]) -> N
print(f"Cache folder: {CIBW_CACHE_PATH}") print(f"Cache folder: {CIBW_CACHE_PATH}")
warnings = detect_warnings(platform=platform, options=options, identifiers=identifiers) warnings = detect_warnings(options=options, identifiers=identifiers)
if warnings: if warnings:
print("\nWarnings:") print("\nWarnings:")
for warning in warnings: for warning in warnings:
@@ -273,7 +273,7 @@ def get_build_identifiers(
return [config.identifier for config in python_configurations] return [config.identifier for config in python_configurations]
def detect_warnings(platform: str, options: Options, identifiers: List[str]) -> List[str]: def detect_warnings(*, options: Options, identifiers: List[str]) -> List[str]:
warnings = [] warnings = []
# warn about deprecated {python} and {pip} # warn about deprecated {python} and {pip}
+6 -8
View File
@@ -73,20 +73,18 @@ class Architecture(Enum):
@staticmethod @staticmethod
def all_archs(platform: PlatformName) -> "Set[Architecture]": def all_archs(platform: PlatformName) -> "Set[Architecture]":
if platform == "linux": all_archs_map = {
return { "linux": {
Architecture.x86_64, Architecture.x86_64,
Architecture.i686, Architecture.i686,
Architecture.aarch64, Architecture.aarch64,
Architecture.ppc64le, Architecture.ppc64le,
Architecture.s390x, Architecture.s390x,
},
"macos": {Architecture.x86_64, Architecture.arm64, Architecture.universal2},
"windows": {Architecture.x86, Architecture.AMD64, Architecture.ARM64},
} }
elif platform == "macos": return all_archs_map[platform]
return {Architecture.x86_64, Architecture.arm64, Architecture.universal2}
elif platform == "windows":
return {Architecture.x86, Architecture.AMD64, Architecture.ARM64}
else:
assert_never(platform)
@staticmethod @staticmethod
def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> "Set[Architecture]": def bitness_archs(platform: PlatformName, bitness: Literal["64", "32"]) -> "Set[Architecture]":
+3 -1
View File
@@ -108,7 +108,9 @@ class DockerContainer:
assert isinstance(self.name, str) assert isinstance(self.name, str)
subprocess.run(["docker", "rm", "--force", "-v", self.name], stdout=subprocess.DEVNULL) subprocess.run(
["docker", "rm", "--force", "-v", self.name], stdout=subprocess.DEVNULL, check=False
)
self.name = None self.name = None
def copy_into(self, from_path: Path, to_path: PurePath) -> None: def copy_into(self, from_path: Path, to_path: PurePath) -> None:
+1 -1
View File
@@ -66,7 +66,7 @@ class EnvironmentAssignmentRaw:
def __repr__(self) -> str: def __repr__(self) -> str:
return f"{self.name}: {self.value}" return f"{self.name}: {self.value}"
def evaluated_value(self, **kwargs: Any) -> str: def evaluated_value(self, **_: Any) -> str:
return self.value return self.value
@@ -0,0 +1,65 @@
from threading import RLock
from typing import Any, Callable, Generic, Optional, Type, TypeVar, overload
__all__ = ["cached_property"]
_NOT_FOUND = object()
_T = TypeVar("_T")
class cached_property(Generic[_T]):
def __init__(self, func: Callable[[Any], _T]):
self.func = func
self.attrname: Optional[str] = None
self.__doc__ = func.__doc__
self.lock = RLock()
def __set_name__(self, owner: Type[Any], name: str) -> None:
if self.attrname is None:
self.attrname = name
elif name != self.attrname:
raise TypeError(
"Cannot assign the same cached_property to two different names "
f"({self.attrname!r} and {name!r})."
)
@overload
def __get__(self, instance: None, owner: Optional[Type[Any]] = ...) -> "cached_property[_T]":
...
@overload
def __get__(self, instance: object, owner: Optional[Type[Any]] = ...) -> _T:
...
def __get__(self, instance: Optional[object], owner: Optional[Type[Any]] = None) -> Any:
if instance is None:
return self
if self.attrname is None:
raise TypeError(
"Cannot use cached_property instance without calling __set_name__ on it."
)
try:
cache = instance.__dict__
except AttributeError: # not all objects have __dict__ (e.g. class defines slots)
msg = (
f"No '__dict__' attribute on {type(instance).__name__!r} "
f"instance to cache {self.attrname!r} property."
)
raise TypeError(msg) from None
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
with self.lock:
# check if another thread filled cache while we awaited lock
val = cache.get(self.attrname, _NOT_FOUND)
if val is _NOT_FOUND:
val = self.func(instance)
try:
cache[self.attrname] = val
except TypeError:
msg = (
f"The '__dict__' attribute on {type(instance).__name__!r} instance "
f"does not support item assignment for caching {self.attrname!r} property."
)
raise TypeError(msg) from None
return val
+2 -2
View File
@@ -303,11 +303,11 @@ def build_on_docker(
log.step_end() log.step_end()
def build(options: Options, tmp_path: Path) -> None: def build(options: Options, tmp_path: Path) -> None: # pylint: disable=unused-argument
try: try:
# check docker is installed # check docker is installed
subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL) subprocess.run(["docker", "--version"], check=True, stdout=subprocess.DEVNULL)
except Exception: except subprocess.CalledProcessError:
print( print(
"cibuildwheel: Docker not found. Docker is required to run Linux builds. " "cibuildwheel: Docker not found. Docker is required to run Linux builds. "
"If you're building on Travis CI, add `services: [docker]` to your .travis.yml." "If you're building on Travis CI, add `services: [docker]` to your .travis.yml."
+5 -12
View File
@@ -158,7 +158,8 @@ class Logger:
sys.stdout.flush() sys.stdout.flush()
self.active_fold_group_name = None self.active_fold_group_name = None
def _fold_group_identifier(self, name: str) -> str: @staticmethod
def _fold_group_identifier(name: str) -> str:
""" """
Travis doesn't like fold groups identifiers that have spaces in. This Travis doesn't like fold groups identifiers that have spaces in. This
method converts them to ascii identifiers method converts them to ascii identifiers
@@ -174,17 +175,11 @@ class Logger:
@property @property
def colors(self) -> "Colors": def colors(self) -> "Colors":
if self.colors_enabled: return Colors(enabled=self.colors_enabled)
return Colors(enabled=True)
else:
return Colors(enabled=False)
@property @property
def symbols(self) -> "Symbols": def symbols(self) -> "Symbols":
if self.unicode_enabled: return Symbols(unicode=self.unicode_enabled)
return Symbols(unicode=True)
else:
return Symbols(unicode=False)
def build_description_from_identifier(identifier: str) -> str: def build_description_from_identifier(identifier: str) -> str:
@@ -263,8 +258,6 @@ def file_supports_unicode(file_obj: IO[AnyStr]) -> bool:
return "utf" in codec_info.name return "utf" in codec_info.name
""" # Global instance of the Logger.
Global instance of the Logger.
"""
# (there's only one stdout per-process, so a global instance is justified) # (there's only one stdout per-process, so a global instance is justified)
log = Logger() log = Logger()
+8 -12
View File
@@ -1,3 +1,4 @@
import functools
import os import os
import platform import platform
import re import re
@@ -5,7 +6,7 @@ import shutil
import subprocess import subprocess
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, NamedTuple, Sequence, Set, Tuple, cast from typing import Dict, List, NamedTuple, Sequence, Set, Tuple, cast
from filelock import FileLock from filelock import FileLock
@@ -108,7 +109,7 @@ def install_cpython(tmp: Path, version: str, url: str) -> Path:
return installation_path / "bin" / "python3" return installation_path / "bin" / "python3"
def install_pypy(tmp: Path, version: str, url: str) -> Path: def install_pypy(tmp: Path, url: str) -> Path:
pypy_tar_bz2 = url.rsplit("/", 1)[-1] pypy_tar_bz2 = url.rsplit("/", 1)[-1]
extension = ".tar.bz2" extension = ".tar.bz2"
assert pypy_tar_bz2.endswith(extension) assert pypy_tar_bz2.endswith(extension)
@@ -136,7 +137,7 @@ def setup_python(
if implementation_id.startswith("cp"): if implementation_id.startswith("cp"):
base_python = install_cpython(tmp, python_configuration.version, python_configuration.url) base_python = install_cpython(tmp, python_configuration.version, python_configuration.url)
elif implementation_id.startswith("pp"): elif implementation_id.startswith("pp"):
base_python = install_pypy(tmp, python_configuration.version, python_configuration.url) base_python = install_pypy(tmp, python_configuration.url)
else: else:
raise ValueError("Unknown Python implementation") raise ValueError("Unknown Python implementation")
assert base_python.exists() assert base_python.exists()
@@ -460,17 +461,12 @@ def build(options: Options, tmp_path: Path) -> None:
# rosetta2 will provide the emulation with just the arch prefix. # rosetta2 will provide the emulation with just the arch prefix.
arch_prefix = ["arch", "-x86_64"] arch_prefix = ["arch", "-x86_64"]
else: else:
raise RuntimeError( msg = f"don't know how to emulate {testing_arch} on {machine_arch}"
"don't know how to emulate {testing_arch} on {machine_arch}" raise RuntimeError(msg)
)
# define a custom 'call' function that adds the arch prefix each time # define a custom 'call' function that adds the arch prefix each time
def call_with_arch(*args: PathOrStr, **kwargs: Any) -> None: call_with_arch = functools.partial(call, *arch_prefix)
call(*arch_prefix, *args, **kwargs) shell_with_arch = functools.partial(shell, *arch_prefix)
def shell_with_arch(command: str, **kwargs: Any) -> None:
command = " ".join(arch_prefix) + " " + command
shell(command, **kwargs)
# Use --no-download to ensure determinism by using seed libraries # Use --no-download to ensure determinism by using seed libraries
# built into virtualenv # built into virtualenv
+15 -16
View File
@@ -1,3 +1,4 @@
import functools
import os import os
import sys import sys
import traceback import traceback
@@ -31,6 +32,7 @@ from .util import (
BuildSelector, BuildSelector,
DependencyConstraints, DependencyConstraints,
TestSelector, TestSelector,
cached_property,
resources_dir, resources_dir,
selector_matches, selector_matches,
strtobool, strtobool,
@@ -139,7 +141,8 @@ def _dig_first(*pairs: Tuple[Mapping[str, Setting], str], ignore_empty: bool = F
return value return value
raise KeyError(key) last_key = pairs[-1][1]
raise KeyError(last_key)
class OptionsReader: class OptionsReader:
@@ -310,13 +313,15 @@ class OptionsReader:
if table is None: if table is None:
raise ConfigOptionError(f"{name} does not accept a table") raise ConfigOptionError(f"{name} does not accept a table")
return table["sep"].join(table["item"].format(k=k, v=v) for k, v in result.items()) return table["sep"].join(table["item"].format(k=k, v=v) for k, v in result.items())
elif isinstance(result, list):
if isinstance(result, list):
if sep is None: if sep is None:
raise ConfigOptionError(f"{name} does not accept a list") raise ConfigOptionError(f"{name} does not accept a list")
return sep.join(result) return sep.join(result)
elif isinstance(result, int):
if isinstance(result, int):
return str(result) return str(result)
else:
return result return result
@@ -345,12 +350,10 @@ class Options:
return None return None
@property @cached_property
def package_requires_python_str(self) -> Optional[str]: def package_requires_python_str(self) -> Optional[str]:
if not hasattr(self, "_package_requires_python_str"):
args = self.command_line_arguments args = self.command_line_arguments
self._package_requires_python_str = get_requires_python_str(Path(args.package_dir)) return get_requires_python_str(Path(args.package_dir))
return self._package_requires_python_str
@property @property
def globals(self) -> GlobalOptions: def globals(self) -> GlobalOptions:
@@ -574,9 +577,7 @@ def compute_options(
return options return options
_all_pinned_docker_images: Optional[ConfigParser] = None @functools.lru_cache(maxsize=None)
def _get_pinned_docker_images() -> Mapping[str, Mapping[str, str]]: def _get_pinned_docker_images() -> Mapping[str, Mapping[str, str]]:
""" """
This looks like a dict of dicts, e.g. This looks like a dict of dicts, e.g.
@@ -585,13 +586,11 @@ def _get_pinned_docker_images() -> Mapping[str, Mapping[str, str]]:
'pypy_x86_64': {'manylinux2010': '...' } 'pypy_x86_64': {'manylinux2010': '...' }
... } ... }
""" """
global _all_pinned_docker_images
if _all_pinned_docker_images is None:
pinned_docker_images_file = resources_dir / "pinned_docker_images.cfg" pinned_docker_images_file = resources_dir / "pinned_docker_images.cfg"
_all_pinned_docker_images = ConfigParser() all_pinned_docker_images = ConfigParser()
_all_pinned_docker_images.read(pinned_docker_images_file) all_pinned_docker_images.read(pinned_docker_images_file)
return _all_pinned_docker_images return all_pinned_docker_images
def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None: def deprecated_selectors(name: str, selector: str, *, error: bool = False) -> None:
+6 -6
View File
@@ -23,11 +23,11 @@ class Analyzer(ast.NodeVisitor):
def __init__(self) -> None: def __init__(self) -> None:
self.requires_python: Optional[str] = None self.requires_python: Optional[str] = None
def visit(self, content: ast.AST) -> None: def visit(self, node: ast.AST) -> None:
for node in ast.walk(content): for inner_node in ast.walk(node):
for child in ast.iter_child_nodes(node): for child in ast.iter_child_nodes(inner_node):
child.parent = node # type: ignore[attr-defined] child.parent = inner_node # type: ignore[attr-defined]
super().visit(content) super().visit(node)
def visit_keyword(self, node: ast.keyword) -> None: def visit_keyword(self, node: ast.keyword) -> None:
self.generic_visit(node) self.generic_visit(node)
@@ -46,7 +46,7 @@ def setup_py_python_requires(content: str) -> Optional[str]:
analyzer = Analyzer() analyzer = Analyzer()
analyzer.visit(tree) analyzer.visit(tree)
return analyzer.requires_python or None return analyzer.requires_python or None
except Exception: except Exception: # pylint: disable=broad-except
return None return None
+32 -14
View File
@@ -41,6 +41,20 @@ from platformdirs import user_cache_path
from cibuildwheel.typing import Final, Literal, PathOrStr, PlatformName from cibuildwheel.typing import Final, Literal, PathOrStr, PlatformName
__all__ = [
"resources_dir",
"MANYLINUX_ARCHS",
"call",
"shell",
"format_safe",
"prepare_command",
"get_build_verbosity_extra_flags",
"read_python_configs",
"selector_matches",
"strtobool",
"cached_property",
]
resources_dir: Final = Path(__file__).parent / "resources" resources_dir: Final = Path(__file__).parent / "resources"
install_certifi_script: Final = resources_dir / "install_certifi.py" install_certifi_script: Final = resources_dir / "install_certifi.py"
@@ -118,8 +132,9 @@ def call(
def shell( def shell(
command: str, env: Optional[Dict[str, str]] = None, cwd: Optional[PathOrStr] = None *commands: str, env: Optional[Dict[str, str]] = None, cwd: Optional[PathOrStr] = None
) -> None: ) -> None:
command = " ".join(commands)
print(f"+ {command}") print(f"+ {command}")
subprocess.run(command, env=env, cwd=cwd, shell=True, check=True) subprocess.run(command, env=env, cwd=cwd, shell=True, check=True)
@@ -152,11 +167,11 @@ def format_safe(template: str, **kwargs: Any) -> str:
re.VERBOSE, re.VERBOSE,
) )
# we use a lambda for repl to prevent re.sub interpreting backslashes # we use a function for repl to prevent re.sub interpreting backslashes
# in repl as escape sequences # in repl as escape sequences.
result = re.sub( result = re.sub(
pattern=find_pattern, pattern=find_pattern,
repl=lambda _: str(value), repl=lambda _: str(value), # pylint: disable=cell-var-from-loop
string=result, string=result,
) )
@@ -288,18 +303,14 @@ def download(url: str, dest: Path) -> None:
repeat_num = 3 repeat_num = 3
for i in range(repeat_num): for i in range(repeat_num):
try: try:
response = urllib.request.urlopen(url, context=context) with urllib.request.urlopen(url, context=context) as response:
except Exception: dest.write_bytes(response.read())
return
except urllib.error.URLError:
if i == repeat_num - 1: if i == repeat_num - 1:
raise raise
sleep(3) sleep(3)
continue
break
try:
dest.write_bytes(response.read())
finally:
response.close()
class DependencyConstraints: class DependencyConstraints:
@@ -319,6 +330,7 @@ class DependencyConstraints:
specific_stem = self.base_file_path.stem + f"-python{version_parts[0]}{version_parts[1]}" specific_stem = self.base_file_path.stem + f"-python{version_parts[0]}{version_parts[1]}"
specific_name = specific_stem + self.base_file_path.suffix specific_name = specific_stem + self.base_file_path.suffix
specific_file_path = self.base_file_path.with_name(specific_name) specific_file_path = self.base_file_path.with_name(specific_name)
if specific_file_path.exists(): if specific_file_path.exists():
return specific_file_path return specific_file_path
else: else:
@@ -477,7 +489,7 @@ def _parse_constraints_for_virtualenv(
assert dependency_constraint_flags[0] == "-c" assert dependency_constraint_flags[0] == "-c"
constraint_path = Path(dependency_constraint_flags[1]) constraint_path = Path(dependency_constraint_flags[1])
assert constraint_path.exists() assert constraint_path.exists()
with constraint_path.open() as constraint_file: with constraint_path.open(encoding="utf-8") as constraint_file:
for line in constraint_file: for line in constraint_file:
line = line.strip() line = line.strip()
if len(line) == 0: if len(line) == 0:
@@ -543,3 +555,9 @@ def virtualenv(
env = os.environ.copy() env = os.environ.copy()
env["PATH"] = os.pathsep.join(paths + [env["PATH"]]) env["PATH"] = os.pathsep.join(paths + [env["PATH"]])
return env return env
if sys.version_info >= (3, 8):
from functools import cached_property
else:
from .functools_cached_property_38 import cached_property
+15 -1
View File
@@ -1,15 +1,19 @@
import os
import shutil import shutil
import sys import sys
from pathlib import Path from pathlib import Path
import nox import nox
nox.options.sessions = ["lint", "check_manifest", "tests"] nox.options.sessions = ["lint", "pylint", "check_manifest", "tests"]
PYTHON_ALL_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10"] PYTHON_ALL_VERSIONS = ["3.6", "3.7", "3.8", "3.9", "3.10"]
DIR = Path(__file__).parent.resolve() DIR = Path(__file__).parent.resolve()
if os.environ.get("CI", None):
nox.options.error_on_missing_interpreters = True
@nox.session @nox.session
def tests(session: nox.Session) -> None: def tests(session: nox.Session) -> None:
@@ -34,6 +38,16 @@ def lint(session: nox.Session) -> None:
session.run("pre-commit", "run", "--all-files", *session.posargs) session.run("pre-commit", "run", "--all-files", *session.posargs)
@nox.session
def pylint(session: nox.Session) -> None:
"""
Run pylint.
"""
session.install("pylint", ".")
session.run("pylint", "cibuildwheel", *session.posargs)
@nox.session @nox.session
def check_manifest(session: nox.Session) -> None: def check_manifest(session: nox.Session) -> None:
""" """
+34
View File
@@ -84,3 +84,37 @@ ignore = [
"requirements-dev.txt", "requirements-dev.txt",
"noxfile.py", "noxfile.py",
] ]
[tool.pylint]
master.py-version = "3.6"
master.jobs = "0"
master.fail-on = ["E", "F"]
master.fail-under = "9.8"
reports.output-format = "colorized"
messages_control.enable = [
"useless-suppression",
]
messages_control.disable = [
"abstract-class-instantiated", # filelock triggers this
"duplicate-code",
"fixme",
"invalid-name",
"line-too-long",
"missing-class-docstring",
"missing-function-docstring",
"missing-module-docstring",
"no-else-break",
"no-else-return",
"protected-access",
"too-few-public-methods",
"too-many-arguments",
"too-many-branches",
"too-many-instance-attributes",
"too-many-lines",
"too-many-locals",
"too-many-nested-blocks",
"too-many-return-statements",
"too-many-statements",
"unsubscriptable-object",
"wrong-import-position",
]
-1
View File
@@ -32,7 +32,6 @@ extras = {
"types-pyyaml", "types-pyyaml",
"types-click", "types-click",
"types-requests", "types-requests",
"types-toml",
], ],
} }