hard-lint-py 0.0.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,267 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hard-lint-py
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Rigorous linting and code quality setup for Python projects with pre-commit hooks
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: linting,pre-commit,code-quality,ruff,black,isort,coverage
|
|
7
|
+
Author: Naylson Ferreira
|
|
8
|
+
Author-email: naylsonfsa@gmail.com
|
|
9
|
+
Requires-Python: >=3.10
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Requires-Dist: black (>=23.0.0)
|
|
18
|
+
Requires-Dist: isort (>=5.12.0)
|
|
19
|
+
Requires-Dist: pre-commit (>=3.0.0)
|
|
20
|
+
Requires-Dist: ruff (>=0.1.0)
|
|
21
|
+
Project-URL: Homepage, https://github.com/naylsonferreira/hard-lint-py
|
|
22
|
+
Project-URL: Issues, https://github.com/naylsonferreira/hard-lint-py/issues
|
|
23
|
+
Project-URL: Repository, https://github.com/naylsonferreira/hard-lint-py.git
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Hard-Lint for Python 🐍
|
|
27
|
+
|
|
28
|
+
Rigorous linting and code quality setup for Python projects with automatic pre-commit hooks.
|
|
29
|
+
|
|
30
|
+
## Features
|
|
31
|
+
|
|
32
|
+
- **Automated Pre-commit Hooks**: Validates code before every commit
|
|
33
|
+
- `pre-commit`: Runs `ruff`, `black`, and `isort`
|
|
34
|
+
- `commit-msg`: Validates messages follow Conventional Commits format
|
|
35
|
+
- **Strict Linting**: Ruff checks for errors, complexity, and code quality
|
|
36
|
+
- **Synchronized Formatters**: Ruff, Black, and isort configured with same profile (no conflicts)
|
|
37
|
+
- **Zero Configuration**: Works out-of-the-box with sensible defaults
|
|
38
|
+
- **Multiplattform**: Works on Windows, macOS, and Linux
|
|
39
|
+
- **Auto-setup**: Single command to enable all checks
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install hard-lint-py
|
|
45
|
+
# or with poetry
|
|
46
|
+
poetry add -D hard-lint-py
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Quick Start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# 1. Install and setup hooks
|
|
53
|
+
hard-lint-py
|
|
54
|
+
|
|
55
|
+
# 2. Make your first commit
|
|
56
|
+
git add .
|
|
57
|
+
git commit -m "feat: initial setup"
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
That's it! Your project now has:
|
|
61
|
+
- **Ruff** for fast Python linting
|
|
62
|
+
- **Black** for code formatting
|
|
63
|
+
- **isort** for import sorting
|
|
64
|
+
- **Commitlint** for commit message validation
|
|
65
|
+
|
|
66
|
+
## What Gets Installed
|
|
67
|
+
|
|
68
|
+
### Git Hooks
|
|
69
|
+
|
|
70
|
+
Hooks are created in `.hardlint/_/`:
|
|
71
|
+
|
|
72
|
+
- **pre-commit**: Runs before commits to validate code
|
|
73
|
+
- Fixes issues with `ruff check --fix`
|
|
74
|
+
- Formats with `black`
|
|
75
|
+
- Sorts imports with `isort`
|
|
76
|
+
- Validates no comments exist
|
|
77
|
+
|
|
78
|
+
- **commit-msg**: Validates commit messages
|
|
79
|
+
- Must follow Conventional Commits format
|
|
80
|
+
- Examples: `feat:`, `fix:`, `chore:`, `docs:`, etc.
|
|
81
|
+
|
|
82
|
+
## Linting Rules
|
|
83
|
+
|
|
84
|
+
### Ruff Rules Enforced
|
|
85
|
+
|
|
86
|
+
| Rule | Category | Description |
|
|
87
|
+
|------|----------|-----------|
|
|
88
|
+
| E | pycodestyle errors | PEP 8 compliance |
|
|
89
|
+
| F | Pyflakes | Unused imports, undefined names |
|
|
90
|
+
| W | pycodestyle warnings | Code warnings |
|
|
91
|
+
| I | isort | Import sorting |
|
|
92
|
+
| N | pep8-naming | Naming conventions |
|
|
93
|
+
| C | mccabe | Code complexity |
|
|
94
|
+
| B | flake8-bugbear | Common bugs and design problems |
|
|
95
|
+
| RUF | Ruff-specific | Additional quality checks |
|
|
96
|
+
| UP | pyupgrade | Modernize Python syntax |
|
|
97
|
+
|
|
98
|
+
### Synchronized Formatter Configuration
|
|
99
|
+
|
|
100
|
+
All three formatters (Ruff, Black, isort) use the **same profile** to prevent conflicts:
|
|
101
|
+
|
|
102
|
+
| Aspect | Configuration | Notes |
|
|
103
|
+
|--------|---------------|-------|
|
|
104
|
+
| **Line Length** | 100 characters | Consistent across all tools |
|
|
105
|
+
| **Python Version** | 3.10+ | Targets modern Python |
|
|
106
|
+
| **Imports Profile** | `isort: black` | Compatible with Black |
|
|
107
|
+
| **Trailing Commas** | Enabled | Multi-line consistency |
|
|
108
|
+
|
|
109
|
+
### Configuration
|
|
110
|
+
|
|
111
|
+
Auto-configured in `pyproject.toml`:
|
|
112
|
+
|
|
113
|
+
```toml
|
|
114
|
+
[tool.black]
|
|
115
|
+
line-length = 100
|
|
116
|
+
target-version = ["py310", "py311", "py312"]
|
|
117
|
+
|
|
118
|
+
[tool.isort]
|
|
119
|
+
profile = "black" # Compatible with Black
|
|
120
|
+
line_length = 100
|
|
121
|
+
known_first_party = ["hard_lint_py"]
|
|
122
|
+
|
|
123
|
+
[tool.ruff]
|
|
124
|
+
line-length = 100
|
|
125
|
+
target-version = "py310"
|
|
126
|
+
select = ["E", "F", "W", "I", "N", "C", "B", "RUF", "UP"]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Usage
|
|
130
|
+
|
|
131
|
+
### Normal workflow
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Make changes
|
|
135
|
+
cat > new_file.py << 'EOF'
|
|
136
|
+
def greet(name):
|
|
137
|
+
# Greet a person by name
|
|
138
|
+
return f"Hello, {name}!"
|
|
139
|
+
EOF
|
|
140
|
+
|
|
141
|
+
# Stage changes
|
|
142
|
+
git add new_file.py
|
|
143
|
+
|
|
144
|
+
# Commit (hooks will run automatically)
|
|
145
|
+
git commit -m "feat: add greeting function"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Pre-commit Hook Behavior
|
|
149
|
+
|
|
150
|
+
When you commit, the hook automatically:
|
|
151
|
+
1. **Ruff**: Checks for linting issues, imports, and code quality
|
|
152
|
+
2. **Black**: Formats code to 100-character lines
|
|
153
|
+
3. **isort**: Sorts and organizes imports
|
|
154
|
+
4. Blocks commit if any issues can't be auto-fixed
|
|
155
|
+
|
|
156
|
+
If pre-commit fails:
|
|
157
|
+
```bash
|
|
158
|
+
# Fix will be auto-applied, just re-add and commit
|
|
159
|
+
git add .
|
|
160
|
+
git commit -m "feat: your message"
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### If commit-msg fails
|
|
164
|
+
|
|
165
|
+
Invalid message example:
|
|
166
|
+
```bash
|
|
167
|
+
git commit -m "added this feature" # ❌ Missing type prefix
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Valid message example:
|
|
171
|
+
```bash
|
|
172
|
+
git commit -m "feat: added new feature" # ✅ Has type prefix
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Skip hooks (use with caution)
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
git commit -m "..." --no-verify
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Configuration
|
|
182
|
+
|
|
183
|
+
All tools read from `pyproject.toml`. Customize as needed:
|
|
184
|
+
|
|
185
|
+
```toml
|
|
186
|
+
[tool.ruff]
|
|
187
|
+
line-length = 120
|
|
188
|
+
select = ["E", "F", "W"]
|
|
189
|
+
|
|
190
|
+
[tool.black]
|
|
191
|
+
line-length = 120
|
|
192
|
+
|
|
193
|
+
[tool.isort]
|
|
194
|
+
profile = "black"
|
|
195
|
+
line_length = 120
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Troubleshooting
|
|
199
|
+
|
|
200
|
+
**Hooks not running?**
|
|
201
|
+
```bash
|
|
202
|
+
# Re-run installation
|
|
203
|
+
hard-lint-py
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
**Commit-msg validation failing?**
|
|
209
|
+
- Check that your message starts with a type: `feat:`, `fix:`, `chore:`, etc.
|
|
210
|
+
|
|
211
|
+
**Formatters conflicting with each other?**
|
|
212
|
+
- They shouldn't! Ruff, Black, and isort are configured with the same profile
|
|
213
|
+
- If you see conflicts, run all three: `ruff check --fix`, `black .`, `isort .`
|
|
214
|
+
|
|
215
|
+
**Line length conflicts?**
|
|
216
|
+
- All tools configured to 100 characters
|
|
217
|
+
- Black and isort won't fight over formatting
|
|
218
|
+
- Ruff respects Black's decisions (E501 ignored)
|
|
219
|
+
|
|
220
|
+
## Supported Python Versions
|
|
221
|
+
|
|
222
|
+
- Python 3.10+
|
|
223
|
+
- Python 3.11
|
|
224
|
+
- Python 3.12
|
|
225
|
+
|
|
226
|
+
## Development
|
|
227
|
+
|
|
228
|
+
When developing with hard-lint-py:
|
|
229
|
+
|
|
230
|
+
### Pre-commit Validation
|
|
231
|
+
Your code will be validated on every commit:
|
|
232
|
+
```bash
|
|
233
|
+
git commit -m "feat: your feature"
|
|
234
|
+
# Runs: ruff check --fix → black → isort → commitlint
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Manual Validation
|
|
238
|
+
Run validation checks manually:
|
|
239
|
+
```bash
|
|
240
|
+
# All checks
|
|
241
|
+
make lint
|
|
242
|
+
make format
|
|
243
|
+
make test
|
|
244
|
+
make test-cov
|
|
245
|
+
|
|
246
|
+
# Individual tools
|
|
247
|
+
poetry run ruff check src/ tests/
|
|
248
|
+
poetry run black --check src/ tests/
|
|
249
|
+
poetry run isort --check-only src/ tests/
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Quality Standards
|
|
253
|
+
- ✅ 99% code coverage
|
|
254
|
+
- ✅ No unused imports or variables
|
|
255
|
+
- ✅ Consistent formatting (100 char lines)
|
|
256
|
+
- ✅ Code complexity within limits
|
|
257
|
+
- ✅ Modern Python syntax
|
|
258
|
+
- ✅ No common bugs or design problems
|
|
259
|
+
|
|
260
|
+
## License
|
|
261
|
+
|
|
262
|
+
MIT
|
|
263
|
+
|
|
264
|
+
## Author
|
|
265
|
+
|
|
266
|
+
Naylson Ferreira
|
|
267
|
+
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
# Hard-Lint for Python 🐍
|
|
2
|
+
|
|
3
|
+
Rigorous linting and code quality setup for Python projects with automatic pre-commit hooks.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Automated Pre-commit Hooks**: Validates code before every commit
|
|
8
|
+
- `pre-commit`: Runs `ruff`, `black`, and `isort`
|
|
9
|
+
- `commit-msg`: Validates messages follow Conventional Commits format
|
|
10
|
+
- **Strict Linting**: Ruff checks for errors, complexity, and code quality
|
|
11
|
+
- **Synchronized Formatters**: Ruff, Black, and isort configured with same profile (no conflicts)
|
|
12
|
+
- **Zero Configuration**: Works out-of-the-box with sensible defaults
|
|
13
|
+
- **Multiplattform**: Works on Windows, macOS, and Linux
|
|
14
|
+
- **Auto-setup**: Single command to enable all checks
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install hard-lint-py
|
|
20
|
+
# or with poetry
|
|
21
|
+
poetry add -D hard-lint-py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Quick Start
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
# 1. Install and setup hooks
|
|
28
|
+
hard-lint-py
|
|
29
|
+
|
|
30
|
+
# 2. Make your first commit
|
|
31
|
+
git add .
|
|
32
|
+
git commit -m "feat: initial setup"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
That's it! Your project now has:
|
|
36
|
+
- **Ruff** for fast Python linting
|
|
37
|
+
- **Black** for code formatting
|
|
38
|
+
- **isort** for import sorting
|
|
39
|
+
- **Commitlint** for commit message validation
|
|
40
|
+
|
|
41
|
+
## What Gets Installed
|
|
42
|
+
|
|
43
|
+
### Git Hooks
|
|
44
|
+
|
|
45
|
+
Hooks are created in `.hardlint/_/`:
|
|
46
|
+
|
|
47
|
+
- **pre-commit**: Runs before commits to validate code
|
|
48
|
+
- Fixes issues with `ruff check --fix`
|
|
49
|
+
- Formats with `black`
|
|
50
|
+
- Sorts imports with `isort`
|
|
51
|
+
- Validates no comments exist
|
|
52
|
+
|
|
53
|
+
- **commit-msg**: Validates commit messages
|
|
54
|
+
- Must follow Conventional Commits format
|
|
55
|
+
- Examples: `feat:`, `fix:`, `chore:`, `docs:`, etc.
|
|
56
|
+
|
|
57
|
+
## Linting Rules
|
|
58
|
+
|
|
59
|
+
### Ruff Rules Enforced
|
|
60
|
+
|
|
61
|
+
| Rule | Category | Description |
|
|
62
|
+
|------|----------|-----------|
|
|
63
|
+
| E | pycodestyle errors | PEP 8 compliance |
|
|
64
|
+
| F | Pyflakes | Unused imports, undefined names |
|
|
65
|
+
| W | pycodestyle warnings | Code warnings |
|
|
66
|
+
| I | isort | Import sorting |
|
|
67
|
+
| N | pep8-naming | Naming conventions |
|
|
68
|
+
| C | mccabe | Code complexity |
|
|
69
|
+
| B | flake8-bugbear | Common bugs and design problems |
|
|
70
|
+
| RUF | Ruff-specific | Additional quality checks |
|
|
71
|
+
| UP | pyupgrade | Modernize Python syntax |
|
|
72
|
+
|
|
73
|
+
### Synchronized Formatter Configuration
|
|
74
|
+
|
|
75
|
+
All three formatters (Ruff, Black, isort) use the **same profile** to prevent conflicts:
|
|
76
|
+
|
|
77
|
+
| Aspect | Configuration | Notes |
|
|
78
|
+
|--------|---------------|-------|
|
|
79
|
+
| **Line Length** | 100 characters | Consistent across all tools |
|
|
80
|
+
| **Python Version** | 3.10+ | Targets modern Python |
|
|
81
|
+
| **Imports Profile** | `isort: black` | Compatible with Black |
|
|
82
|
+
| **Trailing Commas** | Enabled | Multi-line consistency |
|
|
83
|
+
|
|
84
|
+
### Configuration
|
|
85
|
+
|
|
86
|
+
Auto-configured in `pyproject.toml`:
|
|
87
|
+
|
|
88
|
+
```toml
|
|
89
|
+
[tool.black]
|
|
90
|
+
line-length = 100
|
|
91
|
+
target-version = ["py310", "py311", "py312"]
|
|
92
|
+
|
|
93
|
+
[tool.isort]
|
|
94
|
+
profile = "black" # Compatible with Black
|
|
95
|
+
line_length = 100
|
|
96
|
+
known_first_party = ["hard_lint_py"]
|
|
97
|
+
|
|
98
|
+
[tool.ruff]
|
|
99
|
+
line-length = 100
|
|
100
|
+
target-version = "py310"
|
|
101
|
+
select = ["E", "F", "W", "I", "N", "C", "B", "RUF", "UP"]
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Usage
|
|
105
|
+
|
|
106
|
+
### Normal workflow
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
# Make changes
|
|
110
|
+
cat > new_file.py << 'EOF'
|
|
111
|
+
def greet(name):
|
|
112
|
+
# Greet a person by name
|
|
113
|
+
return f"Hello, {name}!"
|
|
114
|
+
EOF
|
|
115
|
+
|
|
116
|
+
# Stage changes
|
|
117
|
+
git add new_file.py
|
|
118
|
+
|
|
119
|
+
# Commit (hooks will run automatically)
|
|
120
|
+
git commit -m "feat: add greeting function"
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Pre-commit Hook Behavior
|
|
124
|
+
|
|
125
|
+
When you commit, the hook automatically:
|
|
126
|
+
1. **Ruff**: Checks for linting issues, imports, and code quality
|
|
127
|
+
2. **Black**: Formats code to 100-character lines
|
|
128
|
+
3. **isort**: Sorts and organizes imports
|
|
129
|
+
4. Blocks commit if any issues can't be auto-fixed
|
|
130
|
+
|
|
131
|
+
If pre-commit fails:
|
|
132
|
+
```bash
|
|
133
|
+
# Fix will be auto-applied, just re-add and commit
|
|
134
|
+
git add .
|
|
135
|
+
git commit -m "feat: your message"
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### If commit-msg fails
|
|
139
|
+
|
|
140
|
+
Invalid message example:
|
|
141
|
+
```bash
|
|
142
|
+
git commit -m "added this feature" # ❌ Missing type prefix
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Valid message example:
|
|
146
|
+
```bash
|
|
147
|
+
git commit -m "feat: added new feature" # ✅ Has type prefix
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Skip hooks (use with caution)
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
git commit -m "..." --no-verify
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Configuration
|
|
157
|
+
|
|
158
|
+
All tools read from `pyproject.toml`. Customize as needed:
|
|
159
|
+
|
|
160
|
+
```toml
|
|
161
|
+
[tool.ruff]
|
|
162
|
+
line-length = 120
|
|
163
|
+
select = ["E", "F", "W"]
|
|
164
|
+
|
|
165
|
+
[tool.black]
|
|
166
|
+
line-length = 120
|
|
167
|
+
|
|
168
|
+
[tool.isort]
|
|
169
|
+
profile = "black"
|
|
170
|
+
line_length = 120
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Troubleshooting
|
|
174
|
+
|
|
175
|
+
**Hooks not running?**
|
|
176
|
+
```bash
|
|
177
|
+
# Re-run installation
|
|
178
|
+
hard-lint-py
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
**Commit-msg validation failing?**
|
|
184
|
+
- Check that your message starts with a type: `feat:`, `fix:`, `chore:`, etc.
|
|
185
|
+
|
|
186
|
+
**Formatters conflicting with each other?**
|
|
187
|
+
- They shouldn't! Ruff, Black, and isort are configured with the same profile
|
|
188
|
+
- If you see conflicts, run all three: `ruff check --fix`, `black .`, `isort .`
|
|
189
|
+
|
|
190
|
+
**Line length conflicts?**
|
|
191
|
+
- All tools configured to 100 characters
|
|
192
|
+
- Black and isort won't fight over formatting
|
|
193
|
+
- Ruff respects Black's decisions (E501 ignored)
|
|
194
|
+
|
|
195
|
+
## Supported Python Versions
|
|
196
|
+
|
|
197
|
+
- Python 3.10+
|
|
198
|
+
- Python 3.11
|
|
199
|
+
- Python 3.12
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
When developing with hard-lint-py:
|
|
204
|
+
|
|
205
|
+
### Pre-commit Validation
|
|
206
|
+
Your code will be validated on every commit:
|
|
207
|
+
```bash
|
|
208
|
+
git commit -m "feat: your feature"
|
|
209
|
+
# Runs: ruff check --fix → black → isort → commitlint
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Manual Validation
|
|
213
|
+
Run validation checks manually:
|
|
214
|
+
```bash
|
|
215
|
+
# All checks
|
|
216
|
+
make lint
|
|
217
|
+
make format
|
|
218
|
+
make test
|
|
219
|
+
make test-cov
|
|
220
|
+
|
|
221
|
+
# Individual tools
|
|
222
|
+
poetry run ruff check src/ tests/
|
|
223
|
+
poetry run black --check src/ tests/
|
|
224
|
+
poetry run isort --check-only src/ tests/
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### Quality Standards
|
|
228
|
+
- ✅ 99% code coverage
|
|
229
|
+
- ✅ No unused imports or variables
|
|
230
|
+
- ✅ Consistent formatting (100 char lines)
|
|
231
|
+
- ✅ Code complexity within limits
|
|
232
|
+
- ✅ Modern Python syntax
|
|
233
|
+
- ✅ No common bugs or design problems
|
|
234
|
+
|
|
235
|
+
## License
|
|
236
|
+
|
|
237
|
+
MIT
|
|
238
|
+
|
|
239
|
+
## Author
|
|
240
|
+
|
|
241
|
+
Naylson Ferreira
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "hard-lint-py"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "Rigorous linting and code quality setup for Python projects with pre-commit hooks"
|
|
5
|
+
authors = [
|
|
6
|
+
{name = "Naylson Ferreira", email = "naylsonfsa@gmail.com"}
|
|
7
|
+
]
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
keywords = ["linting", "pre-commit", "code-quality", "ruff", "black", "isort", "coverage"]
|
|
12
|
+
classifiers = [
|
|
13
|
+
"Development Status :: 3 - Alpha",
|
|
14
|
+
"Intended Audience :: Developers",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3.10",
|
|
18
|
+
"Programming Language :: Python :: 3.11",
|
|
19
|
+
"Programming Language :: Python :: 3.12",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"ruff>=0.1.0",
|
|
23
|
+
"black>=23.0.0",
|
|
24
|
+
"isort>=5.12.0",
|
|
25
|
+
"pre-commit>=3.0.0",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/naylsonferreira/hard-lint-py"
|
|
30
|
+
Repository = "https://github.com/naylsonferreira/hard-lint-py.git"
|
|
31
|
+
Issues = "https://github.com/naylsonferreira/hard-lint-py/issues"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
hard-lint-py = "hard_lint_py.cli:main"
|
|
35
|
+
|
|
36
|
+
[tool.poetry]
|
|
37
|
+
packages = [{include = "hard_lint_py", from = "src"}]
|
|
38
|
+
|
|
39
|
+
[tool.poetry.dependencies]
|
|
40
|
+
python = "^3.10"
|
|
41
|
+
ruff = "^0.1.0"
|
|
42
|
+
black = "^23.0.0"
|
|
43
|
+
isort = "^5.12.0"
|
|
44
|
+
pre-commit = "^3.0.0"
|
|
45
|
+
|
|
46
|
+
[tool.poetry.group.dev.dependencies]
|
|
47
|
+
pytest = "^7.4.0"
|
|
48
|
+
pytest-cov = "^4.1.0"
|
|
49
|
+
coverage = "^7.0.0"
|
|
50
|
+
mypy = "^1.5.0"
|
|
51
|
+
python-semantic-release = "^9.0.0"
|
|
52
|
+
|
|
53
|
+
[tool.black]
|
|
54
|
+
line-length = 100
|
|
55
|
+
target-version = ["py310", "py311", "py312"]
|
|
56
|
+
preview = true
|
|
57
|
+
|
|
58
|
+
[tool.isort]
|
|
59
|
+
profile = "black"
|
|
60
|
+
line_length = 100
|
|
61
|
+
include_trailing_comma = true
|
|
62
|
+
use_parentheses = true
|
|
63
|
+
ensure_newline_before_comments = true
|
|
64
|
+
balanced_wrapping = true
|
|
65
|
+
lines_after_imports = 2
|
|
66
|
+
skip_gitignore = true
|
|
67
|
+
known_first_party = ["hard_lint_py"]
|
|
68
|
+
|
|
69
|
+
[tool.ruff]
|
|
70
|
+
line-length = 100
|
|
71
|
+
target-version = "py310"
|
|
72
|
+
select = [
|
|
73
|
+
"E", # pycodestyle errors
|
|
74
|
+
"F", # Pyflakes (unused imports)
|
|
75
|
+
"W", # pycodestyle warnings
|
|
76
|
+
"I", # isort (import sorting)
|
|
77
|
+
"N", # pep8-naming
|
|
78
|
+
"C", # mccabe complexity
|
|
79
|
+
"B", # flake8-bugbear
|
|
80
|
+
"T", # flake8-print (prohibit print statements)
|
|
81
|
+
"RUF", # Ruff-specific rules
|
|
82
|
+
"UP", # pyupgrade (modernize code)
|
|
83
|
+
]
|
|
84
|
+
ignore = [
|
|
85
|
+
"E501", # Line too long (Black handles this)
|
|
86
|
+
"I001", # Import sorting conflict with isort (let isort handle this)
|
|
87
|
+
"RUF001", # Ambiguous unicode character (too strict)
|
|
88
|
+
"T201", # Allow print statements (CLI tool)
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[tool.ruff.per-file-ignores]
|
|
92
|
+
# Tests excluded due to import complexity
|
|
93
|
+
"tests/*" = ["I001"]
|
|
94
|
+
|
|
95
|
+
[tool.pytest.ini_options]
|
|
96
|
+
testpaths = ["tests"]
|
|
97
|
+
addopts = "--cov=src/hard_lint_py --cov-report=term-missing --strict-markers"
|
|
98
|
+
markers = [
|
|
99
|
+
"unit: Unit tests",
|
|
100
|
+
"integration: Integration tests",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
[tool.mypy]
|
|
104
|
+
python_version = "3.10"
|
|
105
|
+
strict = true
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
[tool.semantic_release]
|
|
109
|
+
version_variable = "src/hard_lint_py/__init__.py:__version__"
|
|
110
|
+
version_toml = ["pyproject.toml:project.version"]
|
|
111
|
+
major_on_zero = false
|
|
112
|
+
build_command = "pip install build && python -m build"
|
|
113
|
+
|
|
114
|
+
[tool.semantic_release.branches.main]
|
|
115
|
+
match = "main"
|
|
116
|
+
|
|
117
|
+
[tool.semantic_release.changelog]
|
|
118
|
+
template_dir = "templates"
|
|
119
|
+
exclude_commit_patterns = []
|
|
120
|
+
|
|
121
|
+
[tool.semantic_release.publish]
|
|
122
|
+
dist_glob_patterns = ["dist/*"]
|
|
123
|
+
upload_to_vcs_release = true
|
|
124
|
+
|
|
125
|
+
[build-system]
|
|
126
|
+
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
127
|
+
build-backend = "poetry.core.masonry.api"
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from hard_lint_py.installer import HardLintInstaller
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def check_install_status():
|
|
10
|
+
"""Check if hard-lint-py install was run and show warning if not."""
|
|
11
|
+
hardlint_dir = Path.cwd() / ".hardlint" / "_"
|
|
12
|
+
if not hardlint_dir.exists():
|
|
13
|
+
print("WARNING: Pre-commit hooks not installed!")
|
|
14
|
+
print("Run 'hard-lint-py install' to set up git hooks.")
|
|
15
|
+
print("(You can still use check/format commands without it)\n")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def run_check(paths=None) -> int:
|
|
19
|
+
check_install_status()
|
|
20
|
+
target_paths = paths if paths else ["."]
|
|
21
|
+
try:
|
|
22
|
+
print(f"Running checks on {target_paths}...")
|
|
23
|
+
subprocess.run(["poetry", "run", "ruff", "check", *target_paths], check=True)
|
|
24
|
+
subprocess.run(
|
|
25
|
+
["poetry", "run", "black", "--check", "--line-length", "100", *target_paths],
|
|
26
|
+
check=True,
|
|
27
|
+
)
|
|
28
|
+
subprocess.run(
|
|
29
|
+
[
|
|
30
|
+
"poetry",
|
|
31
|
+
"run",
|
|
32
|
+
"isort",
|
|
33
|
+
"--check-only",
|
|
34
|
+
"--profile",
|
|
35
|
+
"black",
|
|
36
|
+
"--line-length",
|
|
37
|
+
"100",
|
|
38
|
+
*target_paths,
|
|
39
|
+
],
|
|
40
|
+
check=True,
|
|
41
|
+
)
|
|
42
|
+
print("All checks passed!")
|
|
43
|
+
return 0
|
|
44
|
+
except subprocess.CalledProcessError:
|
|
45
|
+
print("Checks failed.")
|
|
46
|
+
return 1
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run_format(paths=None) -> int:
|
|
50
|
+
check_install_status()
|
|
51
|
+
target_paths = paths if paths else ["."]
|
|
52
|
+
try:
|
|
53
|
+
print(f"Running formatting on {target_paths}...")
|
|
54
|
+
subprocess.run(
|
|
55
|
+
["poetry", "run", "black", "--line-length", "100", *target_paths], check=True
|
|
56
|
+
)
|
|
57
|
+
subprocess.run(
|
|
58
|
+
[
|
|
59
|
+
"poetry",
|
|
60
|
+
"run",
|
|
61
|
+
"isort",
|
|
62
|
+
"--profile",
|
|
63
|
+
"black",
|
|
64
|
+
"--line-length",
|
|
65
|
+
"100",
|
|
66
|
+
*target_paths,
|
|
67
|
+
],
|
|
68
|
+
check=True,
|
|
69
|
+
)
|
|
70
|
+
subprocess.run(["poetry", "run", "ruff", "check", "--fix", *target_paths], check=True)
|
|
71
|
+
print("Formatting complete!")
|
|
72
|
+
return 0
|
|
73
|
+
except subprocess.CalledProcessError:
|
|
74
|
+
print("Formatting failed.")
|
|
75
|
+
return 1
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def run_verify(paths=None) -> int:
|
|
79
|
+
try:
|
|
80
|
+
print("Running verification (lint + tests)...")
|
|
81
|
+
if run_check(paths) != 0:
|
|
82
|
+
print("Lint checks failed. Aborting verification.")
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
print("Running tests...")
|
|
86
|
+
subprocess.run(["poetry", "run", "pytest"], check=True)
|
|
87
|
+
print("Verification complete! All checks and tests passed.")
|
|
88
|
+
return 0
|
|
89
|
+
except subprocess.CalledProcessError:
|
|
90
|
+
print("Tests failed.")
|
|
91
|
+
return 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def main() -> int:
|
|
95
|
+
parser = argparse.ArgumentParser(description="Hard Lint CLI")
|
|
96
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
97
|
+
|
|
98
|
+
subparsers.add_parser("install", help="Install hardlint configuration")
|
|
99
|
+
|
|
100
|
+
check_parser = subparsers.add_parser("check", help="Run lint checks")
|
|
101
|
+
check_parser.add_argument("paths", nargs="*", help="Paths to check")
|
|
102
|
+
|
|
103
|
+
format_parser = subparsers.add_parser("format", help="Run formatting")
|
|
104
|
+
format_parser.add_argument("paths", nargs="*", help="Paths to format")
|
|
105
|
+
|
|
106
|
+
verify_parser = subparsers.add_parser("verify", help="Run verification (lint + tests)")
|
|
107
|
+
verify_parser.add_argument("paths", nargs="*", help="Paths to verify (passed to lint check)")
|
|
108
|
+
|
|
109
|
+
if len(sys.argv) == 1:
|
|
110
|
+
try:
|
|
111
|
+
installer = HardLintInstaller(Path.cwd())
|
|
112
|
+
installer.install()
|
|
113
|
+
print("Hardlint configuration installed successfully.")
|
|
114
|
+
return 0
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print(f"Installation failed: {e}")
|
|
117
|
+
return 1
|
|
118
|
+
|
|
119
|
+
args = parser.parse_args()
|
|
120
|
+
|
|
121
|
+
if args.command == "install":
|
|
122
|
+
try:
|
|
123
|
+
installer = HardLintInstaller(Path.cwd())
|
|
124
|
+
installer.install()
|
|
125
|
+
print("Hardlint configuration installed successfully.")
|
|
126
|
+
return 0
|
|
127
|
+
except Exception as e:
|
|
128
|
+
print(f"Installation failed: {e}")
|
|
129
|
+
return 1
|
|
130
|
+
elif args.command == "check":
|
|
131
|
+
return run_check(args.paths)
|
|
132
|
+
elif args.command == "format":
|
|
133
|
+
return run_format(args.paths)
|
|
134
|
+
elif args.command == "verify":
|
|
135
|
+
return run_verify(args.paths)
|
|
136
|
+
|
|
137
|
+
return 0
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
if __name__ == "__main__":
|
|
141
|
+
sys.exit(main())
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class HardLintInstaller:
|
|
6
|
+
def __init__(self, project_root: Path):
|
|
7
|
+
self.project_root = project_root
|
|
8
|
+
self.git_dir = project_root / ".git"
|
|
9
|
+
self.pre_commit_dir = project_root / ".hardlint"
|
|
10
|
+
self.hooks_dir = self.pre_commit_dir / "_"
|
|
11
|
+
self.pyproject_path = project_root / "pyproject.toml"
|
|
12
|
+
|
|
13
|
+
def install(self) -> None:
|
|
14
|
+
if not self._check_git_repo():
|
|
15
|
+
raise RuntimeError("Not a Git repository. Initialize Git first with: git init")
|
|
16
|
+
|
|
17
|
+
if not self._check_pyproject():
|
|
18
|
+
raise RuntimeError("pyproject.toml not found in project root")
|
|
19
|
+
|
|
20
|
+
self._setup_pre_commit_hooks()
|
|
21
|
+
self._configure_git_hooks_path()
|
|
22
|
+
self._ensure_pyproject_config()
|
|
23
|
+
|
|
24
|
+
def _check_git_repo(self) -> bool:
|
|
25
|
+
return self.git_dir.exists()
|
|
26
|
+
|
|
27
|
+
def _check_pyproject(self) -> bool:
|
|
28
|
+
return self.pyproject_path.exists()
|
|
29
|
+
|
|
30
|
+
def _setup_pre_commit_hooks(self) -> None:
|
|
31
|
+
self.hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
(self.pre_commit_dir / ".gitignore").write_text("*")
|
|
34
|
+
|
|
35
|
+
pre_commit_content = (
|
|
36
|
+
'#!/bin/sh\ncd "$(git rev-parse --show-toplevel)"\npoetry run hard-lint-py format .\n'
|
|
37
|
+
)
|
|
38
|
+
pre_commit_path = self.hooks_dir / "pre-commit"
|
|
39
|
+
pre_commit_path.write_text(pre_commit_content)
|
|
40
|
+
pre_commit_path.chmod(0o755)
|
|
41
|
+
|
|
42
|
+
commit_msg_content = (
|
|
43
|
+
"#!/bin/sh\n"
|
|
44
|
+
'cd "$(git rev-parse --show-toplevel)"\n'
|
|
45
|
+
'MESSAGE=$(cat "$1")\n'
|
|
46
|
+
'poetry run python -c "import sys; import re; '
|
|
47
|
+
"pattern = r'^(feat|fix|docs|style|refactor|perf|test|chore)'; "
|
|
48
|
+
"msg = '''$MESSAGE'''; "
|
|
49
|
+
'sys.exit(0 if re.match(pattern, msg) else 1)"\n'
|
|
50
|
+
)
|
|
51
|
+
commit_msg_path = self.hooks_dir / "commit-msg"
|
|
52
|
+
commit_msg_path.write_text(commit_msg_content)
|
|
53
|
+
commit_msg_path.chmod(0o755)
|
|
54
|
+
|
|
55
|
+
def _configure_git_hooks_path(self) -> None:
|
|
56
|
+
try:
|
|
57
|
+
subprocess.run(
|
|
58
|
+
["git", "config", "core.hooksPath", ".hardlint/_"],
|
|
59
|
+
cwd=self.project_root,
|
|
60
|
+
check=True,
|
|
61
|
+
capture_output=True,
|
|
62
|
+
)
|
|
63
|
+
except subprocess.CalledProcessError as e:
|
|
64
|
+
raise RuntimeError(f"Failed to configure Git: {e.stderr.decode()}") from e
|
|
65
|
+
|
|
66
|
+
def _ensure_pyproject_config(self) -> None:
|
|
67
|
+
pyproject_content = self.pyproject_path.read_text()
|
|
68
|
+
|
|
69
|
+
if "[tool.ruff]" not in pyproject_content:
|
|
70
|
+
pyproject_content += "\n[tool.ruff]\n"
|
|
71
|
+
pyproject_content += "line-length = 100\n"
|
|
72
|
+
pyproject_content += 'target-version = "py310"\n'
|
|
73
|
+
pyproject_content += 'select = ["E", "F", "W", "I", "N", "C", "B"]\n'
|
|
74
|
+
|
|
75
|
+
if "[tool.black]" not in pyproject_content:
|
|
76
|
+
pyproject_content += "\n[tool.black]\n"
|
|
77
|
+
pyproject_content += "line-length = 100\n"
|
|
78
|
+
|
|
79
|
+
if "[tool.isort]" not in pyproject_content:
|
|
80
|
+
pyproject_content += "\n[tool.isort]\n"
|
|
81
|
+
pyproject_content += 'profile = "black"\n'
|
|
82
|
+
pyproject_content += "line_length = 100\n"
|
|
83
|
+
|
|
84
|
+
self.pyproject_path.write_text(pyproject_content)
|