Skip to content

Commit 7844701

Browse files
polish
1 parent b78975c commit 7844701

File tree

7 files changed

+312
-3
lines changed

7 files changed

+312
-3
lines changed

.github/workflows/ci.yml

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,22 @@ jobs:
4747
run: mypy src/importguard
4848

4949
test:
50-
runs-on: ubuntu-latest
50+
runs-on: ${{ matrix.os }}
5151
strategy:
52+
fail-fast: false
5253
matrix:
54+
os: [ubuntu-latest, macos-latest, windows-latest]
5355
python-version: ["3.9", "3.10", "3.11", "3.12"]
56+
exclude:
57+
# Reduce CI time by testing fewer combinations
58+
- os: macos-latest
59+
python-version: "3.9"
60+
- os: macos-latest
61+
python-version: "3.10"
62+
- os: windows-latest
63+
python-version: "3.9"
64+
- os: windows-latest
65+
python-version: "3.10"
5466
steps:
5567
- uses: actions/checkout@v4
5668

@@ -72,3 +84,32 @@ jobs:
7284
python -m importguard check json
7385
python -m importguard check os --max-ms 5000
7486
87+
- name: Test JSON output
88+
run: python -m importguard check json --json
89+
90+
- name: Test repeat functionality
91+
run: python -m importguard check json --repeat 3 --max-ms 100
92+
93+
build:
94+
runs-on: ubuntu-latest
95+
needs: [lint, typecheck, test]
96+
steps:
97+
- uses: actions/checkout@v4
98+
99+
- name: Set up Python
100+
uses: actions/setup-python@v5
101+
with:
102+
python-version: "3.11"
103+
104+
- name: Install build tools
105+
run: pip install build
106+
107+
- name: Build package
108+
run: python -m build
109+
110+
- name: Upload artifacts
111+
uses: actions/upload-artifact@v4
112+
with:
113+
name: dist
114+
path: dist/
115+

.github/workflows/publish.yml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
jobs:
8+
publish:
9+
runs-on: ubuntu-latest
10+
environment: pypi
11+
permissions:
12+
id-token: write # Required for trusted publishing
13+
14+
steps:
15+
- uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: "3.11"
21+
22+
- name: Install build tools
23+
run: pip install build
24+
25+
- name: Build package
26+
run: python -m build
27+
28+
- name: Publish to PyPI
29+
uses: pypa/gh-action-pypi-publish@release/v1
30+
# Uses trusted publishing - no API token needed
31+
# Configure at: https://pypi.org/manage/project/importguard/settings/publishing/
32+

CHANGELOG.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.1.0] - 2025-01-01
9+
10+
### Added
11+
12+
- **CLI**: `importguard check <module>` command with full option support
13+
- `--max-ms MS` — Fail if import exceeds time budget
14+
- `--ban MODULE` — Ban modules from being imported (repeatable)
15+
- `--config FILE` — Load `.importguard.toml` configuration
16+
- `--top N` — Show top N slowest imports
17+
- `--json` — Machine-readable JSON output for CI
18+
- `--quiet` — Only output on failure
19+
- `--repeat K` — Run K times, report median (reduces noise)
20+
- `--python PATH` — Use specific Python interpreter
21+
22+
- **Configuration**: `.importguard.toml` support
23+
- Global `max_total_ms` budget
24+
- Per-module budgets via `[importguard.budgets]`
25+
- Per-module banned imports via `[importguard.banned]`
26+
- Auto-discovery walking up directory tree
27+
28+
- **Python API**: `check_import()` function
29+
- Returns `ImportResult` with full timing data
30+
- Supports `max_ms`, `banned`, `repeat`, `python_path` parameters
31+
- Accepts both `list` and `set` for banned modules
32+
33+
- **Core Engine**
34+
- Uses Python's `-X importtime` for accurate timing
35+
- Wall-clock backup measurement via `time.perf_counter_ns()`
36+
- Runs in isolated subprocess (no interpreter pollution)
37+
- Handles import failures gracefully
38+
39+
- **CI Integration**
40+
- GitHub Actions workflow example
41+
- Pre-commit hook example
42+
- JSON output for machine parsing
43+
- Warnings for near-budget imports
44+
45+
- **Documentation**
46+
- Comprehensive README with examples
47+
- "Why Results Differ on CI?" troubleshooting guide
48+
- "How to Keep Imports Fast" best practices
49+
50+
### Technical Details
51+
52+
- Zero runtime dependencies
53+
- Python 3.9+ support
54+
- Full type annotations (mypy strict)
55+
- 64+ test cases
56+
57+
[0.1.0]: https://github.com/AryanKumar1401/importguard/releases/tag/v0.1.0
58+

README.md

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,164 @@ repos:
193193

194194
---
195195

196+
## Why Results Differ on CI?
197+
198+
Import times can vary significantly between your local machine and CI. Here's why and how to handle it:
199+
200+
### Common Causes
201+
202+
| Factor | Local | CI | Impact |
203+
|--------|-------|-----|--------|
204+
| **CPU** | Fast desktop/laptop | Shared VM cores | 2-5x slower |
205+
| **Disk** | SSD/NVMe | Network storage | Variable latency |
206+
| **Caching** | Warm `.pyc` files | Cold start | First run slower |
207+
| **Python version** | May differ | Matrix testing | Different stdlib |
208+
| **Dependencies** | Pinned versions | Fresh install | Version variance |
209+
210+
### Recommendations
211+
212+
1. **Use `--repeat 3` or `--repeat 5`** — Takes median to smooth out noise
213+
2. **Set CI budgets 2-3x higher** than local measurements
214+
3. **Use separate config sections** for CI vs local:
215+
216+
```toml
217+
# .importguard.toml
218+
[importguard]
219+
max_total_ms = 200 # Local development
220+
221+
# Override in CI with environment-specific config
222+
# or use: importguard check mypkg --max-ms 500
223+
```
224+
225+
4. **Pin Python version** in CI to match local:
226+
227+
```yaml
228+
- uses: actions/setup-python@v5
229+
with:
230+
python-version: '3.11.7' # Exact version
231+
```
232+
233+
5. **Warm up before measuring** (optional):
234+
235+
```yaml
236+
- name: Warm up Python cache
237+
run: python -c "import mypkg" || true
238+
239+
- name: Check import performance
240+
run: importguard check mypkg --repeat 3
241+
```
242+
243+
### Debugging CI Failures
244+
245+
```bash
246+
# Get detailed JSON output for investigation
247+
importguard check mypkg --json > import-report.json
248+
249+
# Compare top imports between local and CI
250+
importguard check mypkg --top 20
251+
```
252+
253+
---
254+
255+
## How to Keep Imports Fast
256+
257+
### 1. Lazy Imports
258+
259+
Move heavy imports inside functions that need them:
260+
261+
```python
262+
# ❌ Bad: imports pandas at module load
263+
import pandas as pd
264+
265+
def process_data(path):
266+
return pd.read_csv(path)
267+
268+
# ✅ Good: imports pandas only when needed
269+
def process_data(path):
270+
import pandas as pd
271+
return pd.read_csv(path)
272+
```
273+
274+
### 2. Optional Dependencies
275+
276+
Use try/except for optional heavy dependencies:
277+
278+
```python
279+
# ✅ Good: graceful degradation
280+
try:
281+
import torch
282+
HAS_TORCH = True
283+
except ImportError:
284+
HAS_TORCH = False
285+
286+
def train_model(data):
287+
if not HAS_TORCH:
288+
raise ImportError("torch required: pip install mypkg[ml]")
289+
# ... use torch
290+
```
291+
292+
### 3. Deferred Imports with `__getattr__`
293+
294+
For library authors, use module-level `__getattr__`:
295+
296+
```python
297+
# mypkg/__init__.py
298+
def __getattr__(name):
299+
if name == "heavy_module":
300+
from . import heavy_module
301+
return heavy_module
302+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
303+
```
304+
305+
### 4. Split Entry Points
306+
307+
Separate your CLI from your library:
308+
309+
```
310+
mypkg/
311+
├── __init__.py # Lightweight, fast to import
312+
├── core.py # Core functionality
313+
├── cli.py # CLI-only, can import Click/Rich
314+
└── optional/
315+
└── ml.py # Heavy ML dependencies
316+
```
317+
318+
```toml
319+
# pyproject.toml
320+
[project.scripts]
321+
mypkg = "mypkg.cli:main" # CLI imports heavy deps
322+
```
323+
324+
### 5. Avoid Import Side Effects
325+
326+
```python
327+
# ❌ Bad: runs on import
328+
config = load_config() # Network call!
329+
logger = setup_logging() # Creates files!
330+
331+
# ✅ Good: explicit initialization
332+
_config = None
333+
def get_config():
334+
global _config
335+
if _config is None:
336+
_config = load_config()
337+
return _config
338+
```
339+
340+
### 6. Profile Before Optimizing
341+
342+
Use importguard to find the actual culprits:
343+
344+
```bash
345+
# Find the slowest imports
346+
importguard check mypkg --top 20
347+
348+
# Ban known-heavy modules from your fast path
349+
importguard check mypkg.cli --ban pandas --ban torch --ban tensorflow
350+
```
351+
352+
---
353+
196354
## Python API
197355

198356
```python
17 KB
Binary file not shown.

pyproject.toml

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,42 @@ version = "0.1.0"
88
description = "Measure and enforce import-time behavior in Python projects"
99
readme = "README.md"
1010
license = "MIT"
11+
license-files = ["LICENSE"]
1112
authors = [{ name = "Aryan Kumar" }]
13+
maintainers = [{ name = "Aryan Kumar" }]
1214
requires-python = ">=3.9"
1315
classifiers = [
14-
"Development Status :: 3 - Alpha",
16+
"Development Status :: 4 - Beta",
1517
"Environment :: Console",
1618
"Intended Audience :: Developers",
1719
"License :: OSI Approved :: MIT License",
1820
"Operating System :: OS Independent",
21+
"Operating System :: POSIX :: Linux",
22+
"Operating System :: MacOS",
23+
"Operating System :: Microsoft :: Windows",
1924
"Programming Language :: Python :: 3",
2025
"Programming Language :: Python :: 3.9",
2126
"Programming Language :: Python :: 3.10",
2227
"Programming Language :: Python :: 3.11",
2328
"Programming Language :: Python :: 3.12",
29+
"Programming Language :: Python :: 3.13",
2430
"Topic :: Software Development :: Quality Assurance",
2531
"Topic :: Software Development :: Testing",
32+
"Topic :: Software Development :: Build Tools",
2633
"Typing :: Typed",
2734
]
28-
keywords = ["import", "performance", "timing", "cli", "ci", "testing"]
35+
keywords = [
36+
"import",
37+
"performance",
38+
"timing",
39+
"cli",
40+
"ci",
41+
"testing",
42+
"startup",
43+
"cold-start",
44+
"profiling",
45+
"lint",
46+
]
2947
dependencies = []
3048

3149
[project.optional-dependencies]
@@ -41,8 +59,10 @@ importguard = "importguard.cli:main"
4159

4260
[project.urls]
4361
Homepage = "https://github.com/AryanKumar1401/importguard"
62+
Documentation = "https://github.com/AryanKumar1401/importguard#readme"
4463
Repository = "https://github.com/AryanKumar1401/importguard"
4564
Issues = "https://github.com/AryanKumar1401/importguard/issues"
65+
Changelog = "https://github.com/AryanKumar1401/importguard/releases"
4666

4767
[tool.hatch.build.targets.wheel]
4868
packages = ["src/importguard"]

src/importguard/py.typed

Whitespace-only changes.

0 commit comments

Comments
 (0)