bubbles-lint 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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Syed (Sadat) Nazrul
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,211 @@
1
+ Metadata-Version: 2.4
2
+ Name: bubbles-lint
3
+ Version: 0.1.0
4
+ Summary: A Python-first architectural linter for small, composable, Unix-like code.
5
+ Author: Bubbles Lint contributors
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Syed (Sadat) Nazrul
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+ License-File: LICENSE
28
+ Keywords: architecture,linter,python,static-analysis,unix-philosophy
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Environment :: Console
31
+ Classifier: Intended Audience :: Developers
32
+ Classifier: License :: OSI Approved :: MIT License
33
+ Classifier: Programming Language :: Python :: 3
34
+ Classifier: Programming Language :: Python :: 3 :: Only
35
+ Classifier: Programming Language :: Python :: 3.9
36
+ Classifier: Programming Language :: Python :: 3.10
37
+ Classifier: Programming Language :: Python :: 3.11
38
+ Classifier: Programming Language :: Python :: 3.12
39
+ Classifier: Programming Language :: Python :: 3.13
40
+ Classifier: Topic :: Software Development :: Quality Assurance
41
+ Requires-Python: >=3.9
42
+ Provides-Extra: dev
43
+ Requires-Dist: pytest>=8; extra == 'dev'
44
+ Description-Content-Type: text/markdown
45
+
46
+ <p align="center">
47
+ <img src="assets/bubbles-lint-logo.png" alt="Bubbles Lint logo" width="220">
48
+ </p>
49
+
50
+ Bubbles Lint is an architectural linter for Python code. It is inspired by Unix and Linux software design principles: do one thing well, keep modules small, compose simple parts, and make boundaries easy to inspect.
51
+
52
+ Think of it as Ruff for architecture. It does not compete with Ruff, Black, Flake8, or Pylint. Bubbles Lint looks for software shape problems: code that is too large, too coupled, too magical, or too monolithic.
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install bubbles-lint
58
+ ```
59
+
60
+ For local development from this repository:
61
+
62
+ ```bash
63
+ pip install -e ".[dev]"
64
+ ```
65
+
66
+ ## Usage
67
+
68
+ Scan the current repository:
69
+
70
+ ```bash
71
+ bubbles-lint scan .
72
+ ```
73
+
74
+ Emit JSON for CI systems:
75
+
76
+ ```bash
77
+ bubbles-lint scan . --json
78
+ ```
79
+
80
+ Bubbles Lint exits with status `1` when findings are present and `0` when the scan is clean.
81
+
82
+ ## Philosophy
83
+
84
+ Bubbles Lint encourages codebases where:
85
+
86
+ - modules have one clear responsibility
87
+ - functions and classes stay small enough to replace
88
+ - dependencies flow through explicit interfaces
89
+ - configuration is loaded at the edge
90
+ - side effects are isolated from pure logic
91
+ - text and data move through well-defined boundaries
92
+ - components are easy to inspect, test, and swap
93
+
94
+ The goal is not style enforcement. The goal is software design discipline, especially in Python codebases that have grown quickly with help from AI coding assistants.
95
+
96
+ ## Rules
97
+
98
+ ### Bubble Burst
99
+
100
+ Flags code that has grown too large:
101
+
102
+ - files over `max_file_lines`
103
+ - functions over `max_function_lines`
104
+ - classes with more than `max_class_methods`
105
+ - functions with more than `max_function_params`
106
+
107
+ ### Bubble Leak
108
+
109
+ Flags hidden coupling and mixed side effects:
110
+
111
+ - global mutable state
112
+ - direct environment variable reads outside config modules
113
+ - functions mixing too many side-effect categories, such as filesystem, network, database, subprocess, logging, and rendering
114
+
115
+ ### Bubble Boundary
116
+
117
+ Flags dependency boundary problems:
118
+
119
+ - circular imports where practical
120
+ - modules importing too many dependencies
121
+ - imports from private modules like `from package._internal import thing`
122
+
123
+ ### AI Smells
124
+
125
+ Flags common broad abstractions produced in rushed or generated code:
126
+
127
+ - oversized `utils.py`, `helpers.py`, `manager.py`, and `service.py` modules
128
+ - broad classes ending in `Manager`, `Service`, or `Handler`
129
+ - deeply nested control flow
130
+
131
+ ## Configuration
132
+
133
+ Configure Bubbles Lint in `pyproject.toml`:
134
+
135
+ ```toml
136
+ [tool.bubbles-lint]
137
+ max_file_lines = 500
138
+ max_function_lines = 50
139
+ max_class_methods = 10
140
+ max_function_params = 5
141
+ max_imports_per_module = 20
142
+ max_nesting_depth = 4
143
+ allow_private_imports = false
144
+ ```
145
+
146
+ Additional knobs:
147
+
148
+ ```toml
149
+ [tool.bubbles-lint]
150
+ max_side_effect_kinds = 3
151
+ max_ai_module_lines = 200
152
+ max_ai_class_dependencies = 8
153
+ excludes = ["generated"]
154
+ ```
155
+
156
+ Bubbles Lint always ignores `.venv`, `venv`, `.git`, `__pycache__`, `build`, and `dist`.
157
+
158
+ Existing `[tool.bubbles]` configs are still read for compatibility, but new projects should use `[tool.bubbles-lint]`.
159
+
160
+ ## Human Output
161
+
162
+ ```text
163
+ Bubble: Bubble Burst
164
+
165
+ src/payment_service.py:1
166
+ warning: File has 1437 lines; recommended maximum is 500.
167
+
168
+ Suggestion:
169
+ Split this module into smaller bubbles with one responsibility each.
170
+ ```
171
+
172
+ ## JSON Output
173
+
174
+ ```json
175
+ {
176
+ "findings": [
177
+ {
178
+ "rule": "bubble-burst/file-too-large",
179
+ "severity": "warning",
180
+ "path": "src/payment_service.py",
181
+ "line": 1,
182
+ "message": "File has 1437 lines; recommended maximum is 500.",
183
+ "suggestion": "Split this module into smaller bubbles with one responsibility each."
184
+ }
185
+ ],
186
+ "files_scanned": 1
187
+ }
188
+ ```
189
+
190
+ ## CI
191
+
192
+ Example GitHub Actions step:
193
+
194
+ ```yaml
195
+ - name: Install Bubbles Lint
196
+ run: pip install .
197
+
198
+ - name: Scan architecture
199
+ run: bubbles-lint scan . --json
200
+ ```
201
+
202
+ ## Development
203
+
204
+ Run tests:
205
+
206
+ ```bash
207
+ pip install -e ".[dev]"
208
+ pytest
209
+ ```
210
+
211
+ The rule engine is intentionally small. New rules implement a `check(context)` method and return `Finding` objects. Parsing, configuration, scanning, reporting, and CLI code are separated so the tool can grow without becoming the kind of monolith it warns about.
@@ -0,0 +1,166 @@
1
+ <p align="center">
2
+ <img src="assets/bubbles-lint-logo.png" alt="Bubbles Lint logo" width="220">
3
+ </p>
4
+
5
+ Bubbles Lint is an architectural linter for Python code. It is inspired by Unix and Linux software design principles: do one thing well, keep modules small, compose simple parts, and make boundaries easy to inspect.
6
+
7
+ Think of it as Ruff for architecture. It does not compete with Ruff, Black, Flake8, or Pylint. Bubbles Lint looks for software shape problems: code that is too large, too coupled, too magical, or too monolithic.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install bubbles-lint
13
+ ```
14
+
15
+ For local development from this repository:
16
+
17
+ ```bash
18
+ pip install -e ".[dev]"
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Scan the current repository:
24
+
25
+ ```bash
26
+ bubbles-lint scan .
27
+ ```
28
+
29
+ Emit JSON for CI systems:
30
+
31
+ ```bash
32
+ bubbles-lint scan . --json
33
+ ```
34
+
35
+ Bubbles Lint exits with status `1` when findings are present and `0` when the scan is clean.
36
+
37
+ ## Philosophy
38
+
39
+ Bubbles Lint encourages codebases where:
40
+
41
+ - modules have one clear responsibility
42
+ - functions and classes stay small enough to replace
43
+ - dependencies flow through explicit interfaces
44
+ - configuration is loaded at the edge
45
+ - side effects are isolated from pure logic
46
+ - text and data move through well-defined boundaries
47
+ - components are easy to inspect, test, and swap
48
+
49
+ The goal is not style enforcement. The goal is software design discipline, especially in Python codebases that have grown quickly with help from AI coding assistants.
50
+
51
+ ## Rules
52
+
53
+ ### Bubble Burst
54
+
55
+ Flags code that has grown too large:
56
+
57
+ - files over `max_file_lines`
58
+ - functions over `max_function_lines`
59
+ - classes with more than `max_class_methods`
60
+ - functions with more than `max_function_params`
61
+
62
+ ### Bubble Leak
63
+
64
+ Flags hidden coupling and mixed side effects:
65
+
66
+ - global mutable state
67
+ - direct environment variable reads outside config modules
68
+ - functions mixing too many side-effect categories, such as filesystem, network, database, subprocess, logging, and rendering
69
+
70
+ ### Bubble Boundary
71
+
72
+ Flags dependency boundary problems:
73
+
74
+ - circular imports where practical
75
+ - modules importing too many dependencies
76
+ - imports from private modules like `from package._internal import thing`
77
+
78
+ ### AI Smells
79
+
80
+ Flags common broad abstractions produced in rushed or generated code:
81
+
82
+ - oversized `utils.py`, `helpers.py`, `manager.py`, and `service.py` modules
83
+ - broad classes ending in `Manager`, `Service`, or `Handler`
84
+ - deeply nested control flow
85
+
86
+ ## Configuration
87
+
88
+ Configure Bubbles Lint in `pyproject.toml`:
89
+
90
+ ```toml
91
+ [tool.bubbles-lint]
92
+ max_file_lines = 500
93
+ max_function_lines = 50
94
+ max_class_methods = 10
95
+ max_function_params = 5
96
+ max_imports_per_module = 20
97
+ max_nesting_depth = 4
98
+ allow_private_imports = false
99
+ ```
100
+
101
+ Additional knobs:
102
+
103
+ ```toml
104
+ [tool.bubbles-lint]
105
+ max_side_effect_kinds = 3
106
+ max_ai_module_lines = 200
107
+ max_ai_class_dependencies = 8
108
+ excludes = ["generated"]
109
+ ```
110
+
111
+ Bubbles Lint always ignores `.venv`, `venv`, `.git`, `__pycache__`, `build`, and `dist`.
112
+
113
+ Existing `[tool.bubbles]` configs are still read for compatibility, but new projects should use `[tool.bubbles-lint]`.
114
+
115
+ ## Human Output
116
+
117
+ ```text
118
+ Bubble: Bubble Burst
119
+
120
+ src/payment_service.py:1
121
+ warning: File has 1437 lines; recommended maximum is 500.
122
+
123
+ Suggestion:
124
+ Split this module into smaller bubbles with one responsibility each.
125
+ ```
126
+
127
+ ## JSON Output
128
+
129
+ ```json
130
+ {
131
+ "findings": [
132
+ {
133
+ "rule": "bubble-burst/file-too-large",
134
+ "severity": "warning",
135
+ "path": "src/payment_service.py",
136
+ "line": 1,
137
+ "message": "File has 1437 lines; recommended maximum is 500.",
138
+ "suggestion": "Split this module into smaller bubbles with one responsibility each."
139
+ }
140
+ ],
141
+ "files_scanned": 1
142
+ }
143
+ ```
144
+
145
+ ## CI
146
+
147
+ Example GitHub Actions step:
148
+
149
+ ```yaml
150
+ - name: Install Bubbles Lint
151
+ run: pip install .
152
+
153
+ - name: Scan architecture
154
+ run: bubbles-lint scan . --json
155
+ ```
156
+
157
+ ## Development
158
+
159
+ Run tests:
160
+
161
+ ```bash
162
+ pip install -e ".[dev]"
163
+ pytest
164
+ ```
165
+
166
+ The rule engine is intentionally small. New rules implement a `check(context)` method and return `Finding` objects. Parsing, configuration, scanning, reporting, and CLI code are separated so the tool can grow without becoming the kind of monolith it warns about.
@@ -0,0 +1,3 @@
1
+ """Bubbles Lint keeps Python modules small, composable, and inspectable."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from bubbles_lint.config import load_config
8
+ from bubbles_lint.report import format_human, format_json
9
+ from bubbles_lint.scanner import scan_path
10
+
11
+
12
+ def main(argv: list[str] | None = None) -> int:
13
+ parser = build_parser()
14
+ args = parser.parse_args(argv)
15
+
16
+ if args.command == "scan":
17
+ target = Path(args.path)
18
+ config = load_config(target)
19
+ result = scan_path(target, config=config)
20
+ output = format_json(result) if args.json else format_human(result)
21
+ print(output)
22
+ return 1 if result.has_findings else 0
23
+
24
+ parser.print_help()
25
+ return 2
26
+
27
+
28
+ def build_parser() -> argparse.ArgumentParser:
29
+ parser = argparse.ArgumentParser(
30
+ prog="bubbles-lint",
31
+ description="Architectural linting for small, composable Python code.",
32
+ )
33
+ subcommands = parser.add_subparsers(dest="command")
34
+
35
+ scan = subcommands.add_parser("scan", help="Scan Python files for architecture findings.")
36
+ scan.add_argument("path", nargs="?", default=".", help="File or directory to scan.")
37
+ scan.add_argument("--json", action="store_true", help="Emit machine-readable JSON output.")
38
+
39
+ return parser
40
+
41
+
42
+ if __name__ == "__main__":
43
+ sys.exit(main())
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import tomllib
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+
9
+ DEFAULT_EXCLUDES = frozenset({
10
+ ".git",
11
+ ".venv",
12
+ "__pycache__",
13
+ "build",
14
+ "dist",
15
+ "venv",
16
+ })
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Config:
21
+ max_file_lines: int = 500
22
+ max_function_lines: int = 50
23
+ max_class_methods: int = 10
24
+ max_function_params: int = 5
25
+ max_imports_per_module: int = 20
26
+ max_nesting_depth: int = 4
27
+ max_side_effect_kinds: int = 3
28
+ max_ai_module_lines: int = 200
29
+ max_ai_class_dependencies: int = 8
30
+ allow_private_imports: bool = False
31
+ excludes: frozenset[str] = DEFAULT_EXCLUDES
32
+
33
+
34
+ def load_config(start: Path) -> Config:
35
+ pyproject = find_pyproject(start)
36
+ if pyproject is None:
37
+ return Config()
38
+
39
+ try:
40
+ data = tomllib.loads(pyproject.read_text(encoding="utf-8"))
41
+ except (OSError, tomllib.TOMLDecodeError):
42
+ return Config()
43
+
44
+ tool_config = data.get("tool", {})
45
+ if not isinstance(tool_config, dict):
46
+ return Config()
47
+
48
+ values = tool_config.get("bubbles-lint", tool_config.get("bubbles", {}))
49
+ if not isinstance(values, dict):
50
+ return Config()
51
+
52
+ return build_config(values)
53
+
54
+
55
+ def find_pyproject(start: Path) -> Path | None:
56
+ current = start.resolve()
57
+ if current.is_file():
58
+ current = current.parent
59
+
60
+ for directory in (current, *current.parents):
61
+ pyproject = directory / "pyproject.toml"
62
+ if pyproject.exists():
63
+ return pyproject
64
+ return None
65
+
66
+
67
+ def build_config(values: dict[str, Any]) -> Config:
68
+ defaults = Config()
69
+ kwargs: dict[str, Any] = {}
70
+ for field_name in defaults.__dataclass_fields__:
71
+ if field_name not in values:
72
+ continue
73
+ value = values[field_name]
74
+ if field_name == "excludes":
75
+ if isinstance(value, list) and all(isinstance(item, str) for item in value):
76
+ kwargs[field_name] = DEFAULT_EXCLUDES | frozenset(value)
77
+ continue
78
+ kwargs[field_name] = value
79
+ return Config(**kwargs)
@@ -0,0 +1,76 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from pathlib import Path
6
+ from typing import Protocol
7
+
8
+ from bubbles_lint.config import Config
9
+
10
+
11
+ class Severity(str, Enum):
12
+ INFO = "info"
13
+ WARNING = "warning"
14
+ ERROR = "error"
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class Finding:
19
+ rule: str
20
+ severity: Severity
21
+ path: Path
22
+ line: int
23
+ message: str
24
+ suggestion: str
25
+
26
+ def to_json(self) -> dict[str, object]:
27
+ return {
28
+ "rule": self.rule,
29
+ "severity": self.severity.value,
30
+ "path": self.path.as_posix(),
31
+ "line": self.line,
32
+ "message": self.message,
33
+ "suggestion": self.suggestion,
34
+ }
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ModuleContext:
39
+ path: Path
40
+ root: Path
41
+ source: str
42
+ tree: object
43
+ line_count: int
44
+ config: Config
45
+
46
+ @property
47
+ def relative_path(self) -> Path:
48
+ try:
49
+ return self.path.relative_to(self.root)
50
+ except ValueError:
51
+ return self.path
52
+
53
+
54
+ class Rule(Protocol):
55
+ id: str
56
+ title: str
57
+
58
+ def check(self, context: ModuleContext) -> list[Finding]:
59
+ ...
60
+
61
+
62
+ @dataclass
63
+ class ScanResult:
64
+ findings: list[Finding] = field(default_factory=list)
65
+ files_scanned: int = 0
66
+
67
+ @property
68
+ def has_errors(self) -> bool:
69
+ return any(finding.severity is Severity.ERROR for finding in self.findings)
70
+
71
+ @property
72
+ def has_findings(self) -> bool:
73
+ return bool(self.findings)
74
+
75
+ def extend(self, findings: list[Finding]) -> None:
76
+ self.findings.extend(findings)
@@ -0,0 +1,52 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from collections import defaultdict
5
+ from types import MappingProxyType
6
+
7
+ from bubbles_lint.models import Finding, ScanResult
8
+
9
+
10
+ RULE_TITLES = MappingProxyType({
11
+ "ai-smells": "AI Smells",
12
+ "bubble-boundary": "Bubble Boundary",
13
+ "bubble-burst": "Bubble Burst",
14
+ "bubble-leak": "Bubble Leak",
15
+ "parser": "Parser",
16
+ })
17
+
18
+
19
+ def format_json(result: ScanResult) -> str:
20
+ return json.dumps(
21
+ {
22
+ "findings": [finding.to_json() for finding in result.findings],
23
+ "files_scanned": result.files_scanned,
24
+ },
25
+ indent=2,
26
+ )
27
+
28
+
29
+ def format_human(result: ScanResult) -> str:
30
+ if not result.findings:
31
+ return f"No bubbles burst. Scanned {result.files_scanned} Python file(s)."
32
+
33
+ grouped: dict[str, list[Finding]] = defaultdict(list)
34
+ for finding in result.findings:
35
+ grouped[_family(finding.rule)].append(finding)
36
+
37
+ chunks: list[str] = []
38
+ for family in sorted(grouped):
39
+ chunks.append(f"Bubble: {RULE_TITLES.get(family, family)}")
40
+ for finding in grouped[family]:
41
+ chunks.append("")
42
+ chunks.append(f"{finding.path}:{finding.line}")
43
+ chunks.append(f"{finding.severity.value}: {finding.message}")
44
+ chunks.append("")
45
+ chunks.append("Suggestion:")
46
+ chunks.append(finding.suggestion)
47
+
48
+ return "\n".join(chunks)
49
+
50
+
51
+ def _family(rule: str) -> str:
52
+ return rule.split("/", 1)[0]
@@ -0,0 +1 @@
1
+ """Architecture rules shipped with Bubbles Lint."""