-
Notifications
You must be signed in to change notification settings - Fork 1
Replace pylint with ruff for repository linting, modernize packaging, and update CI for Python 3.10+ with uv sync #23
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
Merged
AlexeyKozhevin
merged 13 commits into
master
from
copilot/fix-ae0638ec-bc45-4a4b-bb2c-83a3564e5ed7
Oct 21, 2025
Merged
Changes from 3 commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
1d16220
Initial plan
Copilot 0c863e8
Replace pylint with ruff for notebook linting
Copilot 04b5255
Remove old pylintrc file and complete ruff migration
Copilot 1d5331e
Use ruff for repository linting instead of replacing pylint_notebook
Copilot be08aa8
Replace pylint disable comments with ruff noqa and remove others
Copilot ea229af
Replace setup.py with pyproject.toml for uv compatibility
Copilot e91e426
Update workflows for Python 3.10+ and add uv installation tests
Copilot 90d1ca3
Use uv sync instead of uv pip for more efficient dependency management
Copilot b3c09f9
Fix cross-platform compatibility by using uv run instead of manual ve…
Copilot 5ddf9cf
Replace pynvml by nvidia-ml-py
AlexeyKozhevin eb8d914
Bump version
AlexeyKozhevin e1795e5
Add __version__ attr
AlexeyKozhevin 3c608c4
Add linter group
AlexeyKozhevin File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,6 @@ | ||
| """ Init file. """ | ||
| #pylint: disable=wildcard-import | ||
| from .core import * | ||
| from .exec_notebook import exec_notebook, run_notebook | ||
| from .pylint_notebook import pylint_notebook | ||
| from .ruff_notebook import ruff_notebook, pylint_notebook | ||
|
|
||
| __version__ = '0.9.14' |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,234 @@ | ||
| """ Functions for code quality control of Jupyter Notebooks using ruff. """ | ||
| import os | ||
| import subprocess | ||
| import tempfile | ||
| from .core import StringWithDisabledRepr, get_notebook_path, notebook_to_script | ||
|
|
||
|
|
||
| RUFF_TOML_TEMPLATE = """line-length = {max_line_length} | ||
|
|
||
| [lint] | ||
| select = [ | ||
| "F", # Pyflakes | ||
| "E", # pycodestyle (Error) | ||
| "W", # pycodestyle (Warning) | ||
| "N", # pep8-naming | ||
| "RET", # flake8-return | ||
| "S", # flake8-bandit | ||
| "SLF", # flake8-self | ||
| "BLE", # flake8-blind-except | ||
| "UP", # pyupgrade | ||
| "YTT", # flake8-2020 | ||
| ] | ||
|
|
||
| ignore = [ | ||
| {ignore_rules} | ||
| ] | ||
|
|
||
| [lint.per-file-ignores] | ||
| "__init__.py" = ["F401"] # unused-import | ||
| "utils_notebook.py" = ["F401"] # unused-import | ||
| """ | ||
|
|
||
|
|
||
| def generate_ruff_toml(path, ignore=(), max_line_length=120, **ruff_params): | ||
| """ Create `ruff.toml` file. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| path : str | ||
| Path to save the file. | ||
| ignore : sequence | ||
| Which checks to ignore. Each element should be a rule code. | ||
| max_line_length : int | ||
| Allowed line length. | ||
| ruff_params : dict | ||
| Additional parameters for ruff configuration. | ||
| """ | ||
| ignore = [ignore] if isinstance(ignore, str) else ignore | ||
|
|
||
| # Build the full ignore list including defaults | ||
| default_ignore = [ | ||
| "FBT", # flake8-boolean-trap | ||
| "E402", # module-import-not-at-top-of-file | ||
| "E731", # lambda-assignment | ||
| "F403", # undefined-local-with-import-star | ||
| "F405", # undefined-local-with-import-star-usage | ||
| "UP015", # redundant-open-modes | ||
| "RET504", # unnecessary-assign | ||
| "NPY002", # numpy-legacy-random | ||
| "S101", | ||
| "S301", | ||
| "S102", | ||
| ] | ||
|
|
||
| all_ignore = default_ignore + list(ignore) | ||
| ignore_str = ',\n '.join(f'"{rule}"' for rule in all_ignore) | ||
|
|
||
| ruff_toml = RUFF_TOML_TEMPLATE.format( | ||
| ignore_rules=ignore_str, | ||
| max_line_length=max_line_length | ||
| ) | ||
|
|
||
| with open(path, 'w', encoding='utf-8') as file: | ||
| file.write(ruff_toml) | ||
|
|
||
| return ruff_toml | ||
|
|
||
|
|
||
| def ruff_notebook(path=None, config=None, ignore=(), printer=print, | ||
| remove_files=True, return_info=False, **ruff_params): | ||
| """ Execute ``ruff`` for a provided Jupyter Notebook. | ||
|
|
||
| Under the hood, roughly does the following: | ||
| - Creates a ``ruff.toml`` file next to the ``path``, if needed. | ||
| - Converts the notebook to `.py` file next to the ``path``. | ||
| - Runs ``ruff`` with the configuration. | ||
| - Create a report and display it, if needed. | ||
|
|
||
| Parameters | ||
| ---------- | ||
| path : str, optional | ||
| Path to the Jupyter notebook. If not provided, the current notebook is used. | ||
| config : str, None | ||
| Path to a ruff config file. If not provided, a temporary one is created. | ||
| printer : callable or None | ||
| Function to display the report. | ||
| remove_files : bool | ||
| Whether to remove temporary files after execution. | ||
| return_info : bool | ||
| Whether to return a dictionary with intermediate results. | ||
| ignore : sequence | ||
| Which rules to ignore. Each element should be a rule code (e.g., 'E402'). | ||
| ruff_params : dict | ||
| Additional parameters for ruff configuration. | ||
| """ | ||
| try: | ||
| subprocess.run(['ruff', '--version'], check=True, capture_output=True) | ||
| except (subprocess.CalledProcessError, FileNotFoundError) as exception: | ||
| raise ImportError('Install ruff') from exception | ||
|
|
||
| path = path or get_notebook_path() | ||
| if path is None: | ||
| raise ValueError('Provide path to Jupyter Notebook or run `ruff_notebook` inside of it!') | ||
|
|
||
| # Convert notebook to a script | ||
| path_script = os.path.splitext(path)[0] + '.py' | ||
| script_name = os.path.basename(path_script) | ||
|
|
||
| code, cell_line_numbers = notebook_to_script(path_notebook=path, path_script=path_script, return_info=True).values() | ||
|
|
||
| # Create ruff config file | ||
| if config is None: | ||
| path_ruff_toml = os.path.splitext(path)[0] + '.ruff.toml' | ||
| ruff_toml = generate_ruff_toml(path_ruff_toml, ignore=ignore, **ruff_params) | ||
| else: | ||
| path_ruff_toml = config | ||
| # Open config for output | ||
| if return_info: | ||
| with open(path_ruff_toml, 'r', encoding='utf-8') as configfile: | ||
| ruff_toml = configfile.read() | ||
|
|
||
| # Run ruff on script with configuration | ||
| try: | ||
| result = subprocess.run([ | ||
| 'ruff', 'check', path_script, '--config', path_ruff_toml, '--output-format', 'full' | ||
| ], capture_output=True, text=True, check=False) | ||
|
|
||
| report = result.stdout | ||
| errors = result.stderr | ||
| except Exception as e: | ||
| report = "" | ||
| errors = str(e) | ||
|
|
||
| # Prepare custom report | ||
| output = [] | ||
|
|
||
| if not report.strip() and not errors.strip(): | ||
| output.append("No issues found.") | ||
| else: | ||
| # Parse ruff's full format output | ||
| lines = report.split('\n') | ||
| current_error = None | ||
|
|
||
| for line in lines: | ||
| if not line.strip(): | ||
| continue | ||
|
|
||
| # Look for error code lines like "E401 [*] Multiple imports on one line" | ||
| if line and not line.startswith(' ') and not line.startswith('-->') and not line.startswith('|') and not line.startswith('help:'): | ||
| # This is an error header line | ||
| current_error = {'code': '', 'message': '', 'line': 0, 'cell': -1} | ||
| parts = line.split(' ', 2) | ||
| if len(parts) >= 2: | ||
| current_error['code'] = parts[0] | ||
| if len(parts) >= 3: | ||
| # Remove [*] if present and get message | ||
| message = parts[2] | ||
| if message.startswith('[*] '): | ||
| message = message[4:] | ||
| current_error['message'] = message | ||
|
|
||
| # Look for location lines like "--> /tmp/test_notebook.py:9:1" | ||
| elif line.strip().startswith('-->') and current_error is not None: | ||
| location_part = line.strip()[4:].strip() # Remove "-> " | ||
| if path_script in location_part: | ||
| try: | ||
| # Extract line number from "filename:line:col" | ||
| filename_part = location_part.split(':') | ||
| if len(filename_part) >= 2: | ||
| code_line_number = int(filename_part[1]) | ||
| current_error['line'] = code_line_number | ||
|
|
||
| # Locate the cell and line inside the cell | ||
| for cell_number, cell_ranges in cell_line_numbers.items(): | ||
| if code_line_number in cell_ranges: | ||
| cell_line_number = code_line_number - cell_ranges[0] | ||
| current_error['cell'] = cell_number | ||
| current_error['cell_line'] = cell_line_number | ||
| break | ||
| else: | ||
| current_error['cell'] = -1 | ||
| current_error['cell_line'] = code_line_number | ||
|
|
||
| # Add to output | ||
| message = f'Cell {current_error["cell"]}:{current_error["cell_line"]}, code={current_error["code"]}' | ||
| message += f'\n Ruff message ::: {current_error["message"]}\n' | ||
| output.append(message) | ||
| except (ValueError, IndexError): | ||
| pass | ||
|
|
||
| if errors.strip(): | ||
| output.append(f'\nRuff errors:\n{errors}') | ||
|
|
||
| output_text = '\n'.join(output).strip() | ||
|
|
||
| if remove_files: | ||
| if os.path.exists(path_script): | ||
| os.remove(path_script) | ||
| if config is None and os.path.exists(path_ruff_toml): | ||
| os.remove(path_ruff_toml) | ||
|
|
||
| if printer is not None: | ||
| printer(output_text) | ||
|
|
||
| if return_info: | ||
| enumerated_code = code.split('\n') | ||
| n_digits = len(str(len(enumerated_code))) | ||
| enumerated_code = [f'{i:0>{n_digits}} ' + item | ||
| for i, item in enumerate(enumerated_code, start=1)] | ||
| enumerated_code = '\n'.join(enumerated_code) | ||
|
|
||
| return { | ||
| 'report': StringWithDisabledRepr(output_text), | ||
| 'code': StringWithDisabledRepr(code), | ||
| 'enumerated_code': StringWithDisabledRepr(enumerated_code), | ||
| 'ruff_toml': StringWithDisabledRepr(ruff_toml if config is None else ''), | ||
| 'ruff_errors': StringWithDisabledRepr(errors), | ||
| 'ruff_report': StringWithDisabledRepr(report), | ||
| } | ||
| return None | ||
|
|
||
|
|
||
| # Backward compatibility alias | ||
| pylint_notebook = ruff_notebook |
This file was deleted.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| line-length = 120 | ||
|
|
||
| [lint] | ||
| select = [ | ||
| "F", # Pyflakes | ||
| "E", # pycodestyle (Error) | ||
| "W", # pycodestyle (Warning) | ||
| "N", # pep8-naming | ||
| "RET", # flake8-return | ||
| "S", # flake8-bandit | ||
| "SLF", # flake8-self | ||
| "BLE", # flake8-blind-except | ||
| "UP", # pyupgrade | ||
| "YTT", # flake8-2020 | ||
|
|
||
| # TODO: | ||
| # "I", | ||
| # "D", # pydocstyle | ||
| # "B", # flake8-bugbear | ||
| # "A", # flake8-builtins | ||
| # "NPY" # NumPy-specific rules | ||
| ] | ||
|
|
||
| ignore = [ | ||
| # Groups of rules | ||
| "FBT", # flake8-boolean-trap | ||
|
|
||
| # Single rules | ||
| "E402", # module-import-not-at-top-of-file | ||
| "E731", # lambda-assignment | ||
| "F403", # undefined-local-with-import-star | ||
| "F405", # undefined-local-with-import-star-usage | ||
| "UP015", # redundant-open-modes | ||
| "RET504", # unnecessary-assign | ||
| "NPY002", # numpy-legacy-random | ||
| "S101", | ||
| "S301", | ||
| "S102", | ||
| ] | ||
|
|
||
| [lint.per-file-ignores] | ||
| "__init__.py" = ["F401"] # unused-import | ||
| "utils_notebook.py" = ["F401"] # unused-import |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -31,6 +31,7 @@ | |
| 'blessed>=1.17', | ||
| 'psutil>=5.6', | ||
| 'requests>=2.24', | ||
| 'ruff>=0.1.0', | ||
| ], | ||
| extras_require={ | ||
| 'nbrun': [ | ||
|
|
||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't replace pylint_noteboook function but keep ruff for linting nbtools itself.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@copilot
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I've updated the implementation based on your feedback. Instead of replacing the
pylint_notebookfunction, I've preserved it completely and used ruff only for linting the nbtools repository code itself.Changes made:
pylint_notebookfunctionality (kept original intact).github/workflows/status.ymlto use ruff instead of pylint for repository lintingThe
pylint_notebookfunction now remains unchanged while ruff is used to lint the nbtools codebase. Commit: 04b5255