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.
@@ -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,5 @@
1
+ *.egg*
2
+ __pycache__/
3
+ .python-version
4
+ .coverage
5
+ .vscode
@@ -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"]
@@ -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,5 @@
1
+ import sys
2
+
3
+ from dejalint.main import main
4
+
5
+ sys.exit(main())
@@ -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,2 @@
1
+ [console_scripts]
2
+ dejalint = dejalint.main:main
@@ -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,8 @@
1
+ {
2
+ "tag": "0.0.1",
3
+ "distance": 0,
4
+ "node": "geb22192f489b4ba0f840bf89ab01467790320396",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-23"
8
+ }
@@ -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
@@ -0,0 +1,3 @@
1
+ pytest
2
+ pytest-cov
3
+ pre-commit
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ import dejalint
2
+
3
+
4
+ def test_import():
5
+ assert dejalint is not None
@@ -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) == []