From 875b7454428522c2b716e1b41045452ef9b804c8 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 11:34:25 -0500 Subject: [PATCH 01/31] Replace Pylint with Ruff linter --- .pylintrc | 502 ------------------------------ .run/commands/pylint.sh | 38 --- .run/commands/ruffautofix.sh | 9 + .run/commands/ruffformat.sh | 2 +- .run/commands/rufflint.sh | 25 ++ .run/tasks/checks.sh | 4 +- .run/tasks/format.sh | 1 + .run/tasks/{pylint.sh => lint.sh} | 6 +- Changelog | 2 +- DEVELOPER.md | 36 +-- docs/conf.py | 3 - poetry.lock | 93 +----- pyproject.toml | 130 +++++++- src/tests/uciparse/test_uci.py | 2 - src/uciparse/cli.py | 1 - src/uciparse/uci.py | 10 +- 16 files changed, 197 insertions(+), 667 deletions(-) delete mode 100644 .pylintrc delete mode 100644 .run/commands/pylint.sh create mode 100644 .run/commands/ruffautofix.sh create mode 100644 .run/commands/rufflint.sh rename .run/tasks/{pylint.sh => lint.sh} (66%) diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index bcd825e..0000000 --- a/.pylintrc +++ /dev/null @@ -1,502 +0,0 @@ -# Pylint configuration file, originally generated from pylint --generate-rcfile -# vim: set ft=conf: - -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code. -extension-pkg-whitelist= - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the -# number of processors available to use. -jobs=1 - -# Control the amount of potential inferred values when inferring a single -# object. This can help the performance when dealing with large functions or -# complex, nested conditions. -limit-inference-results=100 - -# List of plugins (as comma separated values of python module names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages. -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once). You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use "--disable=all --enable=classes -# --disable=W". -disable=fixme, - c-extension-no-member, - missing-class-docstring, - missing-module-docstring, - missing-function-docstring, - unused-wildcard-import, - trailing-whitespace, - too-few-public-methods, - no-else-return, - no-else-raise, - duplicate-code, - consider-using-f-string - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable= - - -[REPORTS] - -# Python expression which should return a score less than or equal to 10. You -# have access to the variables 'error', 'warning', 'refactor', and 'convention' -# which contain the number of messages in each category, as well as 'statement' -# which is the total number of statements analyzed. This score is used by the -# global evaluation report (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details. -# KJP: this has been customized to make paths clickable in IntelliJ -msg-template={abspath}:{line}:{column} - {C} - ({symbol}) - {msg} - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio). You can also give a reporter class, e.g. -# mypackage.mymodule.MyReporterClass. -output-format=colorized - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=sys.exit - - -[LOGGING] - -# Format style used to check logging format string. `old` means using % -# formatting, `new` is for `{}` formatting,and `fstr` is for f-strings. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes. -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it work, -# install the python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains the private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to the private dictionary (see the -# --spelling-private-dict-file option) instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members= - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# Tells whether to warn about missing members when the owner of the attribute -# is inferred to be None. -ignore-none=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis). It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules= - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - -# List of decorators that change the signature of a decorated function. -signature-mutators= - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid defining new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expected to -# not be used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored|^unused - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore. -ignored-argument-names=_.*|^ignored|^unused - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=132 - -# Maximum number of lines in a module. -max-module-lines=1000 - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=yes - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. -#argument-rgx= - -# Naming style matching correct attribute names. -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=snake_case - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. -#class-attribute-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. -#class-rgx= - -# Naming style matching correct constant names. -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=e, - f, - i, - j, - k, - p, - r, - id, - _ - -# Include a hint for the correct naming format with invalid-name. -include-naming-hint=no - -# Naming style matching correct inline iteration names. -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style. -#inlinevar-rgx= - -# Naming style matching correct method names. -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style. -#method-rgx= - -# Naming style matching correct module names. -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style. -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -# These decorators are taken in consideration only for invalid-name. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. -#variable-rgx= - - -[STRING] - -# This flag controls whether the implicit-str-concat-in-sequence should -# generate a warning on implicit string concatenation in sequences defined over -# several lines. -check-str-concat-over-line-jumps=no - - -[IMPORTS] - -# List of modules that can be imported at any level, not just the top level -# one. -allow-any-import-level= - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma. -deprecated-modules=optparse,tkinter.tix - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled). -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled). -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled). -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - -# Couples of modules and preferred modules, separated by a comma. -preferred-modules= - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__ - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected= - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=cls - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=10 - -# Maximum number of attributes for a class (see R0902). -max-attributes=10 - -# Maximum number of boolean expressions in an if statement (see R0916). -max-bool-expr=5 - -# Maximum number of branch for function / method body. -max-branches=12 - -# Maximum number of locals for function / method body. -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body. -max-returns=10 - -# Maximum number of statements in function / method body. -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "BaseException, Exception". -overgeneral-exceptions=builtins.BaseException, - builtins.Exception diff --git a/.run/commands/pylint.sh b/.run/commands/pylint.sh deleted file mode 100644 index 57a9d13..0000000 --- a/.run/commands/pylint.sh +++ /dev/null @@ -1,38 +0,0 @@ -# vim: set ft=bash ts=3 sw=3 expandtab: -# Run the Pylint code checker - -# We generate relative paths in the output to make integration with Pycharm work better - -command_pylint() { - local OPTIND OPTARG option tests template - - echo "Running pylint checks..." - - template="{path}:{line}:{column} - {C} - ({symbol}) - {msg}" - - if [ -f "tests/__init__.py" ]; then - tests="tests" - elif [ -f "src/tests/__init__.py" ]; then - tests="src/tests" - else - tests="" - fi - - while getopts "t" option; do - case $option in - t) - echo "Tests will be ignored" - tests="" # -t means to ignore the tests - ;; - ?) - echo "invalid option -$OPTARG" - exit 1 - ;; - esac - done - - shift $((OPTIND -1)) # pop off the options consumed by getopts - - poetry_run pylint --msg-template="$template" -j 0 "$@" $(ls -d src/*) $tests - echo "done" -} diff --git a/.run/commands/ruffautofix.sh b/.run/commands/ruffautofix.sh new file mode 100644 index 0000000..b16ef84 --- /dev/null +++ b/.run/commands/ruffautofix.sh @@ -0,0 +1,9 @@ +# vim: set ft=bash ts=3 sw=3 expandtab: +# Run the Ruff linter, applying automatic fixes only + +command_ruffautofix() { + echo "Applying Ruff automatic fixes..." + CLICOLOR_FORCE=1 poetry_run ruff check --fix --fix-only + echo "done" +} + diff --git a/.run/commands/ruffformat.sh b/.run/commands/ruffformat.sh index 05e9e07..b6100fd 100644 --- a/.run/commands/ruffformat.sh +++ b/.run/commands/ruffformat.sh @@ -3,7 +3,7 @@ command_ruffformat() { echo "Running Ruff formatter..." - poetry_run ruff format "$@" + CLICOLOR_FORCE=1 poetry_run ruff format "$@" echo "done" } diff --git a/.run/commands/rufflint.sh b/.run/commands/rufflint.sh new file mode 100644 index 0000000..72e0f4e --- /dev/null +++ b/.run/commands/rufflint.sh @@ -0,0 +1,25 @@ +# vim: set ft=bash ts=3 sw=3 expandtab: +# Run the Ruff linter with no automatic fixes + +# The command line below is a bit of a hack. The goal is to generate output +# that's compatible with the PyCharm output filter, which expects this: +# +# $FILE_PATH$:$LINE$ +# +# However, right now ruff generates some extra stuff at the front of the line, +# (" --> "), which needs to be stripped so PyCharm will recognize the pattern. +# In theory it seems like it should be possible to do this entirely within the +# PyCharm output filter, without needing sed, but I haven't been able to make +# it work. +# +# Note that the extra .* in the regex below is needed to handle the ANSI color +# escape sequences, which aren't immediately obvious. +# +# See: https://github.com/astral-sh/ruff/issues/19983 + +command_rufflint() { + echo "Running Ruff linter..." + CLICOLOR_FORCE=1 poetry_run ruff check --no-fix | sed 's/ .*-->.* //' + echo "done" +} + diff --git a/.run/tasks/checks.sh b/.run/tasks/checks.sh index ce741a9..9afa60b 100644 --- a/.run/tasks/checks.sh +++ b/.run/tasks/checks.sh @@ -10,8 +10,8 @@ task_checks() { echo "" run_command ruffformat --check echo "" - run_command mypy + run_command rufflint echo "" - run_command pylint + run_command mypy } diff --git a/.run/tasks/format.sh b/.run/tasks/format.sh index 5cc55a7..a35d929 100644 --- a/.run/tasks/format.sh +++ b/.run/tasks/format.sh @@ -6,5 +6,6 @@ help_format() { task_format() { run_command ruffformat + run_command ruffautofix } diff --git a/.run/tasks/pylint.sh b/.run/tasks/lint.sh similarity index 66% rename from .run/tasks/pylint.sh rename to .run/tasks/lint.sh index af36d1f..6d9a94c 100644 --- a/.run/tasks/pylint.sh +++ b/.run/tasks/lint.sh @@ -1,11 +1,11 @@ # vim: set ft=bash sw=3 ts=3 expandtab: -help_pylint() { +help_lint() { # No help - exists for PyCharm integration echo -n "" } -task_pylint() { - run_command pylint +task_lint() { + run_command rufflint } diff --git a/Changelog b/Changelog index dfd8fef..957462d 100644 --- a/Changelog +++ b/Changelog @@ -7,7 +7,7 @@ Version 0.1.27 unreleased * Add a new `run clean` target to clean up generated data. * Move unit tests from `tests` into `src/tests/uciparse`. * Update the MyPy configuration so we're using latest rules. - * Replace black and isort tools with the Ruff formatter. + * Replace black, isort, and pylint with the Ruff formatter and linter. * Update the jinja2 transitive dependency to address CVE-2025-27516. * Update the requests transitive dependency to address CVE-2024-47081. * Update the urllib3 transitive dependency to address CVE-2025-50181. diff --git a/DEVELOPER.md b/DEVELOPER.md index 8a2dcc6..bf9cf42 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -8,7 +8,7 @@ This code should work equivalently on MacOS, Linux, and Windows. This project uses [Poetry v2](https://python-poetry.org/) to manage Python packaging and dependencies. Most day-to-day tasks (such as running unit tests from the command line) are orchestrated through Poetry. -A coding standard is enforced using [Ruff](https://docs.astral.sh/ruff/) and [Pylint](https://pypi.org/project/pylint/). Python 3 type hinting is validated using [MyPy](https://pypi.org/project/mypy/). +A coding standard is enforced using [Ruff](https://docs.astral.sh/ruff/). Python 3 type hinting is validated using [MyPy](https://pypi.org/project/mypy/). ## Pre-Commit Hooks @@ -187,7 +187,7 @@ Go to the PyCharm settings and find the `uci-parse` project. Under folder. In the **Exclude Files** box, enter the following: ``` -LICENSE;NOTICE;PyPI.md;build;dist;docs/_build;out;poetry.lock;poetry.toml;run;.coverage;.coverage.lcov;.coveragerc;.gitattributes;.github;.gitignore;.htmlcov;.idea;.mypy_cache;.poetry;.pre-commit-config.yaml;.pylintrc;.pytest_cache;.python-version;.readthedocs.yml;.run;.tabignore;.venv +LICENSE;NOTICE;PyPI.md;build;dist;docs/_build;out;poetry.lock;poetry.toml;run;.coverage;.coverage.lcov;.coveragerc;.gitattributes;.github;.gitignore;.htmlcov;.idea;.mypy_cache;.poetry;.pre-commit-config.yaml;.pytest_cache;.python-version;.readthedocs.yml;.ruff_cache;.run;.tabignore;.venv ``` When you're done, click **Ok**. Then, go to the gear icon in the project panel @@ -217,11 +217,11 @@ run configuration before PyCharm will find the right test suite. ### External Tools Optionally, you might want to set up external tools for some of common -developer tasks: code reformatting and the PyLint and MyPy checks. One nice +developer tasks: code reformatting and the Ruff and MyPy checks. One nice advantage of doing this is that you can configure an output filter, which makes -the Pylint and MyPy errors clickable. To set up external tools, go to PyCharm -settings and find **Tools > External Tools**. Add the tools as described -below. +the Ruff linter and MyPy errors clickable. To set up external tools, go to +PyCharm settings and find **Tools > External Tools**. Add the tools as +described below. #### Linux or MacOS @@ -272,23 +272,23 @@ source ~/.bash_profile |Open console for tool outout|_Checked_| |Make console active on message in stdout|_Checked_| |Make console active on message in stderr|_Checked_| -|Output filters|`$FILE_PATH$:$LINE$:$COLUMN$:.*`| +|Output filters|`$FILE_PATH$:$LINE$`| -##### Run Pylint Checks +##### Run Ruff Linter |Field|Value| |-----|-----| -|Name|`Run Pylint Checks`| -|Description|`Run the Pylint code checks`| +|Name|`Run Ruff Linter`| +|Description|`Run the Ruff linter code checks`| |Group|`Developer Tools`| |Program|`$ProjectFileDir$/run`| -|Arguments|`pylint`| +|Arguments|`lint`| |Working directory|`$ProjectFileDir$`| |Synchronize files after execution|_Unchecked_| |Open console for tool outout|_Checked_| |Make console active on message in stdout|_Checked_| |Make console active on message in stderr|_Checked_| -|Output filters|`$FILE_PATH$:$LINE$:$COLUMN.*`| +|Output filters|`$FILE_PATH$:$LINE$`| #### Windows @@ -328,23 +328,23 @@ change the path for `bash.exe`. |Open console for tool outout|_Checked_| |Make console active on message in stdout|_Checked_| |Make console active on message in stderr|_Checked_| -|Output filters|`$FILE_PATH$:$LINE$:$COLUMN$:.*`| +|Output filters|`$FILE_PATH$:$LINE$`| -##### Run Pylint Checks +##### Run Ruff Linter |Field|Value| |-----|-----| -|Name|`Run Pylint Checks`| -|Description|`Run the Pylint code checks`| +|Name|`Run Ruff Linter`| +|Description|`Run the Ruff linter code checks`| |Group|`Developer Tools`| |Program|`powershell.exe`| -|Arguments|`& 'C:\Program Files\Git\bin\bash.exe' -l "./run" pylint | Out-String`| +|Arguments|`& 'C:\Program Files\Git\bin\bash.exe' -l "./run" lint | Out-String`| |Working directory|`$ProjectFileDir$`| |Synchronize files after execution|_Unchecked_| |Open console for tool outout|_Checked_| |Make console active on message in stdout|_Checked_| |Make console active on message in stderr|_Checked_| -|Output filters|`$FILE_PATH$:$LINE$:$COLUMN.*`| +|Output filters|`$FILE_PATH$:$LINE$`| ## Release Process diff --git a/docs/conf.py b/docs/conf.py index 27e6bc3..9fd882e 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -# pylint: skip-file -# # UCI Parse documentation build configuration file, based on existing # documentation for Requests (https://github.com/psf/requests). # diff --git a/poetry.lock b/poetry.lock index abf75c7..9543257 100644 --- a/poetry.lock +++ b/poetry.lock @@ -17,14 +17,14 @@ files = [ name = "astroid" version = "3.3.8" description = "An abstract syntax tree for Python with inference support." -optional = false +optional = true python-versions = ">=3.9.0" -groups = ["main", "dev"] +groups = ["main"] +markers = "extra == \"docs\"" files = [ {file = "astroid-3.3.8-py3-none-any.whl", hash = "sha256:187ccc0c248bfbba564826c26f070494f7bc964fd286b6d9fff4420e55de828c"}, {file = "astroid-3.3.8.tar.gz", hash = "sha256:a88c7994f914a4ea8572fac479459f4955eeccc877be3f2d959a33273b0cf40b"}, ] -markers = {main = "extra == \"docs\""} [package.dependencies] typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} @@ -260,22 +260,6 @@ files = [ [package.extras] toml = ["tomli"] -[[package]] -name = "dill" -version = "0.3.9" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, - {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - [[package]] name = "distlib" version = "0.3.9" @@ -415,21 +399,6 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] -[[package]] -name = "isort" -version = "5.13.2" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.8.0" -groups = ["dev"] -files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, -] - -[package.extras] -colors = ["colorama (>=0.4.6)"] - [[package]] name = "jinja2" version = "3.1.6" @@ -521,18 +490,6 @@ files = [ {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - [[package]] name = "mypy" version = "1.14.1" @@ -698,36 +655,6 @@ files = [ [package.extras] windows-terminal = ["colorama (>=0.4.6)"] -[[package]] -name = "pylint" -version = "3.3.3" -description = "python code static checker" -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "pylint-3.3.3-py3-none-any.whl", hash = "sha256:26e271a2bc8bce0fc23833805a9076dd9b4d5194e2a02164942cb3cdc37b4183"}, - {file = "pylint-3.3.3.tar.gz", hash = "sha256:07c607523b17e6d16e2ae0d7ef59602e332caa762af64203c24b41c27139f36a"}, -] - -[package.dependencies] -astroid = ">=3.3.8,<=3.4.0-dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = [ - {version = ">=0.2", markers = "python_version < \"3.11\""}, - {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, -] -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - [[package]] name = "pytest" version = "8.3.4" @@ -1103,18 +1030,6 @@ files = [ ] markers = {main = "extra == \"docs\" and python_version < \"3.11\"", dev = "python_version < \"3.11\""} -[[package]] -name = "tomlkit" -version = "0.13.2" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" @@ -1195,4 +1110,4 @@ docs = ["importlib-metadata", "sphinx", "sphinx-autoapi"] [metadata] lock-version = "2.1" python-versions = ">=3.10,<4" -content-hash = "c5acbb8b26737b4dffcb0312cea63228c5dc265a8d1c4a96d10027e79c0b8ecb" +content-hash = "160c5eda719b41724a5eb2bea683d2c5a145e1f3813252aa798f47729530aa73" diff --git a/pyproject.toml b/pyproject.toml index dd82194..22ed2e8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,6 @@ docs = [ pytest = ">=8.0.2,<9.0.0" pytest-testdox = ">=3.1.0,<4.0.0" coverage = ">=7.4.4,<8.0.0" -pylint = ">=3.0.1,<4.0.0" pre-commit = ">=4.0.1,<5.0.0" mypy = ">=1.6.0,<2.0.0" colorama = ">=0.4.6,<1.0.0" @@ -97,6 +96,135 @@ line-ending = "lf" docstring-code-format = true docstring-code-line-length = 80 +[tool.ruff.lint.flake8-tidy-imports] +ban-relative-imports = "all" + +[tool.ruff.lint.flake8-pytest-style] +fixture-parentheses = false +mark-parentheses = false +parametrize-names-type = "csv" +parametrize-values-type = "list" +parametrize-values-row-type = "list" + +# Note: requires TID251 to be selected below +[tool.ruff.lint.flake8-tidy-imports.banned-api] +"conftest".msg = "use 'tests.conftest' instead" +"unittest.TestCase".msg = "use pytest, not unittest" + +[tool.ruff.lint] +select = [ + # Start with all Ruff linter rules, and exclude some specific rules and categories (see below) + "ALL" +] + +ignore = [ + # Ruff-recommended exclusions, not needed because we're using the Ruff formatter + "COM812", # missing-trailing-comma + "COM819", # prohibited-trailing-comma + "D206", # docstring-tab-indentation + "D300", # triple-single-quotes + "E111", # indentation-with-invalid-multiple + "E114", # indentation-with-invalid-multiple-comment + "E117", # over-indented + "E501", # line-too-long + "Q000", # bad-quotes-inline-string + "Q001", # bad-quotes-multiline-string + "Q002", # bad-quotes-docstring + "Q003", # avoidable-escaped-quote + "W191", # tab-indentation + + # Exclusions of entire rule categories, which we don't think are worth enforcing + "C90", # mccabe cyclomatic complexity + "CPY", # flake8-copyright + "D", # pydocstyle + "DJ", # flake8-django + "DOC", # pydoclint + "ERA", # eradicate + "EXE", # flake8-executable + "FIX", # flake8-fixme + "PTH", # flake8-use-pathlib + "TD", # flake8-todo + + # Exclusions of specific rules, which we don't think are worth enforcing + "ANN401", # allow dynamically typed expressions using `typing.Any` + "EM101", # allow raw string messages in exceptions; having to pull out a variable makes code harder to read + "EM102", # allow f-string messages in exceptions; having to pull out a variable makes code harder to read + "FURB140", # don't automatically convert generator expressions to use `itertools.starmap()` + "PGH004", # allow `# noqa` with no error code; sometimes this is needed to prevent unwanted magical auto-fixes + "PLR2004", # allow magic numbers; using a constant doesn't necessarily make a magic number any more legible + "PT011", # allow `pytest.raises` for any exception type; this is often a false-positive that has little benefit + "PT018", # allow `assert` with multiple conditions; ruff suggestions often make the code less legible + "PT019", # allow unused PyTest fixtures; this check has a lot of false positives for `@unittest.mock.patch()` + "S101", # allow use of `assert`; we sometimes use assert as a type hint for MyPy, and do not run with -O + "S404", # allow use of `subprocess`; it's a common way to run shell commands, and this check is overly paranoid + "SIM102", # allow nested `if` clauses; ruff suggestions often make the code less legible + "SIM117", # allow nested `with` clauses; ruff suggestions often make the code less legible + "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit + + # Exclusions of specific rules that we want to work toward being compliant with + "ANN001", + "ANN202", + "ANN204", + "ARG001", + "ARG002", + "BLE001", + "C414", + "C420", + "E303", + "E713", + "F841", + "FBT001", + "FBT002", + "FLY002", + "FURB101", + "FURB110", + "FURB118", + "FURB136", + "PERF401", + "PGH003", + "PIE808", + "PLR0911", + "PLR0912", + "PLR0913", + "PLR0917", + "PLR1702", + "PLR5501", + "PLR6201", + "PLR6301", + "PLW1514", + "PT012", + "RET505", + "RET506", + "S311", + "SIM110", + "SIM113", + "T201", + "TC001", + "TC003", + "TID252", + "TRY004", + "TRY201", + "TRY300", + "UP006", + "UP009", + "UP015", + "UP031", + "UP035", + "UP045", + "W291", +] + +[tool.ruff.lint.per-file-ignores] +"src/tests/**/*" = [ + # Exclusions that apply to unit tests only + "ANN", # don't require type annotations in tests + "FBT", # allow use of boolean positional arguments + "PLC1901", # allow comparison to empty string + "PLC2701", # allow imports of private names + "PLR", # don't warn about too many arguments, methods, etc. + "SLF001", # allow access to private members +] + [tool.mypy] files = [ "src" ] pretty = true diff --git a/src/tests/uciparse/test_uci.py b/src/tests/uciparse/test_uci.py index 0e758d1..2148ff1 100644 --- a/src/tests/uciparse/test_uci.py +++ b/src/tests/uciparse/test_uci.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- # vim: set ft=python ts=4 sw=4 expandtab: -# pylint: disable=redefined-outer-name import os from typing import Dict, List @@ -164,7 +163,6 @@ def test_normalized(self): assert UciCommentLine(comment="# comment", indented=True).normalized() == " # comment\n" -# pylint: disable=too-many-public-methods class TestUciFile: """Unit tests for UciFile.""" diff --git a/src/uciparse/cli.py b/src/uciparse/cli.py index 312b807..e6a92e5 100644 --- a/src/uciparse/cli.py +++ b/src/uciparse/cli.py @@ -32,7 +32,6 @@ def parse() -> None: raise SystemExit from e -# pylint: disable=invalid-name def diff() -> None: """Run the ucidiff command.""" parser = argparse.ArgumentParser( diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 2a87fcc..250407d 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -188,7 +188,6 @@ _PACKAGE_REGEX = re.compile(r"(^)((([\"'])([a-zA-Z0-9_-]+)(?:\4))|([a-zA-Z0-9_-]+))((\s*)(#.*))?($)") # Matches the remainder of a config line -# pylint: disable=line-too-long _CONFIG_REGEX = re.compile( r"(^)((([\"'])([a-zA-Z0-9_-]+)(?:\4))|([a-zA-Z0-9_-]+))((\s+)((([\"'])([a-zA-Z0-9_-]+)(?:\11))|([a-zA-Z0-9_-]+)))?((\s*)(#.*))?($)" ) @@ -205,7 +204,7 @@ def _contains_single(string: str) -> bool: return match is not None -def _parse_line(lineno: int, line: str) -> Optional[UciLine]: # pylint: disable=unsubscriptable-object +def _parse_line(lineno: int, line: str) -> Optional[UciLine]: """Parse a line, raising UciParseError if it is not valid.""" match = _LINE_REGEX.match(line) if not match: @@ -282,7 +281,7 @@ def _parse_comment(_lineno: int, prefix: str, remainder: str) -> UciCommentLine: return UciCommentLine(comment=comment, indented=indented) -def _serialize_identifier(prefix: str, identifier: Optional[str]) -> str: # pylint: disable=unsubscriptable-object +def _serialize_identifier(prefix: str, identifier: Optional[str]) -> str: """Serialize an identifier, which is never quoted.""" return "%s%s" % (prefix, identifier) if identifier else "" @@ -293,7 +292,7 @@ def _serialize_value(prefix: str, value: str) -> str: return "%s%s%s%s" % (prefix, quote, value, quote) -def _serialize_comment(prefix: str, comment: Optional[str]) -> str: # pylint: disable=unsubscriptable-object +def _serialize_comment(prefix: str, comment: Optional[str]) -> str: """Serialize a comment, with an optional prefix.""" return "%s%s" % (prefix, comment) if comment else "" @@ -399,11 +398,10 @@ def normalized(self) -> List[str]: # We join the lines first and then re-split so we don't end up with lines that have an embedded newline return "".join([line.normalized() for line in self.lines]).splitlines(keepends=True) - # pylint: disable=invalid-name @staticmethod def from_file(path: str) -> UciFile: """Generate a UciFile from a file on disk.""" - with open(path, "r") as fp: # pylint: disable=unspecified-encoding + with open(path, "r") as fp: return UciFile.from_fp(fp) @staticmethod From 3881e9ca775f383f38dbad24da28a7a9238e990b Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 11:55:55 -0500 Subject: [PATCH 02/31] Remove unnecessary exclusions --- pyproject.toml | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 22ed2e8..8f5adee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,20 +162,8 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "ANN001", - "ANN202", - "ANN204", - "ARG001", - "ARG002", - "BLE001", - "C414", - "C420", - "E303", - "E713", - "F841", "FBT001", "FBT002", - "FLY002", "FURB101", "FURB110", "FURB118", From f497af2ab2162543305454271efc0c0c3e86f4c3 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 11:57:17 -0500 Subject: [PATCH 03/31] Fixes for FBT001 --- pyproject.toml | 1 - src/uciparse/uci.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 8f5adee..60a483d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "FBT001", "FBT002", "FURB101", "FURB110", diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 250407d..6c0b5f4 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -378,7 +378,7 @@ def normalized(self) -> str: class UciCommentLine(UciLine): """A comment line in a UCI config file.""" - def __init__(self, comment: str, indented: bool = False) -> None: + def __init__(self, comment: str, *, indented: bool = False) -> None: self.comment = comment self.indented = indented From 6d07d9398801fa9405d16e7b71a8a4229ec1c44c Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 11:59:24 -0500 Subject: [PATCH 04/31] Fixes for FURB110 --- pyproject.toml | 3 --- src/uciparse/uci.py | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 60a483d..e1ea183 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,9 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "FBT002", - "FURB101", - "FURB110", "FURB118", "FURB136", "PERF401", diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 6c0b5f4..65a3eb7 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -228,7 +228,7 @@ def _parse_package(lineno: int, remainder: str) -> UciPackageLine: match = _PACKAGE_REGEX.match(remainder) if not match: raise UciParseError("Error on line %d: invalid package line" % lineno) - name = match[5] if match[5] else match[6] + name = match[5] or match[6] comment = match[9] return UciPackageLine(name=name, comment=comment) @@ -238,15 +238,15 @@ def _parse_config(lineno: int, remainder: str) -> UciConfigLine: match = _CONFIG_REGEX.match(remainder) if not match: raise UciParseError("Error on line %d: invalid config line" % lineno) - section = match[5] if match[5] else match[6] - name = match[12] if match[12] else match[9] + section = match[5] or match[6] + name = match[12] or match[9] comment = match[16] return UciConfigLine(section=section, name=name, comment=comment) def _extract_data_of_remainder_match(match: typing.Match[str]) -> Tuple[str, str, str]: """Extracts a 3-tuple containing (name,value,comment) out of a {_OPTION_REGEX, LIST_REGEX} matcher""" - name = match[5] if match[5] else match[6] + name = match[5] or match[6] value = "" if match[11]: value = match[11] From 5be578d38e4d01e13ddfe2c27a673c027e3c908d Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:00:18 -0500 Subject: [PATCH 05/31] Remove unnecessary exclusions --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e1ea183..63cb4c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,9 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "FURB118", - "FURB136", - "PERF401", "PGH003", "PIE808", "PLR0911", From 192bb3d63b8baf5fe90292ac36e2c11d2a30ec34 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:03:48 -0500 Subject: [PATCH 06/31] Fix rufflint regex --- .run/commands/rufflint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.run/commands/rufflint.sh b/.run/commands/rufflint.sh index 72e0f4e..effee70 100644 --- a/.run/commands/rufflint.sh +++ b/.run/commands/rufflint.sh @@ -19,7 +19,7 @@ command_rufflint() { echo "Running Ruff linter..." - CLICOLOR_FORCE=1 poetry_run ruff check --no-fix | sed 's/ .*-->.* //' + CLICOLOR_FORCE=1 poetry_run ruff check --no-fix | sed 's/ *.*-->.* //' echo "done" } From bb8d0f8f6b68e923c5696638a39fd6d87fabf944 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:03:58 -0500 Subject: [PATCH 07/31] Fixes for PGH003 --- pyproject.toml | 1 - src/uciparse/__init__.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 63cb4c5..bd7eacf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "PGH003", "PIE808", "PLR0911", "PLR0912", diff --git a/src/uciparse/__init__.py b/src/uciparse/__init__.py index 82ee591..e69de29 100644 --- a/src/uciparse/__init__.py +++ b/src/uciparse/__init__.py @@ -1 +0,0 @@ -__all__ = [] # type: ignore From 3c0215b9ded9f05db16cb7715a57d16e514cf030 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:05:02 -0500 Subject: [PATCH 08/31] Remove unnecessary exclusions --- pyproject.toml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index bd7eacf..4fa5e97 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,13 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "PIE808", - "PLR0911", - "PLR0912", - "PLR0913", - "PLR0917", - "PLR1702", - "PLR5501", "PLR6201", "PLR6301", "PLW1514", From 83afb9d7235918245c650cd939981e352d9084a3 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:05:55 -0500 Subject: [PATCH 09/31] Fixes for PLR6201 --- pyproject.toml | 1 - src/uciparse/uci.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 4fa5e97..0d14aa8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "PLR6201", "PLR6301", "PLW1514", "PT012", diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 65a3eb7..499e128 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -250,7 +250,7 @@ def _extract_data_of_remainder_match(match: typing.Match[str]) -> Tuple[str, str value = "" if match[11]: value = match[11] - elif match[8] not in ('""', "''"): + elif match[8] not in {'""', "''"}: value = match[8] comment = match[15] return name, value, comment From 0d5bcaf0f43f517f62b1bb97fa07f87089959a17 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:07:31 -0500 Subject: [PATCH 10/31] Fixes for PLW1514 --- pyproject.toml | 2 -- src/uciparse/uci.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d14aa8..ef203c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,8 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "PLR6301", - "PLW1514", "PT012", "RET505", "RET506", diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 499e128..37d057b 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -401,7 +401,7 @@ def normalized(self) -> List[str]: @staticmethod def from_file(path: str) -> UciFile: """Generate a UciFile from a file on disk.""" - with open(path, "r") as fp: + with open(path, "r", encoding=None) as fp: # use platform-specific encoding return UciFile.from_fp(fp) @staticmethod From 0f0293d04bc7442419d360a952b512de8f8c6296 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:08:11 -0500 Subject: [PATCH 11/31] Fixes for PT012 --- pyproject.toml | 1 - src/uciparse/uci.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ef203c2..0ab7f73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "PT012", "RET505", "RET506", "S311", diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 37d057b..b3f3d5b 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -401,7 +401,7 @@ def normalized(self) -> List[str]: @staticmethod def from_file(path: str) -> UciFile: """Generate a UciFile from a file on disk.""" - with open(path, "r", encoding=None) as fp: # use platform-specific encoding + with open(path, "r", encoding=None) as fp: # use platform-specific encoding return UciFile.from_fp(fp) @staticmethod From e9aa12c2b70f4324df7371781c90257e5a1d9bdf Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:09:01 -0500 Subject: [PATCH 12/31] Fixes for RET505 --- pyproject.toml | 1 - src/uciparse/uci.py | 8 ++++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ab7f73..53695eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "RET505", "RET506", "S311", "SIM110", diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index b3f3d5b..ac98e92 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -211,14 +211,14 @@ def _parse_line(lineno: int, line: str) -> Optional[UciLine]: raise UciParseError("Error on line %d: unrecognized line type" % lineno) if match[4] == "#": return _parse_comment(lineno, match[3], match[5]) - elif match[8]: + if match[8]: if match[8] == "package": return _parse_package(lineno, match[10]) - elif match[8] == "config": + if match[8] == "config": return _parse_config(lineno, match[10]) - elif match[8] == "option": + if match[8] == "option": return _parse_option(lineno, match[10]) - elif match[8] == "list": + if match[8] == "list": return _parse_list(lineno, match[10]) return None From bac4a80b6c76cd7201f8f1e4a0e91ce3e10ce558 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:09:36 -0500 Subject: [PATCH 13/31] Remove unnecessary exclusions --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 53695eb..1c8aa10 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,9 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "RET506", - "S311", - "SIM110", "SIM113", "T201", "TC001", From b2044e5c2495638545d53fe212f5d0e1624da420 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:11:02 -0500 Subject: [PATCH 14/31] Fixes for SIM113 --- pyproject.toml | 1 - src/uciparse/uci.py | 4 +--- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1c8aa10..e6e3170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "SIM113", "T201", "TC001", "TC003", diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index ac98e92..1654d7f 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -412,10 +412,8 @@ def from_fp(fp: TextIO) -> UciFile: @staticmethod def from_lines(lines: Sequence[str]) -> UciFile: """Generate a UciFile from a list of lines.""" - lineno = 0 ucilines: List[UciLine] = [] - for line in lines: - lineno += 1 + for lineno, line in enumerate(lines, start=1): parsed = _parse_line(lineno, line) if parsed: ucilines.append(parsed) From c5bc2635b0fcf7a467ab7570ce17cff986057dc7 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:11:35 -0500 Subject: [PATCH 15/31] Remove unnecessary exclusions --- pyproject.toml | 3 --- 1 file changed, 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e6e3170..c87419d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,9 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "T201", - "TC001", - "TC003", "TID252", "TRY004", "TRY201", From be55f2e8c6ce1a18f1af7a167da0c928cf6de6a9 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:12:19 -0500 Subject: [PATCH 16/31] Fixes for TID252 --- pyproject.toml | 2 +- src/uciparse/cli.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c87419d..66f768e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,7 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "TID252", +# "TID252", "TRY004", "TRY201", "TRY300", diff --git a/src/uciparse/cli.py b/src/uciparse/cli.py index e6a92e5..63ee6bf 100644 --- a/src/uciparse/cli.py +++ b/src/uciparse/cli.py @@ -9,7 +9,7 @@ import difflib import sys -from .uci import UciFile, UciParseError +from uciparse.uci import UciFile, UciParseError def parse() -> None: From a6e6831033c19f6edfdb395b25b38e9228eea945 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:13:40 -0500 Subject: [PATCH 17/31] Remove unnecessary exclusions --- pyproject.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 66f768e..343eec1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,17 +162,12 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with -# "TID252", - "TRY004", - "TRY201", - "TRY300", "UP006", "UP009", "UP015", "UP031", "UP035", "UP045", - "W291", ] [tool.ruff.lint.per-file-ignores] From 18c16d625cec7ee052c14e33d708b70ef2811bee Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:14:25 -0500 Subject: [PATCH 18/31] Fixes for UP006 --- pyproject.toml | 1 - src/tests/uciparse/test_uci.py | 11 +++++------ src/uciparse/uci.py | 10 +++++----- 3 files changed, 10 insertions(+), 12 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 343eec1..61cec00 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "UP006", "UP009", "UP015", "UP031", diff --git a/src/tests/uciparse/test_uci.py b/src/tests/uciparse/test_uci.py index 2148ff1..ec1ab7c 100644 --- a/src/tests/uciparse/test_uci.py +++ b/src/tests/uciparse/test_uci.py @@ -2,7 +2,6 @@ # vim: set ft=python ts=4 sw=4 expandtab: import os -from typing import Dict, List from unittest.mock import MagicMock import pytest @@ -21,7 +20,7 @@ FIXTURE_DIR = os.path.join(os.path.dirname(__file__), "fixtures/test_uci") -def load(path: str) -> Dict[str, List[str]]: +def load(path: str) -> dict[str, list[str]]: data = {} for f in os.listdir(path): p = os.path.join(path, f) @@ -32,22 +31,22 @@ def load(path: str) -> Dict[str, List[str]]: @pytest.fixture -def original() -> Dict[str, List[str]]: +def original() -> dict[str, list[str]]: return load(os.path.join(FIXTURE_DIR, "original")) @pytest.fixture -def normalized() -> Dict[str, List[str]]: +def normalized() -> dict[str, list[str]]: return load(os.path.join(FIXTURE_DIR, "normalized")) @pytest.fixture -def invalid() -> Dict[str, List[str]]: +def invalid() -> dict[str, list[str]]: return load(os.path.join(FIXTURE_DIR, "invalid")) @pytest.fixture -def real() -> Dict[str, List[str]]: +def real() -> dict[str, list[str]]: return load(os.path.join(FIXTURE_DIR, "real")) diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 1654d7f..52b4e16 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -176,7 +176,7 @@ import re import typing from abc import ABC, abstractmethod -from typing import List, Optional, Sequence, TextIO, Tuple +from typing import Optional, Sequence, TextIO # Standard indent of 4 spaces _INDENT = " " @@ -244,7 +244,7 @@ def _parse_config(lineno: int, remainder: str) -> UciConfigLine: return UciConfigLine(section=section, name=name, comment=comment) -def _extract_data_of_remainder_match(match: typing.Match[str]) -> Tuple[str, str, str]: +def _extract_data_of_remainder_match(match: typing.Match[str]) -> tuple[str, str, str]: """Extracts a 3-tuple containing (name,value,comment) out of a {_OPTION_REGEX, LIST_REGEX} matcher""" name = match[5] or match[6] value = "" @@ -390,10 +390,10 @@ def normalized(self) -> str: class UciFile: - def __init__(self, lines: List[UciLine]) -> None: + def __init__(self, lines: list[UciLine]) -> None: self.lines = lines - def normalized(self) -> List[str]: + def normalized(self) -> list[str]: """Return a list of normalized lines comprising the file.""" # We join the lines first and then re-split so we don't end up with lines that have an embedded newline return "".join([line.normalized() for line in self.lines]).splitlines(keepends=True) @@ -412,7 +412,7 @@ def from_fp(fp: TextIO) -> UciFile: @staticmethod def from_lines(lines: Sequence[str]) -> UciFile: """Generate a UciFile from a list of lines.""" - ucilines: List[UciLine] = [] + ucilines: list[UciLine] = [] for lineno, line in enumerate(lines, start=1): parsed = _parse_line(lineno, line) if parsed: From b4c2fc7b09d4982a7e87b8028a71ad0b894deccc Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:15:00 -0500 Subject: [PATCH 19/31] Fixes for UP009 --- pyproject.toml | 1 - src/tests/uciparse/test_cli.py | 1 - src/tests/uciparse/test_uci.py | 1 - src/uciparse/cli.py | 1 - src/uciparse/uci.py | 1 - 5 files changed, 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 61cec00..07ad1cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "UP009", "UP015", "UP031", "UP035", diff --git a/src/tests/uciparse/test_cli.py b/src/tests/uciparse/test_cli.py index ae0457c..6af5229 100644 --- a/src/tests/uciparse/test_cli.py +++ b/src/tests/uciparse/test_cli.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # vim: set ft=python ts=4 sw=4 expandtab: from unittest.mock import MagicMock, call, patch diff --git a/src/tests/uciparse/test_uci.py b/src/tests/uciparse/test_uci.py index ec1ab7c..62e05e3 100644 --- a/src/tests/uciparse/test_uci.py +++ b/src/tests/uciparse/test_uci.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # vim: set ft=python ts=4 sw=4 expandtab: import os diff --git a/src/uciparse/cli.py b/src/uciparse/cli.py index 63ee6bf..a5fbec3 100644 --- a/src/uciparse/cli.py +++ b/src/uciparse/cli.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # vim: set ft=python ts=4 sw=4 expandtab: """ diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 52b4e16..869de12 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # vim: set ft=python ts=4 sw=4 expandtab: r""" From 6f7780a4b13274f5a73fc4dc4c5913243e0c180f Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:15:48 -0500 Subject: [PATCH 20/31] Fixes for UP015 --- pyproject.toml | 1 - src/uciparse/uci.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 07ad1cd..e00a39a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "UP015", "UP031", "UP035", "UP045", diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 869de12..a5913b0 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -400,7 +400,7 @@ def normalized(self) -> list[str]: @staticmethod def from_file(path: str) -> UciFile: """Generate a UciFile from a file on disk.""" - with open(path, "r", encoding=None) as fp: # use platform-specific encoding + with open(path, encoding=None) as fp: # use platform-specific encoding return UciFile.from_fp(fp) @staticmethod From d4fb9af03686aa7385dbf8ef79f450b8cf3f134f Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:19:02 -0500 Subject: [PATCH 21/31] Fixes for UP031 --- pyproject.toml | 1 - src/uciparse/uci.py | 28 ++++++++++++++-------------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e00a39a..64f2ed2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "UP031", "UP035", "UP045", ] diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index a5913b0..32a5c94 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -207,7 +207,7 @@ def _parse_line(lineno: int, line: str) -> Optional[UciLine]: """Parse a line, raising UciParseError if it is not valid.""" match = _LINE_REGEX.match(line) if not match: - raise UciParseError("Error on line %d: unrecognized line type" % lineno) + raise UciParseError(f"Error on line {lineno}: unrecognized line type") if match[4] == "#": return _parse_comment(lineno, match[3], match[5]) if match[8]: @@ -226,7 +226,7 @@ def _parse_package(lineno: int, remainder: str) -> UciPackageLine: """Parse a package line, raising UciParseError if it is not valid.""" match = _PACKAGE_REGEX.match(remainder) if not match: - raise UciParseError("Error on line %d: invalid package line" % lineno) + raise UciParseError(f"Error on line {lineno}: invalid package line") name = match[5] or match[6] comment = match[9] return UciPackageLine(name=name, comment=comment) @@ -236,7 +236,7 @@ def _parse_config(lineno: int, remainder: str) -> UciConfigLine: """Parse a config line, raising UciParseError if it is not valid.""" match = _CONFIG_REGEX.match(remainder) if not match: - raise UciParseError("Error on line %d: invalid config line" % lineno) + raise UciParseError(f"Error on line {lineno}: invalid config line") section = match[5] or match[6] name = match[12] or match[9] comment = match[16] @@ -259,7 +259,7 @@ def _parse_option(lineno: int, remainder: str) -> UciOptionLine: """Parse an option line, raising UciParseError if it is not valid.""" match = _OPTION_REGEX.match(remainder) if not match: - raise UciParseError("Error on line %d: invalid option line" % lineno) + raise UciParseError(f"Error on line {lineno}: invalid option line") name, value, comment = _extract_data_of_remainder_match(match) return UciOptionLine(name=name, value=value, comment=comment) @@ -268,7 +268,7 @@ def _parse_list(lineno: int, remainder: str) -> UciListLine: """Parse a list line, raising UciParseError if it is not valid.""" match = LIST_REGEX.match(remainder) if not match: - raise UciParseError("Error on line %d: invalid list line" % lineno) + raise UciParseError(f"Error on line {lineno}: invalid list line") name, value, comment = _extract_data_of_remainder_match(match) return UciListLine(name=name, value=value, comment=comment) @@ -276,24 +276,24 @@ def _parse_list(lineno: int, remainder: str) -> UciListLine: def _parse_comment(_lineno: int, prefix: str, remainder: str) -> UciCommentLine: """Parse a comment-only line, raising UciParseError if it is not valid.""" indented = len(prefix) > 0 if prefix else False # all we care about is whether it's indented, not the actual indent - comment = "#%s" % remainder + comment = f"#{remainder}" return UciCommentLine(comment=comment, indented=indented) def _serialize_identifier(prefix: str, identifier: Optional[str]) -> str: """Serialize an identifier, which is never quoted.""" - return "%s%s" % (prefix, identifier) if identifier else "" + return f"{prefix}{identifier}" if identifier else "" def _serialize_value(prefix: str, value: str) -> str: """Serialize an identifier, which is quoted if it contains whitespace or a quote character.""" quote = "'" if not _contains_single(value) else '"' - return "%s%s%s%s" % (prefix, quote, value, quote) + return f"{prefix}{quote}{value}{quote}" def _serialize_comment(prefix: str, comment: Optional[str]) -> str: """Serialize a comment, with an optional prefix.""" - return "%s%s" % (prefix, comment) if comment else "" + return f"{prefix}{comment}" if comment else "" class UciParseError(ValueError): @@ -323,7 +323,7 @@ def normalized(self) -> str: """Serialize the line in normalized form.""" name_field = _serialize_identifier("package ", self.name) comment_field = _serialize_comment(" ", self.comment) - return "%s%s\n" % (name_field, comment_field) + return f"{name_field}{comment_field}\n" class UciConfigLine(UciLine): @@ -339,7 +339,7 @@ def normalized(self) -> str: section_field = _serialize_identifier("\nconfig ", self.section) name_field = _serialize_identifier(" ", self.name) comment_field = _serialize_comment(" ", self.comment) - return "%s%s%s\n" % (section_field, name_field, comment_field) + return f"{section_field}{name_field}{comment_field}\n" class UciOptionLine(UciLine): @@ -355,7 +355,7 @@ def normalized(self) -> str: name_field = _serialize_identifier(_INDENT + "option ", self.name) value_field = _serialize_value(" ", self.value) comment_field = _serialize_comment(" ", self.comment) - return "%s%s%s\n" % (name_field, value_field, comment_field) + return f"{name_field}{value_field}{comment_field}\n" class UciListLine(UciLine): @@ -371,7 +371,7 @@ def normalized(self) -> str: name_field = _serialize_identifier(_INDENT + "list ", self.name) value_field = _serialize_value(" ", self.value) comment_field = _serialize_comment(" ", self.comment) - return "%s%s%s\n" % (name_field, value_field, comment_field) + return f"{name_field}{value_field}{comment_field}\n" class UciCommentLine(UciLine): @@ -385,7 +385,7 @@ def normalized(self) -> str: """Serialize the line in normalized form.""" indent = _INDENT if self.indented else "" comment_field = _serialize_comment(indent, self.comment) - return "%s\n" % comment_field + return f"{comment_field}\n" class UciFile: From dba085706f745e997c82e284b3c3550a217155ec Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:20:04 -0500 Subject: [PATCH 22/31] Fixes for UP035 --- pyproject.toml | 1 - src/uciparse/uci.py | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 64f2ed2..662f2fa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,7 +162,6 @@ ignore = [ "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit # Exclusions of specific rules that we want to work toward being compliant with - "UP035", "UP045", ] diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 32a5c94..2870ce9 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -175,7 +175,10 @@ import re import typing from abc import ABC, abstractmethod -from typing import Optional, Sequence, TextIO +from typing import Optional, TextIO + +if typing.TYPE_CHECKING: + from collections.abc import Sequence # Standard indent of 4 spaces _INDENT = " " From f87429be5c57f821f164bc9b45a00882cf0782e1 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 12:20:56 -0500 Subject: [PATCH 23/31] Fixes for UP045 --- pyproject.toml | 3 --- src/uciparse/uci.py | 16 ++++++++-------- 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 662f2fa..d1890b7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -160,9 +160,6 @@ ignore = [ "SIM102", # allow nested `if` clauses; ruff suggestions often make the code less legible "SIM117", # allow nested `with` clauses; ruff suggestions often make the code less legible "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit - - # Exclusions of specific rules that we want to work toward being compliant with - "UP045", ] [tool.ruff.lint.per-file-ignores] diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 2870ce9..28f6d8f 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -175,7 +175,7 @@ import re import typing from abc import ABC, abstractmethod -from typing import Optional, TextIO +from typing import TextIO if typing.TYPE_CHECKING: from collections.abc import Sequence @@ -206,7 +206,7 @@ def _contains_single(string: str) -> bool: return match is not None -def _parse_line(lineno: int, line: str) -> Optional[UciLine]: +def _parse_line(lineno: int, line: str) -> UciLine | None: """Parse a line, raising UciParseError if it is not valid.""" match = _LINE_REGEX.match(line) if not match: @@ -283,7 +283,7 @@ def _parse_comment(_lineno: int, prefix: str, remainder: str) -> UciCommentLine: return UciCommentLine(comment=comment, indented=indented) -def _serialize_identifier(prefix: str, identifier: Optional[str]) -> str: +def _serialize_identifier(prefix: str, identifier: str | None) -> str: """Serialize an identifier, which is never quoted.""" return f"{prefix}{identifier}" if identifier else "" @@ -294,7 +294,7 @@ def _serialize_value(prefix: str, value: str) -> str: return f"{prefix}{quote}{value}{quote}" -def _serialize_comment(prefix: str, comment: Optional[str]) -> str: +def _serialize_comment(prefix: str, comment: str | None) -> str: """Serialize a comment, with an optional prefix.""" return f"{prefix}{comment}" if comment else "" @@ -318,7 +318,7 @@ def normalized(self) -> str: class UciPackageLine(UciLine): """A package line in a UCI config file.""" - def __init__(self, name: str, comment: Optional[str] = None) -> None: + def __init__(self, name: str, comment: str | None = None) -> None: self.name = name self.comment = comment @@ -332,7 +332,7 @@ def normalized(self) -> str: class UciConfigLine(UciLine): """A config line in a UCI config file.""" - def __init__(self, section: str, name: Optional[str] = None, comment: Optional[str] = None) -> None: + def __init__(self, section: str, name: str | None = None, comment: str | None = None) -> None: self.section = section self.name = name self.comment = comment @@ -348,7 +348,7 @@ def normalized(self) -> str: class UciOptionLine(UciLine): """An option line in a UCI config file.""" - def __init__(self, name: str, value: str, comment: Optional[str] = None) -> None: + def __init__(self, name: str, value: str, comment: str | None = None) -> None: self.name = name self.value = value self.comment = comment @@ -364,7 +364,7 @@ def normalized(self) -> str: class UciListLine(UciLine): """A list line in a UCI config file.""" - def __init__(self, name: str, value: str, comment: Optional[str] = None) -> None: + def __init__(self, name: str, value: str, comment: str | None = None) -> None: self.name = name self.value = value self.comment = comment From 239abe52e3dfa64cb0a8a44193ad6e07ddfd65ea Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 15:22:33 -0500 Subject: [PATCH 24/31] Remove 'from __future__ import annotations' --- src/uciparse/uci.py | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index 28f6d8f..f0a2d84 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -170,16 +170,12 @@ .. _UCI: https://openwrt.org/docs/guide-user/base-system/uci """ -from __future__ import annotations # see: https://stackoverflow.com/a/33533514/2907667 - import re import typing from abc import ABC, abstractmethod +from collections.abc import Sequence from typing import TextIO -if typing.TYPE_CHECKING: - from collections.abc import Sequence - # Standard indent of 4 spaces _INDENT = " " @@ -206,7 +202,7 @@ def _contains_single(string: str) -> bool: return match is not None -def _parse_line(lineno: int, line: str) -> UciLine | None: +def _parse_line(lineno: int, line: str) -> "UciLine | None": """Parse a line, raising UciParseError if it is not valid.""" match = _LINE_REGEX.match(line) if not match: @@ -225,7 +221,7 @@ def _parse_line(lineno: int, line: str) -> UciLine | None: return None -def _parse_package(lineno: int, remainder: str) -> UciPackageLine: +def _parse_package(lineno: int, remainder: str) -> "UciPackageLine": """Parse a package line, raising UciParseError if it is not valid.""" match = _PACKAGE_REGEX.match(remainder) if not match: @@ -235,7 +231,7 @@ def _parse_package(lineno: int, remainder: str) -> UciPackageLine: return UciPackageLine(name=name, comment=comment) -def _parse_config(lineno: int, remainder: str) -> UciConfigLine: +def _parse_config(lineno: int, remainder: str) -> "UciConfigLine": """Parse a config line, raising UciParseError if it is not valid.""" match = _CONFIG_REGEX.match(remainder) if not match: @@ -258,7 +254,7 @@ def _extract_data_of_remainder_match(match: typing.Match[str]) -> tuple[str, str return name, value, comment -def _parse_option(lineno: int, remainder: str) -> UciOptionLine: +def _parse_option(lineno: int, remainder: str) -> "UciOptionLine": """Parse an option line, raising UciParseError if it is not valid.""" match = _OPTION_REGEX.match(remainder) if not match: @@ -267,7 +263,7 @@ def _parse_option(lineno: int, remainder: str) -> UciOptionLine: return UciOptionLine(name=name, value=value, comment=comment) -def _parse_list(lineno: int, remainder: str) -> UciListLine: +def _parse_list(lineno: int, remainder: str) -> "UciListLine": """Parse a list line, raising UciParseError if it is not valid.""" match = LIST_REGEX.match(remainder) if not match: @@ -276,7 +272,7 @@ def _parse_list(lineno: int, remainder: str) -> UciListLine: return UciListLine(name=name, value=value, comment=comment) -def _parse_comment(_lineno: int, prefix: str, remainder: str) -> UciCommentLine: +def _parse_comment(_lineno: int, prefix: str, remainder: str) -> "UciCommentLine": """Parse a comment-only line, raising UciParseError if it is not valid.""" indented = len(prefix) > 0 if prefix else False # all we care about is whether it's indented, not the actual indent comment = f"#{remainder}" @@ -401,18 +397,18 @@ def normalized(self) -> list[str]: return "".join([line.normalized() for line in self.lines]).splitlines(keepends=True) @staticmethod - def from_file(path: str) -> UciFile: + def from_file(path: str) -> "UciFile": """Generate a UciFile from a file on disk.""" with open(path, encoding=None) as fp: # use platform-specific encoding return UciFile.from_fp(fp) @staticmethod - def from_fp(fp: TextIO) -> UciFile: + def from_fp(fp: TextIO) -> "UciFile": """Generate a UciFile from the contents of a file pointer.""" return UciFile.from_lines(fp.readlines()) @staticmethod - def from_lines(lines: Sequence[str]) -> UciFile: + def from_lines(lines: Sequence[str]) -> "UciFile": """Generate a UciFile from a list of lines.""" ucilines: list[UciLine] = [] for lineno, line in enumerate(lines, start=1): From 740e2efcf451b931df3e42e898ab9df16379745e Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 15:38:36 -0500 Subject: [PATCH 25/31] Fix rufflint error handling --- .run/commands/rufflint.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.run/commands/rufflint.sh b/.run/commands/rufflint.sh index effee70..3b9deaa 100644 --- a/.run/commands/rufflint.sh +++ b/.run/commands/rufflint.sh @@ -19,7 +19,14 @@ command_rufflint() { echo "Running Ruff linter..." - CLICOLOR_FORCE=1 poetry_run ruff check --no-fix | sed 's/ *.*-->.* //' + + # normally we would just run the command, but the $() subshell messes with error handling + OUTPUT=$(CLICOLOR_FORCE=1 poetry_run ruff check --no-fix 2>&1) + if [ $? != 0 ]; then + echo "$OUTPUT" | sed 's/ *.*-->.* //' + exit 1 + fi + echo "done" } From 149398b681a379c09c447b97c193cc3b75a3ade7 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 15:50:01 -0500 Subject: [PATCH 26/31] Fix rufflint error handling --- .run/commands/rufflint.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.run/commands/rufflint.sh b/.run/commands/rufflint.sh index 72e0f4e..3b9deaa 100644 --- a/.run/commands/rufflint.sh +++ b/.run/commands/rufflint.sh @@ -19,7 +19,14 @@ command_rufflint() { echo "Running Ruff linter..." - CLICOLOR_FORCE=1 poetry_run ruff check --no-fix | sed 's/ .*-->.* //' + + # normally we would just run the command, but the $() subshell messes with error handling + OUTPUT=$(CLICOLOR_FORCE=1 poetry_run ruff check --no-fix 2>&1) + if [ $? != 0 ]; then + echo "$OUTPUT" | sed 's/ *.*-->.* //' + exit 1 + fi + echo "done" } From c9077ede9956e5bdc7b7c1d31ef84964dab9c230 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 16:00:44 -0500 Subject: [PATCH 27/31] Update changelog --- Changelog | 1 + 1 file changed, 1 insertion(+) diff --git a/Changelog b/Changelog index 957462d..b35cfe1 100644 --- a/Changelog +++ b/Changelog @@ -8,6 +8,7 @@ Version 0.1.27 unreleased * Move unit tests from `tests` into `src/tests/uciparse`. * Update the MyPy configuration so we're using latest rules. * Replace black, isort, and pylint with the Ruff formatter and linter. + * Address Ruff linter warnings and modernize the code to 2025 standards. * Update the jinja2 transitive dependency to address CVE-2025-27516. * Update the requests transitive dependency to address CVE-2024-47081. * Update the urllib3 transitive dependency to address CVE-2025-50181. From 1825fe486ac55efb6d8cb3ffe0c0dae5b6c37b11 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 16:10:08 -0500 Subject: [PATCH 28/31] Un-exclude ERA --- pyproject.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index d1890b7..a2d4170 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -139,7 +139,6 @@ ignore = [ "D", # pydocstyle "DJ", # flake8-django "DOC", # pydoclint - "ERA", # eradicate "EXE", # flake8-executable "FIX", # flake8-fixme "PTH", # flake8-use-pathlib From fbb8bfbde09b079ac44a54db0c231268ed36851e Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 16:13:32 -0500 Subject: [PATCH 29/31] Adjust rufflint output --- .run/commands/rufflint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.run/commands/rufflint.sh b/.run/commands/rufflint.sh index 3b9deaa..853d4c9 100644 --- a/.run/commands/rufflint.sh +++ b/.run/commands/rufflint.sh @@ -27,6 +27,7 @@ command_rufflint() { exit 1 fi + echo "$OUTPUT" echo "done" } From ce444e47ea3754d806ea3ef7eb9525cb7a25a703 Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 16:21:13 -0500 Subject: [PATCH 30/31] Adjust rufflint output --- .run/commands/rufflint.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/.run/commands/rufflint.sh b/.run/commands/rufflint.sh index 3b9deaa..853d4c9 100644 --- a/.run/commands/rufflint.sh +++ b/.run/commands/rufflint.sh @@ -27,6 +27,7 @@ command_rufflint() { exit 1 fi + echo "$OUTPUT" echo "done" } From dc444940305136908b5cab3c7cb8e6d6badb16bd Mon Sep 17 00:00:00 2001 From: "Kenneth J. Pronovici" Date: Mon, 25 Aug 2025 16:31:10 -0500 Subject: [PATCH 31/31] Fall back formatting changes --- pyproject.toml | 1 + src/uciparse/uci.py | 28 ++++++++++++++-------------- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index a2d4170..49c579d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -159,6 +159,7 @@ ignore = [ "SIM102", # allow nested `if` clauses; ruff suggestions often make the code less legible "SIM117", # allow nested `with` clauses; ruff suggestions often make the code less legible "TRY003", # allow long messages in exceptions; this is often a false-positive that has little benefit + "UP031", # allow format-specifiers instead of f-strings in this codebase ] [tool.ruff.lint.per-file-ignores] diff --git a/src/uciparse/uci.py b/src/uciparse/uci.py index f0a2d84..f311997 100644 --- a/src/uciparse/uci.py +++ b/src/uciparse/uci.py @@ -206,7 +206,7 @@ def _parse_line(lineno: int, line: str) -> "UciLine | None": """Parse a line, raising UciParseError if it is not valid.""" match = _LINE_REGEX.match(line) if not match: - raise UciParseError(f"Error on line {lineno}: unrecognized line type") + raise UciParseError("Error on line %d: unrecognized line type" % lineno) if match[4] == "#": return _parse_comment(lineno, match[3], match[5]) if match[8]: @@ -225,7 +225,7 @@ def _parse_package(lineno: int, remainder: str) -> "UciPackageLine": """Parse a package line, raising UciParseError if it is not valid.""" match = _PACKAGE_REGEX.match(remainder) if not match: - raise UciParseError(f"Error on line {lineno}: invalid package line") + raise UciParseError("Error on line %d: invalid package line" % lineno) name = match[5] or match[6] comment = match[9] return UciPackageLine(name=name, comment=comment) @@ -235,7 +235,7 @@ def _parse_config(lineno: int, remainder: str) -> "UciConfigLine": """Parse a config line, raising UciParseError if it is not valid.""" match = _CONFIG_REGEX.match(remainder) if not match: - raise UciParseError(f"Error on line {lineno}: invalid config line") + raise UciParseError("Error on line %d: invalid config line" % lineno) section = match[5] or match[6] name = match[12] or match[9] comment = match[16] @@ -258,7 +258,7 @@ def _parse_option(lineno: int, remainder: str) -> "UciOptionLine": """Parse an option line, raising UciParseError if it is not valid.""" match = _OPTION_REGEX.match(remainder) if not match: - raise UciParseError(f"Error on line {lineno}: invalid option line") + raise UciParseError("Error on line %d: invalid option line" % lineno) name, value, comment = _extract_data_of_remainder_match(match) return UciOptionLine(name=name, value=value, comment=comment) @@ -267,7 +267,7 @@ def _parse_list(lineno: int, remainder: str) -> "UciListLine": """Parse a list line, raising UciParseError if it is not valid.""" match = LIST_REGEX.match(remainder) if not match: - raise UciParseError(f"Error on line {lineno}: invalid list line") + raise UciParseError("Error on line %d: invalid list line" % lineno) name, value, comment = _extract_data_of_remainder_match(match) return UciListLine(name=name, value=value, comment=comment) @@ -275,24 +275,24 @@ def _parse_list(lineno: int, remainder: str) -> "UciListLine": def _parse_comment(_lineno: int, prefix: str, remainder: str) -> "UciCommentLine": """Parse a comment-only line, raising UciParseError if it is not valid.""" indented = len(prefix) > 0 if prefix else False # all we care about is whether it's indented, not the actual indent - comment = f"#{remainder}" + comment = "#%s" % remainder return UciCommentLine(comment=comment, indented=indented) def _serialize_identifier(prefix: str, identifier: str | None) -> str: """Serialize an identifier, which is never quoted.""" - return f"{prefix}{identifier}" if identifier else "" + return "%s%s" % (prefix, identifier) if identifier else "" def _serialize_value(prefix: str, value: str) -> str: """Serialize an identifier, which is quoted if it contains whitespace or a quote character.""" quote = "'" if not _contains_single(value) else '"' - return f"{prefix}{quote}{value}{quote}" + return "%s%s%s%s" % (prefix, quote, value, quote) def _serialize_comment(prefix: str, comment: str | None) -> str: """Serialize a comment, with an optional prefix.""" - return f"{prefix}{comment}" if comment else "" + return "%s%s" % (prefix, comment) if comment else "" class UciParseError(ValueError): @@ -322,7 +322,7 @@ def normalized(self) -> str: """Serialize the line in normalized form.""" name_field = _serialize_identifier("package ", self.name) comment_field = _serialize_comment(" ", self.comment) - return f"{name_field}{comment_field}\n" + return "%s%s\n" % (name_field, comment_field) class UciConfigLine(UciLine): @@ -338,7 +338,7 @@ def normalized(self) -> str: section_field = _serialize_identifier("\nconfig ", self.section) name_field = _serialize_identifier(" ", self.name) comment_field = _serialize_comment(" ", self.comment) - return f"{section_field}{name_field}{comment_field}\n" + return "%s%s%s\n" % (section_field, name_field, comment_field) class UciOptionLine(UciLine): @@ -354,7 +354,7 @@ def normalized(self) -> str: name_field = _serialize_identifier(_INDENT + "option ", self.name) value_field = _serialize_value(" ", self.value) comment_field = _serialize_comment(" ", self.comment) - return f"{name_field}{value_field}{comment_field}\n" + return "%s%s%s\n" % (name_field, value_field, comment_field) class UciListLine(UciLine): @@ -370,7 +370,7 @@ def normalized(self) -> str: name_field = _serialize_identifier(_INDENT + "list ", self.name) value_field = _serialize_value(" ", self.value) comment_field = _serialize_comment(" ", self.comment) - return f"{name_field}{value_field}{comment_field}\n" + return "%s%s%s\n" % (name_field, value_field, comment_field) class UciCommentLine(UciLine): @@ -384,7 +384,7 @@ def normalized(self) -> str: """Serialize the line in normalized form.""" indent = _INDENT if self.indented else "" comment_field = _serialize_comment(indent, self.comment) - return f"{comment_field}\n" + return "%s\n" % comment_field class UciFile: