Initial commit
This commit is contained in:
commit
1ce8864a60
34
.github/workflows/build.yaml
vendored
Normal file
34
.github/workflows/build.yaml
vendored
Normal file
@ -0,0 +1,34 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- '**'
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
python_version: ['3.9']
|
||||
os: [ubuntu-latest]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python ${{ matrix.python_version }}
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python_version }}
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade --upgrade-strategy eager tox
|
||||
|
||||
- name: Build and install wheel
|
||||
run: |
|
||||
tox run -m build
|
||||
python -m pip install dist/*.whl
|
55
.github/workflows/release.yaml
vendored
Normal file
55
.github/workflows/release.yaml
vendored
Normal file
@ -0,0 +1,55 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "[0-9]+.[0-9]+.[0-9]+*"
|
||||
|
||||
jobs:
|
||||
build-and-publish:
|
||||
runs-on: ubuntu-latest
|
||||
# Specifying a GitHub environment is optional, but strongly encouraged
|
||||
environment: release
|
||||
permissions:
|
||||
# IMPORTANT: this permission is mandatory for trusted publishing
|
||||
id-token: write
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Python 3.9
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9
|
||||
|
||||
- name: Install tox
|
||||
run: |
|
||||
python -m pip install --upgrade --upgrade-strategy eager tox
|
||||
|
||||
- name: Build and install wheel
|
||||
run: |
|
||||
tox run -m build
|
||||
python -m pip install dist/*.whl
|
||||
|
||||
- name: "Publish distribution 📦 to PyPI"
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
- name: Get release name
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
shell: bash
|
||||
run: |
|
||||
git fetch --depth=1 origin +refs/tags/*:refs/tags/* # github is dumb
|
||||
echo "release_name=$(git tag -l --format "%(refname:strip=2): %(contents:lines=1)" ${{ github.ref_name }})" >> $GITHUB_ENV
|
||||
|
||||
- name: Release
|
||||
uses: softprops/action-gh-release@v1
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
name: "${{ env.release_name }}"
|
||||
draft: false
|
||||
files: |
|
||||
dist/*.whl
|
88
.gitignore
vendored
Normal file
88
.gitignore
vendored
Normal file
@ -0,0 +1,88 @@
|
||||
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion
|
||||
|
||||
*.iml
|
||||
|
||||
## Directory-based project format:
|
||||
.idea/
|
||||
|
||||
### Other editors
|
||||
.*.swp
|
||||
nbproject/
|
||||
.vscode
|
||||
|
||||
*.exe
|
||||
*.zip
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# ruff
|
||||
.ruff_cache
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# for testing
|
||||
temp/
|
||||
tmp/
|
33
.pre-commit-config.yaml
Normal file
33
.pre-commit-config.yaml
Normal file
@ -0,0 +1,33 @@
|
||||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.5.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
- id: end-of-file-fixer
|
||||
- id: check-yaml
|
||||
- id: debug-statements
|
||||
- id: double-quote-string-fixer
|
||||
- id: name-tests-test
|
||||
- repo: https://github.com/asottile/reorder-python-imports
|
||||
rev: v3.12.0
|
||||
hooks:
|
||||
- id: reorder-python-imports
|
||||
args: [--py38-plus, --add-import, 'from __future__ import annotations']
|
||||
- repo: https://github.com/hhatto/autopep8
|
||||
rev: v2.0.4
|
||||
hooks:
|
||||
- id: autopep8
|
||||
- repo: https://github.com/asottile/add-trailing-comma
|
||||
rev: v3.1.0
|
||||
hooks:
|
||||
- id: add-trailing-comma
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.1.14
|
||||
hooks:
|
||||
- id: ruff
|
||||
args: [ --fix ]
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
- id: mypy
|
19
LICENSE
Normal file
19
LICENSE
Normal file
@ -0,0 +1,19 @@
|
||||
Copyright (c) 2014 pre-commit dev team: Anthony Sottile, Ken Struys
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
38
README.md
Normal file
38
README.md
Normal file
@ -0,0 +1,38 @@
|
||||
[](https://github.com/lordwelch/flake8-no-nested-comprehensions/actions/workflows/build.yaml)
|
||||
[](https://results.pre-commit.ci/latest/github/lordwelch/flake8-no-nested-comprehensions/main)
|
||||
|
||||
flake8-no-nested-comprehensions
|
||||
================
|
||||
|
||||
flake8 plugin which forbids nested comprehensions
|
||||
|
||||
## installation
|
||||
|
||||
```bash
|
||||
pip install flake8-no-nested-comprehensions
|
||||
```
|
||||
|
||||
## flake8 codes
|
||||
|
||||
| Code | Description |
|
||||
|--------|----------------------------------|
|
||||
| CMP100 | do not use nested comprehensions |
|
||||
|
||||
## rationale
|
||||
|
||||
I don't like them.
|
||||
If you need them for performance you can put a `# noqa: CMP100` and preferrably put a comment in explaining it.
|
||||
|
||||
## as a pre-commit hook
|
||||
|
||||
See [pre-commit](https://github.com/pre-commit/pre-commit) for instructions
|
||||
|
||||
Sample `.pre-commit-config.yaml`:
|
||||
|
||||
```yaml
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 3.8.1
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [flake8-no-nested-comprehensions==1.0.0]
|
||||
```
|
54
flake8_no_nested_comprehensions.py
Normal file
54
flake8_no_nested_comprehensions.py
Normal file
@ -0,0 +1,54 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import importlib.metadata
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
import argparse
|
||||
import flake8.options.manager
|
||||
from collections.abc import Generator
|
||||
|
||||
|
||||
class Visitor(ast.NodeVisitor):
|
||||
def __init__(self, max_generators: int) -> None:
|
||||
self.problems: list[tuple[int, int]] = []
|
||||
self.max_generators = max_generators
|
||||
|
||||
self.visit_ListComp = self._visit_max_generators
|
||||
self.visit_SetComp = self._visit_max_generators
|
||||
self.visit_GeneratorExp = self._visit_max_generators
|
||||
self.visit_DictComp = self._visit_max_generators
|
||||
|
||||
def _visit_max_generators(self, node: ast.ListComp | ast.SetComp | ast.GeneratorExp | ast.DictComp) -> None:
|
||||
if len(node.generators) > self.max_generators:
|
||||
self.problems.append((node.lineno, node.col_offset))
|
||||
self.generic_visit(node)
|
||||
|
||||
|
||||
class Plugin:
|
||||
name = __name__
|
||||
version = importlib.metadata.version(__name__)
|
||||
max_generators = 1
|
||||
|
||||
def __init__(self, tree: ast.AST) -> None:
|
||||
self._tree = tree
|
||||
|
||||
def add_options(manager: flake8.options.manager.OptionManager) -> None: # pragma: no cover
|
||||
manager.add_option(
|
||||
'--max-comprehensions', type=int, metavar='n',
|
||||
default=1, parse_from_config=True,
|
||||
help='Maximum allowed generators in a single comprehension. (Default: %(default)s)',
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def parse_options(options: argparse.Namespace) -> None: # pragma: no cover
|
||||
Plugin.max_generators = options.max_comprehensions
|
||||
|
||||
def run(self) -> Generator[tuple[int, int, str, type[Any]], None, None]:
|
||||
visitor = Visitor(self.max_generators)
|
||||
visitor.visit(self._tree)
|
||||
for line, col in visitor.problems:
|
||||
yield line, col, 'CMP100 no nested comprehension', type(self)
|
114
pyproject.toml
Normal file
114
pyproject.toml
Normal file
@ -0,0 +1,114 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=61.2", "wheel", "setuptools_scm[toml]>=3.4"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "flake8_no_nested_comprehensions"
|
||||
description = "A flake8 plugin to disallow nested comprehensions"
|
||||
authors = [{name = "Timmy Welch", email = "timmy@narnian.us"}]
|
||||
license = {text = "MIT"}
|
||||
classifiers = [
|
||||
"License :: OSI Approved :: MIT License",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3 :: Only",
|
||||
"Programming Language :: Python :: Implementation :: CPython",
|
||||
"Programming Language :: Python :: Implementation :: PyPy",
|
||||
]
|
||||
urls = {Homepage = "https://github.com/lordwelch/flake8_no_nested_comprehensions"}
|
||||
requires-python = ">=3.9"
|
||||
dependencies = ["flake8>4"]
|
||||
dynamic = ["version"]
|
||||
|
||||
[project.readme]
|
||||
file = "README.md"
|
||||
content-type = "text/markdown"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
local_scheme = "no-local-version"
|
||||
|
||||
[tool.setuptools]
|
||||
py-modules = ["flake8_no_nested_comprehensions"]
|
||||
include-package-data = true
|
||||
license-files = ["LICENSE"]
|
||||
|
||||
[project.entry-points."flake8.extension"]
|
||||
CMP = "flake8_no_nested_comprehensions:Plugin"
|
||||
|
||||
[tool.tox]
|
||||
legacy_tox_ini = """
|
||||
[tox:tox]
|
||||
envlist = py3.9
|
||||
|
||||
[testenv]
|
||||
deps = -rrequirements-dev.txt
|
||||
commands =
|
||||
coverage erase
|
||||
coverage run -m pytest {posargs:tests}
|
||||
coverage report
|
||||
|
||||
[testenv:wheel]
|
||||
description = Generate wheel and tar.gz
|
||||
labels =
|
||||
release
|
||||
build
|
||||
skip_install = true
|
||||
deps =
|
||||
build
|
||||
commands_pre =
|
||||
-python -c 'import shutil,pathlib; \
|
||||
shutil.rmtree("./build/", ignore_errors=True); \
|
||||
shutil.rmtree("./dist/", ignore_errors=True)'
|
||||
commands =
|
||||
python -m build
|
||||
|
||||
[testenv:pypi-upload]
|
||||
description = Upload wheel to PyPi
|
||||
platform = Linux
|
||||
labels =
|
||||
release
|
||||
skip_install = true
|
||||
depends = wheel
|
||||
deps =
|
||||
twine
|
||||
passenv =
|
||||
TWINE_*
|
||||
setenv =
|
||||
TWINE_NON_INTERACTIVE=true
|
||||
commands =
|
||||
python -m twine upload dist/*.whl dist/*.tar.gz
|
||||
"""
|
||||
|
||||
|
||||
[tool.pep8]
|
||||
ignore = "E265,E501"
|
||||
max_line_length = "120"
|
||||
|
||||
[tool.autopep8]
|
||||
max_line_length = 120
|
||||
ignore = "E265,E501"
|
||||
|
||||
[tool.mypy]
|
||||
check_untyped_defs = true
|
||||
disallow_any_generics = true
|
||||
warn_return_any = false
|
||||
disallow_incomplete_defs = true
|
||||
disallow_untyped_defs = true
|
||||
no_implicit_optional = true
|
||||
warn_redundant_casts = true
|
||||
warn_unused_ignores = true
|
||||
disable_error_code = ['method-assign']
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["testing.*"]
|
||||
warn_return_any = false
|
||||
disallow_untyped_defs = false
|
||||
|
||||
[[tool.mypy.overrides]]
|
||||
module = ["tests.*"]
|
||||
warn_return_any = false
|
||||
disallow_untyped_defs = false
|
||||
|
||||
[tool.ruff]
|
||||
line-length = 120
|
||||
extend-safe-fixes = ["TCH"]
|
||||
extend-select = ["COM812", "TCH"]
|
5
requirements-dev.txt
Normal file
5
requirements-dev.txt
Normal file
@ -0,0 +1,5 @@
|
||||
build
|
||||
covdefaults
|
||||
coverage
|
||||
pytest
|
||||
tox
|
25
tests/no_nested_comprehensions_test.py
Normal file
25
tests/no_nested_comprehensions_test.py
Normal file
@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
|
||||
from flake8_no_nested_comprehensions import Plugin
|
||||
|
||||
|
||||
def _results(s: str) -> set[str]:
|
||||
tree = ast.parse(s)
|
||||
plugin = Plugin(tree)
|
||||
return {f'{line}:{col} {msg}' for line, col, msg, _ in plugin.run()}
|
||||
|
||||
|
||||
def test_trivial_case() -> None:
|
||||
assert _results('') == set()
|
||||
|
||||
|
||||
def test_nested_comprehension() -> None:
|
||||
ret = _results('[attr for style in styles for attr in {"testing", "set"}]')
|
||||
assert ret == {'1:0 CMP100 no nested comprehension'}
|
||||
|
||||
|
||||
def test_allowed_comprehension() -> None:
|
||||
ret = _results('[style for style in {"testing", "set"}]')
|
||||
assert ret == set()
|
Loading…
x
Reference in New Issue
Block a user