From ddcce72282361a584912642cff1af7f0e899ad26 Mon Sep 17 00:00:00 2001 From: Danil Kostromin Date: Tue, 16 Jun 2026 21:07:00 +0300 Subject: [PATCH 1/8] ci: use Ruff for Python quality checks Changes: - Replace separate Black, Flynt, Flake8, and Ruff CI jobs with a single Ruff quality check job. - Simplify `scripts/pyformat` so it uses Ruff only for formatting and linting. - Keep pre-commit wired through `scripts/pyformat -c`, but rename the hook to `Ruff Python Quality`. - Remove replaced formatting/lint dependencies from `requirements.txt`. - Update `scripts/pyanalyze` and README docs to reflect Ruff-based checks. Signed-off-by: Danil Kostromin --- .github/workflows/ci.yaml | 46 +++--------------- .pre-commit-config.yaml | 4 +- README.md | 33 ++++++++++--- pyproject.toml | 14 ++++-- requirements.txt | 12 ----- scripts/pyanalyze | 6 +-- scripts/pyformat | 98 ++++++++++----------------------------- 7 files changed, 73 insertions(+), 140 deletions(-) diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index a2f46b342..4705a5d45 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,26 +23,8 @@ jobs: bublik/data/migrations/** bublik/analytics/migrations/** - format-check: - name: Check Code Formatting - needs: detect-changes - runs-on: ubuntu-latest - if: ${{ needs.detect-changes.outputs.python_files != '' }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install Black - run: pip install $(grep '^black==' requirements.txt) - - - name: Run Black in check mode - run: | - echo "${{ needs.detect-changes.outputs.python_files }}" | xargs black --check --diff - - lint-check: - name: Check Code Quality + ruff-check: + name: Check Python Quality needs: detect-changes runs-on: ubuntu-latest if: ${{ needs.detect-changes.outputs.python_files != '' }} @@ -53,26 +35,12 @@ jobs: python-version: '3.12' - name: Install Ruff - run: pip install ruff + run: pip install $(grep '^ruff' requirements.txt) - - name: Run Ruff + - name: Run Ruff formatter in check mode run: | - echo "${{ needs.detect-changes.outputs.python_files }}" | xargs ruff check - - f-string-check: - name: Check F-String Usage - needs: detect-changes - runs-on: ubuntu-latest - if: ${{ needs.detect-changes.outputs.python_files != '' }} - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: '3.12' - - - name: Install Flynt - run: pip install flynt + echo "${{ needs.detect-changes.outputs.python_files }}" | xargs ruff format --check --diff - - name: Run Flynt in check mode + - name: Run Ruff linter run: | - echo "${{ needs.detect-changes.outputs.python_files }}" | xargs flynt -f + echo "${{ needs.detect-changes.outputs.python_files }}" | xargs ruff check diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 07f6e8ebe..7a2ec2eda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,8 +1,8 @@ repos: - repo: local hooks: - - id: run-my-script - name: Run My Script + - id: ruff-python-quality + name: Ruff Python Quality entry: ./scripts/pyformat language: script types: [python] diff --git a/README.md b/README.md index e71b894b4..c3b9e5ce6 100644 --- a/README.md +++ b/README.md @@ -42,23 +42,44 @@ For now some documentation can be found in **doc/wiki** here. # Development -## Pre-commit checkings +## Pre-commit checks -After initial deploy please run the: +Pre-commit installs a local Git hook that runs before `git commit` creates a +commit. In this project, the hook runs Ruff on changed Python files via: +``` +./scripts/pyformat -c +``` + +Install the hook once after setting up the repository: ``` pre-commit install ``` -This will allow the pre-commit tool to run `./scripts/pyformat -c` before each -commit. -Please note that you can always disable the pre-commit validation by running: +After that, every `git commit` will automatically run: +``` +ruff format --check --diff +ruff check +``` + +If the hook fails, fix the reported issues and run `git commit` again. To apply +Ruff formatting and autofixes manually, run: +``` +./scripts/pyformat +``` + +You can run the hook manually for all files with: +``` +pre-commit run --all-files +``` + +You can disable the local hook with: ``` pre-commit uninstall ``` ## Checking your changes -You can use pyformat script to check your changes. +You can use pyformat script to run Ruff formatting and lint checks. For this you need to run: ``` diff --git a/pyproject.toml b/pyproject.toml index 6e9ff08dd..e9fe20cca 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,12 @@ [tool.ruff] line-length = 96 target-version = 'py38' +# Excluded from both the linter and the formatter: Django migrations are +# historical artifacts and reformatting them adds noise while hurting +# `git blame`. force-exclude keeps them excluded even when passed explicitly +# (e.g. by the pre-commit hook). +force-exclude = true +extend-exclude = ['migrations'] [tool.ruff.lint] select = [ @@ -80,6 +86,8 @@ select = [ ignore = [ # overwriting some variables (seems to be usefull in some cases) 'PLW2901', + # Ruff formatter handles trailing commas. + 'COM812', ] exclude = [ '.env', @@ -155,7 +163,5 @@ max-branches = 30 max-statements = 100 max-returns = 7 -[tool.black] -line-length = 96 -target-version = ['py38'] -skip-string-normalization = true +[tool.ruff.format] +quote-style = 'single' diff --git a/requirements.txt b/requirements.txt index e12a91e12..82c06a83c 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,11 +7,9 @@ asn1crypto==0.24.0 astor==0.8.1 attrs==23.1.0 autoflake==1.4 -autopep8==1.4.3 Babel==2.16.0 beautifulsoup4==4.7.1 billiard==4.2.2 -black==26.3.1 celery==5.5.3 certifi==2024.8.30 cffi==2.0.0 @@ -36,9 +34,7 @@ djangorestframework==3.15.2 drf-spectacular-sidecar==2024.11.1 drf-spectacular==0.27.2 fastmcp==3.2.0 -flake8==3.9.2 flower==2.0.1 -flynt==0.64 future==1.0.0 gitdb==4.0.2 GitPython==3.1.50 @@ -48,11 +44,9 @@ httplib2==0.22.0 idna==3.10 importlab==0.8.1 importlib-metadata==8.7.1 -isort==5.8.0 jsonschema==4.26.0 kombu==5.5.4 Markdown==3.9 -mccabe==0.6.1 more-itertools==8.1.0 networkx ninja==1.10.2.3 @@ -60,19 +54,15 @@ ordered-set packaging==24.1 pendulum==3.0.0 pep517==0.12.0 -pep8==1.7.1 pip-review==1.0 pip-tools pipdeptree==2.2.1 pre-commit==3.3.2 psycopg2-binary==2.9.10 py-key-value-aio[disk]==0.4.4 -pycodestyle==2.7.0 pycparser==2.19 pydot==2.0.0 -pyflakes==2.3.1 pykerberos==1.2.4 -pylint==3.3.8 pyparsing==3.1.2 pytest==9.0.3 python-dateutil==2.9.0 @@ -98,11 +88,9 @@ toml==0.10.2 tomli tornado==6.5.5 treelib==1.6.1 -unify==0.5 urllib3==2.7.0 uvicorn[standard] vine==5.1.0 xlwt==1.3.0 yamale==5.2.1 -yapf==0.31.0 zipp==3.20.2 diff --git a/scripts/pyanalyze b/scripts/pyanalyze index fb9603769..a9604e3e6 100755 --- a/scripts/pyanalyze +++ b/scripts/pyanalyze @@ -11,11 +11,9 @@ status=0 pip show -q pytype && pytype $@ status=$(( $status | $? )) -# Flake8 checks Python code compliance to PEP8, McCabe complexity and english grammar. -# Displays the warnings in a per-file, merged output. -# https://flake8.pycqa.org/en/latest/index.html. +# Ruff checks Python code style, imports, complexity, and common lint issues. # -pip show -q flake8 && flake8 $@ +pip show -q ruff && ruff check $@ status=$(( $status | $? )) # Pyanalize status. diff --git a/scripts/pyformat b/scripts/pyformat index 3c2c66622..49653486e 100755 --- a/scripts/pyformat +++ b/scripts/pyformat @@ -2,11 +2,7 @@ # SPDX-License-Identifier: Apache-2.0 # Copyright (C) 2016-2023 OKTET Labs Ltd. All rights reserved. -do_black=true -do_flynt=true -do_unify=true -do_ruff=true -ruff_fix="--fix" +do_check=false do_verbose=false # Files to be fixed @@ -27,11 +23,7 @@ $0 Options: - -n : switch to 'enable' mode - only enabled things will be done - -b : do only black - -f : do f-strings - -s : unify strings - -r : run ruff + -v : verbose mode -c : check-only! EOF } @@ -46,30 +38,8 @@ while [ -n "$1" ] ; do -v) do_verbose=true ;; - -n) - do_black=false - do_flynt=false - do_unify=false - do_ruff=false - ;; -c) do_check=true - black_options="--check" - flynt_options="-f" - unify_options="-c" - ruff_fix="" - ;; - -b) - do_black=true - ;; - -f) - do_flynt=true - ;; - -s) - do_unify=true - ;; - -r) - do_ruff=true ;; -h|--help) help @@ -89,52 +59,34 @@ while [ -n "$1" ] ; do shift done -if $do_unify; then - # Unifies strings - # - echo "Running unify" - which unify 2>&1 >/dev/null && try unify $unify_options -r --quote \' $FILES - unify_status=$? - [ $unify_status -eq 0 ] || echo "Unify failed" - status=$(( $status || $unify_status )) -fi - -if $do_flynt; then - # Converts a code from old "%-formatted" and .format(...) strings into "f-strings". - # https://github.com/ikamensh/flynt. - # - which flynt 2>&1 >/dev/null && try flynt $flynt_options -q $FILES - flynt_status=$? - [ $flynt_status -eq 0 ] || echo "Flynt failed" - status=$(( $status || $flynt_status )) +if ! which ruff >/dev/null 2>&1; then + echo "The ruff executable is not available" + exit 1 fi -if $do_black; then - # Format Python code. Makes changes to files in place. - # https://github.com/psf/black. - # - echo "Running black" - which black 2>&1 >/dev/null && \ - try black $black_options --skip-string-normalization $FILES - black_status=$? - [ $black_status -eq 0 ] || echo "Black failed" - status=$(( $status || $black_status )) +if $do_check; then + echo "Running ruff format check" + try ruff format --check --diff $FILES + format_status=$? +else + echo "Running ruff format" + try ruff format $FILES + format_status=$? fi +[ $format_status -eq 0 ] || echo "Ruff format failed" +status=$(( status || format_status )) -if $do_ruff; then - # Run ruff - . - # https://github.com/charliermarsh/ruff - # - if python -c "import ruff"; then - echo "Running ruff" - which ruff 2>&1 >/dev/null && try ruff check $FILES $ruff_fix - ruff_status=$? - [ $ruff_status -eq 0 ] || echo "Ruff failed" - status=$(( $status || $ruff_status )) - else - echo "The ruff is not installed" - fi +if $do_check; then + echo "Running ruff check" + try ruff check $FILES + ruff_status=$? +else + echo "Running ruff check with fixes" + try ruff check --fix $FILES + ruff_status=$? fi +[ $ruff_status -eq 0 ] || echo "Ruff check failed" +status=$(( status || ruff_status )) # Pyformat status. # From 15bc81ace45bee0c590be4529e220e0cd892e634 Mon Sep 17 00:00:00 2001 From: Danil Kostromin Date: Tue, 16 Jun 2026 21:07:19 +0300 Subject: [PATCH 2/8] style: configure Ruff docstring quote handling as advised in PEP 257 Link: https://peps.python.org/pep-0257/ Signed-off-by: Danil Kostromin --- pyproject.toml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e9fe20cca..a3d461d42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,6 +88,9 @@ ignore = [ 'PLW2901', # Ruff formatter handles trailing commas. 'COM812', + # Ruff formatter handles multiline string and docstring quotes. + 'Q001', + 'Q002', ] exclude = [ '.env', @@ -146,8 +149,8 @@ exclude = [ [tool.ruff.lint.flake8-quotes] avoid-escape = false inline-quotes = 'single' -docstring-quotes = 'single' -multiline-quotes = 'single' +docstring-quotes = 'double' +multiline-quotes = 'double' [tool.ruff.lint.mccabe] max-complexity = 16 From 4fa0a0256e98fbcafeedbe557090e0822784f6d0 Mon Sep 17 00:00:00 2001 From: Danil Kostromin Date: Thu, 18 Jun 2026 03:48:58 +0300 Subject: [PATCH 3/8] style: normalize Python docstring quotes Signed-off-by: Danil Kostromin --- bublik/core/auth.py | 4 +- bublik/core/cache.py | 8 +- bublik/core/config/reformatting/dispatcher.py | 4 +- bublik/core/config/reformatting/steps.py | 52 ++++---- bublik/core/dashboard/services.py | 32 ++--- bublik/core/datetime_formatting.py | 4 +- bublik/core/fields.py | 4 +- bublik/core/filter_backends.py | 8 +- bublik/core/hash_system.py | 10 +- bublik/core/history/services.py | 40 +++--- bublik/core/importruns/categorization.py | 4 +- bublik/core/importruns/live/context.py | 36 +++--- bublik/core/importruns/live/plan_tracking.py | 30 ++--- bublik/core/importruns/milog.py | 8 +- bublik/core/importruns/telog.py | 36 +++--- bublik/core/job_task/services.py | 12 +- bublik/core/log/services.py | 16 +-- bublik/core/logging.py | 2 +- bublik/core/mail.py | 8 +- bublik/core/measurement/filters.py | 4 +- bublik/core/measurement/representation.py | 56 ++++----- bublik/core/measurement/services.py | 16 +-- bublik/core/pagination_helpers.py | 4 +- bublik/core/project/services.py | 34 ++--- bublik/core/report/components.py | 32 ++--- bublik/core/report/services.py | 28 ++--- bublik/core/result/services.py | 24 ++-- bublik/core/routers.py | 4 +- bublik/core/run/external_links.py | 8 +- bublik/core/run/fields.py | 2 +- bublik/core/run/filter_expression.py | 4 +- bublik/core/run/metadata.py | 4 +- bublik/core/run/services.py | 72 +++++------ bublik/core/run/stats.py | 12 +- bublik/core/server/services.py | 4 +- bublik/core/shortcuts.py | 4 +- bublik/core/testing_coverage.py | 4 +- bublik/core/tree/representation.py | 6 +- bublik/core/tree/services.py | 8 +- bublik/core/utils.py | 12 +- bublik/data/managers/result.py | 8 +- bublik/data/models/__init__.py | 4 +- bublik/data/models/config.py | 12 +- bublik/data/models/endpoint_url.py | 4 +- bublik/data/models/eventlog.py | 8 +- bublik/data/models/expectation.py | 12 +- bublik/data/models/measurement.py | 32 ++--- bublik/data/models/meta.py | 20 +-- bublik/data/models/project.py | 4 +- bublik/data/models/reference.py | 8 +- bublik/data/models/result.py | 92 +++++++------- bublik/data/models/user.py | 18 +-- bublik/data/serializers/config.py | 8 +- bublik/data/serializers/expectation.py | 10 +- bublik/interfaces/api_v2/comments.py | 12 +- bublik/interfaces/api_v2/config/schemas.py | 28 ++--- bublik/interfaces/api_v2/config/views.py | 4 +- bublik/interfaces/api_v2/dashboard.py | 24 ++-- bublik/interfaces/api_v2/importruns/views.py | 18 +-- bublik/interfaces/api_v2/job_task/schemas.py | 8 +- .../interfaces/api_v2/job_task/serializers.py | 8 +- bublik/interfaces/api_v2/log.py | 12 +- bublik/interfaces/api_v2/performance.py | 4 +- bublik/interfaces/api_v2/project.py | 4 +- bublik/interfaces/api_v2/report.py | 8 +- bublik/interfaces/api_v2/url_shortener.py | 4 +- bublik/interfaces/celery/tasks.py | 4 +- .../management/commands/add_tags.py | 10 +- .../commands/assign_project_by_meta.py | 20 +-- .../management/commands/cleanup_db.py | 20 +-- .../management/commands/delete_run.py | 6 +- .../commands/fix_result_timestamps.py | 40 +++--- .../management/commands/initialize_configs.py | 4 +- .../commands/meta_categorization.py | 4 +- .../management/commands/migrate_configs.py | 8 +- .../management/commands/reformat_configs.py | 4 +- .../management/commands/reset_run_statuses.py | 6 +- .../management/commands/run_cache.py | 4 +- .../commands/update_all_hashed_objects.py | 2 +- bublik/mcp/models.py | 44 +++---- bublik/mcp/processor.py | 88 ++++++------- bublik/mcp/tests/conftest.py | 24 ++-- .../mcp/tests/test_log_processor_snapshots.py | 72 +++++------ bublik/mcp/tools.py | 116 +++++++++--------- bublik/wsgi.py | 4 +- scripts/generate_secret_key.py | 4 +- scripts/import_logs_converter.py | 20 +-- 87 files changed, 769 insertions(+), 769 deletions(-) diff --git a/bublik/core/auth.py b/bublik/core/auth.py index 12d721dcb..9a4469cc4 100644 --- a/bublik/core/auth.py +++ b/bublik/core/auth.py @@ -70,9 +70,9 @@ def wrapper(*args, **kwargs): def check_action_permission(action): - ''' + """ Check if the action requires permission. - ''' + """ def wrapper(func): @wraps(func) diff --git a/bublik/core/cache.py b/bublik/core/cache.py index 66f6fd805..87ca1f7cc 100644 --- a/bublik/core/cache.py +++ b/bublik/core/cache.py @@ -159,12 +159,12 @@ def tests(self): class _ProjectSectionCache: - ''' + """ Base class for project section caches. Each section cache stores data under keys like 'project:{project_id}:{SECTION}:{data_key}'. - ''' + """ SECTION: ClassVar[str] | None = None KEY_DATA_CHOICES: ClassVar[set[str]] | None = None @@ -220,7 +220,7 @@ class _TagsCache(_ProjectSectionCache): } def load(self): - ''' + """ Populate tags cache for the project. Each cache entry stores a mapping of meta tag IDs to their string @@ -235,7 +235,7 @@ def load(self): Value format: {meta_id: 'tag_name=tag_value'} - ''' + """ tags = models.Meta.objects.filter(type='tag') diff --git a/bublik/core/config/reformatting/dispatcher.py b/bublik/core/config/reformatting/dispatcher.py index 7929e8de2..61cde24b5 100644 --- a/bublik/core/config/reformatting/dispatcher.py +++ b/bublik/core/config/reformatting/dispatcher.py @@ -62,9 +62,9 @@ def update_config(reformatted_config): class ConfigReformatStatuses(str, Enum): - ''' + """ All available config reformatting statuses. - ''' + """ SUCCESS = 'success' SKIPPED = 'skipped' diff --git a/bublik/core/config/reformatting/steps.py b/bublik/core/config/reformatting/steps.py index ff7b08096..c5430314c 100644 --- a/bublik/core/config/reformatting/steps.py +++ b/bublik/core/config/reformatting/steps.py @@ -12,10 +12,10 @@ class BaseReformatStep: def apply(self, config, **kwargs): - ''' + """ Reformats the provided content if it has not been reformatted yet, and outputs the execution status. Returns the content. - ''' + """ try: if not self.applied(config, **kwargs): config = self.reformat(config, **kwargs) @@ -28,25 +28,25 @@ def apply(self, config, **kwargs): raise err def applied(self, config, **kwargs): - ''' + """ Checks whether the step has already been applied. - ''' + """ msg = 'Subclasses must implement `applied`.' raise NotImplementedError(msg) def reformat(self, config, **kwargs): - ''' + """ Reformats the provided content. - ''' + """ msg = 'Subclasses must implement `reformat`.' raise NotImplementedError(msg) class UpdateAxisXStructure(BaseReformatStep): - ''' + """ Reformat passed report config content: "axis_x": -> "axis_x": {"arg": } - ''' + """ def applied(self, config, **kwargs): content = config.content @@ -68,12 +68,12 @@ def reformat(self, config, **kwargs): class UpdateSeqSettingsStructure(BaseReformatStep): - ''' + """ Reformat passed report config content: Add the 'sequences' foreign key for all sequences settings (sequence_group_arg, percentage_base_value, sequence_name_conversion). Rename 'sequence_name_conversion' to 'arg_vals_labels'. - ''' + """ def applied(self, config, **kwargs): content = config.content @@ -109,11 +109,11 @@ def reformat(self, config, **kwargs): class UpdateDashboardHeaderStructure(BaseReformatStep): - ''' + """ Reformat passed global per_conf config content: DASHBOARD_HEADER: {: