diff --git a/.github/workflows/checks.yaml b/.github/workflows/checks.yaml new file mode 100644 index 0000000..54c30db --- /dev/null +++ b/.github/workflows/checks.yaml @@ -0,0 +1,116 @@ +name: Checks + +on: + push: + branches: + - master + pull_request: + branches: + - master + +env: + CACHE_VERSION: 1 + KEY_PREFIX: base-venv + DEFAULT_PYTHON: "3.11" + PRE_COMMIT_CACHE: ~/.cache/pre-commit + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + prepare-base: + name: Prepare base dependencies + runs-on: ubuntu-latest + timeout-minutes: 10 + outputs: + python-key: ${{ steps.generate-python-key.outputs.key }} + pre-commit-key: ${{ steps.generate-pre-commit-key.outputs.key }} + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.7.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Generate partial Python venv restore key + id: generate-python-key + run: >- + echo "key=${{ env.KEY_PREFIX }}-${{ env.CACHE_VERSION }}-${{ + hashFiles('pyproject.toml', 'requirements_test.txt', + 'requirements_test_min.txt', 'requirements_test_pre_commit.txt') }}" >> + $GITHUB_OUTPUT + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.3.2 + with: + path: venv + key: >- + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + steps.generate-python-key.outputs.key }} + - name: Create Python virtual environment + if: steps.cache-venv.outputs.cache-hit != 'true' + run: | + python -m venv venv + . venv/bin/activate + python -m pip install -U pip setuptools wheel + pip install . + pip install pre-commit + - name: Generate pre-commit restore key + id: generate-pre-commit-key + run: >- + echo "key=pre-commit-${{ env.CACHE_VERSION }}-${{ + hashFiles('.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT + - name: Restore pre-commit environment + id: cache-precommit + uses: actions/cache@v3.3.2 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + key: >- + ${{ runner.os }}-${{ steps.generate-pre-commit-key.outputs.key }} + - name: Install pre-commit dependencies + if: steps.cache-precommit.outputs.cache-hit != 'true' + run: | + . venv/bin/activate + pre-commit install --install-hooks + + pylint: + name: pylint + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: prepare-base + steps: + - name: Check out code from GitHub + uses: actions/checkout@v4.1.0 + - name: Set up Python ${{ env.DEFAULT_PYTHON }} + id: python + uses: actions/setup-python@v4.7.1 + with: + python-version: ${{ env.DEFAULT_PYTHON }} + check-latest: true + - name: Restore Python virtual environment + id: cache-venv + uses: actions/cache@v3.3.2 + with: + path: venv + fail-on-cache-miss: true + key: + ${{ runner.os }}-${{ steps.python.outputs.python-version }}-${{ + needs.prepare-base.outputs.python-key }} + - name: Restore pre-commit environment + id: cache-precommit + uses: actions/cache@v3.3.2 + with: + path: ${{ env.PRE_COMMIT_CACHE }} + fail-on-cache-miss: true + key: ${{ runner.os }}-${{ needs.prepare-base.outputs.pre-commit-key }} + - name: Run pylint checks + run: | + . venv/bin/activate + pip install . + pre-commit run pylint --all-files diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a15a151..399a480 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,6 @@ +ci: + skip: [pylint] + repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 @@ -42,12 +45,11 @@ repos: # rev: v1.6.0 # hooks: # - id: mypy - # - repo: local - # hooks: - # - id: pylint - # name: pylint - # entry: bash -c 'test -d .venv && . .venv/bin/activate ; pylint ${CI:+--reports=yes} "$@"' - - # language: system - # types: [ python ] - # args: - # - --disable=R,C + - repo: local + hooks: + - id: pylint + name: pylint + entry: bash -c 'test -d .venv && . .venv/bin/activate ; pylint "$@"' - + language: system + types: [ python ] + args: ["-sn", "-rn"] diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 4bb1398..0000000 --- a/.pylintrc +++ /dev/null @@ -1,284 +0,0 @@ -[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-allow-list=jq - -load-plugins= - pylint_pytest, - pylint.extensions.bad_builtin, - pylint.extensions.broad_try_clause, - pylint.extensions.check_elif, - pylint.extensions.code_style, - pylint.extensions.comparetozero, - pylint.extensions.comparison_placement, - pylint.extensions.confusing_elif, - # pylint.extensions.consider_ternary_expression, # Not a pretty refactoring - pylint.extensions.docparams, - pylint.extensions.docstyle, - pylint.extensions.emptystring, - pylint.extensions.eq_without_hash, - pylint.extensions.for_any_all, - pylint.extensions.mccabe, - pylint.extensions.no_self_use, - pylint.extensions.overlapping_exceptions, - pylint.extensions.redefined_loop_name, - pylint.extensions.redefined_variable_type, - pylint.extensions.typing, - # pylint.extensions.while_used, # highly opinionated - pylint.extensions.dict_init_mutate, - pylint.extensions.dunder, - pylint.extensions.typing, - pylint.extensions.magic_value, - -# Pickle collected data for later comparisons. -persistent=yes - -# 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 - - -[BASIC] - -# Naming style matching correct argument names. -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style. If left empty, argument names will be checked with the set -# 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. If left empty, attribute names will be checked with the set naming -# style. -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma. -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Bad variable names regexes, separated by a comma. If names match any regex, -# they will always be refused -bad-names-rgxs= - -# Naming style matching correct class attribute names. -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style. If left empty, class attribute names will be checked -# with the set naming style. -#class-attribute-rgx= - -# Naming style matching correct class constant names. -class-const-naming-style=UPPER_CASE - -# Regular expression matching correct class constant names. Overrides class- -# const-naming-style. If left empty, class constant names will be checked with -# the set naming style. -#class-const-rgx= - -# Naming style matching correct class names. -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming- -# style. If left empty, class names will be checked with the set 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. If left empty, constant names will be checked with the set naming -# style. -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=4 - -# Naming style matching correct function names. -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style. If left empty, function names will be checked with the set -# naming style. -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma. -good-names=ex,Run,_,i,j,k, # Defaults - rc, # Return variable of `subprocess.xxx` methods - df, # Panda's DataFrame variable - cd, # Method/Context function that does `cd`. `cwd` is not much better - -# Good variable names regexes, separated by a comma. If names match any regex, -# they will always be accepted -good-names-rgxs= - -# 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. If left empty, inline iteration names will be checked -# with the set 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. If left empty, method names will be checked with the set 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. If left empty, module names will be checked with the set 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 - -# Regular expression matching correct type variable names. If left empty, type -# variable names will be checked with the set naming style. -#typevar-rgx= - -# Naming style matching correct variable names. -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style. If left empty, variable names will be checked with the set -# naming style. -#variable-rgx= - - -[DESIGN] - -# Maximum number of arguments for function / method. -max-args=7 - -# Argument names that match this expression will be ignored. -# Defaults to name with leading underscore -ignored-argument-names=_.* - - -[FORMAT] - -# Maximum number of characters on a single line. -max-line-length=120 - -extension-pkg-allow-list=jq - - -[LOGGING] - -# The type of string formatting that logging methods do. `old` means using % -# formatting, `new` is for `{}` formatting. -logging-format-style=old - -# Logging modules to check that the string format arguments are in logging -# function parameter format. -logging-modules=logging - - -[MAGIC-VALUE] - -# List of valid magic values that `magic-value-compare` will not detect. -# Supports integers, floats, negative numbers, for empty string enter ``''``, -# for backslash values just use one backslash e.g \n. -valid-magic-values=0,-1,1,,__main__ - - -[MESSAGES CONTROL] - -# 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= - docstring-first-line-empty, # C0199 - -# 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= - useless-suppression - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME,XXX,TODO - -# Regular expression of note tags to take in consideration. -#notes-rgx= - - -[REFACTORING] - -# 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,argparse.parse_error - - -[REPORTS] - -# Tells whether to display a full report or only the messages. -reports=no - -# Activate the evaluation score. -score=yes - -# 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 - - -[SIMILARITIES] - -# Minimum lines number of a similarity. -min-similarity-lines=4 - -# Ignore comments when computing similarities. -ignore-comments=no - -# Ignore docstrings when computing similarities. -ignore-docstrings=no - -# Ignore signatures when computing similarities. -ignore-signatures=yes diff --git a/pylint_pytest/__init__.py b/pylint_pytest/__init__.py index 3245018..93e4690 100644 --- a/pylint_pytest/__init__.py +++ b/pylint_pytest/__init__.py @@ -6,7 +6,6 @@ from .checkers import BasePytestChecker -# pylint: disable=protected-access def register(linter): """auto discover pylint checker classes""" dirname = os.path.dirname(__file__) diff --git a/pylint_pytest/checkers/class_attr_loader.py b/pylint_pytest/checkers/class_attr_loader.py index f00a1df..34ce714 100644 --- a/pylint_pytest/checkers/class_attr_loader.py +++ b/pylint_pytest/checkers/class_attr_loader.py @@ -32,7 +32,7 @@ def visit_assign(self, node): and node.value.expr.name == "request" ): # storing the aliases for cls from request.cls - self.request_cls = set(map(lambda t: t.name, node.targets)) + self.request_cls = set(t.name for t in node.targets) def visit_assignattr(self, node): if ( diff --git a/pylint_pytest/checkers/fixture.py b/pylint_pytest/checkers/fixture.py index cca224b..3ff7400 100644 --- a/pylint_pytest/checkers/fixture.py +++ b/pylint_pytest/checkers/fixture.py @@ -20,6 +20,9 @@ # TODO: support pytest python_files configuration FILE_NAME_PATTERNS = ("test_*.py", "*_test.py") +ARGUMENT_ARE_KEYWORD_ONLY = ( + "https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only" +) class FixtureCollector: @@ -56,7 +59,7 @@ class FixtureChecker(BasePytestChecker): "deprecated-positional-argument-for-pytest-fixture", ( "Pass scope as a kwarg, not positional arg, which is deprecated in future pytest. " - "Take a look at: https://docs.pytest.org/en/stable/deprecations.html#pytest-fixture-arguments-are-keyword-only" + f"Take a look at: {ARGUMENT_ARE_KEYWORD_ONLY}" ), ), "F6401": ( @@ -206,7 +209,7 @@ def visit_functiondef(self, node): for arg in node.args.args: self._invoked_with_func_args.add(arg.name) - # pylint: disable=protected-access,bad-staticmethod-argument + # pylint: disable=bad-staticmethod-argument @staticmethod def patch_add_message( self, msgid, line=None, node=None, args=None, confidence=None, col_offset=None diff --git a/pyproject.toml b/pyproject.toml index 17a7d80..68ad0a8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,10 +81,14 @@ select = [ "W", # pycodestyle "B", # bugbear "I", # isort -# "RUF", # ruff + "RUF", # ruff "UP", # pyupgrade ] +ignore = [ + "RUF012", # Mutable class attributes should be annotated with `typing.ClassVar` +] + [tool.ruff.pydocstyle] convention = "google" @@ -103,3 +107,66 @@ convention = "google" "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes "PLR2004", # Magic value used in comparison ] + +[tool.pylint] + +ignore-paths="tests/input" # Ignore test inputs + +load-plugins= [ + "pylint_pytest", + "pylint.extensions.bad_builtin", + "pylint.extensions.broad_try_clause", + "pylint.extensions.check_elif", + "pylint.extensions.code_style", + "pylint.extensions.comparetozero", + "pylint.extensions.comparison_placement", + "pylint.extensions.confusing_elif", + # "pylint.extensions.consider_ternary_expression", # Not a pretty refactoring + "pylint.extensions.docparams", + "pylint.extensions.docstyle", + "pylint.extensions.emptystring", + "pylint.extensions.eq_without_hash", + "pylint.extensions.for_any_all", + "pylint.extensions.mccabe", + "pylint.extensions.no_self_use", + "pylint.extensions.overlapping_exceptions", + "pylint.extensions.redefined_loop_name", + "pylint.extensions.redefined_variable_type", + "pylint.extensions.typing", + # "pylint.extensions.while_used", # highly opinionated + "pylint.extensions.dict_init_mutate", + "pylint.extensions.dunder", + "pylint.extensions.typing", + # "pylint.extensions.magic_value", # highly opinionated +] +disable=[ + "docstring-first-line-empty", # C0199; not-an-issue + + # Temporary disables + "cannot-enumerate-pytest-fixtures", # ToDo: Our own message, fix first + "fixme", # needs-work, and probably regex + "attribute-defined-outside-init", + "confusing-consecutive-elif", + "duplicate-code", + "missing-docstring", + "redefined-loop-name", + "too-complex", + "too-many-arguments", + "too-many-nested-blocks", + "too-many-try-statements", + "unspecified-encoding", + "use-maxsplit-arg", + "used-before-assignment", +] + +[tool.pylint.design] +max-args = 7 + +[tool.pylint.reports] +output-format = "colorized" + +[tool.pylint.variables] +ignored-argument-names = "_.*" + +[tool.pylint."messages control"] +enable = ["useless-suppression"] diff --git a/tests/base_tester.py b/tests/base_tester.py index 2ad64c5..5653cfd 100644 --- a/tests/base_tester.py +++ b/tests/base_tester.py @@ -23,7 +23,7 @@ class BasePytestTester: CHECKER_CLASS = BaseChecker IMPACTED_CHECKER_CLASSES = [] MSG_ID = None - MESSAGES = None + msgs = None CONFIG = {} enable_plugin = True @@ -42,18 +42,18 @@ def run_linter(self, enable_plugin, file_path=None): module.file = fin.name self.walk(module) # run all checkers - self.MESSAGES = self.linter.release_messages() + self.msgs = self.linter.release_messages() def verify_messages(self, msg_count, msg_id=None): msg_id = msg_id or self.MSG_ID matched_count = 0 - for message in self.MESSAGES: + for message in self.msgs: # only care about ID and count, not the content if message.msg_id == msg_id: matched_count += 1 - pprint(self.MESSAGES) + pprint(self.msgs) assert matched_count == msg_count, f"expecting {msg_count}, actual {matched_count}" def setup_method(self):