dejalint 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dejalint-0.0.1/.github/workflows/publish.yml +33 -0
- dejalint-0.0.1/.github/workflows/tests.yml +40 -0
- dejalint-0.0.1/.gitignore +5 -0
- dejalint-0.0.1/.pre-commit-config.yaml +24 -0
- dejalint-0.0.1/PKG-INFO +30 -0
- dejalint-0.0.1/README.rst +23 -0
- dejalint-0.0.1/dejalint/__init__.py +0 -0
- dejalint-0.0.1/dejalint/__main__.py +5 -0
- dejalint-0.0.1/dejalint/main.py +91 -0
- dejalint-0.0.1/dejalint.egg-info/PKG-INFO +30 -0
- dejalint-0.0.1/dejalint.egg-info/SOURCES.txt +19 -0
- dejalint-0.0.1/dejalint.egg-info/dependency_links.txt +1 -0
- dejalint-0.0.1/dejalint.egg-info/entry_points.txt +2 -0
- dejalint-0.0.1/dejalint.egg-info/scm_file_list.json +16 -0
- dejalint-0.0.1/dejalint.egg-info/scm_version.json +8 -0
- dejalint-0.0.1/dejalint.egg-info/top_level.txt +1 -0
- dejalint-0.0.1/pyproject.toml +61 -0
- dejalint-0.0.1/requirements.txt +3 -0
- dejalint-0.0.1/setup.cfg +4 -0
- dejalint-0.0.1/tests/test_import.py +5 -0
- dejalint-0.0.1/tests/test_main.py +35 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [created]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
deploy:
|
|
9
|
+
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # Required for OIDC authentication
|
|
14
|
+
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
- name: Set up Python
|
|
18
|
+
uses: actions/setup-python@v4
|
|
19
|
+
with:
|
|
20
|
+
python-version: '3.x'
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: |
|
|
24
|
+
python -m pip install --upgrade pip
|
|
25
|
+
pip install build
|
|
26
|
+
|
|
27
|
+
- name: Build package
|
|
28
|
+
run: python -m build
|
|
29
|
+
|
|
30
|
+
- name: Publish to PyPI
|
|
31
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
32
|
+
with:
|
|
33
|
+
verbose: true
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
name: tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- "**"
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
build:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v3
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v4
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install dependencies
|
|
25
|
+
run: |
|
|
26
|
+
pip install -r requirements.txt
|
|
27
|
+
pip install -e .
|
|
28
|
+
|
|
29
|
+
- name: Run the tests
|
|
30
|
+
run: |
|
|
31
|
+
PYTHONFAULTHANDLER=2 pytest tests/
|
|
32
|
+
|
|
33
|
+
- name: Run pre-commit checks
|
|
34
|
+
uses: pre-commit/action@v3.0.1
|
|
35
|
+
|
|
36
|
+
- name: Check package metadata rendering (twine check)
|
|
37
|
+
run: |
|
|
38
|
+
pip install build twine
|
|
39
|
+
python -m build --sdist --wheel
|
|
40
|
+
twine check dist/*
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: check-added-large-files
|
|
6
|
+
args: ["--maxkb=1000"]
|
|
7
|
+
- id: check-json
|
|
8
|
+
- id: check-docstring-first
|
|
9
|
+
- id: end-of-file-fixer
|
|
10
|
+
- id: trailing-whitespace
|
|
11
|
+
|
|
12
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
13
|
+
rev: v0.16.3
|
|
14
|
+
hooks:
|
|
15
|
+
- id: ruff-check
|
|
16
|
+
args: ["--fix"]
|
|
17
|
+
- id: ruff-format
|
|
18
|
+
|
|
19
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
20
|
+
rev: v1.14.1
|
|
21
|
+
hooks:
|
|
22
|
+
- id: mypy
|
|
23
|
+
additional_dependencies: ["types-PyYAML"]
|
|
24
|
+
args: ["--config-file", "pyproject.toml"]
|
dejalint-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dejalint
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Yet another linter
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/x-rst
|
|
7
|
+
|
|
8
|
+
dejalint
|
|
9
|
+
========
|
|
10
|
+
|
|
11
|
+
.. image:: https://github.com/kqf/dejalint/actions/workflows/tests.yml/badge.svg
|
|
12
|
+
:target: https://github.com/kqf/dejalint/actions
|
|
13
|
+
:alt: Tests
|
|
14
|
+
|
|
15
|
+
.. image:: https://img.shields.io/pypi/dm/dejalint.svg
|
|
16
|
+
:target: https://pypi.org/project/dejalint/
|
|
17
|
+
:alt: PyPI Downloads
|
|
18
|
+
|
|
19
|
+
``dejalint`` is yet another linter.
|
|
20
|
+
|
|
21
|
+
----
|
|
22
|
+
|
|
23
|
+
Installation
|
|
24
|
+
------------
|
|
25
|
+
|
|
26
|
+
Install via pip:
|
|
27
|
+
|
|
28
|
+
.. code-block:: bash
|
|
29
|
+
|
|
30
|
+
pip install dejalint
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
dejalint
|
|
2
|
+
========
|
|
3
|
+
|
|
4
|
+
.. image:: https://github.com/kqf/dejalint/actions/workflows/tests.yml/badge.svg
|
|
5
|
+
:target: https://github.com/kqf/dejalint/actions
|
|
6
|
+
:alt: Tests
|
|
7
|
+
|
|
8
|
+
.. image:: https://img.shields.io/pypi/dm/dejalint.svg
|
|
9
|
+
:target: https://pypi.org/project/dejalint/
|
|
10
|
+
:alt: PyPI Downloads
|
|
11
|
+
|
|
12
|
+
``dejalint`` is yet another linter.
|
|
13
|
+
|
|
14
|
+
----
|
|
15
|
+
|
|
16
|
+
Installation
|
|
17
|
+
------------
|
|
18
|
+
|
|
19
|
+
Install via pip:
|
|
20
|
+
|
|
21
|
+
.. code-block:: bash
|
|
22
|
+
|
|
23
|
+
pip install dejalint
|
|
File without changes
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
FunctionNode = ast.FunctionDef | ast.AsyncFunctionDef
|
|
8
|
+
DefinitionNode = FunctionNode | ast.ClassDef
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def strip_docstring(body: list[ast.stmt]) -> list[ast.stmt]:
|
|
12
|
+
first = body[0] if body else None
|
|
13
|
+
if isinstance(first, ast.Expr) and isinstance(first.value, ast.Constant):
|
|
14
|
+
return body[1:]
|
|
15
|
+
return body
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def size(body: list[ast.stmt]) -> int:
|
|
19
|
+
return sum(1 for stmt in body for _ in ast.walk(stmt))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def iter_files(paths: list[Path]) -> list[Path]:
|
|
23
|
+
def relative(path):
|
|
24
|
+
for py in sorted(path.rglob("*.py")):
|
|
25
|
+
if not any(p.startswith(".") for p in py.relative_to(path).parts):
|
|
26
|
+
yield py
|
|
27
|
+
|
|
28
|
+
files = []
|
|
29
|
+
for path in paths:
|
|
30
|
+
if not path.is_dir():
|
|
31
|
+
files.append(path)
|
|
32
|
+
continue
|
|
33
|
+
files.extend(relative(path))
|
|
34
|
+
return files
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def report(code: str, message: str, groups: dict) -> list[str]:
|
|
38
|
+
errors: list[str] = []
|
|
39
|
+
for locs in groups.values():
|
|
40
|
+
first, *copies = locs
|
|
41
|
+
where = f"{first[0]}:{first[1].lineno}"
|
|
42
|
+
errors.extend(
|
|
43
|
+
f"{py}:{node.lineno}:{node.col_offset + 1}: {code} "
|
|
44
|
+
f"{message} '{node.name}' (first at {where})"
|
|
45
|
+
for py, node in copies
|
|
46
|
+
)
|
|
47
|
+
return errors
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def check(paths: list[Path], min_nodes: int) -> list[str]:
|
|
51
|
+
bodies = defaultdict(list)
|
|
52
|
+
names = defaultdict(list)
|
|
53
|
+
for py in iter_files(paths):
|
|
54
|
+
tree = ast.parse(py.read_text(), str(py))
|
|
55
|
+
for bnode in tree.body:
|
|
56
|
+
if isinstance(bnode, DefinitionNode):
|
|
57
|
+
names[bnode.name].append((py, bnode))
|
|
58
|
+
|
|
59
|
+
for tnode in ast.walk(tree):
|
|
60
|
+
if not isinstance(tnode, FunctionNode):
|
|
61
|
+
continue
|
|
62
|
+
body = strip_docstring(tnode.body)
|
|
63
|
+
# Skip stubs and one-liners like `...` or `return x`
|
|
64
|
+
if size(body) < min_nodes:
|
|
65
|
+
continue
|
|
66
|
+
key = ast.dump(ast.Module(body=body, type_ignores=[]))
|
|
67
|
+
bodies[key].append((py, tnode))
|
|
68
|
+
return sorted(
|
|
69
|
+
report("DJL001", "Duplicate function body", bodies)
|
|
70
|
+
+ report("DJL002", "Duplicate name", names)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> int:
|
|
75
|
+
parser = argparse.ArgumentParser(
|
|
76
|
+
prog="dejalint",
|
|
77
|
+
description=__doc__,
|
|
78
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument("paths", nargs="*", type=Path, default=[Path()])
|
|
81
|
+
parser.add_argument("--min-nodes", type=int, default=8)
|
|
82
|
+
args = parser.parse_args()
|
|
83
|
+
|
|
84
|
+
errors = check(args.paths, args.min_nodes)
|
|
85
|
+
for error in errors:
|
|
86
|
+
print(error)
|
|
87
|
+
return 1 if errors else 0
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
if __name__ == "__main__":
|
|
91
|
+
sys.exit(main())
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dejalint
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Yet another linter
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/x-rst
|
|
7
|
+
|
|
8
|
+
dejalint
|
|
9
|
+
========
|
|
10
|
+
|
|
11
|
+
.. image:: https://github.com/kqf/dejalint/actions/workflows/tests.yml/badge.svg
|
|
12
|
+
:target: https://github.com/kqf/dejalint/actions
|
|
13
|
+
:alt: Tests
|
|
14
|
+
|
|
15
|
+
.. image:: https://img.shields.io/pypi/dm/dejalint.svg
|
|
16
|
+
:target: https://pypi.org/project/dejalint/
|
|
17
|
+
:alt: PyPI Downloads
|
|
18
|
+
|
|
19
|
+
``dejalint`` is yet another linter.
|
|
20
|
+
|
|
21
|
+
----
|
|
22
|
+
|
|
23
|
+
Installation
|
|
24
|
+
------------
|
|
25
|
+
|
|
26
|
+
Install via pip:
|
|
27
|
+
|
|
28
|
+
.. code-block:: bash
|
|
29
|
+
|
|
30
|
+
pip install dejalint
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
.gitignore
|
|
2
|
+
.pre-commit-config.yaml
|
|
3
|
+
README.rst
|
|
4
|
+
pyproject.toml
|
|
5
|
+
requirements.txt
|
|
6
|
+
.github/workflows/publish.yml
|
|
7
|
+
.github/workflows/tests.yml
|
|
8
|
+
dejalint/__init__.py
|
|
9
|
+
dejalint/__main__.py
|
|
10
|
+
dejalint/main.py
|
|
11
|
+
dejalint.egg-info/PKG-INFO
|
|
12
|
+
dejalint.egg-info/SOURCES.txt
|
|
13
|
+
dejalint.egg-info/dependency_links.txt
|
|
14
|
+
dejalint.egg-info/entry_points.txt
|
|
15
|
+
dejalint.egg-info/scm_file_list.json
|
|
16
|
+
dejalint.egg-info/scm_version.json
|
|
17
|
+
dejalint.egg-info/top_level.txt
|
|
18
|
+
tests/test_import.py
|
|
19
|
+
tests/test_main.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"files": [
|
|
3
|
+
".github/workflows/publish.yml",
|
|
4
|
+
".github/workflows/tests.yml",
|
|
5
|
+
".gitignore",
|
|
6
|
+
".pre-commit-config.yaml",
|
|
7
|
+
"README.rst",
|
|
8
|
+
"dejalint/__init__.py",
|
|
9
|
+
"dejalint/__main__.py",
|
|
10
|
+
"dejalint/main.py",
|
|
11
|
+
"pyproject.toml",
|
|
12
|
+
"requirements.txt",
|
|
13
|
+
"tests/test_import.py",
|
|
14
|
+
"tests/test_main.py"
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
dejalint
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
[tool.ruff]
|
|
2
|
+
line-length = 79
|
|
3
|
+
target-version = "py310"
|
|
4
|
+
|
|
5
|
+
[tool.ruff.format]
|
|
6
|
+
quote-style = "double"
|
|
7
|
+
line-ending = "lf"
|
|
8
|
+
indent-style = "space"
|
|
9
|
+
skip-magic-trailing-comma = false
|
|
10
|
+
|
|
11
|
+
[tool.ruff.lint]
|
|
12
|
+
extend-select = [
|
|
13
|
+
"E", # pycodestyle
|
|
14
|
+
"F", # pyflakes
|
|
15
|
+
"I", # isort
|
|
16
|
+
"UP", # pyupgrade
|
|
17
|
+
# "B", # bugbear
|
|
18
|
+
# "C90", # mccabe
|
|
19
|
+
# "D", # pydocstyle
|
|
20
|
+
"ERA", # eradicate
|
|
21
|
+
]
|
|
22
|
+
ignore = [
|
|
23
|
+
"E203",
|
|
24
|
+
"B008", # Do not perform function call
|
|
25
|
+
"RUF009", # Don not perform partials
|
|
26
|
+
# "W503",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[tool.ruff.lint.isort]
|
|
30
|
+
combine-as-imports = true
|
|
31
|
+
|
|
32
|
+
[tool.mypy]
|
|
33
|
+
python_version = "3.10"
|
|
34
|
+
strict = false
|
|
35
|
+
ignore_missing_imports = true
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
addopts = "--cov=dejalint --cov-report=term-missing"
|
|
39
|
+
|
|
40
|
+
[build-system]
|
|
41
|
+
requires = ["setuptools", "wheel", "setuptools_scm"]
|
|
42
|
+
build-backend = "setuptools.build_meta"
|
|
43
|
+
|
|
44
|
+
[project]
|
|
45
|
+
# NB: Keep it the same as in PyPI
|
|
46
|
+
name = "dejalint"
|
|
47
|
+
dynamic = ["version"]
|
|
48
|
+
description = "Yet another linter"
|
|
49
|
+
readme = "README.rst"
|
|
50
|
+
requires-python = ">=3.10"
|
|
51
|
+
|
|
52
|
+
dependencies = []
|
|
53
|
+
|
|
54
|
+
[project.scripts]
|
|
55
|
+
dejalint = "dejalint.main:main"
|
|
56
|
+
|
|
57
|
+
[tool.setuptools]
|
|
58
|
+
packages = ["dejalint"]
|
|
59
|
+
|
|
60
|
+
[tool.setuptools_scm]
|
|
61
|
+
version_scheme = "post-release" # Uses Git tags for versioning
|
dejalint-0.0.1/setup.cfg
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from dejalint.main import check
|
|
6
|
+
|
|
7
|
+
BODY = """
|
|
8
|
+
total = 0
|
|
9
|
+
for x in xs:
|
|
10
|
+
total += x * x
|
|
11
|
+
return total
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def src(tmp_path: Path) -> Path:
|
|
17
|
+
(tmp_path / "a.py").write_text(f"def squares(xs):{BODY}")
|
|
18
|
+
(tmp_path / "b.py").write_text(
|
|
19
|
+
f'def other(xs):\n """Docstring is ignored."""{BODY}\n'
|
|
20
|
+
"class squares:\n pass\n"
|
|
21
|
+
)
|
|
22
|
+
(tmp_path / "c.py").write_text("def unique():\n return 1\n")
|
|
23
|
+
return tmp_path
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_check(src: Path):
|
|
27
|
+
a, b = src / "a.py", src / "b.py"
|
|
28
|
+
assert check([src], min_nodes=8) == [
|
|
29
|
+
f"{b}:1:1: DJL001 Duplicate function body 'other' (first at {a}:1)",
|
|
30
|
+
f"{b}:8:1: DJL002 Duplicate name 'squares' (first at {a}:1)",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_clean(src: Path):
|
|
35
|
+
assert check([src / "c.py"], min_nodes=8) == []
|