sloplint 0.1.0__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,44 @@
1
+ name: Deploy
2
+ on:
3
+ push:
4
+ branches: ['main']
5
+ workflow_dispatch:
6
+
7
+ permissions:
8
+ contents: read
9
+ pages: write
10
+ id-token: write
11
+
12
+ concurrency:
13
+ group: 'pages'
14
+ cancel-in-progress: false
15
+
16
+ jobs:
17
+ deploy:
18
+ environment:
19
+ name: github-pages
20
+ url: ${{ steps.deployment.outputs.page_url }}
21
+
22
+ runs-on: ubuntu-latest
23
+
24
+ steps:
25
+ - name: Checkout
26
+ uses: actions/checkout@v5
27
+
28
+ - name: Setup uv
29
+ uses: astral-sh/setup-uv@v5
30
+
31
+ - name: Setup Pages
32
+ uses: actions/configure-pages@v5
33
+
34
+ - name: Build Site
35
+ run: make
36
+
37
+ - name: Upload artifact
38
+ uses: actions/upload-pages-artifact@v3
39
+ with:
40
+ path: '.'
41
+
42
+ - name: Deploy to GitHub Pages
43
+ id: deployment
44
+ uses: actions/deploy-pages@v4
@@ -0,0 +1,15 @@
1
+ name: Lint
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ pull_request:
7
+ branches:
8
+ - main
9
+ jobs:
10
+ Lint:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v5
14
+ - uses: astral-sh/setup-uv@v5
15
+ - run: make lint
@@ -0,0 +1,24 @@
1
+ name: PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+
8
+ jobs:
9
+ PyPI:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v5
16
+
17
+ - name: Setup uv
18
+ uses: astral-sh/setup-uv@v5
19
+
20
+ - name: Build
21
+ run: uv build
22
+
23
+ - name: Publish
24
+ run: uv publish
@@ -0,0 +1,28 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+
8
+ jobs:
9
+ Release:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ steps:
14
+ - name: Checkout
15
+ uses: actions/checkout@v5
16
+
17
+ - name: Setup uv
18
+ uses: astral-sh/setup-uv@v5
19
+
20
+ - name: Build distribution
21
+ run: uv build
22
+
23
+ - name: Release
24
+ uses: softprops/action-gh-release@v2
25
+ with:
26
+ files: |
27
+ dist/*.tar.gz
28
+ dist/*.whl
@@ -0,0 +1,15 @@
1
+ name: Test
2
+ on:
3
+ push:
4
+ branches:
5
+ - main
6
+ pull_request:
7
+ branches:
8
+ - main
9
+ jobs:
10
+ Test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v5
14
+ - uses: astral-sh/setup-uv@v5
15
+ - run: make test
@@ -0,0 +1,2 @@
1
+ uv.lock
2
+ **/__pycache__
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: sloplint
3
+ Version: 0.1.0
4
+ Summary: A linter to detect AI-generated markdown prose
5
+ Author-email: Jon Craton <jncraton@gmail.com>
6
+ Classifier: License :: OSI Approved :: MIT License
7
+ Classifier: Operating System :: OS Independent
8
+ Classifier: Programming Language :: Python :: 3
9
+ Requires-Python: >=3.10
10
+ Description-Content-Type: text/markdown
11
+
12
+ # sloplint
13
+
14
+ A linter to detect AI-generated markdown prose
15
+
16
+ ## Installation
17
+
18
+ ```sh
19
+ pip install sloplint
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```sh
25
+ sloplint myfile.md
26
+ ```
27
+
28
+ Or run without installation as:
29
+
30
+ ```sh
31
+ uxv sloplint readme.md
32
+ ```
33
+
34
+ If AI-generated text issues are identified, sloplint will exit with a failure code reporting each issue it discovered just like a linter would.
35
+
36
+ ## Triggers
37
+
38
+ - em-dash
39
+ - emojis
40
+ - bold text
@@ -0,0 +1,13 @@
1
+ all: test
2
+
3
+ lint:
4
+ uv run --with black==24.1.0 python -m black --check sloplint tests
5
+
6
+ format:
7
+ uv run --with black==24.1.0 python -m black sloplint tests
8
+
9
+ test:
10
+ uv run --with pytest==7.4.0 python -m pytest
11
+
12
+ clean:
13
+ rm -rf .pytest_cache **/__pycache__ build dist *.egg-info
@@ -0,0 +1,25 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "sloplint"
7
+ version = "0.1.0"
8
+ description = "A linter to detect AI-generated markdown prose"
9
+ readme = "readme.md"
10
+ authors = [
11
+ { name = "Jon Craton", email = "jncraton@gmail.com" }
12
+ ]
13
+ classifiers = [
14
+ "Programming Language :: Python :: 3",
15
+ "License :: OSI Approved :: MIT License",
16
+ "Operating System :: OS Independent",
17
+ ]
18
+ requires-python = ">=3.10"
19
+ dependencies = []
20
+
21
+ [project.scripts]
22
+ sloplint = "sloplint.cli:main"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["sloplint"]
@@ -0,0 +1,29 @@
1
+ # sloplint
2
+
3
+ A linter to detect AI-generated markdown prose
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ pip install sloplint
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```sh
14
+ sloplint myfile.md
15
+ ```
16
+
17
+ Or run without installation as:
18
+
19
+ ```sh
20
+ uxv sloplint readme.md
21
+ ```
22
+
23
+ If AI-generated text issues are identified, sloplint will exit with a failure code reporting each issue it discovered just like a linter would.
24
+
25
+ ## Triggers
26
+
27
+ - em-dash
28
+ - emojis
29
+ - bold text
@@ -0,0 +1,3 @@
1
+ from .lint import find_markdown_issues
2
+
3
+ __all__ = ["find_markdown_issues"]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,43 @@
1
+ import argparse
2
+ from pathlib import Path
3
+ from typing import Sequence
4
+
5
+ from .lint import find_markdown_issues
6
+
7
+
8
+ def _parse_args(args: Sequence[str] | None = None) -> argparse.Namespace:
9
+ parser = argparse.ArgumentParser(
10
+ prog="sloplint",
11
+ description="A linter to detect AI-generated markdown prose",
12
+ )
13
+ parser.add_argument("paths", nargs="+", help="Markdown file paths to lint")
14
+ return parser.parse_args(args)
15
+
16
+
17
+ def main(argv: Sequence[str] | None = None) -> int:
18
+ args = _parse_args(argv)
19
+ messages: list[str] = []
20
+
21
+ for path_text in args.paths:
22
+ path = Path(path_text)
23
+ if not path.exists():
24
+ print(f"{path}: file not found")
25
+ return 2
26
+ if not path.is_file():
27
+ print(f"{path}: not a file")
28
+ return 2
29
+
30
+ content = path.read_text(encoding="utf-8")
31
+ issues = find_markdown_issues(content)
32
+ for issue in issues:
33
+ messages.append(f"{path}:{issue}")
34
+
35
+ if messages:
36
+ print("\n".join(messages))
37
+ return 1
38
+
39
+ return 0
40
+
41
+
42
+ if __name__ == "__main__":
43
+ raise SystemExit(main())
@@ -0,0 +1,35 @@
1
+ import re
2
+ from typing import Iterable
3
+
4
+ EM_DASH_RE = re.compile(r"—")
5
+ BOLD_RE = re.compile(r"(?:\*\*[^*\n]+\*\*|__[^_\n]+__)")
6
+ EMOJI_RE = re.compile(
7
+ r"["
8
+ r"\U0001F300-\U0001F5FF"
9
+ r"\U0001F600-\U0001F64F"
10
+ r"\U0001F680-\U0001F6FF"
11
+ r"\U0001F700-\U0001F77F"
12
+ r"\U0001F780-\U0001F7FF"
13
+ r"\U0001F800-\U0001F8FF"
14
+ r"\U0001F900-\U0001F9FF"
15
+ r"\U0001FA00-\U0001FA6F"
16
+ r"\U0001FA70-\U0001FAFF"
17
+ r"\u2600-\u26FF"
18
+ r"\u2700-\u27BF"
19
+ r"]"
20
+ )
21
+
22
+
23
+ def find_markdown_issues(content: str) -> list[str]:
24
+ """Return a list of markdown issues detected in the content."""
25
+ issues: list[str] = []
26
+
27
+ for line_number, line in enumerate(content.splitlines(), start=1):
28
+ if EM_DASH_RE.search(line):
29
+ issues.append(f"{line_number}: em-dash detected")
30
+ if EMOJI_RE.search(line):
31
+ issues.append(f"{line_number}: emoji detected")
32
+ if BOLD_RE.search(line):
33
+ issues.append(f"{line_number}: bold markdown detected")
34
+
35
+ return issues
@@ -0,0 +1,41 @@
1
+ from sloplint.cli import main
2
+ from sloplint.lint import find_markdown_issues
3
+
4
+
5
+ def test_find_markdown_issues():
6
+ content = "First line — with em dash\nSecond line **bold**\nThird line 😊\n"
7
+ issues = find_markdown_issues(content)
8
+
9
+ assert "1: em-dash detected" in issues
10
+ assert "2: bold markdown detected" in issues
11
+ assert "3: emoji detected" in issues
12
+
13
+
14
+ def test_main_returns_non_zero_for_issues(tmp_path, capsys):
15
+ path = tmp_path / "sample.md"
16
+ path.write_text("This is **bold**\n", encoding="utf-8")
17
+
18
+ exit_code = main([str(path)])
19
+ captured = capsys.readouterr()
20
+
21
+ assert exit_code == 1
22
+ assert f"{path}:1: bold markdown detected" in captured.out
23
+
24
+
25
+ def test_main_returns_zero_for_clean_file(tmp_path):
26
+ path = tmp_path / "clean.md"
27
+ path.write_text("This is plain markdown\n", encoding="utf-8")
28
+
29
+ exit_code = main([str(path)])
30
+
31
+ assert exit_code == 0
32
+
33
+
34
+ def test_main_reports_missing_file(tmp_path, capsys):
35
+ path = tmp_path / "missing.md"
36
+
37
+ exit_code = main([str(path)])
38
+ captured = capsys.readouterr()
39
+
40
+ assert exit_code == 2
41
+ assert "file not found" in captured.out