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

Use ruff instead of black and flake8 #599

Merged
merged 13 commits into from
Sep 26, 2024
14 changes: 9 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ jobs:
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install dev dependencies
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U -e .[lint]
- name: Flake8
pip install ruff
- name: Ruff lint
run: |
ruff check --output-format=github .
- name: Ruff format
run: |
flake8 .
ruff format --check .

test-codegen-build:
name: Test Codegen
Expand Down Expand Up @@ -140,7 +143,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -U -e .[tests] glfw pyinstaller
pip install -U -e .
pip install -U pytest numpy psutil pyinstaller glfw
- name: Test PyInstaller
run: |
pyinstaller --version
Expand Down
13 changes: 6 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,9 +120,8 @@ This code is distributed under the 2-clause BSD license.
binaries.
* You can use `python tools/download_wgpu_native.py` when needed.
* Or point the `WGPU_LIB_PATH` environment variable to a custom build of `wgpu-native`.
* Use `black .` to apply autoformatting.
* Use `flake8 .` to check for flake errors.
* Use `pytest .` to run the tests.
* Use `ruff format` to apply autoformatting.
* Use `ruff check` to check for linting errors.


### Updating to a later version of WebGPU or wgpu-native
Expand All @@ -136,11 +135,11 @@ for more information.

The test suite is divided into multiple parts:

* `pytest -v tests` runs the core unit tests.
* `pytest -v tests` runs the unit tests.
* `pytest -v examples` tests the examples.
* `pytest -v wgpu/__pyinstaller` tests if wgpu is properly supported by
pyinstaller.
* `pytest -v codegen` lints the generated binding code.
* `pytest -v wgpu/__pyinstaller` tests if wgpu is properly supported by pyinstaller.
* `pytest -v codegen` tests the code that autogenerates the API.
* `pytest -v tests_mem` tests against memoryleaks.

There are two types of tests for examples included:

Expand Down
2 changes: 1 addition & 1 deletion codegen/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
sys.path.insert(0, os.path.abspath(os.path.join(__file__, "..", "..")))


from codegen import main, file_cache # noqa: E402
from codegen import main, file_cache


if __name__ == "__main__":
Expand Down
9 changes: 4 additions & 5 deletions codegen/apipatcher.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
spec (IDL), and the backend implementations from the base API.
"""

from codegen.utils import print, blacken, to_snake_case, to_camel_case, Patcher
from codegen.utils import print, format_code, to_snake_case, to_camel_case, Patcher
from codegen.idlparser import get_idl_parser
from codegen.files import file_cache

Expand Down Expand Up @@ -268,7 +268,6 @@ def __init__(self):
self.detect_async_props_and_methods()

def detect_async_props_and_methods(self):

self.async_idl_names = async_idl_names = {} # (sync-name, async-name)

for classname, interface in self.idl.classes.items():
Expand Down Expand Up @@ -434,13 +433,13 @@ def get_method_def(self, classname, methodname):
py_args = [self._arg_from_struct_field(field) for field in fields]
if py_args[0].startswith("label: str"):
py_args[0] = 'label=""'
py_args = ["self", "*"] + py_args
py_args = ["self", "*", *py_args]
else:
py_args = ["self"] + argnames
py_args = ["self", *argnames]

# Construct final def
line = preamble + ", ".join(py_args) + "): pass\n"
line = blacken(line, True).split("):")[0] + "):"
line = format_code(line, True).split("):")[0] + "):"
return " " + line

def _arg_from_struct_field(self, field):
Expand Down
8 changes: 4 additions & 4 deletions codegen/apiwriter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import re

from codegen.utils import print, blacken, to_snake_case
from codegen.utils import print, format_code, to_snake_case
from codegen.idlparser import get_idl_parser
from codegen.files import file_cache

Expand Down Expand Up @@ -59,7 +59,7 @@ def write_flags():
pylines.append(f" {key} = {val!r}") # note: can add docs using "#: "
pylines.append("\n")
# Write
code = blacken("\n".join(pylines))
code = format_code("\n".join(pylines))
file_cache.write("flags.py", code)
print(f"Wrote {n} flags to flags.py")

Expand Down Expand Up @@ -88,7 +88,7 @@ def write_enums():
pylines.append(f' {key} = "{val}"') # note: can add docs using "#: "
pylines.append("\n")
# Write
code = blacken("\n".join(pylines))
code = format_code("\n".join(pylines))
file_cache.write("enums.py", code)
print(f"Wrote {n} enums to enums.py")

Expand Down Expand Up @@ -135,6 +135,6 @@ def write_structs():
pylines.append(")\n")

# Write
code = blacken("\n".join(pylines))
code = format_code("\n".join(pylines))
file_cache.write("structs.py", code)
print(f"Wrote {n} structs to structs.py")
2 changes: 1 addition & 1 deletion codegen/idlparser.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ def _remove_comments(self, text):
return "\n".join(lines)

def resolve_type(self, typename):
"""Resolve a type to a suitable name that is also valid so that flake8
"""Resolve a type to a suitable name that is also valid so that the linter
wont complain when this is used as a type annotation.
"""

Expand Down
8 changes: 3 additions & 5 deletions codegen/tests/test_codegen_apipatcher.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
""" Test some parts of apipatcher.py, and Implicitly tests idlparser.py.
"""
"""Test some parts of apipatcher.py, and Implicitly tests idlparser.py."""

from codegen.utils import blacken
from codegen.utils import format_code
from codegen.apipatcher import CommentRemover, AbstractCommentInjector, IdlPatcherMixin


Expand Down Expand Up @@ -101,7 +100,7 @@ def spam(self):
def eggs(self):
pass
"""
code3 = blacken(dedent(code3)).strip()
code3 = format_code(dedent(code3)).strip()

p = MyCommentInjector()
p.apply(dedent(code1))
Expand All @@ -111,7 +110,6 @@ def eggs(self):


def test_async_api_logic():

class Object(object):
pass

Expand Down
3 changes: 1 addition & 2 deletions codegen/tests/test_codegen_result.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
""" Test some aspects of the generated code.
"""
"""Test some aspects of the generated code."""

from codegen.files import read_file

Expand Down
3 changes: 1 addition & 2 deletions codegen/tests/test_codegen_rspatcher.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
""" Test some parts of rsbackend.py, and implicitly tests hparser.py.
"""
"""Test some parts of rsbackend.py, and implicitly tests hparser.py."""

from codegen.wgpu_native_patcher import patch_wgpu_native_backend

Expand Down
22 changes: 11 additions & 11 deletions codegen/tests/test_codegen_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from codegen.utils import (
remove_c_comments,
blacken,
format_code,
Patcher,
to_snake_case,
to_camel_case,
Expand Down Expand Up @@ -59,7 +59,7 @@ def test_remove_c_comments():
assert code2 == code3


def test_blacken_singleline():
def test_format_code_singleline():
code1 = """
def foo():
pass
Expand Down Expand Up @@ -98,20 +98,20 @@ def foo(a1, a2, a3):
code1 = dedent(code1).strip()
code2 = dedent(code2).strip()

code3 = blacken(code1, True)
code3 = format_code(code1, True)
code3 = code3.replace("\n\n", "\n").replace("\n\n", "\n").strip()

assert code3 == code2

# Also test simply long lines
code = "foo = 1" + " + 1" * 100
assert len(code) > 300
code = "foo = 1" + " + 1" * 75
assert len(code) > 300 # Ruff's max line-length is 320
assert code.count("\n") == 0
assert blacken(code, False).strip().count("\n") > 3
assert blacken(code, True).strip().count("\n") == 0
assert format_code(code, False).strip().count("\n") > 3
assert format_code(code, True).strip().count("\n") == 0


def test_blacken_comments():
def test_format_code_comments():
code1 = """
def foo(): # hi
pass
Expand All @@ -133,7 +133,7 @@ def foo(a1, a2, a3): # hi ha ho
code1 = dedent(code1).strip()
code2 = dedent(code2).strip()

code3 = blacken(code1, True)
code3 = format_code(code1, True)
code3 = code3.replace("\n\n", "\n").replace("\n\n", "\n").strip()

assert code3 == code2
Expand All @@ -160,7 +160,7 @@ def bar3(self):
pass
"""

code = blacken(dedent(code))
code = format_code(dedent(code))
p = Patcher(code)

# Dump before doing anything, should yield original
Expand Down Expand Up @@ -201,7 +201,7 @@ def bar3(self):
for line, i in p.iter_lines():
if line.lstrip().startswith("#"):
p.replace_line(i, "# comment")
with raises(Exception):
with raises(AssertionError):
p.replace_line(i, "# comment")
code2 = p.dumps()
assert code2.count("#") == 4
Expand Down
50 changes: 35 additions & 15 deletions codegen/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@
import os
import sys
import tempfile

import black
import subprocess


def to_snake_case(name):
Expand Down Expand Up @@ -48,7 +47,7 @@ def print(*args, **kwargs):
"""Report something (will be printed and added to a file."""
# __builtins__.print(*args, **kwargs)
if args and not args[0].lstrip().startswith("#"):
args = ("*",) + args
args = ("*", *args)
for f in _file_objects_to_print_to:
__builtins__["print"](*args, file=f, flush=True, **kwargs)

Expand Down Expand Up @@ -103,14 +102,35 @@ def remove_c_comments(code):
return new_code


def blacken(src, singleline=False):
"""Format the given src string using black. If singleline is True,
all function signatures become single-line, so they can be parsed
and updated.
class FormatError(Exception):
pass


def format_code(src, singleline=False):
"""Format the given src string. If singleline is True, all function
signatures become single-line, so they can be parsed and updated.
"""
# Normal black
mode = black.FileMode(line_length=999 if singleline else 88)
result = black.format_str(src, mode=mode)

# Use Ruff to format the line. Ruff does not yet have a Python API, so we use its CLI.
tempfilename = os.path.join(tempfile.gettempdir(), "wgpupy_codegen_format.py")
with open(tempfilename, "wb") as fp:
fp.write(src.encode())
line_length = 320 if singleline else 88
cmd = [
sys.executable,
"-m",
"ruff",
"format",
"--line-length",
str(line_length),
tempfilename,
]
p = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if p.returncode:
raise FormatError(p.stdout.decode(errors="ignore"))
with open(tempfilename, "rb") as fp:
result = fp.read().decode()
os.remove(tempfilename)

# Make defs single-line. You'd think that setting the line length
# to a very high number would do the trick, but it does not.
Expand Down Expand Up @@ -175,7 +195,7 @@ def _init(self, code):
self._diffs = {}
self._classes = {}
if code:
self.lines = blacken(code, True).splitlines() # inf line length
self.lines = format_code(code, True).splitlines() # inf line length

def remove_line(self, i):
"""Remove the line at the given position. There must not have been
Expand Down Expand Up @@ -221,8 +241,8 @@ def dumps(self, format=True):
text = "\n".join(lines)
if format:
try:
text = blacken(text)
except black.InvalidInput as err: # pragma: no cover
text = format_code(text)
except FormatError as err: # pragma: no cover
# If you get this error, it really helps to load the code
# in an IDE to see where the error is. Let's help with that ...
filename = os.path.join(tempfile.gettempdir(), "wgpu_patcher_fail.py")
Expand All @@ -233,8 +253,8 @@ def dumps(self, format=True):
raise RuntimeError(
f"It appears that the patcher has generated invalid Python:"
f"\n\n {err}\n\n"
f'Wrote the generated (but unblackened) code to:\n\n "{filename}"'
)
f'Wrote the generated (but unformatted) code to:\n\n "{filename}"'
) from None

return text

Expand Down
Loading