Initial commit

This commit is contained in:
Timmy Welch 2024-02-02 18:30:05 -08:00
commit 1ce8864a60
10 changed files with 465 additions and 0 deletions

34
.github/workflows/build.yaml vendored Normal file
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1,38 @@
[![build status](https://github.com/lordwelch/flake8-no-nested-comprehensions/actions/workflows/build.yaml/badge.svg)](https://github.com/lordwelch/flake8-no-nested-comprehensions/actions/workflows/build.yaml)
[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/lordwelch/flake8-no-nested-comprehensions/main.svg)](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]
```

View 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
View 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
View File

@ -0,0 +1,5 @@
build
covdefaults
coverage
pytest
tox

View 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()