feat: migrate from pylint/black to ruff (#19)

* feat: migrate from pylint/black to ruff

* test: verify python package distribution build when running make test
This commit is contained in:
Muhammad Labeeb 2025-08-28 19:00:47 +05:00 committed by GitHub
parent 4b7de83a72
commit c79d196a8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 50 additions and 21 deletions

View File

@ -18,6 +18,8 @@ jobs:
uses: actions/setup-python@v5 uses: actions/setup-python@v5
with: with:
python-version: ${{ matrix.python-version }} python-version: ${{ matrix.python-version }}
- name: Install sass
run: npm install -g sass
- name: Install dependencies - name: Install dependencies
run: | run: |
pip install .[dev] pip install .[dev]

View File

@ -26,5 +26,5 @@ def load_about() -> dict[str, str]:
with open( with open(
os.path.join(HERE, "tutordeck", "__about__.py"), "rt", encoding="utf-8" os.path.join(HERE, "tutordeck", "__about__.py"), "rt", encoding="utf-8"
) as f: ) as f:
exec(f.read(), about) # pylint: disable=exec-used exec(f.read(), about)
return about return about

View File

@ -1,7 +1,6 @@
.DEFAULT_GOAL := help .DEFAULT_GOAL := help
.PHONY: docs .PHONY: docs
SRC_DIRS = ./tutordeck SRC_DIRS = ./tutordeck
BLACK_OPTS = --exclude templates ${SRC_DIRS}
runserver: ## Run a development server runserver: ## Run a development server
tutor deck runserver --dev tutor deck runserver --dev
@ -13,22 +12,28 @@ scss-watch: ## Compile SCSS files to CSS and watch for changes
$(MAKE) scss SASS_OPTS="--watch" $(MAKE) scss SASS_OPTS="--watch"
# Warning: These checks are not necessarily run on every PR. # Warning: These checks are not necessarily run on every PR.
test: test-lint test-types test-format # Run some static checks. test: test-lint test-types test-format test-pythonpackage # Run some static checks.
test-format: ## Run code formatting tests test-format: ## Run code formatting tests
black --check --diff $(BLACK_OPTS) ruff format --check --diff ${SRC_DIRS}
test-lint: ## Run code linting tests test-lint: ## Run code linting tests
pylint --errors-only --enable=unused-import,unused-argument --ignore=templates --ignore=docs/_ext ${SRC_DIRS} ruff check ${SRC_DIRS}
test-types: ## Run type checks. test-types: ## Run type checks.
mypy --exclude=templates --ignore-missing-imports --implicit-reexport --strict ${SRC_DIRS} mypy --exclude=templates --ignore-missing-imports --implicit-reexport --strict ${SRC_DIRS}
format: ## Format code automatically build-pythonpackage: ## Build the "tutor-deck" python package for upload to pypi
black $(BLACK_OPTS) python -m build --sdist
isort: ## Sort imports. This target is not mandatory because the output may be incompatible with black formatting. Provided for convenience purposes. test-pythonpackage: build-pythonpackage ## Test that package can be uploaded to pypi
isort --skip=templates ${SRC_DIRS} twine check dist/tutor_deck-$(shell make version).tar.gz
format: ## Format code automatically
ruff format ${SRC_DIRS}
fix-lint: ## Fix lint errors automatically
ruff check --fix ${SRC_DIRS}
changelog-entry: ## Create a new changelog entry. changelog-entry: ## Create a new changelog entry.
scriv create scriv create
@ -36,6 +41,9 @@ changelog-entry: ## Create a new changelog entry.
changelog: ## Collect changelog entries in the CHANGELOG.md file. changelog: ## Collect changelog entries in the CHANGELOG.md file.
scriv collect scriv collect
version: ## Print the current tutor-deck version
@python -c 'import io, os; about = {}; exec(io.open(os.path.join("tutordeck", "__about__.py"), "rt", encoding="utf-8").read(), about); print(about["__version__"])'
ESCAPE =  ESCAPE = 
help: ## Print this help help: ## Print this help
@grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \ @grep -E '^([a-zA-Z_-]+:.*?## .*|######* .+)$$' Makefile \

View File

@ -0,0 +1,2 @@
- [Improvement] Migrate from pylint and black to ruff. (by @mlabeeb03)
- [Improvement] Test python package distribution build when running make test. (by @mlabeeb03)

View File

@ -42,8 +42,7 @@ dev = [
"tutor[dev]>=20.0.0,<21.0.0", "tutor[dev]>=20.0.0,<21.0.0",
"types-aiofiles", "types-aiofiles",
"types-Markdown", "types-Markdown",
"pylint", "ruff",
"black",
] ]
[project.entry-points."tutor.plugin.v1"] [project.entry-points."tutor.plugin.v1"]
@ -75,3 +74,19 @@ path = ".hatch_build.py"
[tool.hatch.build.targets.wheel] [tool.hatch.build.targets.wheel]
packages = ["tutordeck"] packages = ["tutordeck"]
[tool.ruff]
exclude = ["templates", "docs/_ext"]
[tool.ruff.lint]
# E: pycodestyle errors
# I: isort
# N: pep8-naming
select = ["E", "I", "N"]
# F401: unused-import
# F841: unused-variable
# W292: missing-newline-at-end-of-file
extend-select = ["F401", "F841", "W292"]
[tool.ruff.format]

View File

@ -18,14 +18,13 @@ from quart import (
url_for, url_for,
) )
from quart.typing import ResponseTypes from quart.typing import ResponseTypes
from werkzeug.sansio.response import Response as BaseResponse
from tutor.plugins.v1 import discover_package from tutor.plugins.v1 import discover_package
from werkzeug.sansio.response import Response as BaseResponse
from tutordeck.server.utils import current_page_plugins, pagination_context from tutordeck.server.utils import current_page_plugins, pagination_context
from . import constants, tutorclient from . import constants, tutorclient
app = Quart( app = Quart(
__name__, __name__,
static_url_path="/static", static_url_path="/static",
@ -372,8 +371,9 @@ async def process_config_update_request() -> None:
cmd = ["config", "save"] cmd = ["config", "save"]
for key, value in form.items(): for key, value in form.items():
if value.startswith("{{"): if value.startswith("{{"):
# Templated values that start with {{ should be explicitely converted to string # Templated values that start with {{ should be explicitely
# Otherwise there will be a parsing error because it might be considered a dictionary # converted to string otherwise there will be a parsing
# error because it might be considered a dictionary
value = f"'{value}'" value = f"'{value}'"
cmd.extend(["--set", f"{key}={value}"]) cmd.extend(["--set", f"{key}={value}"])
tutorclient.CliPool.run_sequential(cmd) tutorclient.CliPool.run_sequential(cmd)
@ -489,9 +489,9 @@ def update_plugins_requiring_launch(
response: Response, add: t.Optional[str] = None, remove: t.Optional[str] = None response: Response, add: t.Optional[str] = None, remove: t.Optional[str] = None
) -> None: ) -> None:
""" """
Store the list of plugins for which a recent set of changes require running "local launch". Store the list of plugins for which a recent set of changes require
running "local launch". This list is stored as a "+"-separated string
This list is stored as a "+"-separated string in a cookie. Note that flask will automatically put the content in quotes. in a cookie. Note that flask will automatically put the content in quotes.
""" """
# Note that comma, colon and semi-colon are not supported in cookie values # Note that comma, colon and semi-colon are not supported in cookie values
separator = "+" separator = "+"

View File

@ -50,7 +50,9 @@ class Project:
@classmethod @classmethod
def get_user_config(cls) -> Config: def get_user_config(cls) -> Config:
""" """
TODO load config dynamically from root anytime it is changed on disk? Maybe take the chance to clear sys.modules cache on reload? TODO
Load config dynamically from root anytime it is changed on disk?
Maybe take the chance to clear sys.modules cache on reload?
""" """
return tutor.config.get_user(cls.ROOT) return tutor.config.get_user(cls.ROOT)
@ -131,8 +133,8 @@ class Cli:
The first item is the log file path. Second item is the running command. The first item is the log file path. Second item is the running command.
This will handle gracefully file deletion. Note however that if the file is This will handle gracefully file deletion. Note however that if the file is
truncated, all contents added to the beginning until the current position will be truncated, all contents added to the beginning until the current position
missed. will be missed.
""" """
yield f"$ {self.command}\n" yield f"$ {self.command}\n"
async with aiofiles.open(self.log_path, "rb") as f: async with aiofiles.open(self.log_path, "rb") as f: