imexp 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.
- imexp-0.1.0/.github/workflows/ci.yml +35 -0
- imexp-0.1.0/.github/workflows/release.yml +93 -0
- imexp-0.1.0/.gitignore +177 -0
- imexp-0.1.0/AGENTS.md +301 -0
- imexp-0.1.0/LICENSE +674 -0
- imexp-0.1.0/PKG-INFO +139 -0
- imexp-0.1.0/README.md +111 -0
- imexp-0.1.0/TODO.md +16 -0
- imexp-0.1.0/docs/dev/commits.md +199 -0
- imexp-0.1.0/docs/dev/linting.md +3 -0
- imexp-0.1.0/packaging/bundle_exporter.py +134 -0
- imexp-0.1.0/packaging/clean_build_artifacts.py +49 -0
- imexp-0.1.0/packaging/hatch_build.py +129 -0
- imexp-0.1.0/packaging/imessage-exporter-assets.json +20 -0
- imexp-0.1.0/packaging/update_upstream_manifest.py +125 -0
- imexp-0.1.0/pyproject.toml +55 -0
- imexp-0.1.0/src/imexp/__init__.py +0 -0
- imexp-0.1.0/src/imexp/__main__.py +7 -0
- imexp-0.1.0/src/imexp/cli/__init__.py +0 -0
- imexp-0.1.0/src/imexp/cli/config.py +180 -0
- imexp-0.1.0/src/imexp/cli/main.py +1225 -0
- imexp-0.1.0/src/imexp/core/__init__.py +0 -0
- imexp-0.1.0/src/imexp/core/exporter_binary.py +83 -0
- imexp-0.1.0/src/imexp/core/utils/__init__.py +0 -0
- imexp-0.1.0/src/imexp/core/utils/ansi.py +33 -0
- imexp-0.1.0/src/imexp/core/utils/helpformatter.py +68 -0
- imexp-0.1.0/tests/test_cli_flow.py +183 -0
- imexp-0.1.0/tests/test_config.py +163 -0
- imexp-0.1.0/tests/test_exporter_binary.py +79 -0
- imexp-0.1.0/tests/test_hatch_build.py +79 -0
- imexp-0.1.0/tests/test_help_formatting.py +32 -0
- imexp-0.1.0/tests/test_helpers.py +39 -0
- imexp-0.1.0/tests/test_relabel.py +64 -0
- imexp-0.1.0/tests/test_update.py +337 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
push:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
runs-on: ${{ matrix.os }}
|
|
10
|
+
strategy:
|
|
11
|
+
fail-fast: false
|
|
12
|
+
matrix:
|
|
13
|
+
os:
|
|
14
|
+
- ubuntu-latest
|
|
15
|
+
- macos-latest
|
|
16
|
+
- windows-latest
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v6
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.12"
|
|
22
|
+
- run: python -m pip install --upgrade pip
|
|
23
|
+
- run: python -m pip install -e .[dev]
|
|
24
|
+
- run: python -m pytest -q
|
|
25
|
+
|
|
26
|
+
package:
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
steps:
|
|
29
|
+
- uses: actions/checkout@v6
|
|
30
|
+
- uses: actions/setup-python@v6
|
|
31
|
+
with:
|
|
32
|
+
python-version: "3.12"
|
|
33
|
+
- run: python -m pip install --upgrade pip build
|
|
34
|
+
- run: python packaging/clean_build_artifacts.py
|
|
35
|
+
- run: python -m build --sdist --wheel
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
workflow_dispatch:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
sdist:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: "3.12"
|
|
20
|
+
- run: python -m pip install --upgrade pip build
|
|
21
|
+
- run: python packaging/clean_build_artifacts.py
|
|
22
|
+
- run: python -m build --sdist
|
|
23
|
+
- uses: actions/upload-artifact@v7
|
|
24
|
+
with:
|
|
25
|
+
name: dist-sdist
|
|
26
|
+
path: dist/*
|
|
27
|
+
|
|
28
|
+
wheel:
|
|
29
|
+
runs-on: ${{ matrix.runner }}
|
|
30
|
+
strategy:
|
|
31
|
+
fail-fast: false
|
|
32
|
+
matrix:
|
|
33
|
+
include:
|
|
34
|
+
- runner: macos-15-intel
|
|
35
|
+
target: macos-x86_64
|
|
36
|
+
- runner: macos-14
|
|
37
|
+
target: macos-arm64
|
|
38
|
+
- runner: windows-latest
|
|
39
|
+
target: windows-x86_64
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v6
|
|
42
|
+
- uses: actions/setup-python@v6
|
|
43
|
+
with:
|
|
44
|
+
python-version: "3.12"
|
|
45
|
+
- run: python -m pip install --upgrade pip build
|
|
46
|
+
- run: python packaging/clean_build_artifacts.py
|
|
47
|
+
- run: python packaging/bundle_exporter.py --target "${{ matrix.target }}"
|
|
48
|
+
- run: python -m build --wheel
|
|
49
|
+
- uses: actions/upload-artifact@v7
|
|
50
|
+
with:
|
|
51
|
+
name: dist-${{ matrix.target }}
|
|
52
|
+
path: dist/*
|
|
53
|
+
|
|
54
|
+
publish-testpypi:
|
|
55
|
+
if: github.event_name == 'workflow_dispatch'
|
|
56
|
+
needs:
|
|
57
|
+
- sdist
|
|
58
|
+
- wheel
|
|
59
|
+
runs-on: ubuntu-latest
|
|
60
|
+
environment:
|
|
61
|
+
name: testpypi
|
|
62
|
+
permissions:
|
|
63
|
+
id-token: write
|
|
64
|
+
steps:
|
|
65
|
+
- uses: actions/download-artifact@v8
|
|
66
|
+
with:
|
|
67
|
+
path: dist
|
|
68
|
+
merge-multiple: true
|
|
69
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
70
|
+
with:
|
|
71
|
+
repository-url: https://test.pypi.org/legacy/
|
|
72
|
+
packages-dir: dist
|
|
73
|
+
skip-existing: true
|
|
74
|
+
|
|
75
|
+
publish-pypi:
|
|
76
|
+
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
|
|
77
|
+
needs:
|
|
78
|
+
- sdist
|
|
79
|
+
- wheel
|
|
80
|
+
runs-on: ubuntu-latest
|
|
81
|
+
environment:
|
|
82
|
+
name: pypi
|
|
83
|
+
permissions:
|
|
84
|
+
id-token: write
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/download-artifact@v8
|
|
87
|
+
with:
|
|
88
|
+
path: dist
|
|
89
|
+
merge-multiple: true
|
|
90
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
91
|
+
with:
|
|
92
|
+
packages-dir: dist
|
|
93
|
+
skip-existing: true
|
imexp-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# Custom Folders
|
|
2
|
+
/.refs
|
|
3
|
+
/.pylintcache
|
|
4
|
+
/data/logs
|
|
5
|
+
/data/base
|
|
6
|
+
/data/config/
|
|
7
|
+
.staging/
|
|
8
|
+
/src/imexp/bin/
|
|
9
|
+
|
|
10
|
+
# Custom Extensions
|
|
11
|
+
*.log.*
|
|
12
|
+
|
|
13
|
+
# Custom Files
|
|
14
|
+
.DS_Store
|
|
15
|
+
|
|
16
|
+
# Byte-compiled / optimized / DLL files
|
|
17
|
+
__pycache__/
|
|
18
|
+
*.py[cod]
|
|
19
|
+
*$py.class
|
|
20
|
+
|
|
21
|
+
# C extensions
|
|
22
|
+
*.so
|
|
23
|
+
|
|
24
|
+
# Distribution / packaging
|
|
25
|
+
.Python
|
|
26
|
+
build/
|
|
27
|
+
develop-eggs/
|
|
28
|
+
dist/
|
|
29
|
+
downloads/
|
|
30
|
+
eggs/
|
|
31
|
+
.eggs/
|
|
32
|
+
lib/
|
|
33
|
+
lib64/
|
|
34
|
+
parts/
|
|
35
|
+
sdist/
|
|
36
|
+
var/
|
|
37
|
+
wheels/
|
|
38
|
+
share/python-wheels/
|
|
39
|
+
*.egg-info/
|
|
40
|
+
.installed.cfg
|
|
41
|
+
*.egg
|
|
42
|
+
MANIFEST
|
|
43
|
+
|
|
44
|
+
# PyInstaller
|
|
45
|
+
# Usually these files are written by a python script from a template
|
|
46
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
47
|
+
*.manifest
|
|
48
|
+
*.spec
|
|
49
|
+
|
|
50
|
+
# Installer logs
|
|
51
|
+
pip-log.txt
|
|
52
|
+
pip-delete-this-directory.txt
|
|
53
|
+
|
|
54
|
+
# Unit test / coverage reports
|
|
55
|
+
htmlcov/
|
|
56
|
+
.tox/
|
|
57
|
+
.nox/
|
|
58
|
+
.coverage
|
|
59
|
+
.coverage.*
|
|
60
|
+
.cache
|
|
61
|
+
nosetests.xml
|
|
62
|
+
coverage.xml
|
|
63
|
+
*.cover
|
|
64
|
+
*.py,cover
|
|
65
|
+
.hypothesis/
|
|
66
|
+
.pytest_cache/
|
|
67
|
+
cover/
|
|
68
|
+
|
|
69
|
+
# Translations
|
|
70
|
+
*.mo
|
|
71
|
+
*.pot
|
|
72
|
+
|
|
73
|
+
# Django stuff:
|
|
74
|
+
*.log
|
|
75
|
+
local_settings.py
|
|
76
|
+
db.sqlite3
|
|
77
|
+
db.sqlite3-journal
|
|
78
|
+
|
|
79
|
+
# Flask stuff:
|
|
80
|
+
instance/
|
|
81
|
+
.webassets-cache
|
|
82
|
+
|
|
83
|
+
# Scrapy stuff:
|
|
84
|
+
.scrapy
|
|
85
|
+
|
|
86
|
+
# Sphinx documentation
|
|
87
|
+
docs/_build/
|
|
88
|
+
|
|
89
|
+
# PyBuilder
|
|
90
|
+
.pybuilder/
|
|
91
|
+
target/
|
|
92
|
+
|
|
93
|
+
# Jupyter Notebook
|
|
94
|
+
.ipynb_checkpoints
|
|
95
|
+
|
|
96
|
+
# IPython
|
|
97
|
+
profile_default/
|
|
98
|
+
ipython_config.py
|
|
99
|
+
|
|
100
|
+
# pyenv
|
|
101
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
102
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
103
|
+
# .python-version
|
|
104
|
+
|
|
105
|
+
# pipenv
|
|
106
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
107
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
108
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
109
|
+
# install all needed dependencies.
|
|
110
|
+
#Pipfile.lock
|
|
111
|
+
|
|
112
|
+
# poetry
|
|
113
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
114
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
115
|
+
# commonly ignored for libraries.
|
|
116
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
117
|
+
#poetry.lock
|
|
118
|
+
|
|
119
|
+
# pdm
|
|
120
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
121
|
+
#pdm.lock
|
|
122
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
123
|
+
# in version control.
|
|
124
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
125
|
+
.pdm.toml
|
|
126
|
+
.pdm-python
|
|
127
|
+
.pdm-build/
|
|
128
|
+
|
|
129
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
130
|
+
__pypackages__/
|
|
131
|
+
|
|
132
|
+
# Celery stuff
|
|
133
|
+
celerybeat-schedule
|
|
134
|
+
celerybeat.pid
|
|
135
|
+
|
|
136
|
+
# SageMath parsed files
|
|
137
|
+
*.sage.py
|
|
138
|
+
|
|
139
|
+
# Environments
|
|
140
|
+
.env
|
|
141
|
+
.venv
|
|
142
|
+
env/
|
|
143
|
+
venv/
|
|
144
|
+
ENV/
|
|
145
|
+
env.bak/
|
|
146
|
+
venv.bak/
|
|
147
|
+
|
|
148
|
+
# Spyder project settings
|
|
149
|
+
.spyderproject
|
|
150
|
+
.spyproject
|
|
151
|
+
|
|
152
|
+
# Rope project settings
|
|
153
|
+
.ropeproject
|
|
154
|
+
|
|
155
|
+
# mkdocs documentation
|
|
156
|
+
/site
|
|
157
|
+
|
|
158
|
+
# mypy
|
|
159
|
+
.mypy_cache/
|
|
160
|
+
.dmypy.json
|
|
161
|
+
dmypy.json
|
|
162
|
+
|
|
163
|
+
# Pyre type checker
|
|
164
|
+
.pyre/
|
|
165
|
+
|
|
166
|
+
# pytype static type analyzer
|
|
167
|
+
.pytype/
|
|
168
|
+
|
|
169
|
+
# Cython debug symbols
|
|
170
|
+
cython_debug/
|
|
171
|
+
|
|
172
|
+
# PyCharm
|
|
173
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
174
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
175
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
176
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
177
|
+
#.idea/
|
imexp-0.1.0/AGENTS.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Agent Guide
|
|
2
|
+
|
|
3
|
+
Always check for a virtual environment first. Do not assume a virtual environment is activated. Use the venv when running commands.
|
|
4
|
+
|
|
5
|
+
if Unix:
|
|
6
|
+
|
|
7
|
+
- `./.venv/bin/python`
|
|
8
|
+
- `./.venv/bin/pip`
|
|
9
|
+
- `./.venv/bin/pylint`
|
|
10
|
+
- `./.venv/bin/pytest`
|
|
11
|
+
|
|
12
|
+
if Windows:
|
|
13
|
+
|
|
14
|
+
- `.\.venv\Scripts\python`
|
|
15
|
+
- `.\.venv\Scripts\pip`
|
|
16
|
+
- `.\.venv\Scripts\pylint`
|
|
17
|
+
- `.\.venv\Scripts\pytest`
|
|
18
|
+
|
|
19
|
+
Run pytest before and after each turn so you know if your work broke any logic.
|
|
20
|
+
|
|
21
|
+
Once we come to an agreement on what code should be committed craft a commit mesage for the new code based on ./docs/dev/commits.md and commit the code, make sure the body is a hyphenated bullet list. Do not push unless asked.
|
|
22
|
+
|
|
23
|
+
## Brainstorming
|
|
24
|
+
|
|
25
|
+
Ask me clarifying questions until you know what I want to build and walk me through the setup step by step.
|
|
26
|
+
|
|
27
|
+
## Syntax
|
|
28
|
+
|
|
29
|
+
Follow these rules when generating or modifying code in this repository:
|
|
30
|
+
|
|
31
|
+
- Use guard clauses to exit early. Keep the happy path straight and flat.
|
|
32
|
+
- Avoid deep nesting; split logic into small functions instead.
|
|
33
|
+
- Prefer multiple `if` statements over large `if/elif/else` pyramids.
|
|
34
|
+
- Use `else` sparingly and only when it clarifies binary logic.
|
|
35
|
+
- Avoid using try except blocks unless absolutely necessary.
|
|
36
|
+
- Catch only specific exceptions you expect and know how to handle.
|
|
37
|
+
- Do NOT use `except Exception:`
|
|
38
|
+
- If you catch broadly for logging, re-raise so errors aren’t swallowed.
|
|
39
|
+
- Keep functions focused on one responsibility with clear inputs/outputs.
|
|
40
|
+
- Fail fast when invariants are violated; use precise error messages.
|
|
41
|
+
- Prioritize clarity over cleverness, code should read top-to-bottom like a story.
|
|
42
|
+
- For CLI tools, use `Declarative` control flow style. Do NOT use exit-code style control flow. Let the entrypoint handle process termination.
|
|
43
|
+
- We are in the pre-alpha building stage, there is no need for fallbacks or backwards compatability. Cutover. This is unreleased, all code needs to be canonical.
|
|
44
|
+
|
|
45
|
+
(See below for full guidelines and examples.)
|
|
46
|
+
|
|
47
|
+
# AGENTS: Coding Style & Control-Flow Guidelines
|
|
48
|
+
|
|
49
|
+
This project prefers explicit, predictable control flow and narrow, intentional error handling.
|
|
50
|
+
|
|
51
|
+
The goals:
|
|
52
|
+
|
|
53
|
+
- Code that reads top-to-bottom like a story
|
|
54
|
+
- Minimal nesting and cognitive load
|
|
55
|
+
- If necessary, use exceptions and branches that reflect *specific, known* conditions
|
|
56
|
+
|
|
57
|
+
If you’re an agent writing or editing code here, follow the rules below.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 1. Control Flow: Prefer Guard Clauses, Avoid Deep Nesting
|
|
62
|
+
|
|
63
|
+
### 1.1. Guard clauses over `else` pyramids
|
|
64
|
+
|
|
65
|
+
Use early returns (guard clauses) for invalid inputs, edge cases, and “nothing to do” states.
|
|
66
|
+
|
|
67
|
+
✅ Preferred:
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
def process_item(item):
|
|
71
|
+
if item is None:
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
if not item.active:
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
if item.value < 0:
|
|
78
|
+
raise ValueError("value must be non-negative")
|
|
79
|
+
|
|
80
|
+
return item.value * 2
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
❌ Avoid:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
def process_item(item):
|
|
87
|
+
if item is not None:
|
|
88
|
+
if item.active:
|
|
89
|
+
if item.value >= 0:
|
|
90
|
+
return item.value * 2
|
|
91
|
+
else:
|
|
92
|
+
raise ValueError("value must be non-negative")
|
|
93
|
+
else:
|
|
94
|
+
return None
|
|
95
|
+
else:
|
|
96
|
+
return None
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Rules:
|
|
100
|
+
|
|
101
|
+
- Prefer a flat, linear “happy path”.
|
|
102
|
+
- Use multiple `if` guard clauses instead of a single `if/elif/else` tower when it improves clarity.
|
|
103
|
+
- `else:` is not banned, but avoid it when it hides which conditions actually lead there.
|
|
104
|
+
|
|
105
|
+
### 1.2. Keep functions small and focused
|
|
106
|
+
|
|
107
|
+
If you feel tempted to add multiple `else` branches or deeply nested conditionals:
|
|
108
|
+
|
|
109
|
+
- First try splitting logic into smaller functions.
|
|
110
|
+
- Each function should do one clear thing and return early when preconditions fail.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## 2. Exceptions: Be Specific, Avoid Catch-Alls
|
|
115
|
+
|
|
116
|
+
### 2.1. No `except Exception:` (almost always)
|
|
117
|
+
|
|
118
|
+
Do not use `except Exception:` or `except BaseException:` unless you are:
|
|
119
|
+
|
|
120
|
+
- Logging/cleaning up and then re-raising.
|
|
121
|
+
|
|
122
|
+
Rules:
|
|
123
|
+
|
|
124
|
+
- Catch only the specific exceptions you expect and know how to handle.
|
|
125
|
+
- If you don't know how to recover, don’t catch. Let it propagate.
|
|
126
|
+
|
|
127
|
+
### 2.2. Group related exceptions explicitly
|
|
128
|
+
|
|
129
|
+
If multiple known failure modes can be handled in the same way, catch them as a tuple.
|
|
130
|
+
|
|
131
|
+
### 2.3. Logging + re-raise instead of swallowing
|
|
132
|
+
|
|
133
|
+
If you need to observe unexpected exceptions, log and re-raise.
|
|
134
|
+
|
|
135
|
+
This is acceptable at boundaries or in “monitoring” code. The key is: don’t swallow.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 3. Where Catch-Alls *Are* Allowed
|
|
140
|
+
|
|
141
|
+
There are a few places where a broad catch is acceptable and sometimes required.
|
|
142
|
+
|
|
143
|
+
### 3.1. Top-level entry points
|
|
144
|
+
|
|
145
|
+
Guidelines:
|
|
146
|
+
|
|
147
|
+
- Keep the handler small (log, cleanup, exit).
|
|
148
|
+
- Do not bury business logic inside such a handler.
|
|
149
|
+
|
|
150
|
+
### 3.2. Custom exception hierarchies
|
|
151
|
+
|
|
152
|
+
For libraries or complex modules, define a project-specific base exception.
|
|
153
|
+
|
|
154
|
+
✅ Preferred:
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
class AppError(Exception): pass
|
|
158
|
+
class ConfigError(AppError): pass
|
|
159
|
+
class NetworkError(AppError): pass
|
|
160
|
+
|
|
161
|
+
def run_step():
|
|
162
|
+
raise ConfigError("Missing token")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Then callers can do:
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
try:
|
|
169
|
+
run_step()
|
|
170
|
+
except AppError as e:
|
|
171
|
+
handle_expected_error(e)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
This avoids `except Exception:` while still providing a single, meaningful “umbrella”.
|
|
175
|
+
|
|
176
|
+
---
|
|
177
|
+
|
|
178
|
+
## 4. EAFP vs LBYL
|
|
179
|
+
|
|
180
|
+
Python style encourages EAFP (Easier to Ask Forgiveness than Permission) — using try/except instead of pre-checking everything — but we still want specificity.
|
|
181
|
+
|
|
182
|
+
### 4.1. Use EAFP for specific exceptions
|
|
183
|
+
|
|
184
|
+
### 4.2. Use LBYL sparingly for cheap, obvious checks
|
|
185
|
+
|
|
186
|
+
For very cheap validations or when the exception would be ambiguous, use LBYL.
|
|
187
|
+
|
|
188
|
+
Avoid doing heavy or redundant pre-checks that just duplicate what the operation itself will tell you via exceptions.
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## 5. Conditionals: How to Use `if`, `elif`, `else`
|
|
193
|
+
|
|
194
|
+
### 5.1. Prefer multiple `if` + early return to giant `if/elif/else`
|
|
195
|
+
|
|
196
|
+
✅ Preferred:
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
def categorize(user):
|
|
200
|
+
if user is None:
|
|
201
|
+
return "anonymous"
|
|
202
|
+
|
|
203
|
+
if user.is_admin:
|
|
204
|
+
return "admin"
|
|
205
|
+
|
|
206
|
+
if user.is_staff:
|
|
207
|
+
return "staff"
|
|
208
|
+
|
|
209
|
+
return "user"
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
❌ Less preferred:
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
def categorize(user):
|
|
216
|
+
if user is None:
|
|
217
|
+
return "anonymous"
|
|
218
|
+
elif user.is_admin:
|
|
219
|
+
return "admin"
|
|
220
|
+
elif user.is_staff:
|
|
221
|
+
return "staff"
|
|
222
|
+
else:
|
|
223
|
+
return "user"
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Both are valid Python. The first style is preferred here because:
|
|
227
|
+
|
|
228
|
+
- Each condition stands alone.
|
|
229
|
+
- The “fall-through” default path is visually obvious at the end.
|
|
230
|
+
- You don’t need to mentally manage the full `if/elif/elif/else` ladder.
|
|
231
|
+
|
|
232
|
+
### 5.2. When `else` *is* okay
|
|
233
|
+
|
|
234
|
+
`else` is fine when:
|
|
235
|
+
|
|
236
|
+
- There are only 1–2 branches and it aids readability.
|
|
237
|
+
- The logic is truly binary.
|
|
238
|
+
|
|
239
|
+
Example:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
def normalize_flag(value: str) -> bool:
|
|
243
|
+
value = value.lower().strip()
|
|
244
|
+
if value in {"1", "true", "yes", "y"}:
|
|
245
|
+
return True
|
|
246
|
+
else:
|
|
247
|
+
return False
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
Use judgement: prefer clarity over dogma.
|
|
251
|
+
|
|
252
|
+
---
|
|
253
|
+
|
|
254
|
+
## 6. Error Messages and Fail-Fast Behavior
|
|
255
|
+
|
|
256
|
+
- Fail early and loudly when invariants are broken.
|
|
257
|
+
- Raise exceptions with messages that explain what and why, not just that something failed.
|
|
258
|
+
|
|
259
|
+
✅ Good:
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
def get_user(id: int) -> User:
|
|
263
|
+
if id <= 0:
|
|
264
|
+
raise ValueError(f"id must be positive, got {id}")
|
|
265
|
+
...
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
---
|
|
269
|
+
|
|
270
|
+
## 7. Linting, Pylint, and Broad Exception Rules
|
|
271
|
+
|
|
272
|
+
We care about linters (e.g. Pylint) and generally do not want to disable rules globally.
|
|
273
|
+
|
|
274
|
+
- Do not disable warnings
|
|
275
|
+
|
|
276
|
+
Example:
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
def worker_loop():
|
|
280
|
+
while True:
|
|
281
|
+
try:
|
|
282
|
+
process_one_job()
|
|
283
|
+
except Exception: # pylint: disable=broad-exception-caught
|
|
284
|
+
log_exception_and_continue()
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Use this pattern only where absolutely necessary and structurally justified.
|
|
288
|
+
|
|
289
|
+
---
|
|
290
|
+
|
|
291
|
+
## 8. Summary for Agents
|
|
292
|
+
|
|
293
|
+
When generating or editing code in this repository:
|
|
294
|
+
|
|
295
|
+
1. Use guard clauses and early returns.
|
|
296
|
+
2. Keep control flow flat when possible. Avoid deep nesting and giant `if/elif/else` ladders.
|
|
297
|
+
3. Catch specific exceptions, not `Exception`
|
|
298
|
+
4. Don’t swallow exceptions silently.
|
|
299
|
+
5. Favor small, focused functions that reflect one clear responsibility.
|
|
300
|
+
|
|
301
|
+
If a simpler structure conflicts with these rules, prioritize clarity and explicitness over cleverness.
|