├── .coveragerc ├── .github ├── ISSUE_TEMPLATE │ └── continous-integration-issue.md └── workflows │ └── my_github_action.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── RelatedPythonIssues.md ├── didyoumean ├── __init__.py ├── didyoumean_api.py ├── didyoumean_api_tests.py ├── didyoumean_common_tests.py ├── didyoumean_internal.py ├── didyoumean_internal_tests.py ├── didyoumean_re.py ├── didyoumean_re_tests.py ├── didyoumean_sugg_tests.py ├── readme_examples.py └── readme_examples_cpython3.11.md ├── setup.cfg └── setup.py /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = didyoumean 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/continous-integration-issue.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Continous Integration Issue 3 | about: Report that Continuous Integration started failing 4 | title: 'Continous Integration on : ' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **First failure** 11 | 12 | Date: 13 | Build: 14 | Jobs: 15 | 16 | ``` 17 | logs with Python version information (version, build date, git sha1) 18 | ``` 19 | 20 | ``` 21 | logs with error 22 | ``` 23 | 24 | 25 | **Last success** 26 | 27 | Date: 28 | Build: 29 | Jobs: 30 | 31 | ``` 32 | logs with Python version information (version, build date, git sha1) 33 | ``` 34 | -------------------------------------------------------------------------------- /.github/workflows/my_github_action.yml: -------------------------------------------------------------------------------- 1 | name: my_github_actions 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' # every day at midnight 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | python-version: 16 | # Support for Python 2.7 removed - https://github.com/actions/setup-python/issues/672 17 | # - '2.7' 18 | # - '3.0.1' 19 | # - '3.1.4' 20 | # - '3.2.5' 21 | # - '3.3.7' 22 | # - '3.4.10' 23 | # - '3.5' - not available on ubuntu-latest for x64 24 | # - '3.6' - not available on ubuntu-latest for x64 25 | # - '3.7' - not available on ubuntu-latest for x64 26 | - '3.8' 27 | - '3.9' 28 | # - '3.9.0' - not available on ubuntu-latest for x64 29 | # - '3.9.1' - not available on ubuntu-latest for x64 30 | # - '3.9.2' - not available on ubuntu-latest for x64 31 | # - '3.9.3' - not available on ubuntu-latest for x64 32 | # - '3.9.4' - not available on ubuntu-latest for x64 33 | # - '3.9.5' - not available on ubuntu-latest for x64 34 | # - '3.9.6' - not available on ubuntu-latest for x64 35 | # - '3.9.7' - not available on ubuntu-latest for x64 36 | # - '3.9.8' - not available on ubuntu-latest for x64 37 | # - '3.9.9' - not available on ubuntu-latest for x64 38 | # - '3.9.10' - not available on ubuntu-latest for x64 39 | # - '3.9.11' - not available on ubuntu-latest for x64 40 | - '3.9.12' 41 | - '3.9.13' 42 | - '3.9.14' 43 | - '3.9.15' 44 | - '3.9.16' 45 | - '3.9.17' 46 | - '3.9-dev' 47 | # Disabled because of issue #61 48 | # - '3.10.0-alpha.1' 49 | # - '3.10.0-alpha.2' 50 | # - '3.10.0-alpha.3' 51 | # - '3.10.0-alpha.4' 52 | # - '3.10.0-alpha.5' 53 | # - '3.10.0-alpha.6' 54 | # - '3.10.0-alpha.7' 55 | # Disabled because of issue #66 56 | # - '3.10.0-beta.1' 57 | # - '3.10.0-beta.2' 58 | # - '3.10.0-beta.3' 59 | # - '3.10.0-beta.4' 60 | # - '3.10.0-rc.1' - not available on ubuntu-latest for x64 61 | # - '3.10.0-rc.2' - not available on ubuntu-latest for x64 62 | # - '3.10.0' - not available on ubuntu-latest for x64 63 | # - '3.10.1' - not available on ubuntu-latest for x64 64 | # - '3.10.2' - not available on ubuntu-latest for x64 65 | # - '3.10.3' - not available on ubuntu-latest for x64 66 | - '3.10.4' 67 | - '3.10.5' 68 | - '3.10.6' 69 | - '3.10.7' 70 | - '3.10.8' 71 | - '3.10.9' 72 | - '3.10.10' 73 | - '3.10.11' 74 | - '3.10.12' 75 | - '3.10.13' 76 | - '3.10.14' 77 | - '3.10-dev' 78 | # Disabled because of issue #66 79 | # - '3.11.0-alpha.1' 80 | # - '3.11.0-alpha.2' 81 | # - '3.11.0-alpha.3' 82 | # - '3.11.0-alpha.4' 83 | # - '3.11.0-alpha.5' 84 | # - '3.11.0-alpha.6' 85 | # - '3.11.0-alpha.7' - not available on ubuntu-latest for x64 86 | # - '3.11.0-beta.1' - not available on ubuntu-latest for x64 87 | # - '3.11.0-beta.2' - not available on ubuntu-latest for x64 88 | # - '3.11.0-beta.3' - not available on ubuntu-latest for x64 89 | # - '3.11.0-beta.4' - not available on ubuntu-latest for x64 90 | # - '3.11.0-beta.5' - not available on ubuntu-latest for x64 91 | # - '3.11.0-rc.1' - not available on ubuntu-latest for x64 92 | # - '3.11.0-rc.2' - not available on ubuntu-latest for x64 93 | - '3.11.0' 94 | - '3.11.1' 95 | - '3.11.2' 96 | - '3.11.3' 97 | - '3.11.4' 98 | - '3.11.5' 99 | - '3.11.6' 100 | - '3.11.7' 101 | - '3.11.8' 102 | - '3.11.9' 103 | - '3.11-dev' 104 | - '3.11' 105 | # Disabled because of issue #62 - https://github.com/nedbat/coveragepy/issues/1634 106 | # - '3.12.0-alpha.1' 107 | # - '3.12.0-alpha.2' 108 | # - '3.12.0-alpha.3' 109 | # - '3.12.0-alpha.4' 110 | # - '3.12.0-alpha.5' 111 | # - '3.12.0-alpha.6' 112 | # - '3.12.0-alpha.7' 113 | # - '3.12.0-beta.1' - not available on ubuntu-latest for x64 114 | # - '3.12.0-beta.2' - not available on ubuntu-latest for x64 115 | # - '3.12.0-beta.3' - not available on ubuntu-latest for x64 116 | # - '3.12.0-beta.4' - not available on ubuntu-latest for x64 117 | # - '3.12.0-rc.1' - not available on ubuntu-latest for x64 118 | # - '3.12.0-rc.2' - not available on ubuntu-latest for x64 119 | # - '3.12.0-rc.3' - not available on ubuntu-latest for x64 120 | - '3.12-dev' 121 | - '3.12.0' 122 | - '3.12.1' 123 | - '3.12.2' 124 | - '3.12.3' 125 | - '3.12.4' 126 | - '3.12.5' 127 | - '3.12.6' 128 | - '3.12.7' 129 | - '3.12.8' 130 | - '3.12.9' 131 | # - '3.13.0-alpha.1' - not available on ubuntu-latest for x64 132 | # - '3.13.0-alpha.2' - not available on ubuntu-latest for x64 133 | # - '3.13.0-alpha.3' - not available on ubuntu-latest for x64 134 | # - '3.13.0-alpha.4' - not available on ubuntu-latest for x64 135 | # - '3.13.0-alpha.5' - not available on ubuntu-latest for x64 136 | # - '3.13.0-alpha.6' - not available on ubuntu-latest for x64 137 | - '3.13.0-beta.1' 138 | - '3.13.0-beta.2' 139 | - '3.13.0-beta.3' 140 | - '3.13.0-beta.4' 141 | - '3.13.0-rc.1' 142 | - '3.13.0-rc.2' 143 | - '3.13.0-rc.3' 144 | - '3.13.0' 145 | - '3.13.1' 146 | - '3.13.2' 147 | - '3.14.0-alpha.0' 148 | - '3.14.0-alpha.1' 149 | - '3.14.0-alpha.2' 150 | - '3.14.0-alpha.3' 151 | - '3.14.0-alpha.4' 152 | - '3.14.0-alpha.5' 153 | - '3.14.0-alpha.6' 154 | - '3.14.0-alpha.7' 155 | - '3.14.0-beta.1' 156 | - 'pypy-2.7' 157 | - 'pypy-2.7-nightly' 158 | - 'pypy-3.6' # the latest available version of PyPy that supports Python 3.6 159 | - 'pypy-3.6-nightly' 160 | - 'pypy-3.7' # the latest available version of PyPy that supports Python 3.7 161 | - 'pypy-3.7-v7.3.3' # Python 3.7 and PyPy 7.3.3 162 | - 'pypy-3.7-nightly' # Python 3.7 and nightly PyPy 163 | - 'pypy-3.8-nightly' 164 | - 'pypy-3.9-nightly' 165 | - 'pypy-3.10-nightly' 166 | # TODO: Add nightly somehow 167 | 168 | steps: 169 | - uses: actions/checkout@v3 170 | - name: Set up Python ${{ matrix.python-version }} 171 | uses: actions/setup-python@v4 172 | with: 173 | python-version: ${{ matrix.python-version }} 174 | allow-prereleases: true 175 | env: # Temporary workaround for Python 3.5 failures - May 2024 176 | PIP_TRUSTED_HOST: "pypi.python.org pypi.org files.pythonhosted.org" 177 | - name: Show Python version information 178 | run: | 179 | # Information about Python version 180 | python -VV 181 | python -c "import sys; print(sys.version_info)" || true 182 | python -c "import sys; print(sys._git)" || true 183 | # Information about pip version 184 | pip --version 185 | # Information about Python builtins/keywords 186 | python -c "import sys; import keyword; print({sys.version_info: {'kword': set(keyword.kwlist), 'builtins': set(dir(__builtins__))}})" || true 187 | python -c "import sysconfig; print(dir(sysconfig))" || true 188 | - name: Install dependencies 189 | run: | 190 | pip install --upgrade pip 191 | pip --version 192 | pip install --upgrade pycodestyle 193 | pip install pep257 194 | pip install pydocstyle 195 | pip install --upgrade pyflakes || true 196 | pip install coverage 197 | pip install unittest2 198 | - name: Check codestyle 199 | run: | 200 | # W391, E122 temporarily disabled - https://github.com/PyCQA/pycodestyle/issues/1142 201 | pycodestyle --ignore=W391,E122,E501,E231,W503,E126,E123 *.py */*.py 202 | pycodestyle --select=W391,E122,E501,E231,W503,E126,E123 *.py */*.py || true 203 | pep257 *.py */*.py || true 204 | pydocstyle *.py */*.py || true 205 | pyflakes . || true 206 | - name: Run examples (from unit tests) 207 | run: | 208 | python didyoumean/didyoumean_sugg_tests.py 209 | - name: Run examples (from Readme) 210 | run: | 211 | python didyoumean/readme_examples.py 212 | - name: Run unit-tests without coverage 213 | run: | 214 | python -m unittest discover --start-directory=didyoumean --pattern=*.py 215 | python -m unittest2 discover --start-directory=didyoumean --pattern=*.py || true 216 | - name: Run unit-tests with coverage 217 | run: | 218 | coverage run -m unittest discover --start-directory=didyoumean --pattern=*.py || true 219 | coverage run -m unittest2 discover --start-directory=didyoumean --pattern=*.py || true 220 | - name: Install locally 221 | run: | 222 | pip install . --use-pep517 223 | - name: Run coverage tools 224 | run: | 225 | coveralls || true 226 | 227 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | # https://docs.travis-ci.com/user/languages/python/ 3 | # Because the code depends a lot on the Python interpreter 4 | # used (Python version and Python implementation), we want 5 | # to test all the possible configurations, including 6 | # deprecated versions. 7 | # Also, on Travis, all versions do not exist on all distros 8 | # (and when they do, they might actually correspond to 9 | # different minor versions of the interpreter). Thus, we end 10 | # end with a matrix to perform tests on different distros. 11 | # Failure is allowed on versions that are known to be missing 12 | # (these configs are tested anyway in case they come back). 13 | matrix: 14 | include: 15 | - os: linux 16 | dist: trusty 17 | python: 2.6 18 | - os: linux 19 | dist: xenial 20 | python: 2.6 21 | - os: linux 22 | dist: trusty 23 | python: 2.7 24 | - os: linux 25 | dist: xenial 26 | python: 2.7 27 | - os: linux 28 | dist: trusty 29 | python: 3.2 30 | - os: linux 31 | dist: xenial 32 | python: 3.2 33 | - os: linux 34 | dist: trusty 35 | python: 3.3 36 | - os: linux 37 | dist: xenial 38 | python: 3.3 39 | - os: linux 40 | dist: trusty 41 | python: 3.4 42 | - os: linux 43 | dist: xenial 44 | python: 3.4 45 | - os: linux 46 | dist: trusty 47 | python: 3.5 48 | - os: linux 49 | dist: xenial 50 | python: 3.5 51 | - os: linux 52 | dist: trusty 53 | python: 3.5-dev 54 | - os: linux 55 | dist: xenial 56 | python: 3.5-dev 57 | - os: linux 58 | dist: trusty 59 | python: 3.6 60 | - os: linux 61 | dist: xenial 62 | python: 3.6 63 | - os: linux 64 | dist: trusty 65 | python: 3.6-dev 66 | - os: linux 67 | dist: xenial 68 | python: 3.6-dev 69 | - os: linux 70 | dist: trusty 71 | python: 3.7 72 | - os: linux 73 | dist: xenial 74 | python: 3.7 75 | - os: linux 76 | dist: trusty 77 | python: 3.7-dev 78 | - os: linux 79 | dist: xenial 80 | python: 3.7-dev 81 | - os: linux 82 | dist: trusty 83 | python: 3.8 84 | - os: linux 85 | dist: xenial 86 | python: 3.8 87 | - os: linux 88 | dist: trusty 89 | python: 3.8-dev 90 | - os: linux 91 | dist: xenial 92 | python: 3.8-dev 93 | - os: linux 94 | dist: trusty 95 | python: 3.9 96 | - os: linux 97 | dist: xenial 98 | python: 3.9 99 | - os: linux 100 | dist: trusty 101 | python: 3.9-dev 102 | - os: linux 103 | dist: xenial 104 | python: 3.10-dev 105 | - os: linux 106 | dist: trusty 107 | python: nightly 108 | - os: linux 109 | dist: xenial 110 | python: nightly 111 | - os: linux 112 | dist: trusty 113 | python: pypy-5.3.1 114 | - os: linux 115 | dist: xenial 116 | python: pypy-5.3.1 117 | - os: linux 118 | dist: trusty 119 | python: pypy-5.4.1 120 | - os: linux 121 | dist: xenial 122 | python: pypy-5.4.1 123 | - os: linux 124 | dist: trusty 125 | python: pypy 126 | - os: linux 127 | dist: xenial 128 | python: pypy 129 | - os: linux 130 | dist: trusty 131 | python: pypy3 132 | - os: linux 133 | dist: xenial 134 | python: pypy3 135 | allow_failures: 136 | - os: linux 137 | dist: xenial 138 | python: 2.6 139 | - os: linux 140 | dist: xenial 141 | python: 3.2 142 | - os: linux 143 | dist: xenial 144 | python: 3.3 145 | - os: linux 146 | dist: trusty 147 | python: 3.7 148 | - os: linux 149 | dist: trusty 150 | python: 3.8 151 | - os: linux 152 | dist: trusty 153 | python: 3.8-dev 154 | - os: linux 155 | dist: trusty 156 | python: 3.9 157 | - os: linux 158 | dist: xenial 159 | python: 3.9 160 | - os: linux 161 | dist: trusty 162 | python: 3.9-dev 163 | - os: linux 164 | dist: xenial 165 | python: pypy-5.3.1 166 | - os: linux 167 | dist: xenial 168 | python: pypy-5.4.1 169 | - os: linux 170 | dist: xenial 171 | python: pypy 172 | - os: linux 173 | dist: xenial 174 | python: pypy3 175 | 176 | 177 | install: 178 | # From https://github.com/frol/flask-restplus-server-example/blob/master/.travis.yml 179 | # Travis has pypy 2.5.0, which is way too old, so we upgrade it on the fly: 180 | - | 181 | if ([ "$TRAVIS_PYTHON_VERSION" = "pypy-5.3.1" ] || 182 | [ "$TRAVIS_PYTHON_VERSION" = "pypy-5.4.1" ]); then 183 | export PYENV_ROOT="$HOME/.pyenv" 184 | if [ -f "$PYENV_ROOT/bin/pyenv" ]; then 185 | pushd "$PYENV_ROOT" && git pull && popd 186 | else 187 | rm -rf "$PYENV_ROOT" && git clone --depth 1 https://github.com/yyuu/pyenv.git "$PYENV_ROOT" 188 | fi 189 | case "$TRAVIS_PYTHON_VERSION" in 190 | "pypy-5.3.1") export PYPY_VERSION="5.3.1";; 191 | "pypy-5.4.1") export PYPY_VERSION="5.4.1";; 192 | esac 193 | "$PYENV_ROOT/bin/pyenv" install --skip-existing "pypy-$PYPY_VERSION" 194 | virtualenv --python="$PYENV_ROOT/versions/pypy-$PYPY_VERSION/bin/python" "$HOME/virtualenvs/pypy-$PYPY_VERSION" 195 | source "$HOME/virtualenvs/pypy-$PYPY_VERSION/bin/activate" 196 | fi 197 | - pip install --upgrade pycodestyle 198 | - pip install pep257 || true 199 | - pip install pydocstyle || true 200 | - | 201 | if ([ "$TRAVIS_PYTHON_VERSION" != "2.6" ]); then 202 | pip install --upgrade pyflakes 203 | fi 204 | - | 205 | if ([ "$TRAVIS_PYTHON_VERSION" != "nightly" ]); then 206 | pip install unittest2 207 | fi 208 | - | 209 | # https://bitbucket.org/ned/coveragepy/issues/407/coverage-failing-on-python-325-using 210 | if ([ "$TRAVIS_PYTHON_VERSION" != "3.2" ]); then 211 | pip install coverage 212 | else 213 | pip install coverage==3.7.1 214 | fi 215 | - | 216 | case "$TRAVIS_PYTHON_VERSION" in 217 | "2.6") ;; # coveralls not supported 218 | "pypy-5.3.1") ;; # coveralls not supported 219 | "pypy-5.4.1") ;; # coveralls installation fails 220 | "pypy") ;; # coveralls installation fails 221 | *) pip install coveralls;; 222 | esac 223 | 224 | before_script: 225 | # Information about Python version 226 | - echo $TRAVIS_PYTHON_VERSION 227 | - python -VV 228 | - python -c "import sys; print(sys.version_info)" || true 229 | - python -c "import sys; print(sys._git)" || true 230 | - pycodestyle --ignore=E501,E231,E203,W503,E126,E123,E223,E226 *.py */*.py 231 | - pycodestyle --select=E501,E231,E203,W503,E126,E123,E223,E226 *.py */*.py || true 232 | - pep257 *.py */*.py || true 233 | - pydocstyle *.py */*.py || true 234 | - pyflakes . || true 235 | 236 | script: 237 | # Coverage leads to a different behavior sometimes - so we run the tests 238 | # without and with coverage and failures are allowed on the second- #36 239 | # (Except for Python 2.6 because the direct run fails) 240 | - | 241 | if ([ "$TRAVIS_PYTHON_VERSION" == "2.6" ]); then 242 | : 243 | elif ([ "$TRAVIS_PYTHON_VERSION" == "nightly" ]); then 244 | python -m unittest discover --start-directory=didyoumean --pattern=*.py 245 | else 246 | python -m unittest2 discover --start-directory=didyoumean --pattern=*.py 247 | fi 248 | - | 249 | if ([ "$TRAVIS_PYTHON_VERSION" == "2.6" ]); then 250 | coverage run -m unittest2 discover --start-directory=didyoumean --pattern=*.py 251 | elif ([ "$TRAVIS_PYTHON_VERSION" == "nightly" ]); then 252 | coverage run -m unittest discover --start-directory=didyoumean --pattern=*.py || true 253 | else 254 | coverage run -m unittest2 discover --start-directory=didyoumean --pattern=*.py || true 255 | fi 256 | - python didyoumean/didyoumean_sugg_tests.py 257 | - python didyoumean/readme_examples.py 258 | - | 259 | if ([ "$TRAVIS_PYTHON_VERSION" != "pypy-5.4.1" ]); then 260 | pip install . 261 | else 262 | pip install . || true # May fail but let's try anyway (see Issue #45) 263 | fi 264 | 265 | after_success: 266 | - coveralls 267 | # Information about Python builtins/keywords 268 | # - python -c "import sys; import keyword; print({sys.version_info: {'kword': set(keyword.kwlist), 'builtins': set(dir(__builtins__))}})" || true 269 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Sylvain 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | DidYouMean-Python (aka BetterErrorMessages) 2 | =========================================== 3 | 4 | [![BetterErrorMessages on PyPI](https://badge.fury.io/py/BetterErrorMessages.svg)](https://badge.fury.io/py/BetterErrorMessages) 5 | 6 | [![Coverage Status](https://coveralls.io/repos/SylvainDe/DidYouMean-Python/badge.svg?branch=master)](https://coveralls.io/r/SylvainDe/DidYouMean-Python?branch=master) 7 | 8 | [![Code Climate](https://codeclimate.com/github/SylvainDe/DidYouMean-Python/badges/gpa.svg)](https://codeclimate.com/github/SylvainDe/DidYouMean-Python) 9 | [![Scrutinizer](https://scrutinizer-ci.com/g/SylvainDe/DidYouMean-Python/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/SylvainDe/DidYouMean-Python/?branch=master) 10 | [![Codacy Badge](https://www.codacy.com/project/badge/Grade/54bed0a6466b48ea973325cff2594376)](https://www.codacy.com/app/sylvain-desodt-github/DidYouMean-Python) 11 | 12 | Logic to have various kind of suggestions in case of errors (NameError, AttributeError, ImportError, TypeError, ValueError, SyntaxError, MemoryError, OverflowError, IOError, OSError). 13 | 14 | Inspired by "Did you mean" for Ruby ([Explanation](http://www.yukinishijima.net/2014/10/21/did-you-mean-experience-in-ruby.html), [Github Page](https://github.com/yuki24/did_you_mean)), this is a simple implementation for/in Python. I wanted to see if I could mess around and create something similar in Python and it seems to be possible. 15 | 16 | This project is not maintained anymore 17 | -------------------------------------- 18 | 19 | As of October 2021, this project is not maintained anymore. Indeed, improvements to the exception messages have been implemented in Python 3.8, 3.9, 3.10 and 3.11 making this project useless (which is a good thing). 20 | 21 | If you are interested in projects aiming to make exception messages better, please refer to the [list of projects below](https://github.com/SylvainDe/DidYouMean-Python#see-also-similar-projectsideas). In particular [friendly-traceback/friendly-traceback](https://github.com/friendly-traceback/friendly-traceback). 22 | 23 | I'll try to ensure that the test suite runs fine on all Python versions as it was useful in the past to catch issues in the Python project because things got released. 24 | 25 | Usage 26 | ----- 27 | 28 | Once the package is installed (see below), the logic adding suggestions can be invoked in different ways: 29 | 30 | * hook on `sys.excepthook` : just call `didyoumean_enablehook()` and you'll have the suggestions for any uncaught exception: 31 | 32 | ``` 33 | >>> abc = 3 34 | >>> abcd 35 | Traceback (most recent call last): 36 | File "", line 1, in 37 | NameError: name 'abcd' is not defined 38 | >>> didyoumean.didyoumean_api.didyoumean_enablehook() 39 | >>> abcd 40 | Traceback (most recent call last): 41 | File "", line 1, in 42 | NameError: name 'abcd' is not defined. Did you mean 'abc' (local)? 43 | ``` 44 | 45 | * post-mortem function `didyoumean_postmortem()` on the last uncaught exception during interactive sessions: 46 | 47 | ``` 48 | >>> abc = 3 49 | >>> abcd 50 | Traceback (most recent call last): 51 | File "", line 1, in 52 | NameError: name 'abcd' is not defined 53 | >>> didyoumean.didyoumean_api.didyoumean_postmortem() 54 | NameError("name 'abcd' is not defined. Did you mean 'abc' (local)?",) 55 | ``` 56 | 57 | * context manager `didyoumean_contextmanager()`: 58 | 59 | ``` 60 | >>> with didyoumean.didyoumean_api.didyoumean_contextmanager(): 61 | ... abcd 62 | ... 63 | Traceback (most recent call last): 64 | File "", line 2, in 65 | NameError: name 'abcd' is not defined. Did you mean 'abc' (local) 66 | ``` 67 | 68 | * decorator : just add the `@didyoumean` decorator before any function (the `main()` could be a good choice) and you'll have the suggestions for any exception happening through a call to that method. 69 | 70 | 71 | ``` 72 | >>> @didyoumean.didyoumean_api.didyoumean_decorator 73 | ... def foo(): return abcd 74 | ... 75 | >>> foo() 76 | Traceback (most recent call last): 77 | File "", line 2, in foo 78 | NameError: global name 'abcd' is not defined. Did you mean 'abc' (global)? 79 | ``` 80 | 81 | _The API does not look great and may be updated in the near future._ 82 | 83 | 84 | Example 85 | ------- 86 | 87 | _More examples can be found from the test file `didyoumean/didyoumean_sugg_tests.py`._ 88 | 89 | 90 | ### NameError 91 | 92 | ##### Fuzzy matches on existing names (local, builtin, keywords, modules, etc) 93 | 94 | ```python 95 | def my_func(foo, bar): 96 | return foob 97 | my_func(1, 2) 98 | #>>> Before: NameError("global name 'foob' is not defined",) 99 | #>>> After: NameError("global name 'foob' is not defined. Did you mean 'foo' (local)?",) 100 | ``` 101 | ```python 102 | leng([0]) 103 | #>>> Before: NameError("name 'leng' is not defined",) 104 | #>>> After: NameError("name 'leng' is not defined. Did you mean 'len' (builtin)?",) 105 | ``` 106 | ```python 107 | import math 108 | maths.pi 109 | #>>> Before: NameError("name 'maths' is not defined",) 110 | #>>> After: NameError("name 'maths' is not defined. Did you mean 'math' (local)?",) 111 | ``` 112 | ```python 113 | passs 114 | #>>> Before: NameError("name 'passs' is not defined",) 115 | #>>> After: NameError("name 'passs' is not defined. Did you mean 'pass' (keyword)?",) 116 | ``` 117 | ```python 118 | def my_func(): 119 | foo = 1 120 | foob +=1 121 | my_func() 122 | #>>> Before: UnboundLocalError("local variable 'foob' referenced before assignment",) 123 | #>>> After: UnboundLocalError("local variable 'foob' referenced before assignment. Did you mean 'foo' (local)?",) 124 | ``` 125 | ##### Checking if name is the attribute of a defined object 126 | 127 | ```python 128 | class Duck(): 129 | def __init__(self): 130 | quack() 131 | def quack(self): 132 | pass 133 | d = Duck() 134 | #>>> Before: NameError("global name 'quack' is not defined",) 135 | #>>> After: NameError("global name 'quack' is not defined. Did you mean 'self.quack'?",) 136 | ``` 137 | ```python 138 | import math 139 | pi 140 | #>>> Before: NameError("name 'pi' is not defined",) 141 | #>>> After: NameError("name 'pi' is not defined. Did you mean 'math.pi'?",) 142 | ``` 143 | ##### Looking for missing imports 144 | 145 | ```python 146 | string.ascii_lowercase 147 | #>>> Before: NameError("name 'string' is not defined",) 148 | #>>> After: NameError("name 'string' is not defined. Did you mean to import string first?",) 149 | ``` 150 | ##### Looking in missing imports 151 | 152 | ```python 153 | choice 154 | #>>> Before: NameError("name 'choice' is not defined",) 155 | #>>> After: NameError("name 'choice' is not defined. Did you mean 'choice' from random (not imported)?",) 156 | ``` 157 | ##### Special cases 158 | 159 | ```python 160 | assert j ** 2 == -1 161 | #>>> Before: NameError("name 'j' is not defined",) 162 | #>>> After: NameError("name 'j' is not defined. Did you mean '1j' (imaginary unit)?",) 163 | ``` 164 | ### AttributeError 165 | 166 | ##### Fuzzy matches on existing attributes 167 | 168 | ```python 169 | lst = [1, 2, 3] 170 | lst.appendh(4) 171 | #>>> Before: AttributeError("'list' object has no attribute 'appendh'",) 172 | #>>> After: AttributeError("'list' object has no attribute 'appendh'. Did you mean 'append'?",) 173 | ``` 174 | ```python 175 | import math 176 | math.pie 177 | #>>> Before: AttributeError("'module' object has no attribute 'pie'",) 178 | #>>> After: AttributeError("'module' object has no attribute 'pie'. Did you mean 'pi'?",) 179 | ``` 180 | ##### Trying to find method with similar meaning (hardcoded) 181 | 182 | ```python 183 | lst = [1, 2, 3] 184 | lst.add(4) 185 | #>>> Before: AttributeError("'list' object has no attribute 'add'",) 186 | #>>> After: AttributeError("'list' object has no attribute 'add'. Did you mean 'append'?",) 187 | ``` 188 | ```python 189 | lst = [1, 2, 3] 190 | lst.get(5, None) 191 | #>>> Before: AttributeError("'list' object has no attribute 'get'",) 192 | #>>> After: AttributeError("'list' object has no attribute 'get'. Did you mean 'obj[key]' with a len() check or try: except: KeyError or IndexError?",) 193 | ``` 194 | ##### Detection of mis-used builtins 195 | 196 | ```python 197 | lst = [1, 2, 3] 198 | lst.max() 199 | #>>> Before: AttributeError("'list' object has no attribute 'max'") 200 | #>>> After: AttributeError("'list' object has no attribute 'max'. Did you mean 'max(list)'?") 201 | ``` 202 | ##### Period used instead of comma 203 | 204 | ```python 205 | a, b = 1, 2 206 | max(a. b) 207 | #>>> Before: AttributeError("'int' object has no attribute 'b'") 208 | #>>> After: AttributeError("'int' object has no attribute 'b'. Did you mean to use a comma instead of a period?") 209 | ``` 210 | ### ImportError 211 | 212 | ##### Fuzzy matches on existing modules 213 | 214 | ```python 215 | from maths import pi 216 | #>>> Before: ImportError('No module named maths',) 217 | #>>> After: ImportError("No module named maths. Did you mean 'math'?",) 218 | ``` 219 | ##### Fuzzy matches on elements of the module 220 | 221 | ```python 222 | from math import pie 223 | #>>> Before: ImportError('cannot import name pie',) 224 | #>>> After: ImportError("cannot import name pie. Did you mean 'pi'?",) 225 | ``` 226 | ##### Looking for import from wrong module 227 | 228 | ```python 229 | from itertools import pi 230 | #>>> Before: ImportError('cannot import name pi',) 231 | #>>> After: ImportError("cannot import name pi. Did you mean 'from math import pi'?",) 232 | ``` 233 | ### TypeError 234 | 235 | ##### Fuzzy matches on keyword arguments 236 | 237 | ```python 238 | def my_func(abcde): 239 | pass 240 | my_func(abcdf=1) 241 | #>>> Before: TypeError("my_func() got an unexpected keyword argument 'abcdf'",) 242 | #>>> After: TypeError("my_func() got an unexpected keyword argument 'abcdf'. Did you mean 'abcde'?",) 243 | ``` 244 | ##### Confusion between brackets and parenthesis 245 | 246 | ```python 247 | lst = [1, 2, 3] 248 | lst(0) 249 | #>>> Before: TypeError("'list' object is not callable",) 250 | #>>> After: TypeError("'list' object is not callable. Did you mean 'list[value]'?",) 251 | ``` 252 | ```python 253 | def my_func(a): 254 | pass 255 | my_func[1] 256 | #>>> Before: TypeError("'function' object has no attribute '__getitem__'",) 257 | #>>> After: TypeError("'function' object has no attribute '__getitem__'. Did you mean 'function(value)'?",) 258 | ``` 259 | ### ValueError 260 | 261 | ##### Special cases 262 | 263 | ```python 264 | 'Foo{}'.format('bar') 265 | #>>> Before: ValueError('zero length field name in format',) 266 | #>>> After: ValueError('zero length field name in format. Did you mean {0}?',) 267 | ``` 268 | ```python 269 | import datetime 270 | datetime.datetime.strptime("%d %b %y", "30 Nov 00") 271 | #> Before: ValueError("time data '%d %b %y' does not match format '30 Nov 00'",) 272 | #> After: ValueError("time data '%d %b %y' does not match format '30 Nov 00'. Did you mean to swap value and format parameters?",) 273 | ``` 274 | 275 | ### SyntaxError 276 | 277 | ##### Fuzzy matches when importing from __future__ 278 | 279 | ```python 280 | from __future__ import divisio 281 | #>>> Before: SyntaxError('future feature divisio is not defined',) 282 | #>>> After: SyntaxError("future feature divisio is not defined. Did you mean 'division'?",) 283 | ``` 284 | ##### Various 285 | 286 | ```python 287 | return 288 | #>>> Before: SyntaxError("'return' outside function", ('', 1, 0, None)) 289 | #>>> After: SyntaxError("'return' outside function. Did you mean to indent it, 'sys.exit([arg])'?", ('', 1, 0, None)) 290 | ``` 291 | ### MemoryError 292 | 293 | ##### Search for a memory-efficient equivalent 294 | 295 | ```python 296 | range(99999999999) 297 | #>>> Before: MemoryError() 298 | #>>> After: MemoryError(". Did you mean 'xrange'?",) 299 | ``` 300 | ### OverflowError 301 | 302 | ##### Search for a memory-efficient equivalent 303 | 304 | ```python 305 | range(999999999999999) 306 | #>>> Before: OverflowError('range() result has too many items',) 307 | #>>> After: OverflowError("range() result has too many items. Did you mean 'xrange'?",) 308 | ``` 309 | ### OSError/IOError 310 | 311 | ##### Suggestion for tilde/variable expansions 312 | 313 | ```python 314 | os.listdir('~') 315 | #>>> Before: OSError(2, 'No such file or directory') 316 | #>>> After: OSError(2, "No such file or directory. Did you mean '/home/user' (calling os.path.expanduser)?") 317 | ``` 318 | ### RuntimeError 319 | 320 | ##### Suggestion to avoid reaching maximum recursion depth 321 | 322 | ```python 323 | global rec 324 | def rec(n): return rec(n-1) 325 | rec(0) 326 | #>>> Before: RuntimeError('maximum recursion depth exceeded',) 327 | #>>> After: RuntimeError('maximum recursion depth exceeded. Did you mean to avoid recursion (cf http://neopythonic.blogspot.fr/2009/04/tail-recursion-elimination.html), increase the limit with `sys.setrecursionlimit(limit)` (current value is 1000)?',) 328 | ``` 329 | 330 | Installation 331 | ------------ 332 | 333 | The package is available on [Pypi](https://pypi.python.org/pypi) as [BetterErrorMessages](https://pypi.python.org/pypi/BetterErrorMessages/). 334 | 335 | Installation can be done from the package index with `pip install BetterErrorMessages`. 336 | 337 | Installation from sources can be done just as easily: 338 | 339 | ``` 340 | git clone https://github.com/SylvainDe/DidYouMean-Python.git 341 | cd DidYouMean-Python 342 | git install . 343 | ``` 344 | 345 | 346 | 347 | Making things automatic in your interactive sessions 348 | ---------------------------------------------------- 349 | 350 | You can have the suggestions automatically in your interactive sessions by adding the following code in your [${PYTHONSTARTUP} file](https://docs.python.org/3.6/using/cmdline.html#envvar-PYTHONSTARTUP): 351 | 352 | ``` 353 | try: 354 | import didyoumean 355 | except ImportError: 356 | print("Did you mean to install BetterErrorMessages first (`pip install BetterErrorMessages`)") 357 | else: 358 | didyoumean.didyoumean_api.didyoumean_enablehook() 359 | ``` 360 | 361 | Implementation 362 | -------------- 363 | 364 | All external APIs (decorator, hook, etc) use the same logic behind the scene. It works in a pretty simple way : when an exception happens, we try to get the relevant information out of the error message and of the backtrace to find the most relevant suggestions. To filter the best suggestions out of everything in case of fuzzy match, I am currently using ```difflib```. 365 | 366 | 367 | See also (similar projects/ideas) 368 | --------------------------------- 369 | 370 | Projects: 371 | 372 | - ["Did you mean" for Ruby (yuki24/did_you_mean)](https://github.com/yuki24/did_you_mean): source for the idea. 373 | 374 | - [dutc/didyoumean](https://github.com/dutc/didyoumean) : a quite similar project developed in pretty much the same time. A few differences though : written in C, works only for AttributeError, etc. 375 | 376 | - [nvbn/TheF*ck](https://github.com/nvbn/thefuck) : Correct and execute your previous shell command. 377 | 378 | - [asweigart/PyDidYouMean](https://github.com/asweigart/pydidyoumean) : Improve "file/command not found" errors with suggestions. 379 | 380 | - [Qix-/better-exceptions](https://github.com/Qix-/better-exceptions) : Pretty and useful exceptions in Python 381 | 382 | - [danrobinson/tracestack](https://github.com/danrobinson/tracestack) : Search your Python error messages on the web. 383 | 384 | - [cfbolz/syntaxerrors](https://github.com/cfbolz/syntaxerrors) : Python parser that can recover from errors (also, cfbolz added many suggestions for errors to PyPy). 385 | 386 | - [friendly-traceback/friendly-traceback](https://github.com/friendly-traceback/friendly-traceback) : Replace standard traceback by something easier to understand. 387 | 388 | - [Did You Mean in Perl](http://perltricks.com/article/122/2014/10/31/Implementing-Did-You-Mean-in-Perl) 389 | 390 | - [Commit in iPython](https://github.com/ipython/ipython/pull/9073/files) to add suggestions in case of errors 391 | 392 | 393 | Discussions: 394 | 395 | - [PEP 473 : Adding structured data to built-in exceptions](http://legacy.python.org/dev/peps/pep-0473/). 396 | 397 | - Ideas from the Python Ideas mailing list : ["Improve error message when missing 'self' in method definition"](https://mail.python.org/pipermail/python-ideas/2016-October/042672.html), "Better error messages" [part 1](https://mail.python.org/pipermail/python-ideas/2016-November/043848.html) and [part 2](https://mail.python.org/pipermail/python-ideas/2016-December/043910.html) 398 | 399 | - In [Raymond Hettinger's PyconCA keynote](https://www.youtube.com/watch?v=-TdrFjDJn5E), the part about the `hint` builtin (at 14 minutes) looks a lot like `didyoumean_postmortem`. 400 | 401 | 402 | Contributing 403 | ------------ 404 | 405 | Feedback is welcome, feel free to : 406 | * send me an email for any question/advice/comment/criticism 407 | * open issues if something goes wrong (please provide at least the version of Python you are using). 408 | 409 | Also, pull-requests are welcome to : 410 | * fix issues 411 | * enhance the documentation 412 | * improve the code 413 | * bring awesomeness 414 | 415 | As for the technical details : 416 | 417 | * this is under MIT License : you can do anything you want as long as you provide attribution back to this project. 418 | * I try to follow [PEP 8](http://legacy.python.org/dev/peps/pep-0008/) and [PEP 257](https://www.python.org/dev/peps/pep-0257/) as much as possible. Compliancy is checked during continuous integration using the [pycodestyle](https://pypi.python.org/pypi/pycodestyle) and [pep257](https://pypi.python.org/pypi/pep257) checkers. 419 | * I try to have most of the code covered by unit tests. 420 | * I try to write the code in such a way that it works on all Python versions from 2.6 (included). 421 | -------------------------------------------------------------------------------- /RelatedPythonIssues.md: -------------------------------------------------------------------------------- 1 | 2 | Issue that I've been involved with as part of this particular project. 3 | 4 | 5 | | Python issue | Title | Issue type | Fix | 6 | |------------------------------------|------------------------------------------------------------------------------------------------------|--------------------------|----------------------------------------------| 7 | | https://bugs.python.org/issue29924 | Useless argument in call to PyErr_Format | Code quality | https://github.com/python/cpython/pull/854 | 8 | | https://bugs.python.org/issue29932 | Missing word ("be") in error message ("first argument must a type object") | Error message | https://github.com/python/cpython/pull/888 | 9 | | https://bugs.python.org/issue30592 | Bad error message 'bool()() takes no keyword arguments' | Error message/regression | https://github.com/python/cpython/pull/1996 | 10 | | https://bugs.python.org/issue30600 | Error message incorrect when index is called with keyword argument ("[].index(x=2)") | Argument clinic | https://github.com/python/cpython/pull/2051 | 11 | | https://bugs.python.org/issue30627 | Incorrect error message for a few functions called with keywod argument | Argument handling | https://github.com/python/cpython/pull/2115 | 12 | | https://bugs.python.org/issue35965 | Behavior for unittest.assertRaisesRegex differs depending on whether it is used as a context manager | Invalid issue | | 13 | | https://bugs.python.org/issue36026 | Different error message when sys.settrace is used | Error message | https://github.com/python/cpython/pull/11930 | 14 | | https://bugs.python.org/issue30878 | The staticmethod doesn't properly reject keyword arguments | Argument handling | https://github.com/python/cpython/pull/2635 | 15 | -------------------------------------------------------------------------------- /didyoumean/__init__.py: -------------------------------------------------------------------------------- 1 | """Empty file. Might grow in the future.""" 2 | import didyoumean_api 3 | -------------------------------------------------------------------------------- /didyoumean/didyoumean_api.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """APIs to add suggestions to exceptions.""" 3 | from didyoumean_internal import add_suggestions_to_exception 4 | import functools 5 | import sys 6 | 7 | 8 | def didyoumean_decorator(func): 9 | """Decorator to add suggestions to exceptions. 10 | 11 | To use it, decorate one of the functions called, for instance 'main()': 12 | @didyoumean_decorator 13 | def main(): 14 | some_code 15 | """ 16 | @functools.wraps(func) 17 | def decorated(*args, **kwargs): 18 | """Function returned by the decorator.""" 19 | try: 20 | return func(*args, **kwargs) 21 | except Exception: 22 | type_, value, traceback = sys.exc_info() 23 | add_suggestions_to_exception(type_, value, traceback) 24 | raise 25 | return decorated 26 | 27 | 28 | def didyoumean_postmortem(): 29 | """Post postem function to add suggestions to last exception thrown. 30 | 31 | Add suggestions to last exception thrown (in interactive mode) and 32 | return it (which should print it). 33 | """ 34 | if hasattr(sys, 'last_type'): 35 | typ, val, trace = sys.last_type, sys.last_value, sys.last_traceback 36 | add_suggestions_to_exception(typ, val, trace) 37 | return val 38 | return None 39 | 40 | 41 | class didyoumean_contextmanager(object): 42 | """Context manager to add suggestions to exceptions. 43 | 44 | To use it, create a context: 45 | with didyoumean_contextmanager(): 46 | some_code. 47 | """ 48 | 49 | def __enter__(self): 50 | """Method called when entering the context manager. 51 | 52 | Not relevant here (does not do anything). 53 | """ 54 | pass 55 | 56 | def __exit__(self, type_, value, traceback): 57 | """Method called when exiting the context manager. 58 | 59 | Add suggestions to the exception (if any). 60 | """ 61 | assert (type_ is None) == (value is None) 62 | if value is not None: 63 | if isinstance(value, type_): 64 | # Error is not re-raised as it is the caller's responsability 65 | # but the error is enhanced nonetheless 66 | add_suggestions_to_exception(type_, value, traceback) 67 | else: 68 | # Python 2.6 bug : http://bugs.python.org/issue7853 69 | # Instead of having the exception, we have its representation 70 | # We can try to rebuild the exception, add suggestions to it 71 | # and re-raise it (re-raise shouldn't be done normally but it 72 | # is a dirty work-around for a dirty issue). 73 | if isinstance(value, str): 74 | value = type_(value) 75 | else: 76 | value = type_(*value) 77 | add_suggestions_to_exception(type_, value, traceback) 78 | raise value 79 | 80 | 81 | def didyoumean_hook(type_, value, traceback, prev_hook=sys.excepthook): 82 | """Hook to be substituted to sys.excepthook to enhance exceptions.""" 83 | add_suggestions_to_exception(type_, value, traceback) 84 | return prev_hook(type_, value, traceback) 85 | 86 | 87 | def didyoumean_custom_exc(shell, etype, evalue, tb, tb_offset=None): 88 | """Custom exception handler to replace the iPython one.""" 89 | add_suggestions_to_exception(etype, evalue, tb) 90 | return shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) 91 | 92 | 93 | def set_ipython_custom_exc(func): 94 | """Try to set the custom exception handler for iPython.""" 95 | # https://mail.scipy.org/pipermail/ipython-dev/2012-April/008945.html 96 | # http://stackoverflow.com/questions/1261668/cannot-override-sys-excepthook 97 | try: 98 | get_ipython().set_custom_exc((Exception,), func) 99 | except NameError: 100 | pass # get_ipython does not exist - ignore 101 | 102 | 103 | def didyoumean_enablehook(): 104 | """Function to set hooks to their custom value.""" 105 | sys.excepthook = didyoumean_hook 106 | set_ipython_custom_exc(didyoumean_custom_exc) 107 | 108 | 109 | def didyoumean_disablehook(): 110 | """Function to set hooks to their normal value.""" 111 | sys.excepthook = sys.__excepthook__ 112 | set_ipython_custom_exc(None) 113 | 114 | # NOTE: It could be funny to have a magic command in Python 115 | # https://ipython.org/ipython-doc/dev/config/custommagics.html 116 | -------------------------------------------------------------------------------- /didyoumean/didyoumean_api_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """Unit tests for didyoumean APIs.""" 3 | from didyoumean_api import didyoumean_decorator, didyoumean_contextmanager,\ 4 | didyoumean_postmortem, didyoumean_enablehook, didyoumean_disablehook 5 | from didyoumean_common_tests import TestWithStringFunction,\ 6 | get_exception, no_exception, NoFileIoError, unittest_module 7 | import contextlib 8 | import sys 9 | import os 10 | 11 | 12 | class ApiTest(TestWithStringFunction): 13 | """Tests about the didyoumean APIs. 14 | 15 | The aim is to test the different APIs in various situations: 16 | - when no exception is raised 17 | - when a NameError leading to no suggestion is raised 18 | - when a NameError leading to a suggestion is raised 19 | - when a SyntaxError leading to a suggestion is raised 20 | - when a NoFileIoError leading to a suggestion is raised 21 | In all these situations, the exception with and without using 22 | the API are retrieved and the test checks that the suggestion 23 | is added (when relevant) to the exceptions in both the __str__ 24 | form and the __repr__ form. 25 | 26 | In order to do so, one just needs to inherit from ApiTest and 27 | override `run_with_api`. 28 | """ 29 | 30 | def run_with_api(self, code): 31 | """Abstract method to run code with tested API.""" 32 | raise NotImplementedError("'run_with_api' needs to be implemented") 33 | 34 | def get_exc_with_api(self, code): 35 | """Get exception raised with running code with tested API.""" 36 | try: 37 | self.run_with_api(code) 38 | except Exception: 39 | return sys.exc_info() 40 | assert False, "No exception thrown" 41 | 42 | def get_exc_as_str(self, code, type_arg): 43 | """Retrieve string representations of exceptions raised by code. 44 | 45 | String representations are provided for the same code run 46 | with and without the API. 47 | """ 48 | type1, value1, _ = get_exception(code) 49 | details1 = "%s %s" % (str(type1), str(value1)) 50 | self.assertTrue(isinstance(value1, type1), details1) 51 | self.assertEqual(type_arg, type1, details1) 52 | str1, repr1 = str(value1), repr(value1) 53 | type2, value2, _ = self.get_exc_with_api(code) 54 | details2 = "%s %s" % (str(type2), str(value2)) 55 | self.assertTrue(isinstance(value2, type2), details2) 56 | self.assertEqual(type_arg, type2, details2) 57 | str2, repr2 = str(value2), repr(value2) 58 | return (str1, repr1, str2, repr2) 59 | 60 | def check_sugg_added(self, code, type_, sugg, normalise_quotes=False): 61 | """Check that the suggestion gets added to the exception. 62 | 63 | Get the string representations for the exception before and after 64 | and check that the suggestion `sugg` is added to `before` to get 65 | `after`. `normalise_quotes` can be provided to replace all quotes 66 | by double quotes before checking the `repr()` representations as 67 | they may get changed sometimes. 68 | """ 69 | str1, repr1, str2, repr2 = self.get_exc_as_str( 70 | code, type_) 71 | self.assertStringAdded(sugg, str1, str2, True) 72 | if normalise_quotes: 73 | sugg = sugg.replace("'", '"') 74 | repr1 = repr1.replace("'", '"') 75 | repr2 = repr2.replace("'", '"') 76 | self.assertStringAdded(sugg, repr1, repr2, True) 77 | 78 | def test_api_no_exception(self): 79 | """Check the case with no exception.""" 80 | code = 'babar = 0\nbabar' 81 | no_exception(code) 82 | self.run_with_api(code) 83 | 84 | def test_api_suggestion(self): 85 | """Check the case with a suggestion.""" 86 | type_ = NameError 87 | sugg = ". Did you mean 'babar' (local)?" 88 | code = 'babar = 0\nbaba' 89 | self.check_sugg_added(code, type_, sugg) 90 | 91 | def test_api_no_suggestion(self): 92 | """Check the case with no suggestion.""" 93 | type_ = NameError 94 | sugg = "" 95 | code = 'babar = 0\nfdjhflsdsqfjlkqs' 96 | self.check_sugg_added(code, type_, sugg) 97 | 98 | def test_api_syntax(self): 99 | """Check the case with syntax error suggestion.""" 100 | type_ = SyntaxError 101 | sugg = ". Did you mean to indent it, 'sys.exit([arg])'?" 102 | code = 'return' 103 | self.check_sugg_added(code, type_, sugg, True) 104 | 105 | def test_api_ioerror(self): 106 | """Check the case with IO error suggestion.""" 107 | type_ = NoFileIoError 108 | home = os.path.expanduser("~") 109 | sugg = ". Did you mean '" + home + "' (calling os.path.expanduser)?" 110 | code = 'with open("~") as f:\n\tpass' 111 | self.check_sugg_added(code, type_, sugg, True) 112 | 113 | 114 | class DecoratorTest(unittest_module.TestCase, ApiTest): 115 | """Tests about the didyoumean decorator.""" 116 | 117 | def run_with_api(self, code): 118 | """Run code with didyoumean decorator.""" 119 | @didyoumean_decorator 120 | def my_func(): 121 | no_exception(code) 122 | my_func() 123 | 124 | 125 | class ContextManagerTest(unittest_module.TestCase, ApiTest): 126 | """Tests about the didyoumean context manager.""" 127 | 128 | def run_with_api(self, code): 129 | """Run code with didyoumean context manager.""" 130 | with didyoumean_contextmanager(): 131 | no_exception(code) 132 | 133 | 134 | class PostMortemTest(unittest_module.TestCase, ApiTest): 135 | """Tests about the didyoumean post mortem.""" 136 | 137 | # A bit of an ugly way to proceed with "exc.last_" attributes: 138 | # in real life scenario, these would not be set except in case of 139 | # uncaught exception in interactive interpreter where they would be 140 | # set automatically. 141 | 142 | def check_sys_last_attr_not_set(self): 143 | """Check that attributes 'last_' do not exist.""" 144 | for a in ('last_type', 'last_value', 'last_traceback'): 145 | self.assertFalse(hasattr(sys, a)) 146 | 147 | def set_sys_last_attr_from_exc(self, code): 148 | """Set attributes 'last_' from exception thrown by code if any.""" 149 | try: 150 | no_exception(code) 151 | except Exception: 152 | sys.last_type, sys.last_value, sys.last_traceback = sys.exc_info() 153 | 154 | def cleanup_sys_last_attr(self): 155 | """Delete attributes 'last_' artificially added.""" 156 | for a in ('last_type', 'last_value', 'last_traceback'): 157 | if hasattr(sys, a): 158 | delattr(sys, a) 159 | 160 | def run_with_api(self, code): 161 | """Run code with didyoumean post mortem.""" 162 | self.check_sys_last_attr_not_set() 163 | self.set_sys_last_attr_from_exc(code) 164 | ret = didyoumean_postmortem() 165 | self.cleanup_sys_last_attr() 166 | self.check_sys_last_attr_not_set() 167 | if ret is not None: 168 | raise ret 169 | 170 | 171 | class HookTest(ApiTest): 172 | """Tests about the didyoumean hooks. 173 | 174 | These tests are somewhat artificial as one needs to explicitely catch 175 | the exception, simulate a call to the function that would have been 176 | called for an uncatched exception and reraise it (so that then it gets 177 | caught by yet another try-except). 178 | Realistically it might not catch any real-life problems (because these 179 | would happen when the shell does not behave as expected) but it might be 180 | useful to prevent regressions. 181 | """ 182 | 183 | pass # Can't write tests as the hook seems to be ignored. 184 | 185 | 186 | @contextlib.contextmanager 187 | def suppress_stderr(): 188 | """Decorator to ignore content sent to stderr.""" 189 | with open(os.devnull, "w") as devnull: 190 | old_stderr = sys.stderr 191 | sys.stderr = devnull 192 | try: 193 | yield 194 | finally: 195 | sys.stderr = old_stderr 196 | 197 | 198 | class ExceptHookTest(unittest_module.TestCase, HookTest): 199 | """Tests about the didyoumean excepthook.""" 200 | 201 | def run_with_api(self, code): 202 | """Run code with didyoumean after enabling didyoumean hook.""" 203 | prev_hook = sys.excepthook 204 | self.assertEqual(prev_hook, sys.excepthook) 205 | didyoumean_enablehook() 206 | self.assertNotEqual(prev_hook, sys.excepthook) 207 | try: 208 | no_exception(code) 209 | except Exception: 210 | last_type, last_value, last_traceback = sys.exc_info() 211 | with suppress_stderr(): 212 | sys.excepthook(last_type, last_value, last_traceback) 213 | raise 214 | finally: 215 | self.assertNotEqual(prev_hook, sys.excepthook) 216 | didyoumean_disablehook() 217 | self.assertEqual(prev_hook, sys.excepthook) 218 | 219 | 220 | class DummyShell: 221 | """Dummy class to emulate the iPython interactive shell. 222 | 223 | https://ipython.org/ipython-doc/dev/api/generated/IPython.core.interactiveshell.html 224 | """ 225 | 226 | def __init__(self): 227 | """Init.""" 228 | self.handler = None 229 | self.exc_tuple = None 230 | 231 | def set_custom_exc(self, exc_tuple, handler): 232 | """Emulate the interactiveshell.set_custom_exc method.""" 233 | self.handler = handler 234 | self.exc_tuple = exc_tuple 235 | 236 | def showtraceback(self, exc_tuple=None, 237 | filename=None, tb_offset=None, exception_only=False): 238 | """Emulate the interactiveshell.showtraceback method. 239 | 240 | Calls the custom exception handler if is it set. 241 | """ 242 | if self.handler is not None and self.exc_tuple is not None: 243 | etype, evalue, tb = exc_tuple 244 | func, self.handler = self.handler, None # prevent recursive calls 245 | func(self, etype, evalue, tb, tb_offset) 246 | self.handler = func 247 | 248 | def set(self, module): 249 | """Make shell accessible in module via 'get_ipython'.""" 250 | assert 'get_ipython' not in dir(module) 251 | module.get_ipython = lambda: self 252 | 253 | def remove(self, module): 254 | """Make shell un-accessible in module via 'get_ipython'.""" 255 | del module.get_ipython 256 | 257 | 258 | class IPythonHookTest(unittest_module.TestCase, HookTest): 259 | """Tests about the didyoumean custom exception handler for iPython. 260 | 261 | These tests need a dummy shell to be create to be able to use/define 262 | its functions related to the custom exception handlers. 263 | """ 264 | 265 | def run_with_api(self, code): 266 | """Run code with didyoumean after enabling didyoumean hook.""" 267 | prev_handler = None 268 | shell = DummyShell() 269 | module = sys.modules['didyoumean_api'] 270 | shell.set(module) 271 | self.assertEqual(shell.handler, prev_handler) 272 | didyoumean_enablehook() 273 | self.assertNotEqual(shell.handler, prev_handler) 274 | try: 275 | no_exception(code) 276 | except Exception: 277 | shell.showtraceback(sys.exc_info()) 278 | raise 279 | finally: 280 | self.assertNotEqual(shell.handler, prev_handler) 281 | didyoumean_disablehook() 282 | self.assertEqual(shell.handler, prev_handler) 283 | shell.remove(module) 284 | shell = None 285 | 286 | 287 | if __name__ == '__main__': 288 | print(sys.version_info) 289 | unittest_module.main() 290 | -------------------------------------------------------------------------------- /didyoumean/didyoumean_common_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """Common logic for unit tests.""" 3 | import sys 4 | 5 | try: 6 | import unittest2 7 | unittest_module = unittest2 8 | except ImportError: 9 | import unittest 10 | unittest_module = unittest 11 | except AttributeError: 12 | import unittest 13 | unittest_module = unittest 14 | 15 | # Tests based on MemoryError may required some tweaking to run depending 16 | # on both the hardware and the software used. This flag can be used to 17 | # disable the corresponding tests easily. 18 | SKIP_MEMORY_ERROR_TESTS = False 19 | 20 | old_errors = (IOError, OSError) 21 | 22 | try: 23 | NoFileIoError = NoFileOsError = FileNotFoundError 24 | except NameError: 25 | NoFileIoError, NoFileOsError = old_errors 26 | 27 | try: 28 | IsDirIoError = IsDirOsError = IsADirectoryError 29 | except NameError: 30 | IsDirIoError, IsDirOsError = old_errors 31 | 32 | 33 | try: 34 | NotDirIoError = NotDirOsError = NotADirectoryError 35 | except NameError: 36 | NotDirIoError, NotDirOsError = old_errors 37 | 38 | 39 | def no_exception(code): 40 | """Helper function to run code and check it works.""" 41 | exec(code) 42 | 43 | 44 | def get_exception(code): 45 | """Helper function to run code and get what it throws.""" 46 | try: 47 | no_exception(code) 48 | except Exception: 49 | return sys.exc_info() 50 | return None 51 | 52 | 53 | class CommonTestOldStyleClass: 54 | """Dummy class for testing purposes.""" 55 | 56 | pass 57 | 58 | 59 | class CommonTestOldStyleClass2: 60 | """Dummy class for testing purposes.""" 61 | 62 | pass 63 | 64 | 65 | class CommonTestNewStyleClass(object): 66 | """Dummy class for testing purposes.""" 67 | 68 | pass 69 | 70 | 71 | class CommonTestNewStyleClass2(object): 72 | """Dummy class for testing purposes.""" 73 | 74 | pass 75 | 76 | 77 | class TestWithStringFunction(object): 78 | """Unit test class with an helper method.""" 79 | 80 | def assertIn(self, first, second): 81 | """Check that `first` argument is in `second`. 82 | 83 | Just like self.assertTrue(a in b), but with a nicer default message. 84 | This is part of standard library but only from Python 2.7. 85 | """ 86 | msg = '"%s" not found in "%s"' % (first, second) 87 | self.assertTrue(first in second, msg) 88 | 89 | def assertNotIn(self, first, second): 90 | """Check that `first` argument is NOT in `second`. 91 | 92 | Just like self.assertFalse(a in b), but with a nicer default message. 93 | This is part of standard library but only from Python 2.7. 94 | """ 95 | msg = '"%s" unexpectedly found in "%s"' % (first, second) 96 | self.assertFalse(first in second, msg) 97 | 98 | def assertStringAdded(self, string, before, after, check_str_sum): 99 | """Check that `string` has been added to `before` to get `after`. 100 | 101 | If the `check_str_sum` argument is True, we check that adding `string` 102 | somewhere in the `before` string gives the `after` string. If the 103 | argument is false, we just check that `string` can be found in `after` 104 | but not in `before`. 105 | """ 106 | if string: 107 | self.assertNotEqual(before, after) 108 | self.assertNotIn(string, before) 109 | self.assertIn(string, after) 110 | # Removing string and checking that we get the original string 111 | begin, mid, end = after.partition(string) 112 | self.assertEqual(mid, string) 113 | if check_str_sum: 114 | self.assertEqual(begin + end, before) 115 | else: 116 | self.assertEqual(before, after) 117 | 118 | 119 | if __name__ == '__main__': 120 | print(sys.version_info) 121 | -------------------------------------------------------------------------------- /didyoumean/didyoumean_internal.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """Logic to add suggestions to exceptions.""" 3 | import keyword 4 | import difflib 5 | import didyoumean_re as re 6 | import itertools 7 | import inspect 8 | import errno 9 | import os 10 | import sys 11 | from collections import namedtuple 12 | 13 | 14 | #: Standard modules we'll consider while searching for symbols, for instance: 15 | # - NameError and the name is an attribute of a std (imported or not) module 16 | # - NameError and the name is the name of a standard (non imported) module 17 | # - ImportError and the name looks like a standard (imported or not) module 18 | # - TODO: AttributeError and the attribute is the one of a module 19 | # Not that in the first case, the modules must be considered safe to import 20 | # (no side-effects) but in some other cases, we only care about the names 21 | # of the module and a more extended list could be used. 22 | # The list is to be completed 23 | # Potential candidates : 24 | # - sys.builtin_module_names 25 | # https://docs.python.org/2/library/sys.html#sys.builtin_module_names 26 | # - sys.modules 27 | # https://docs.python.org/2/library/sys.html#sys.modules 28 | # - pkgutil.iter_modules 29 | # https://docs.python.org/2/library/pkgutil.html#pkgutil.iter_modules 30 | STAND_MODULES = set(['string', 'os', 'sys', 're', 'math', 'random', 31 | 'datetime', 'timeit', 'unittest', 'itertools', 32 | 'functools', 'collections', '__future__']) 33 | 34 | #: Almost synonyms methods that can be confused from one type to another 35 | # To be completed 36 | SYNONYMS_SETS = [ 37 | set(['add', 'append', 'push']), 38 | set(['extend', 'update']), 39 | set(['remove', 'discard', '__delitem__']) 40 | ] 41 | 42 | #: Maximum number of files suggested 43 | MAX_NB_FILES = 4 44 | 45 | #: Message to suggest not using recursion 46 | AVOID_REC_MSG = \ 47 | "to avoid recursion (cf " \ 48 | "http://neopythonic.blogspot.fr/2009/04/tail-recursion-elimination.html)" 49 | #: Messages for functions removed from one version to another 50 | APPLY_REMOVED_MSG = "to call the function directly (`apply` is deprecated " \ 51 | "since Python 2.3, removed since Python 3)" 52 | BUFFER_REMOVED_MSG = '"memoryview" (`buffer` has been removed " \ 53 | "since Python 3)' 54 | CMP_REMOVED_MSG = "to use comparison operators (`cmp` is removed since " \ 55 | "Python 3 but you can define `def cmp(a, b): return (a > b) - (a < b)` " \ 56 | "if needed)" 57 | CMP_ARG_REMOVED_MSG = 'to use "key" (`cmp` has been replaced by `key` ' \ 58 | "since Python 3 - `functools.cmp_to_key` provides a convenient way " \ 59 | "to convert cmp function to key function)" 60 | EXC_ATTR_REMOVED_MSG = 'to use "sys.exc_info()" returning a tuple ' \ 61 | 'of the form (type, value, traceback) ("exc_type", "exc_value" and ' \ 62 | '"exc_traceback" are removed from sys since Python 3)' 63 | LONG_REMOVED_MSG = 'to use "int" (since Python 3, there is only one ' \ 64 | 'integer type: `int`)' 65 | MEMVIEW_ADDED_MSG = '"buffer" (`memoryview` is added in Python 2.7 and " \ 66 | "completely replaces `buffer` since Python 3)' 67 | RELOAD_REMOVED_MSG = '"importlib.reload" or (`reload` is removed " \ 68 | "since Python 3)' 69 | STDERR_REMOVED_MSG = '"Exception" (`StandardError` has been removed since " \ 70 | "Python 3)' 71 | BREAKPOINT_ADDED_MSG = 'to use "import pdb; pdb.set_trace()" (`breakpoint` " \ 72 | "is added in Python 3.7)' 73 | NO_KEYWORD_ARG_MSG = "use positional arguments (functions written in C \ 74 | do not accept keyword arguments, only positional arguments)" 75 | #: Message to suggest using comma instead of period 76 | COMMA_INSTEAD_OF_PERIOD_MSG = "to use a comma instead of a period" 77 | 78 | 79 | # Helper function for string manipulation 80 | def quote(string): 81 | """Surround string with single quotes.""" 82 | return "'{0}'".format(string) 83 | 84 | 85 | def get_close_matches(word, possibilities): 86 | """ 87 | Return a list of the best "good enough" matches. 88 | 89 | Wrapper around difflib.get_close_matches() to be able to 90 | change default values or implementation details easily. 91 | """ 92 | return [w 93 | for w in difflib.get_close_matches(word, possibilities, 3, 0.7) 94 | if w != word] 95 | 96 | 97 | def get_suggestion_string(sugg): 98 | """Return the suggestion list as a string.""" 99 | sugg = list(sugg) 100 | return ". Did you mean " + ", ".join(sugg) + "?" if sugg else "" 101 | 102 | 103 | # Helper functions for code introspection 104 | def subclasses_wrapper(klass): 105 | """Wrapper around __subclass__ as it is not as easy as it should.""" 106 | method = getattr(klass, '__subclasses__', None) 107 | if method is None: 108 | return [] 109 | try: 110 | return method() 111 | except TypeError: 112 | try: 113 | return method(klass) 114 | except TypeError: 115 | return [] 116 | 117 | 118 | def get_subclasses(klass): 119 | """Get the subclasses of a class. 120 | 121 | Get the set of direct/indirect subclasses of a class including itself. 122 | """ 123 | subclasses = set(subclasses_wrapper(klass)) 124 | for derived in set(subclasses): 125 | subclasses.update(get_subclasses(derived)) 126 | subclasses.add(klass) 127 | return subclasses 128 | 129 | 130 | def get_types_for_str_using_inheritance(name): 131 | """Get types corresponding to a string name. 132 | 133 | This goes through all defined classes. Therefore, it : 134 | - does not include old style classes on Python 2.x 135 | - is to be called as late as possible to ensure wanted type is defined. 136 | """ 137 | return set(c for c in get_subclasses(object) if c.__name__ == name) 138 | 139 | 140 | def get_types_for_str_using_names(name, frame): 141 | """Get types corresponding to a string name using names in frame. 142 | 143 | This does not find everything as builtin types for instance may not 144 | be in the names. 145 | """ 146 | return set(obj 147 | for obj, _ in get_objects_in_frame(frame).get(name, []) 148 | if inspect.isclass(obj) and obj.__name__ == name) 149 | 150 | 151 | def get_types_for_str(tp_name, frame): 152 | """Get a list of candidate types from a string. 153 | 154 | String corresponds to the tp_name as described in : 155 | https://docs.python.org/2/c-api/typeobj.html#c.PyTypeObject.tp_name 156 | as it is the name used in exception messages. It may include full path 157 | with module, subpackage, package but this is just removed in current 158 | implementation to search only based on the type name. 159 | 160 | Lookup uses both class hierarchy and name lookup as the first may miss 161 | old style classes on Python 2 and second does find them. 162 | Just like get_types_for_str_using_inheritance, this needs to be called 163 | as late as possible but because it requires a frame, there is not much 164 | choice anyway. 165 | """ 166 | name = tp_name.split('.')[-1] 167 | res = set.union( 168 | get_types_for_str_using_inheritance(name), 169 | get_types_for_str_using_names(name, frame)) 170 | assert all(inspect.isclass(t) and t.__name__ == name for t in res) 171 | return res 172 | 173 | 174 | def merge_dict(*dicts): 175 | """Merge dicts and return a dictionary mapping key to list of values. 176 | 177 | Order of the values corresponds to the order of the original dicts. 178 | """ 179 | ret = dict() 180 | for dict_ in dicts: 181 | for key, val in dict_.items(): 182 | ret.setdefault(key, []).append(val) 183 | return ret 184 | 185 | 186 | ScopedObj = namedtuple('ScopedObj', 'obj scope') 187 | 188 | 189 | def add_scope_to_dict(dict_, scope): 190 | """Convert name:obj dict to name:ScopedObj(obj,scope) dict.""" 191 | return dict((k, ScopedObj(v, scope)) for k, v in dict_.items()) 192 | 193 | 194 | def get_objects_in_frame(frame): 195 | """Get objects defined in a given frame. 196 | 197 | This includes variable, types, builtins, etc. 198 | The function returns a dictionary mapping names to a (non empty) 199 | list of ScopedObj objects in the order following the LEGB Rule. 200 | """ 201 | # https://www.python.org/dev/peps/pep-0227/ PEP227 Statically Nested Scopes 202 | # "Under this proposal, it will not be possible to gain dictionary-style 203 | # access to all visible scopes." 204 | # https://www.python.org/dev/peps/pep-3104/ PEP 3104 Access to Names in 205 | # Outer Scopes 206 | # LEGB Rule : missing E (enclosing) at the moment. 207 | # I'm not sure if it can be fixed but if it can, suggestions 208 | # tagged TODO_ENCLOSING could be implemented (and tested). 209 | return merge_dict( 210 | add_scope_to_dict(frame.f_locals, 'local'), 211 | add_scope_to_dict(frame.f_globals, 'global'), 212 | add_scope_to_dict(frame.f_builtins, 'builtin'), 213 | ) 214 | 215 | 216 | def import_from_frame(module_name, frame): 217 | """Wrapper around import to use information from frame.""" 218 | if frame is None: 219 | return None 220 | return __import__( 221 | module_name, 222 | frame.f_globals, 223 | frame.f_locals) 224 | 225 | 226 | # To be used in `get_suggestions_for_exception`. 227 | SUGGESTION_FUNCTIONS = dict() 228 | 229 | 230 | def register_suggestion_for(error_type, regex): 231 | """Decorator to register a function to be called to get suggestions. 232 | 233 | Parameters correspond to the fact that the registration is done for a 234 | specific error type and if the error message matches a given regex 235 | (if the regex is None, the error message is assumed to match before being 236 | retrieved). 237 | 238 | The decorated function is expected to yield any number (0 included) of 239 | suggestions (as string). 240 | The parameters are: (value, frame, groups): 241 | - value: Exception object 242 | - frame: Last frame of the traceback (may be None when the traceback is 243 | None which happens only in edge cases) 244 | - groups: Groups from the error message matched by the error message. 245 | """ 246 | def internal_decorator(func): 247 | def registered_function(value, frame): 248 | if regex is None: 249 | return func(value, frame, []) 250 | error_msg = value.args[0] 251 | match = re.match(regex, error_msg) 252 | if match: 253 | return func(value, frame, match.groups()) 254 | return [] 255 | SUGGESTION_FUNCTIONS.setdefault(error_type, []) \ 256 | .append(registered_function) 257 | return func # return original function 258 | return internal_decorator 259 | 260 | 261 | # Functions related to NameError 262 | @register_suggestion_for(NameError, re.VARREFBEFOREASSIGN_RE) 263 | @register_suggestion_for(NameError, re.NAMENOTDEFINED_RE) 264 | def suggest_name_not_defined(value, frame, groups): 265 | """Get the suggestions for name in case of NameError.""" 266 | del value # unused param 267 | name, = groups 268 | objs = get_objects_in_frame(frame) 269 | return itertools.chain( 270 | suggest_name_as_attribute(name, objs), 271 | suggest_name_as_standard_module(name), 272 | suggest_name_as_name_typo(name, objs), 273 | suggest_name_as_keyword_typo(name), 274 | suggest_name_as_missing_import(name, objs, frame), 275 | suggest_name_as_special_case(name)) 276 | 277 | 278 | def suggest_name_as_attribute(name, objdict): 279 | """Suggest that name could be an attribute of an object. 280 | 281 | Example: 'do_stuff()' -> 'self.do_stuff()'. 282 | """ 283 | for nameobj, objs in objdict.items(): 284 | prev_scope = None 285 | for obj, scope in objs: 286 | if hasattr(obj, name): 287 | yield quote(nameobj + '.' + name) + \ 288 | ('' if prev_scope is None else 289 | ' ({0} hidden by {1})'.format(scope, prev_scope)) 290 | break 291 | prev_scope = scope 292 | 293 | 294 | def suggest_name_as_missing_import(name, objdict, frame): 295 | """Suggest that name could come from missing import. 296 | 297 | Example: 'foo' -> 'import mod, mod.foo'. 298 | """ 299 | for mod in STAND_MODULES: 300 | if mod not in objdict and name in dir(import_from_frame(mod, frame)): 301 | yield "'{0}' from {1} (not imported)".format(name, mod) 302 | 303 | 304 | def suggest_name_as_standard_module(name): 305 | """Suggest that name could be a non-imported standard module. 306 | 307 | Example: 'os.whatever' -> 'import os' and then 'os.whatever'. 308 | """ 309 | if name in STAND_MODULES: 310 | yield 'to import {0} first'.format(name) 311 | 312 | 313 | def suggest_name_as_name_typo(name, objdict): 314 | """Suggest that name could be a typo (misspelled existing name). 315 | 316 | Example: 'foobaf' -> 'foobar'. 317 | """ 318 | for name in get_close_matches(name, objdict.keys()): 319 | yield quote(name) + ' (' + objdict[name][0].scope + ')' 320 | 321 | 322 | def suggest_name_as_keyword_typo(name): 323 | """Suggest that name could be a typo (misspelled keyword). 324 | 325 | Example: 'yieldd' -> 'yield'. 326 | """ 327 | for name in get_close_matches(name, keyword.kwlist): 328 | yield quote(name) + " (keyword)" 329 | 330 | 331 | def suggest_name_as_special_case(name): 332 | """Suggest that name could be handled in a special way.""" 333 | special_cases = { 334 | # Imaginary unit is '1j' in Python 335 | 'i': quote('1j') + " (imaginary unit)", 336 | 'j': quote('1j') + " (imaginary unit)", 337 | # Shell commands entered in interpreter 338 | 'pwd': quote('os.getcwd()'), 339 | 'ls': quote('os.listdir(os.getcwd())'), 340 | 'cd': quote('os.chdir(path)'), 341 | 'rm': "'os.remove(filename)', 'shutil.rmtree(dir)' for recursive", 342 | # Function removed from Python 343 | 'apply': APPLY_REMOVED_MSG, 344 | 'buffer': BUFFER_REMOVED_MSG, 345 | 'cmp': CMP_REMOVED_MSG, 346 | 'long': LONG_REMOVED_MSG, 347 | 'memoryview': MEMVIEW_ADDED_MSG, 348 | 'reload': RELOAD_REMOVED_MSG, 349 | 'StandardError': STDERR_REMOVED_MSG, 350 | 'breakpoint': BREAKPOINT_ADDED_MSG, 351 | } 352 | result = special_cases.get(name) 353 | if result is not None: 354 | yield result 355 | 356 | 357 | # Functions related to AttributeError 358 | @register_suggestion_for(AttributeError, re.ATTRIBUTEERROR_RE) 359 | @register_suggestion_for(TypeError, re.ATTRIBUTEERROR_RE) 360 | def suggest_attribute_error(value, frame, groups): 361 | """Get suggestions in case of ATTRIBUTEERROR.""" 362 | del value # unused param 363 | type_str, attr = groups 364 | return get_attribute_suggestions(type_str, attr, frame) 365 | 366 | 367 | @register_suggestion_for(AttributeError, re.MODULEHASNOATTRIBUTE_RE) 368 | def suggest_module_has_no_attr(value, frame, groups): 369 | """Get suggestions in case of MODULEHASNOATTRIBUTE.""" 370 | del value # unused param 371 | _, attr = groups # name ignored for the time being 372 | return get_attribute_suggestions('module', attr, frame) 373 | 374 | 375 | def get_attribute_suggestions(type_str, attribute, frame): 376 | """Get the suggestions closest to the attribute name for a given type.""" 377 | types = get_types_for_str(type_str, frame) 378 | attributes = set(a for t in types for a in dir(t)) 379 | if type_str == 'module': 380 | # For module, we manage to get the corresponding 'module' type 381 | # but the type doesn't bring much information about its content. 382 | # A hacky way to do so is to assume that the exception was something 383 | # like 'module_name.attribute' so that we can actually find the module 384 | # based on the name. Eventually, we check that the found object is a 385 | # module indeed. This is not failproof but it brings a whole lot of 386 | # interesting suggestions and the (minimal) risk is to have invalid 387 | # suggestions. 388 | module_name = frame.f_code.co_names[0] 389 | objs = get_objects_in_frame(frame) 390 | mod = objs[module_name][0].obj 391 | if inspect.ismodule(mod): 392 | attributes = set(dir(mod)) 393 | 394 | return itertools.chain( 395 | suggest_attribute_is_other_obj(attribute, type_str, frame), 396 | suggest_attribute_alternative(attribute, type_str, attributes), 397 | suggest_attribute_as_typo(attribute, attributes), 398 | suggest_attribute_as_special_case(attribute)) 399 | 400 | 401 | def suggest_attribute_is_other_obj(attribute, type_str, frame): 402 | """Suggest that attribute correspond to another object. 403 | 404 | This can happen in two cases: 405 | - A misused builtin function 406 | * Examples: 'lst.len()' -> 'len(lst)', 'gen.next()' -> 'next(gen)' 407 | - A typo on the '.' which should have been a ',' 408 | * Example: a, b = 1, 2 then: 'min(a. b)' -> 'min(a, b)' 409 | """ 410 | for obj, scope in get_objects_in_frame(frame).get(attribute, []): 411 | if attribute in frame.f_code.co_names: 412 | if scope == 'builtin' and '__call__' in dir(obj): 413 | yield quote(attribute + '(' + type_str + ')') 414 | else: 415 | yield COMMA_INSTEAD_OF_PERIOD_MSG 416 | 417 | 418 | def suggest_attribute_alternative(attribute, type_str, attributes): 419 | """Suggest alternative to the non-found attribute.""" 420 | for s in suggest_attribute_synonyms(attribute, attributes): 421 | yield s 422 | is_iterable = '__iter__' in attributes or \ 423 | ('__getitem__' in attributes and '__len__' in attributes) 424 | if attribute == 'has_key' and '__contains__' in attributes: 425 | yield quote('key in ' + type_str) + ' (has_key is removed)' 426 | elif attribute == 'get' and '__getitem__' in attributes: 427 | yield quote('obj[key]') + \ 428 | ' with a len() check or try: except: KeyError or IndexError' 429 | elif attribute in ('__setitem__', '__delitem__'): 430 | if is_iterable: 431 | msg = 'convert to list to edit the list' 432 | if 'join' in attributes: 433 | msg += ' and use "join()" on the list' 434 | yield msg 435 | elif attribute == '__getitem__': 436 | if '__call__' in attributes: 437 | yield quote(type_str + '(value)') 438 | if is_iterable: 439 | yield 'convert to list first or use the iterator protocol to ' \ 440 | 'get the different elements' 441 | elif attribute == '__call__': 442 | if '__getitem__' in attributes: 443 | yield quote(type_str + '[value]') 444 | elif attribute == '__len__': 445 | if is_iterable: 446 | yield quote('len(list(' + type_str + '))') 447 | elif attribute == 'join': 448 | if is_iterable: 449 | yield quote('my_string.join(' + type_str + ')') 450 | elif attribute == '__or__': 451 | if '__pow__' in attributes: 452 | yield quote('val1 ** val2') 453 | elif attribute == '__index__': 454 | if '__len__' in attributes: 455 | yield quote('len(' + type_str + ')') 456 | if type_str in ('str', 'float'): 457 | yield quote('int(' + type_str + ')') 458 | if type_str == 'float' and sys.version_info >= (3, 0): 459 | # These methods return 'float' before Python 3 460 | yield quote('math.floor(' + type_str + ')') 461 | yield quote('math.ceil(' + type_str + ')') 462 | 463 | 464 | def suggest_attribute_synonyms(attribute, attributes): 465 | """Suggest that a method with a similar meaning was used. 466 | 467 | Example: 'lst.add(e)' -> 'lst.append(e)'. 468 | """ 469 | for set_sub in SYNONYMS_SETS: 470 | if attribute in set_sub: 471 | for syn in sorted(set_sub & attributes): 472 | yield quote(syn) 473 | 474 | 475 | def suggest_attribute_as_typo(attribute, attributes): 476 | """Suggest the attribute could be a typo. 477 | 478 | Example: 'a.do_baf()' -> 'a.do_bar()'. 479 | """ 480 | for name in get_close_matches(attribute, attributes): 481 | # Handle Private name mangling 482 | if name.startswith('_') and '__' in name and not name.endswith('__'): 483 | yield quote(name) + ' (but it is supposed to be private)' 484 | else: 485 | yield quote(name) 486 | 487 | 488 | def suggest_attribute_as_special_case(attribute): 489 | """Suggest that attribute could be handled in a specific way.""" 490 | special_cases = { 491 | 'exc_type': EXC_ATTR_REMOVED_MSG, 492 | 'exc_value': EXC_ATTR_REMOVED_MSG, 493 | 'exc_traceback': EXC_ATTR_REMOVED_MSG, 494 | } 495 | result = special_cases.get(attribute) 496 | if result is not None: 497 | yield result 498 | 499 | 500 | # Functions related to ImportError 501 | @register_suggestion_for(ImportError, re.NOMODULE_RE) 502 | def suggest_no_module(value, frame, groups): 503 | """Get the suggestions closest to the failing module import. 504 | 505 | Example: 'import maths' -> 'import math'. 506 | """ 507 | del value, frame # unused param 508 | module_str, = groups 509 | for name in get_close_matches(module_str, STAND_MODULES): 510 | yield quote(name) 511 | 512 | 513 | @register_suggestion_for(ImportError, re.CANNOTIMPORT_RE) 514 | def suggest_cannot_import(value, frame, groups): 515 | """Get the suggestions closest to the failing import.""" 516 | del value # unused param 517 | imported_name, = groups 518 | module_name = frame.f_code.co_names[0] 519 | return itertools.chain( 520 | suggest_imported_name_as_typo(imported_name, module_name, frame), 521 | suggest_import_from_module(imported_name, frame)) 522 | 523 | 524 | def suggest_imported_name_as_typo(imported_name, module_name, frame): 525 | """Suggest that imported name could be a typo from actual name in module. 526 | 527 | Example: 'from math import pie' -> 'from math import pi'. 528 | """ 529 | dir_mod = dir(import_from_frame(module_name, frame)) 530 | for name in get_close_matches(imported_name, dir_mod): 531 | yield quote(name) 532 | 533 | 534 | def suggest_import_from_module(imported_name, frame): 535 | """Suggest than name could be found in a standard module. 536 | 537 | Example: 'from itertools import pi' -> 'from math import pi'. 538 | """ 539 | for mod in STAND_MODULES: 540 | if imported_name in dir(import_from_frame(mod, frame)): 541 | yield quote('from {0} import {1}'.format(mod, imported_name)) 542 | 543 | 544 | # Functions related to TypeError 545 | def suggest_feature_not_supported(attr, type_str, frame): 546 | """Get suggestion for unsupported feature.""" 547 | # 'Object does not support ' exceptions 548 | # can be somehow seen as attribute errors for magic 549 | # methods except for the fact that we do not want to 550 | # have any fuzzy logic on the magic method name. 551 | # Also, we want to suggest the implementation of the 552 | # missing method (if is it not on a builtin object). 553 | types = get_types_for_str(type_str, frame) 554 | attributes = set(a for t in types for a in dir(t)) 555 | for s in suggest_attribute_alternative(attr, type_str, attributes): 556 | yield s 557 | if type_str not in frame.f_builtins and \ 558 | type_str not in ('function', 'generator', 'builtin_function_or_method'): 559 | yield 'implement "' + attr + '" on ' + type_str 560 | 561 | 562 | @register_suggestion_for(TypeError, re.UNSUBSCRIPTABLE_RE) 563 | def suggest_unsubscriptable(value, frame, groups): 564 | """Get suggestions in case of UNSUBSCRIPTABLE error.""" 565 | del value # unused param 566 | type_str, = groups 567 | return suggest_feature_not_supported('__getitem__', type_str, frame) 568 | 569 | 570 | @register_suggestion_for(TypeError, re.NOT_CALLABLE_RE) 571 | def suggest_not_callable(value, frame, groups): 572 | """Get suggestions in case of NOT_CALLABLE error.""" 573 | del value # unused param 574 | type_str, = groups 575 | return suggest_feature_not_supported('__call__', type_str, frame) 576 | 577 | 578 | @register_suggestion_for(TypeError, re.OBJ_DOES_NOT_SUPPORT_RE) 579 | def suggest_obj_does_not_support(value, frame, groups): 580 | """Get suggestions in case of OBJ DOES NOT SUPPORT error.""" 581 | del value # unused param 582 | type_str, feature = groups 583 | FEATURES = { 584 | 'indexing': '__getitem__', 585 | 'item assignment': '__setitem__', 586 | 'item deletion': '__delitem__', 587 | } 588 | attr = FEATURES.get(feature) 589 | if attr is None: 590 | return [] 591 | return suggest_feature_not_supported(attr, type_str, frame) 592 | 593 | 594 | @register_suggestion_for(TypeError, re.OBJECT_HAS_NO_FUNC_RE) 595 | def suggest_obj_has_no(value, frame, groups): 596 | """Get suggestions in case of OBJECT_HAS_NO_FUNC.""" 597 | del value # unused param 598 | type_str, feature = groups 599 | if feature in ('length', 'len'): 600 | return suggest_feature_not_supported('__len__', type_str, frame) 601 | return [] 602 | 603 | 604 | @register_suggestion_for(TypeError, re.BAD_OPERAND_UNARY_RE) 605 | def suggest_bad_operand_for_unary(value, frame, groups): 606 | """Get suggestions for BAD_OPERAND_UNARY.""" 607 | del value # unused param 608 | unary, type_str = groups 609 | UNARY_OPS = { 610 | '+': '__pos__', 611 | 'pos': '__pos__', 612 | '-': '__neg__', 613 | 'neg': '__neg__', 614 | '~': '__invert__', 615 | 'abs()': '__abs__', 616 | 'abs': '__abs__', 617 | } 618 | attr = UNARY_OPS.get(unary) 619 | if attr is None: 620 | return [] 621 | return suggest_feature_not_supported(attr, type_str, frame) 622 | 623 | 624 | @register_suggestion_for(TypeError, re.UNSUPPORTED_OP_RE) 625 | @register_suggestion_for(TypeError, re.UNSUPPORTED_OP_SUGG_RE) 626 | def suggest_unsupported_op(value, frame, groups): 627 | """Get suggestions for UNSUPPORTED_OP_RE/UNSUPPORTED_OP_SUGG_RE.""" 628 | del value # unused param 629 | binary, type1, type2 = groups[:3] 630 | sugg = "" if len(groups) < 3 + 1 else groups[3] 631 | # Special case for print being used without parenthesis (Python 2 style) 632 | if type1 in ('builtin_function_or_method', 'builtin_function') and \ 633 | 'print' in frame.f_code.co_names and \ 634 | not sugg.startswith('print('): 635 | if binary == '>>': 636 | yield '"print(, file=)"'\ 637 | .format(binary, type2) 638 | else: 639 | yield '"print({0}<{1}>)"'.format(binary, type2) 640 | BINARY_OPS = { 641 | '^': '__or__', 642 | } 643 | attr = BINARY_OPS.get(binary) 644 | # Suggestion is based on first type which may not be the best 645 | if attr is not None: 646 | for s in suggest_feature_not_supported(attr, type1, frame): 647 | yield s 648 | 649 | 650 | @register_suggestion_for(TypeError, re.CANNOT_BE_INTERPRETED_INT_RE) 651 | @register_suggestion_for(TypeError, re.INTEGER_EXPECTED_GOT_RE) 652 | @register_suggestion_for(TypeError, re.INDICES_MUST_BE_INT_RE) 653 | def suggest_integer_type_expected(value, frame, groups): 654 | """Get suggestions when an int is wanted.""" 655 | del value # unused param 656 | type_str, = groups 657 | return suggest_feature_not_supported('__index__', type_str, frame) 658 | 659 | 660 | def get_func_by_name(func_name, frame): 661 | """Get the function with the given name in the frame.""" 662 | # TODO: Handle qualified names such as dict.get 663 | # Dirty workaround is to remove everything before the last '.' 664 | func_name = func_name.split('.')[-1] 665 | objs = get_objects_in_frame(frame) 666 | # Trying to fetch reachable objects: getting objects and attributes 667 | # for objects. We would go deeper (with a fixed point algorithm) but 668 | # it doesn't seem to be worth it. In any case, we'll be missing a few 669 | # possible functions. 670 | objects = [o.obj for lst in objs.values() for o in lst] 671 | for obj in list(objects): 672 | for a in dir(obj): 673 | attr = getattr(obj, a, None) 674 | if attr is not None: 675 | objects.append(attr) 676 | # Then, we filter for function with the correct name (the name being the 677 | # name on the function object which is not always the same from the 678 | # namespace). 679 | return [func 680 | for func in objects 681 | if getattr(func, '__name__', None) == func_name] 682 | 683 | 684 | def suggest_unexpected_keywordarg_for_func(kw_arg, func_name, frame): 685 | """Get suggestions in case of unexpected keyword argument.""" 686 | functions = get_func_by_name(func_name, frame) 687 | func_codes = [f.__code__ for f in functions if hasattr(f, '__code__')] 688 | args = set([var for func in func_codes for var in func.co_varnames]) 689 | for arg_name in get_close_matches(kw_arg, args): 690 | yield quote(arg_name) 691 | if kw_arg == 'cmp' and \ 692 | (('key' in args) or (len(functions) > len(func_codes))): 693 | yield CMP_ARG_REMOVED_MSG 694 | 695 | 696 | @register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG_RE) 697 | def suggest_unexpected_keywordarg(value, frame, groups): 698 | """Get suggestions in case of UNEXPECTED_KEYWORDARG error.""" 699 | del value # unused param 700 | func_name, kw_arg = groups 701 | return suggest_unexpected_keywordarg_for_func(kw_arg, func_name, frame) 702 | 703 | 704 | @register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG4_RE) 705 | def suggest_unexpected_keywordarg4(value, frame, groups): 706 | """Get suggestions in case of UNEXPECTED_KEYWORDARG4 error.""" 707 | del value # unused param 708 | kw_arg, func_name = groups 709 | return suggest_unexpected_keywordarg_for_func(kw_arg, func_name, frame) 710 | 711 | 712 | @register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG2_RE) 713 | def suggest_unexpected_keywordarg2(value, frame, groups): 714 | """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error.""" 715 | del value, frame # unused param 716 | kw_arg, = groups 717 | if kw_arg == 'cmp': 718 | yield CMP_ARG_REMOVED_MSG 719 | 720 | 721 | @register_suggestion_for(TypeError, re.UNEXPECTED_KEYWORDARG3_RE) 722 | def suggest_unexpected_keywordarg3(value, frame, groups): 723 | """Get suggestions in case of UNEXPECTED_KEYWORDARG2 error.""" 724 | del value, frame # unused param 725 | func_name, = groups 726 | del func_name # unused value 727 | return [] # no implementation so far 728 | 729 | 730 | @register_suggestion_for(TypeError, re.NB_ARG_RE) 731 | def suggest_nb_arg(value, frame, groups): 732 | """Get suggestions in case of NB ARGUMENT error.""" 733 | del value # unused param 734 | func_name, expected, given = groups 735 | given_nb = int(given) 736 | beg, to, end = expected.partition(' to ') 737 | if to: 738 | # Find closest value 739 | beg, end = int(beg), int(end) 740 | if given_nb < beg < end: 741 | expect_nb = beg 742 | elif beg < end < given_nb: 743 | expect_nb = end 744 | else: 745 | # Should not happen 746 | return 747 | elif expected == 'no': 748 | expect_nb = 0 749 | else: 750 | expect_nb = int(expected) 751 | objs = get_objects_in_frame(frame) 752 | del expect_nb, given_nb, objs, func_name # for later 753 | return 754 | yield 755 | 756 | 757 | @register_suggestion_for(TypeError, re.FUNC_TAKES_NO_KEYWORDARG_RE) 758 | def suggest_func_no_kw_arg(value, frame, groups): 759 | """Get suggestions for FUNC_TAKES_NO_KEYWORDARG_RE.""" 760 | # C-Level functions don't have actual names for their arguments. 761 | # Therefore, trying to use them with keyword arguments leads to 762 | # errors but using them with positional arguments just work fine. 763 | # This behavior definitly deserves some suggestion. 764 | # More reading: 765 | # http://stackoverflow.com/questions/24463202/typeerror-get-takes-no-keyword-arguments 766 | # https://www.python.org/dev/peps/pep-0457/ 767 | # https://www.python.org/dev/peps/pep-0436/#functions-with-positional-only-parameters 768 | # Note: a proper implementation of this function would: 769 | # - retrieve the function object using the function name 770 | # - check that the function does accept arguments but does not 771 | # accept keyword arguments before yielding the suggestion. 772 | # Unfortunately, introspection of builtin function is not possible as per 773 | # http://bugs.python.org/issue1748064 . Thus, the only thing we can look 774 | # for is if a function has no __code__ attribute. 775 | del value # unused param 776 | func_name, = groups 777 | functions = get_func_by_name(func_name, frame) 778 | if any([not hasattr(f, '__code__') for f in functions]): 779 | yield NO_KEYWORD_ARG_MSG 780 | 781 | 782 | # Functions related to ValueError 783 | @register_suggestion_for(ValueError, re.ZERO_LEN_FIELD_RE) 784 | def suggest_zero_len_field(value, frame, groups): 785 | """Get suggestions in case of ZERO_LEN_FIELD.""" 786 | del value, frame, groups # unused param 787 | yield '{0}' 788 | 789 | 790 | @register_suggestion_for(ValueError, re.TIME_DATA_DOES_NOT_MATCH_FORMAT_RE) 791 | def suggest_time_data_is_wrong(value, frame, groups): 792 | """Get suggestions in case of TIME_DATA_DOES_NOT_MATCH_FORMAT_RE.""" 793 | del value, frame # unused param 794 | timedata, timeformat = groups 795 | if timedata.count('%') > timeformat.count('%%'): 796 | yield "to swap value and format parameters" 797 | 798 | 799 | # Functions related to SyntaxError 800 | @register_suggestion_for(SyntaxError, re.OUTSIDE_FUNCTION_RE) 801 | def suggest_outside_func_error(value, frame, groups): 802 | """Get suggestions in case of OUTSIDE_FUNCTION error.""" 803 | del value, frame # unused param 804 | yield "to indent it" 805 | word, = groups 806 | if word == 'return': 807 | yield "'sys.exit([arg])'" 808 | 809 | 810 | @register_suggestion_for(SyntaxError, re.FUTURE_FEATURE_NOT_DEF_RE) 811 | def suggest_future_feature(value, frame, groups): 812 | """Get suggestions in case of FUTURE_FEATURE_NOT_DEF error.""" 813 | del value # unused param 814 | feature, = groups 815 | return suggest_imported_name_as_typo(feature, '__future__', frame) 816 | 817 | 818 | @register_suggestion_for(SyntaxError, re.INVALID_COMP_RE) 819 | def suggest_invalid_comp(value, frame, groups): 820 | """Get suggestions in case of INVALID_COMP error.""" 821 | del value, frame, groups # unused param 822 | yield quote('!=') 823 | 824 | 825 | @register_suggestion_for(SyntaxError, re.NO_BINDING_NONLOCAL_RE) 826 | def suggest_no_binding_for_nonlocal(value, frame, groups): 827 | """Get suggestions in case of NO BINDING FOR NONLOCAL.""" 828 | del value # unused param 829 | name, = groups 830 | objs = get_objects_in_frame(frame).get(name, []) 831 | for _, scope in objs: 832 | if scope == 'global': 833 | # TODO_ENCLOSING: suggest close matches for enclosing 834 | yield quote('global ' + name) 835 | 836 | 837 | @register_suggestion_for(SyntaxError, re.INVALID_SYNTAX_RE) 838 | def suggest_invalid_syntax(value, frame, groups): 839 | """Get suggestions in case of INVALID_SYNTAX error.""" 840 | del frame, groups # unused param 841 | alternatives = { 842 | '<>': '!=', 843 | '&&': 'and', 844 | '||': 'or', 845 | } 846 | offset = value.offset 847 | if value.offset is not None: 848 | for shift in (0, 1): 849 | offset = value.offset + shift 850 | two_last = value.text[offset - 2:offset] 851 | alt = alternatives.get(two_last) 852 | if alt is not None: 853 | yield quote(alt) 854 | break 855 | 856 | 857 | # Functions related to MemoryError 858 | @register_suggestion_for(MemoryError, None) 859 | def get_memory_error_sugg(value, frame, groups): 860 | """Get suggestions for MemoryError exception.""" 861 | del value, groups # unused param 862 | objs = get_objects_in_frame(frame) 863 | return itertools.chain.from_iterable( 864 | suggest_memory_friendly_equi(name, objs) 865 | for name in frame.f_code.co_names) 866 | 867 | 868 | # Functions related to OverflowError 869 | @register_suggestion_for(OverflowError, re.RESULT_TOO_MANY_ITEMS_RE) 870 | def suggest_too_many_items(value, frame, groups): 871 | """Suggest for TOO_MANY_ITEMS error.""" 872 | del value # unused param 873 | func, = groups 874 | objs = get_objects_in_frame(frame) 875 | return suggest_memory_friendly_equi(func, objs) 876 | 877 | 878 | def suggest_memory_friendly_equi(name, objs): 879 | """Suggest name of a memory friendly equivalent for a function.""" 880 | suggs = {'range': ['xrange']} 881 | return [quote(s) for s in suggs.get(name, []) if s in objs] 882 | 883 | 884 | # Functions related to RuntimeError 885 | @register_suggestion_for(RuntimeError, re.MAX_RECURSION_DEPTH_RE) 886 | def suggest_max_resursion_depth(value, frame, groups): 887 | """Suggest for MAX_RECURSION_DEPTH error.""" 888 | # this is the real solution, make it the first suggestion 889 | del value, frame, groups # unused param 890 | yield AVOID_REC_MSG 891 | yield "increase the limit with " \ 892 | "`sys.setrecursionlimit(limit)` (current value" \ 893 | " is %d)" % sys.getrecursionlimit() 894 | 895 | 896 | # Functions related to IOError/OSError 897 | @register_suggestion_for((IOError, OSError), None) 898 | def get_io_os_error_sugg(value, frame, groups): 899 | """Get suggestions for IOError/OSError exception.""" 900 | # https://www.python.org/dev/peps/pep-3151/ 901 | del frame, groups # unused param 902 | err, _ = value.args 903 | errnos = { 904 | errno.ENOENT: suggest_if_file_does_not_exist, 905 | errno.ENOTDIR: suggest_if_file_is_not_dir, 906 | errno.EISDIR: suggest_if_file_is_dir, 907 | } 908 | return errnos.get(err, lambda x: [])(value) 909 | 910 | 911 | def suggest_if_file_does_not_exist(value): 912 | """Get suggestions when a file does not exist.""" 913 | # TODO: Add fuzzy match 914 | filename = value.filename 915 | for func, name in ( 916 | (os.path.expanduser, 'os.path.expanduser'), 917 | (os.path.expandvars, 'os.path.expandvars')): 918 | expanded = func(filename) 919 | if os.path.exists(expanded) and filename != expanded: 920 | yield quote(expanded) + " (calling " + name + ")" 921 | 922 | 923 | def suggest_if_file_is_not_dir(value): 924 | """Get suggestions when a file should have been a dir and is not.""" 925 | filename = value.filename 926 | yield quote(os.path.dirname(filename)) + " (calling os.path.dirname)" 927 | 928 | 929 | def suggest_if_file_is_dir(value): 930 | """Get suggestions when a file is a dir and should not.""" 931 | filename = value.filename 932 | listdir = sorted(os.listdir(filename)) 933 | if listdir: 934 | trunc_l = listdir[:MAX_NB_FILES] 935 | truncated = listdir != trunc_l 936 | filelist = [quote(f) for f in trunc_l] + (["etc"] if truncated else []) 937 | yield "any of the {0} files in directory ({1})".format( 938 | len(listdir), ", ".join(filelist)) 939 | else: 940 | yield "to add content to {0} first".format(filename) 941 | 942 | 943 | def get_suggestions_for_exception(value, traceback): 944 | """Get suggestions for an exception.""" 945 | frame = get_last_frame(traceback) 946 | return itertools.chain.from_iterable( 947 | func(value, frame) 948 | for error_type, functions in SUGGESTION_FUNCTIONS.items() 949 | if isinstance(value, error_type) 950 | for func in functions) 951 | 952 | 953 | def add_string_to_exception(value, string): 954 | """Add string to the exception parameter.""" 955 | # The point is to have the string visible when the exception is printed 956 | # or converted to string - may it be via `str()`, `repr()` or when the 957 | # exception is uncaught and displayed (which seems to use `str()`). 958 | # In an ideal world, one just needs to update `args` but apparently it 959 | # is not enough for SyntaxError, IOError, etc where other 960 | # attributes (`msg`, `strerror`, `reason`, etc) are to be updated too 961 | # (for `str()`, not for `repr()`). 962 | # Also, elements in args might not be strings or args might me empty 963 | # so we add to the first string and add the element otherwise. 964 | assert isinstance(value.args, tuple) 965 | if string: 966 | lst_args = list(value.args) 967 | for i, arg in enumerate(lst_args): 968 | if isinstance(arg, str): 969 | lst_args[i] = arg + string 970 | break 971 | else: 972 | # if no string arg, add the string anyway 973 | lst_args.append(string) 974 | value.args = tuple(lst_args) 975 | for attr in ['msg', 'strerror', 'reason']: 976 | attrval = getattr(value, attr, None) 977 | if attrval is not None: 978 | setattr(value, attr, attrval + string) 979 | 980 | 981 | def get_last_frame(traceback): 982 | """Extract last frame from a traceback.""" 983 | # In some rare case, the given traceback might be None 984 | if traceback is None: 985 | return None 986 | while traceback.tb_next: 987 | traceback = traceback.tb_next 988 | return traceback.tb_frame 989 | 990 | 991 | def print_frame_information(frame): 992 | """Print information about a frame and the data one can get from it.""" 993 | # For debug purposes 994 | print("-----") 995 | print("Frame", frame) 996 | # print(dir(frame)) 997 | print("-----") 998 | code = frame.f_code 999 | print("Frame.code", code) 1000 | # print(dir(code)) 1001 | cellvars = code.co_cellvars 1002 | print("Frame.code.cellvars", cellvars) 1003 | # print(dir(cellvars)) 1004 | cocode = code.co_code 1005 | print("Frame.code.cocode", cocode) 1006 | coname = code.co_name 1007 | print("Frame.code.coname", coname) 1008 | conames = code.co_names 1009 | print("Frame.code.conames", conames) 1010 | print("-----") 1011 | lasti = frame.f_lasti 1012 | print("Frame.lasti", lasti) 1013 | # print(dir(lasti)) 1014 | print("-----") 1015 | 1016 | 1017 | def add_suggestions_to_exception(type_, value, traceback): 1018 | """Add suggestion to an exception. 1019 | 1020 | Arguments are such as provided by sys.exc_info(). 1021 | """ 1022 | assert isinstance(value, type_) 1023 | add_string_to_exception( 1024 | value, 1025 | get_suggestion_string( 1026 | get_suggestions_for_exception( 1027 | value, 1028 | traceback))) 1029 | -------------------------------------------------------------------------------- /didyoumean/didyoumean_internal_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """Unit tests for code in didyoumean_internal.py.""" 3 | from didyoumean_internal import quote, get_suggestion_string,\ 4 | add_string_to_exception, get_func_by_name,\ 5 | get_objects_in_frame, get_subclasses, get_types_for_str,\ 6 | get_types_for_str_using_inheritance,\ 7 | get_types_for_str_using_names 8 | import didyoumean_common_tests as common 9 | from didyoumean_common_tests import unittest_module,\ 10 | CommonTestOldStyleClass2,\ 11 | CommonTestNewStyleClass2 # to have these 2 in defined names 12 | import itertools 13 | import sys 14 | 15 | 16 | OLD_CLASS_SUPPORT = sys.version_info >= (3, 0) 17 | IS_PYPY = hasattr(sys, "pypy_translation_info") 18 | U_PREFIX_SUPPORT = not ((3, 0) <= sys.version_info < (3, 3)) 19 | U_PREFIX = "u" if U_PREFIX_SUPPORT else "" 20 | global_var = 42 # Please don't change the value 21 | 22 | 23 | class QuoteTests(unittest_module.TestCase): 24 | """Class for tests related to quote.""" 25 | 26 | def test_quote_empty_str(self): 27 | """Test quote on empty string.""" 28 | self.assertEqual(quote(''), "''") 29 | 30 | def test_quote_str(self): 31 | """Test quote on non-empty string.""" 32 | self.assertEqual(quote('abc'), "'abc'") 33 | 34 | 35 | class GetObjectInFrameTests(unittest_module.TestCase): 36 | """Class for tests related to frame/backtrace/etc inspection. 37 | 38 | Tested functions are : get_objects_in_frame. 39 | No tests about 'nonlocal' is written because it is only supported 40 | from Python 3. 41 | """ 42 | 43 | def name_corresponds_to(self, name, expected): 44 | """Helper functions to test get_objects_in_frame. 45 | 46 | Check that the name corresponds to the expected objects (and their 47 | scope) in the frame of calling function. 48 | None can be used to match any object as it can be hard to describe 49 | an object when it is hidden by something in a closer scope. 50 | Also, extra care is to be taken when calling the function because 51 | giving value by names might affect the result (adding in local 52 | scope). 53 | """ 54 | frame = sys._getframe(1) # frame of calling function 55 | lst = get_objects_in_frame(frame).get(name, []) 56 | self.assertEqual(len(lst), len(expected)) 57 | for scopedobj, exp in zip(lst, expected): 58 | obj, scope = scopedobj 59 | expobj, expscope = exp 60 | self.assertEqual(scope, expscope, name) 61 | if expobj is not None: 62 | self.assertEqual(obj, expobj, name) 63 | 64 | def test_builtin(self): 65 | """Test with builtin.""" 66 | builtin = len 67 | name = builtin.__name__ 68 | self.name_corresponds_to(name, [(builtin, 'builtin')]) 69 | 70 | def test_builtin2(self): 71 | """Test with builtin.""" 72 | name = 'True' 73 | self.name_corresponds_to(name, [(bool(1), 'builtin')]) 74 | 75 | def test_global(self): 76 | """Test with global.""" 77 | name = 'global_var' 78 | self.name_corresponds_to(name, [(42, 'global')]) 79 | 80 | def test_local(self): 81 | """Test with local.""" 82 | name = 'toto' 83 | self.name_corresponds_to(name, []) 84 | toto = 0 # noqa 85 | self.name_corresponds_to(name, [(0, 'local')]) 86 | 87 | def test_local_and_global(self): 88 | """Test with local hiding a global.""" 89 | name = 'global_var' 90 | self.name_corresponds_to(name, [(42, 'global')]) 91 | global_var = 1 # noqa 92 | self.name_corresponds_to(name, [(1, 'local'), (42, 'global')]) 93 | 94 | def test_global_keword(self): 95 | """Test with global keyword.""" 96 | # Funny detail : the global keyword works even if at the end of 97 | # the function (after the code it affects) but this raises a 98 | # SyntaxWarning. 99 | global global_var 100 | name = 'global_var' 101 | global_var = 42 # value is unchanged 102 | self.name_corresponds_to(name, [(42, 'global')]) 103 | 104 | def test_del_local(self): 105 | """Test with deleted local.""" 106 | name = 'toto' 107 | self.name_corresponds_to(name, []) 108 | toto = 0 109 | self.name_corresponds_to(name, [(0, 'local')]) 110 | del toto 111 | self.name_corresponds_to(name, []) 112 | 113 | def test_del_local_hiding_global(self): 114 | """Test with deleted local hiding a global.""" 115 | name = 'global_var' 116 | glob_desc = [(42, 'global')] 117 | local_desc = [(1, 'local')] 118 | self.name_corresponds_to(name, glob_desc) 119 | global_var = 1 120 | self.name_corresponds_to(name, local_desc + glob_desc) 121 | del global_var 122 | self.name_corresponds_to(name, glob_desc) 123 | 124 | def test_enclosing(self): 125 | """Test with nested functions.""" 126 | foo = 1 # noqa 127 | bar = 2 # noqa 128 | 129 | def nested_func(foo, baz): 130 | qux = 5 # noqa 131 | self.name_corresponds_to('qux', [(5, 'local')]) 132 | self.name_corresponds_to('baz', [(4, 'local')]) 133 | self.name_corresponds_to('foo', [(3, 'local')]) 134 | self.name_corresponds_to('bar', []) 135 | self.name_corresponds_to( 136 | 'global_var', [(42, 'global')]) 137 | nested_func(3, 4) 138 | self.name_corresponds_to('nested_func', [(nested_func, 'local')]) 139 | self.name_corresponds_to('foo', [(1, 'local')]) 140 | self.name_corresponds_to('baz', []) 141 | 142 | def test_enclosing2(self): 143 | """Test with nested functions.""" 144 | bar = 2 # noqa 145 | 146 | def nested_func(): 147 | self.name_corresponds_to('bar', []) 148 | bar = 3 # noqa 149 | self.name_corresponds_to('bar', [(3, 'local')]) 150 | 151 | nested_func() 152 | self.name_corresponds_to('nested_func', [(nested_func, 'local')]) 153 | 154 | def test_enclosing3(self): 155 | """Test with nested functions.""" 156 | bar = 2 157 | 158 | def nested_func(): 159 | self.name_corresponds_to('bar', [(2, 'local')]) 160 | tmp = bar # noqa 161 | self.name_corresponds_to('bar', [(2, 'local')]) 162 | 163 | nested_func() 164 | self.name_corresponds_to('nested_func', [(nested_func, 'local')]) 165 | 166 | def test_enclosing4(self): 167 | """Test with nested functions.""" 168 | global_var = 1 # noqa 169 | 170 | def nested_func(): 171 | self.name_corresponds_to('global_var', [(42, 'global')]) 172 | 173 | nested_func() 174 | self.name_corresponds_to('global_var', [(1, 'local'), (42, 'global')]) 175 | 176 | def test_enclosing5(self): 177 | """Test with nested functions.""" 178 | bar = 2 # noqa 179 | foo = 3 # noqa 180 | 181 | def nested_func(): 182 | bar = 4 # noqa 183 | baz = 5 # noqa 184 | self.name_corresponds_to('foo', []) 185 | self.name_corresponds_to('bar', [(4, 'local')]) 186 | 187 | def nested_func2(): 188 | self.name_corresponds_to('foo', []) 189 | self.name_corresponds_to('bar', []) 190 | 191 | nested_func2() 192 | 193 | nested_func() 194 | self.name_corresponds_to('nested_func', [(nested_func, 'local')]) 195 | 196 | 197 | class OldStyleBaseClass: 198 | """Dummy class for testing purposes.""" 199 | 200 | pass 201 | 202 | 203 | class OldStyleDerivedClass(OldStyleBaseClass): 204 | """Dummy class for testing purposes.""" 205 | 206 | pass 207 | 208 | 209 | class NewStyleBaseClass(object): 210 | """Dummy class for testing purposes.""" 211 | 212 | pass 213 | 214 | 215 | class NewStyleDerivedClass(NewStyleBaseClass): 216 | """Dummy class for testing purposes.""" 217 | 218 | pass 219 | 220 | 221 | def a_function(): 222 | """Dummy function for testing purposes.""" 223 | pass 224 | 225 | 226 | def a_generator(): 227 | """Dummy generator for testing purposes.""" 228 | yield 1 229 | 230 | 231 | NEW_STYLE_CLASSES = [bool, int, float, str, tuple, list, set, dict, object, 232 | NewStyleBaseClass, NewStyleDerivedClass, 233 | common.CommonTestNewStyleClass, 234 | common.CommonTestNewStyleClass2, 235 | type(a_function), type(a_generator), 236 | type(len), type(None), type(type(None)), 237 | type(object), type(sys), type(range), 238 | type(NewStyleBaseClass), type(NewStyleDerivedClass), 239 | type(OldStyleBaseClass), type(OldStyleDerivedClass)] 240 | OLD_STYLE_CLASSES = [OldStyleBaseClass, OldStyleDerivedClass, 241 | CommonTestOldStyleClass2] 242 | CLASSES = [(c, True) for c in NEW_STYLE_CLASSES] + \ 243 | [(c, False) for c in OLD_STYLE_CLASSES] 244 | 245 | 246 | class GetTypesForStrTests(unittest_module.TestCase): 247 | """Test get_types_for_str.""" 248 | 249 | def test_get_subclasses(self): 250 | """Test the get_subclasses function. 251 | 252 | All types are found when looking for subclasses of object, except 253 | for the old style classes on Python 2.x. 254 | """ 255 | all_classes = get_subclasses(object) 256 | for typ, new in CLASSES: 257 | self.assertTrue(typ in get_subclasses(typ)) 258 | if new or OLD_CLASS_SUPPORT: 259 | self.assertTrue(typ in all_classes) 260 | else: 261 | self.assertFalse(typ in all_classes) 262 | self.assertFalse(0 in all_classes) 263 | 264 | def test_get_types_for_str_using_inheritance(self): 265 | """Test the get_types_for_str_using_inheritance function. 266 | 267 | All types are found when looking for subclasses of object, except 268 | for the old style classes on Python 2.x. 269 | 270 | Also, it seems like the returns is (almost) always precise as the 271 | returned set contains only the expected type and nothing else. 272 | """ 273 | for typ, new in CLASSES: 274 | types = get_types_for_str_using_inheritance(typ.__name__) 275 | if new or OLD_CLASS_SUPPORT: 276 | self.assertEqual(types, set([typ]), typ) 277 | else: 278 | self.assertEqual(types, set(), typ) 279 | 280 | self.assertFalse(get_types_for_str_using_inheritance('faketype')) 281 | 282 | def get_types_using_names(self, type_str): 283 | """Wrapper around the get_types_using_names function.""" 284 | return get_types_for_str_using_names(type_str, sys._getframe(1)) 285 | 286 | def test_get_types_for_str_using_names(self): 287 | """Test the get_types_using_names function. 288 | 289 | Old style classes are retrieved even on Python 2.x. 290 | However, a few builtin types are not in the names so can't be found. 291 | """ 292 | for typ in OLD_STYLE_CLASSES: 293 | types = self.get_types_using_names(typ.__name__) 294 | self.assertEqual(types, set([typ]), typ) 295 | for n in ['generator', 'module', 'function', 'faketype']: 296 | self.assertEqual(self.get_types_using_names(n), set(), n) 297 | n = 'NoneType' 298 | if IS_PYPY: 299 | self.assertEqual(len(self.get_types_using_names(n)), 1, n) 300 | else: 301 | self.assertEqual(self.get_types_using_names(n), set(), n) 302 | 303 | def get_types_for_str(self, type_str): 304 | """Wrapper around the get_types_for_str function.""" 305 | return get_types_for_str(type_str, sys._getframe(1)) 306 | 307 | def test_get_types_for_str(self): 308 | """Test the get_types_for_str function. 309 | 310 | Check that for all tested types, the proper type is retrieved. 311 | """ 312 | for typ, _ in CLASSES: 313 | types = self.get_types_for_str(typ.__name__) 314 | self.assertEqual(types, set([typ]), typ) 315 | 316 | self.assertEqual(self.get_types_for_str('faketype'), set()) 317 | 318 | def test_get_types_for_str2(self): 319 | """Test the get_types_for_str function. 320 | 321 | Check that for all tested strings, a single type is retrived. 322 | This is useful to ensure that we are using the right names. 323 | """ 324 | for n in ['module', 'NoneType', 'function', 325 | 'NewStyleBaseClass', 'NewStyleDerivedClass', 326 | 'OldStyleBaseClass', 'OldStyleDerivedClass']: 327 | types = self.get_types_for_str(n) 328 | self.assertEqual(len(types), 1, n) 329 | for n in ['generator']: # FIXME: with pypy, we find an additional type 330 | types = self.get_types_for_str(n) 331 | self.assertEqual(len(types), 2 if IS_PYPY else 1, n) 332 | 333 | def test_old_class_not_in_namespace(self): 334 | """Test the get_types_for_str function. 335 | 336 | Check that at the moment, CommonTestOldStyleClass is not found 337 | because it is not in the namespace. This behavior is to be improved. 338 | """ 339 | typ = common.CommonTestOldStyleClass 340 | expect_with_inherit = set([typ]) if OLD_CLASS_SUPPORT else set() 341 | name = typ.__name__ 342 | types1 = get_types_for_str_using_inheritance(name) 343 | types2 = self.get_types_using_names(name) 344 | types3 = self.get_types_for_str(name) 345 | self.assertEqual(types1, expect_with_inherit) 346 | self.assertEqual(types2, set()) 347 | self.assertEqual(types3, expect_with_inherit) 348 | 349 | 350 | class GetFuncByNameTests(unittest_module.TestCase): 351 | """Test get_func_by_name.""" 352 | 353 | def get_func_by_name(self, func_name): 354 | """Wrapper around the get_func_by_name function.""" 355 | return get_func_by_name(func_name, sys._getframe(1)) 356 | 357 | def check_get_func_by_name_res(self, function, results, exact_match): 358 | """Check that function is in the list of results.""" 359 | details = "{0}, ({1}), exact_match:{2}".format( 360 | str(function), 361 | str(results), 362 | str(exact_match)) 363 | self.assertTrue(function in results, details) 364 | self.assertTrue(len(results) >= 1, details) 365 | if exact_match: 366 | # Equality above does not hold 367 | # Using set is complicated because everything can't be hashed 368 | # But using id, something seems to be possible 369 | self.assertEqual(len(set(results)), 1, details) 370 | res_ids = [id(e) for e in results] 371 | set_ids = set(res_ids) 372 | self.assertEqual(len(set_ids), 1, set_ids) 373 | 374 | def check_get_func_by_name(self, function, exact_match=True): 375 | """Wrapper around the get_func_by_name to get & check its results.""" 376 | # Using __name__ 377 | self.assertTrue(hasattr(function, '__name__'), function) 378 | res = self.get_func_by_name(function.__name__) 379 | self.check_get_func_by_name_res(function, res, exact_match) 380 | 381 | if sys.version_info >= (3, 3): 382 | # Using __qualname__ 383 | res = self.get_func_by_name(function.__qualname__) 384 | self.check_get_func_by_name_res(function, res, exact_match) 385 | 386 | # Using pyobject_function_str 387 | res = self.get_func_by_name(self.pyobject_function_str(function)) 388 | self.check_get_func_by_name_res(function, res, exact_match) 389 | 390 | def pyobject_function_str(self, x): 391 | """Get function representation as a string.""" 392 | # Based on CPython _PyObject_FunctionStr 393 | try: 394 | qualname = x.__qualname__ 395 | except AttributeError: 396 | return str(x) 397 | try: 398 | mod = x.__module__ 399 | if mod is not None and mod != 'builtins': 400 | return x.__module__ + "." + qualname # original code has () 401 | except AttributeError: 402 | pass 403 | return qualname 404 | 405 | def test_get_builtin_by_name(self): 406 | """Test get_func_by_name on builtin functions.""" 407 | for f in [bool, int, float, str, tuple, list, set, dict, all]: 408 | self.check_get_func_by_name(f) 409 | for f in [object]: 410 | self.check_get_func_by_name(f, False) 411 | 412 | def test_get_builtin_attr_by_name(self): 413 | """Test get_func_by_name on builtin attributes.""" 414 | for f in [dict.get, sys._getframe]: 415 | self.check_get_func_by_name(f, False) 416 | 417 | def test_get_lambda_by_name(self): 418 | """Test get_func_by_name on lambda functions.""" 419 | self.check_get_func_by_name(lambda x: x) 420 | 421 | def test_get_custom_func_by_name(self): 422 | """Test get_func_by_name on custom functions.""" 423 | for f in [a_function, a_generator]: 424 | self.check_get_func_by_name(f) 425 | 426 | def test_get_class_func_by_name(self): 427 | """Test get_func_by_name on custom functions.""" 428 | for f, new in CLASSES: 429 | self.check_get_func_by_name(f, False) 430 | 431 | def test_inexisting_func(self): 432 | """Test get_func_by_name on an inexisting function name.""" 433 | self.assertEqual(self.get_func_by_name('dkalskjdas'), []) 434 | 435 | 436 | class GetSuggStringTests(unittest_module.TestCase): 437 | """Tests about get_suggestion_string.""" 438 | 439 | def test_no_sugg(self): 440 | """Empty list of suggestions.""" 441 | self.assertEqual(get_suggestion_string(()), "") 442 | 443 | def test_one_sugg(self): 444 | """Single suggestion.""" 445 | self.assertEqual(get_suggestion_string(('0',)), ". Did you mean 0?") 446 | 447 | def test_same_sugg(self): 448 | """Identical suggestion.""" 449 | self.assertEqual( 450 | get_suggestion_string(('0', '0')), ". Did you mean 0, 0?") 451 | 452 | def test_multiple_suggs(self): 453 | """Multiple suggestions.""" 454 | self.assertEqual( 455 | get_suggestion_string(('0', '1')), ". Did you mean 0, 1?") 456 | 457 | 458 | class AddStringToExcTest(common.TestWithStringFunction): 459 | """Generic class for tests about add_string_to_exception.""" 460 | 461 | prefix_repr = "" 462 | suffix_repr = "" 463 | check_str_sum = True 464 | 465 | def get_exception(self): 466 | """Abstract method to get an instance of exception.""" 467 | raise NotImplementedError("'get_exception' needs to be implemented") 468 | 469 | def get_exc_before_and_after(self, string, func): 470 | """Retrieve string representations of exceptions. 471 | 472 | Retrieve string representations of exceptions raised by code 473 | before and after calling add_string_to_exception. 474 | """ 475 | value = self.get_exception() 476 | before = func(value) 477 | add_string_to_exception(value, string) 478 | after = func(value) 479 | return (before, after) 480 | 481 | def check_string_added(self, func, string, prefix="", suffix=""): 482 | """Check that add_string_to_exception adds the strings.""" 483 | s1, s2 = self.get_exc_before_and_after(string, func) 484 | self.assertStringAdded( 485 | prefix + string + suffix, s1, s2, self.check_str_sum) 486 | 487 | def test_add_empty_string_to_str(self): 488 | """Empty string added to error's str value.""" 489 | self.check_string_added(str, "") 490 | 491 | def test_add_empty_string_to_repr(self): 492 | """Empty string added to error's repr value.""" 493 | self.check_string_added(repr, "") 494 | 495 | def test_add_string_to_str(self): 496 | """Non-empty string added to error's str value.""" 497 | self.check_string_added(str, "ABCDEstr") 498 | 499 | def test_add_string_to_repr(self): 500 | """Non-empty string added to error's repr value.""" 501 | self.check_string_added( 502 | repr, "ABCDErepr", self.prefix_repr, self.suffix_repr) 503 | 504 | 505 | class AddStringToExcFromCodeTest(AddStringToExcTest): 506 | """Generic class for tests about add_string_to_exception. 507 | 508 | The tested function is called on an exception created by running 509 | some failing code (`self.code`) and catching what it throws. 510 | """ 511 | 512 | code = NotImplemented 513 | 514 | def get_exception(self): 515 | """Get the exception by running the code and catching errors.""" 516 | type_, value, _ = common.get_exception(self.code) 517 | self.assertTrue( 518 | issubclass(type_, self.error_type), 519 | "{0} ({1}) not a subclass of {2}" 520 | .format(type_, value, self.error_type)) 521 | return value 522 | 523 | 524 | class AddStringToNameErrorTest( 525 | unittest_module.TestCase, AddStringToExcFromCodeTest): 526 | """Class for tests of add_string_to_exception on NameError.""" 527 | 528 | code = 'babar = 0\nbaba' 529 | error_type = NameError 530 | 531 | 532 | class AddStringToTypeErrorTest( 533 | unittest_module.TestCase, AddStringToExcFromCodeTest): 534 | """Class for tests of add_string_to_exception on TypeError.""" 535 | 536 | code = '[0](0)' 537 | error_type = TypeError 538 | 539 | 540 | class AddStringToImportErrorTest( 541 | unittest_module.TestCase, AddStringToExcFromCodeTest): 542 | """Class for tests of add_string_to_exception on ImportError.""" 543 | 544 | code = 'import maths' 545 | error_type = ImportError 546 | 547 | 548 | class AddStringToKeyErrorTest( 549 | unittest_module.TestCase, AddStringToExcFromCodeTest): 550 | """Class for tests of add_string_to_exception on KeyError.""" 551 | 552 | code = 'dict()["ffdsqmjklfqsd"]' 553 | error_type = KeyError 554 | 555 | 556 | class AddStringToAttributeErrorTest( 557 | unittest_module.TestCase, AddStringToExcFromCodeTest): 558 | """Class for tests of add_string_to_exception on AttributeError.""" 559 | 560 | code = '[].does_not_exist' 561 | error_type = AttributeError 562 | 563 | 564 | class AddStringToSyntaxErrorTest( 565 | unittest_module.TestCase, AddStringToExcFromCodeTest): 566 | """Class for tests of add_string_to_exception on SyntaxError.""" 567 | 568 | code = 'return' 569 | error_type = SyntaxError 570 | 571 | 572 | @unittest_module.skipIf(common.SKIP_MEMORY_ERROR_TESTS, "Memory test skipped") 573 | class AddStringToMemoryErrorTest( 574 | unittest_module.TestCase, AddStringToExcFromCodeTest): 575 | """Class for tests of add_string_to_exception on MemoryError.""" 576 | 577 | code = '[0] * 999999999999999' 578 | error_type = MemoryError 579 | prefix_repr = "'" 580 | # Trailing comma removed from Python 3.7 581 | # See https://bugs.python.org/issue30399 582 | suffix_repr = "'" if sys.version_info >= (3, 7) else "'," 583 | 584 | 585 | class AddStringToIOErrorTest( 586 | unittest_module.TestCase, AddStringToExcFromCodeTest): 587 | """Class for tests of add_string_to_exception on NoFileIoError.""" 588 | 589 | code = 'with open("/does_not_exist") as f:\n\tpass' 590 | error_type = common.NoFileIoError 591 | 592 | 593 | class AddStringToUnicodeDecodeTest( 594 | unittest_module.TestCase, AddStringToExcFromCodeTest): 595 | """Class for tests of add_string_to_exception on UnicodeDecodeError.""" 596 | 597 | code = "'foo'.encode('utf-16').decode('utf-8')" 598 | error_type = UnicodeDecodeError 599 | 600 | 601 | class AddStringToUnicodeEncodeTest( 602 | unittest_module.TestCase, AddStringToExcFromCodeTest): 603 | """Class for tests of add_string_to_exception on UnicodeEncodeError.""" 604 | 605 | code = U_PREFIX + '"\u0411".encode("iso-8859-15")' 606 | error_type = UnicodeEncodeError 607 | 608 | 609 | class AddStringToExcFromInstanceTest(AddStringToExcTest): 610 | """Generic class for tests about add_string_to_exception. 611 | 612 | The tested function is called on an exception created by calling the 613 | constructor (`self.exc_type`) with the right arguments (`self.args`). 614 | Because of the way it creates exception, the tests are somewhat artificial 615 | (compared to AddStringToExcFromCodeTest for instance). However, the major 616 | advantage is that they can be easily generated (to have all subclasses of 617 | Exception tested). 618 | """ 619 | 620 | check_str_sum = False 621 | exc_type = NotImplemented 622 | args = NotImplemented 623 | 624 | def get_exception(self): 625 | """Get the exception by calling the constructor with correct args.""" 626 | return self.exc_type(*self.args) 627 | 628 | 629 | class AddStringToZeroDivisionError( 630 | unittest_module.TestCase, AddStringToExcFromInstanceTest): 631 | """Class for tests of add_string_to_exception on ZeroDivisionError.""" 632 | 633 | exc_type = ZeroDivisionError 634 | args = ('', '', '', '', '') 635 | 636 | 637 | def get_instance(klass): 638 | """Get instance for class by bruteforcing the parameters. 639 | 640 | Construction is attempted with a decreasing number of arguments so that 641 | the instanciated object has as many non-null attributes set as possible. 642 | This is important not for the creation but when the object gets used 643 | later on. Also, the order of the values has its importance for similar 644 | reasons. 645 | """ 646 | my_unicode = str if sys.version_info >= (3, 0) else unicode 647 | values_tried = [my_unicode(), bytes(), 0] 648 | for nb_arg in reversed(range(6)): 649 | for p in itertools.product(values_tried, repeat=nb_arg): 650 | try: 651 | return klass(*p), p 652 | except (TypeError, AttributeError) as e: 653 | pass 654 | except Exception as e: 655 | print(type(e), e) 656 | return None 657 | 658 | 659 | def generate_add_string_to_exc_tests(): 660 | """Generate tests for add_string_to_exception. 661 | 662 | This function dynamically creates tests cases for the function 663 | add_string_to_exception for as many Exception subclasses as possible. 664 | This is not used at the moment because the generated classes need to 665 | be added in the global namespace and there is no good way to do this. 666 | However, it may be a good idea to call this when new versions of 667 | Python are released to ensure we handle all exceptions properly (and 668 | find the tests to be added manually if need be). 669 | """ 670 | for klass in get_subclasses(Exception): 671 | r = get_instance(klass) 672 | if r is not None: 673 | _, p = r 674 | class_name = ("NameForAddStringToExcFromInstanceTest" 675 | + klass.__name__ + str(id(klass))) 676 | assert class_name not in globals(), class_name 677 | globals()[class_name] = type( 678 | class_name, 679 | (AddStringToExcFromInstanceTest, unittest_module.TestCase), 680 | {'exc_type': klass, 'args': p}) 681 | 682 | 683 | if __name__ == '__main__': 684 | print(sys.version_info) 685 | unittest_module.main() 686 | -------------------------------------------------------------------------------- /didyoumean/didyoumean_re.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """Regular expressions to parse error messages.""" 3 | import re 4 | 5 | # https://docs.python.org/3/reference/grammar.html 6 | IDENTIFIER = r"[^\d\W]\w*" 7 | VAR_NAME = IDENTIFIER 8 | # This ATTR_NAME may be misleading because using getattr, any string may be 9 | # appear in an AttributeError message. The same limitation is probably true 10 | # for any property that can be set to pretty much any string. 11 | # In any case, the whole point of having these pieces of regexp defined here 12 | # is to make things easier to change if we ever have to. 13 | ATTR_NAME = IDENTIFIER 14 | ARG_NAME = IDENTIFIER 15 | TYPE_NAME = r"[\w\.-]+" 16 | MODULE_NAME = r"[\w\.-]+" 17 | FUNC_NAME = r"?" 18 | QUAL_FUNC_NAME = r"(?:{0}\.)*{0}".format(FUNC_NAME) 19 | VARREFBEFOREASSIGN_RE = r"^(?:cannot access )?(?:local|free) variable " \ 20 | r"'(?P{0})' " \ 21 | r"(?:where it is not associated with a value|referenced before assignment)" \ 22 | r"(?: in enclosing scope)?$".format(VAR_NAME) 23 | NAMENOTDEFINED_RE = r"^(?:global )?name '(?P{0})' " \ 24 | r"is not defined$".format(VAR_NAME) 25 | ATTRIBUTEERROR_RE = r"^(?:class |type object )?'?({0})'? " \ 26 | r"(?:object |instance )?has no attribute " \ 27 | r"'(?P{1})'$".format(TYPE_NAME, ATTR_NAME) 28 | MODULEHASNOATTRIBUTE_RE = r"^module '?({0})' has no attribute " \ 29 | r"'(?P{1})'$".format(MODULE_NAME, ATTR_NAME) 30 | UNSUBSCRIPTABLE_RE = r"^'({0})' object " \ 31 | r"(?:is (?:not |un)subscriptable)(?: \(key .*\))?$".format(TYPE_NAME) 32 | CANNOT_BE_INTERPRETED_INT_RE = r"^'({0})' object cannot be interpreted " \ 33 | r"as an integer$".format(TYPE_NAME) 34 | INTEGER_EXPECTED_GOT_RE = r"^" \ 35 | r"(?:range\(\) integer \w+ argument expected|expected integer), " \ 36 | r"got ({0})(?: object|\.)$".format(TYPE_NAME) 37 | INDICES_MUST_BE_INT_RE = "^{0} ind(?:ices|ex)? must be " \ 38 | r"(?:an integer|integers)(?: or slices)?, not ({0})$".format(TYPE_NAME) 39 | UNEXPECTED_KEYWORDARG_RE = r"^(?P{1})\(\) " \ 40 | r"got an unexpected keyword argument " \ 41 | r"'(?P{0})'$".format(ARG_NAME, QUAL_FUNC_NAME) 42 | UNEXPECTED_KEYWORDARG_SUGG_RE = r"^(?P{1})\(\) " \ 43 | r"got an unexpected keyword argument " \ 44 | r"'(?P{0})'\. Did you mean '(?P.*)'\?$".format(ARG_NAME, QUAL_FUNC_NAME) 45 | UNEXPECTED_KEYWORDARG2_RE = r"^'(?P{0})' is an " \ 46 | r"invalid keyword argument for this function$".format(ARG_NAME) 47 | UNEXPECTED_KEYWORDARG3_RE = r"^invalid keyword arguments to " \ 48 | r"(?P{0})\(\)$".format(FUNC_NAME) 49 | UNEXPECTED_KEYWORDARG4_RE = r"^'(?P{0})' is an " \ 50 | r"invalid keyword argument for " \ 51 | r"(?P{1})\(\)$".format(ARG_NAME, FUNC_NAME) 52 | FUNC_TAKES_NO_KEYWORDARG_RE = r"^(?P{0})(?:\(\))? " \ 53 | r"(?:takes no|does not take) keyword arguments$".format(QUAL_FUNC_NAME) 54 | NOMODULE_RE = r"^No module named '?({0})'?$".format(MODULE_NAME) 55 | CANNOTIMPORT_RE = r"^cannot import name '?(?P{0})'?" \ 56 | r"(?: from '{1}' \(.*\))?$".format(IDENTIFIER, MODULE_NAME) 57 | INDEXOUTOFRANGE_RE = r"^list index out of range$" 58 | ZERO_LEN_FIELD_RE = r"^zero length field name in format$" 59 | MATH_DOMAIN_ERROR_RE = r"^math domain error|" \ 60 | r"expected a positive input(?:, got -\d+)?$" 61 | TOO_MANY_VALUES_UNPACK_RE = r"^too many values " \ 62 | r"to unpack(?: \(expected.*\))?$" 63 | OUTSIDE_FUNCTION_RE = r"^'?(\w+)'? outside function$" 64 | NEED_MORE_VALUES_RE = r"^(?:need more than \d+|not enough) values to unpack" \ 65 | r"(?: \(expected \d+, got \d+\))?$" 66 | UNHASHABLE_RE = r"^(?:cannot use '{0}' as a {0} element \(?)?" \ 67 | r"(?:unhashable type: )?'({0})'" \ 68 | r"(?: objects are unhashable)?.?$".format(TYPE_NAME) 69 | MISSING_PARENT_RE = r"^Missing parentheses in call to " \ 70 | r"'(?P{0})'(?:. Did you mean.*)?$".format(FUNC_NAME) 71 | INVALID_LITERAL_RE = r"^invalid literal for (\w+)\(\) with base \d+: '(.*)'$" 72 | NB_ARG_RE = r"^(?P{0})(?:\(\) takes| expected) " \ 73 | r"(?:exactly |at least |at most |from )?(?Pno|\d+|\d+ to \d+) " \ 74 | r"(?:positional |non-keyword )?arguments?,? " \ 75 | r"\(?(?:but |got )?(?P\d+)" \ 76 | r"(?: were given| was given| given)?\)?" \ 77 | r"(?:\. Did you forget 'self' in the function definition\?)?" \ 78 | r"$".format(QUAL_FUNC_NAME) 79 | MISSING_POS_ARG_RE = r"^(?P{0})\(\) missing \d+ required positional " \ 80 | r"arguments?: .*$".format(QUAL_FUNC_NAME) 81 | INVALID_SYNTAX_RE = r"^(?:invalid syntax(?:. Maybe you meant .*)?|" \ 82 | r"invalid syntax \(expected '.*'\)|" \ 83 | r"expected '.*')$" 84 | FUNC_PARAM_CANNOT_BE_PARENTH_RE = r"^Function parameters cannot " \ 85 | r"be parenthesized$" 86 | INVALID_COMP_RE = r"^invalid comparison$" 87 | INVALID_TOKEN_RE = r"^invalid token$" 88 | LEADING_ZEROS_RE = r"^leading zeros in decimal integer literals are " \ 89 | r"not permitted; use an 0o prefix for octal integers$" 90 | EXC_GROUP_PARENTH_RE = r"^(?:exception group|multiple exception types) " \ 91 | r"must be parenthesized$" 92 | EXPECTED_LENGTH_RE = r"^expected length (\d+), got (\d+)$" 93 | FUTURE_FIRST_RE = r"^(?:from )?__future__ (?:imports|statements) must " \ 94 | r"(?:occur|appear) at (?:the )?beginning of (?:the )?file$" 95 | FUTURE_FEATURE_NOT_DEF_RE = r"^future feature (\w+) is not defined$" 96 | RESULT_TOO_MANY_ITEMS_RE = r"^(?P{0})\(\) result has too many items" \ 97 | r"$".format(FUNC_NAME) 98 | UNQUALIFIED_EXEC_RE = r"^unqualified exec is not allowed in function '{0}' " \ 99 | r"(?:because )?it" \ 100 | r" (?:is a nested function|" \ 101 | r"contains a nested function with free variables)$".format(FUNC_NAME) 102 | IMPORTSTAR_RE = r"^import \* (?:only allowed at module level|" \ 103 | r"is not allowed in function '{0}' because it (?:is )?" \ 104 | r"(?:is a nested function|" \ 105 | r"(?:)contains a nested function with free variables))" \ 106 | r"$".format(FUNC_NAME) 107 | UNSUPPORTED_OP_RE = r"^unsupported operand type\(s\) for (?P.*): " \ 108 | r"'(?P{0})' and '(?P{0})'" \ 109 | r"$".format(TYPE_NAME) 110 | UNSUPPORTED_OP_SUGG_RE = r"^unsupported operand type\(s\) for (?P.*): " \ 111 | r"'(?P{0})' and '(?P{0})'\. Did you mean \"(?P.*)\"\?" \ 112 | r"$".format(TYPE_NAME) 113 | BAD_OPERAND_UNARY_RE = r"^(?:bad|unsupported) operand type for " \ 114 | r"(?:unary )?(.*): '(.*)'$" 115 | OBJ_DOES_NOT_SUPPORT_RE = r"^\'({0})\' object (?:does not|doesn't) support " \ 116 | r"(.*)$".format(TYPE_NAME) 117 | CANNOT_CONCAT_RE = r"^cannot concatenate '({0})' and '({0})' " \ 118 | r"objects$".format(TYPE_NAME) 119 | ONLY_CONCAT_RE = r'^can only concatenate {0} \(not "{0}"\) ' \ 120 | r"to {0}$".format(TYPE_NAME) 121 | CANT_CONVERT_RE = r"^Can't convert '({0})' object to ({0}) " \ 122 | r"implicitly$".format(TYPE_NAME) 123 | MUST_BE_TYPE1_NOT_TYPE2_RE = r"^must be ({0}), not ({0})$".format(TYPE_NAME) 124 | NOT_CALLABLE_RE = r"^'({0})' object is not callable$".format(TYPE_NAME) 125 | DESCRIPT_REQUIRES_TYPE_RE = r"^descriptor '(\w+)' " \ 126 | r"(?:requires a|for) '({0})' " \ 127 | r"(?:object but received|objects doesn't apply to) (?:a )?" \ 128 | r"'({0})'(?:| object)$".format(TYPE_NAME) 129 | ARG_NOT_ITERABLE_RE = r"^(?:argument of type )?'({0})'" \ 130 | r"(?: object)? is not (?:a container or )?iterable$".format(TYPE_NAME) 131 | MUST_BE_CALLED_WITH_INST_RE = r"^unbound method (\w+)\(\) must be called " \ 132 | r"with ({0}) instance as first argument " \ 133 | r"\(got ({0}) instance instead\)$".format(TYPE_NAME) 134 | OBJECT_HAS_NO_FUNC_RE = r"^(?:object of type )?'({0})' has no " \ 135 | r"(\w+)(?:\(\))?$".format(TYPE_NAME) 136 | INSTANCE_HAS_NO_METH_RE = r"^({0}) instance has no " \ 137 | r"({1}) method$".format(TYPE_NAME, ATTR_NAME) 138 | NO_BINDING_NONLOCAL_RE = r"^no binding for nonlocal '({0})' " \ 139 | r"found$".format(VAR_NAME) 140 | NONLOCAL_AT_MODULE_RE = r"^nonlocal declaration not allowed at module level$" 141 | UNEXPECTED_EOF_RE = r"^unexpected EOF while parsing$" 142 | NO_SUCH_FILE_RE = r"^No such file or directory$" 143 | TIME_DATA_DOES_NOT_MATCH_FORMAT_RE = r"^time data " \ 144 | r"(?P.*) does not match format (?P.*)$" 145 | MAX_RECURSION_DEPTH_RE = r"^maximum recursion depth exceeded.*$" 146 | SIZE_CHANGED_DURING_ITER_RE = r"^(\w+) changed size during iteration$" 147 | EXC_MUST_DERIVE_FROM_RE = r"^exceptions must .*derive.*from.*BaseException.*$" 148 | UNORDERABLE_TYPES_RE = r"^unorderable types: " \ 149 | r"{0}(?:\(\))? [<=>]+ {0}(?:\(\))?$".format(TYPE_NAME) 150 | OP_NOT_SUPP_BETWEEN_INSTANCES_RE = r"^'[<=>]+' not supported between " \ 151 | r"instances of '{0}' and '{0}'$".format(TYPE_NAME) 152 | 153 | ALL_REGEXPS = dict((k, v) 154 | for k, v in dict(locals()).items() 155 | if k.endswith('_RE')) 156 | 157 | 158 | def match(pattern, string): 159 | """Wrap function from the re module. 160 | 161 | Wrapper around re.match to be able to import this module as re 162 | without having name collisions. 163 | """ 164 | return re.match(pattern, string) 165 | 166 | 167 | if __name__ == '__main__': 168 | for name, val in ALL_REGEXPS.items(): 169 | if not (val.startswith('^') and val.endswith('$')): 170 | print("Missing ^$ for ", name) 171 | -------------------------------------------------------------------------------- /didyoumean/didyoumean_re_tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """Unit tests for regexps from didyoumean_re.py.""" 3 | import didyoumean_re as re 4 | import sys 5 | from didyoumean_internal import get_subclasses 6 | from didyoumean_common_tests import unittest_module 7 | 8 | NO_GROUP = ((), dict()) 9 | # Various technical flags to check more that meet the eyes in tests 10 | # Flag used to check that a text only matches the expected regexp and not 11 | # the other to ensure we do not have ambiguous/double regexp matching. 12 | CHECK_OTHERS_DONT_MATCH = True 13 | # Flag to check that the regexp provided does correspond to a regexp 14 | # listed in re.ALL_REGEXPS 15 | CHECK_RE_LISTED = True 16 | # Flag to check that the name used for the regexp in re.ALL_REGEXPS 17 | # does match the naming convention 18 | CHECK_RE_NAME = True 19 | # Flag to check that the regex does match a few conventions such as: 20 | # starts with ^, ends with $. 21 | CHECK_RE_VALUE = True 22 | 23 | 24 | class RegexTests(unittest_module.TestCase): 25 | """Tests to check that error messages match the regexps.""" 26 | 27 | def assertRegexp(self, text, regex, msg=None): 28 | """Wrapper around the different names for assertRegexp....""" 29 | for name in ['assertRegex', 'assertRegexpMatches']: 30 | if hasattr(self, name): 31 | return getattr(self, name)(text, regex, msg) 32 | self.assertTrue(False, "No method to check assertRegexp") 33 | 34 | def assertNotRegexp(self, text, regex, msg=None): 35 | """Wrapper around the different names for assertRegexpNot....""" 36 | for name in ['assertNotRegex', 'assertNotRegexpMatches']: 37 | if hasattr(self, name): 38 | return getattr(self, name)(text, regex, msg) 39 | self.assertTrue(False, "No method to check assertNotRegexp") 40 | 41 | def re_matches(self, text, regexp, results): 42 | """Check that text matches regexp and gives the right match groups. 43 | 44 | result is a tuple containing the expected return values for groups() 45 | and groupdict(). 46 | """ 47 | groups, named_groups = results 48 | self.assertRegexp(text, regexp) # does pretty printing 49 | match = re.match(regexp, text) 50 | self.assertTrue(match) 51 | self.assertEqual(groups, match.groups()) 52 | self.assertEqual(named_groups, match.groupdict()) 53 | self.check_more_about_re(text, regexp) 54 | 55 | def check_more_about_re(self, text, regexp): 56 | """Check various properties about the regexp. 57 | 58 | Properties checked are configurable via global constants. These 59 | properties are not stricly speaking required but they help to 60 | detect potential issues much more quickly. 61 | """ 62 | if CHECK_RE_VALUE: 63 | self.assertTrue(regexp.startswith('^')) 64 | self.assertTrue(regexp.endswith('$')) 65 | found = False 66 | for other_name, other_re in re.ALL_REGEXPS.items(): 67 | if other_re == regexp: 68 | found = True 69 | if CHECK_RE_NAME: 70 | self.assertTrue(other_name.endswith('_RE')) 71 | elif CHECK_OTHERS_DONT_MATCH: 72 | details = "text '%s' matches %s (on top of %s)" % \ 73 | (text, other_name, regexp) 74 | self.assertNotRegexp(text, other_re, details) 75 | no_match = re.match(other_re, text) 76 | self.assertEqual(no_match, None, details) 77 | if CHECK_RE_LISTED: 78 | self.assertTrue(found) 79 | 80 | def test_var_name(self): 81 | """Test VAR_NAME.""" 82 | regex = r"^" + re.VAR_NAME + r"$" 83 | real_names = set(locals().keys()) | set(globals().keys()) 84 | names = ['a', 'a1', '_a1', 'aa_bb'] + list(real_names) 85 | for name in names: 86 | self.assertRegexp(name, regex) 87 | for name in ['1a']: 88 | self.assertNotRegexp(name, regex) 89 | 90 | def test_attr_name(self): 91 | """Test ATTR_NAME.""" 92 | regex = r"^" + re.ATTR_NAME + r"$" 93 | # Tests based on hardcoded values 94 | attrs = ["do_stuff", "__magic__"] 95 | for attr in attrs: 96 | self.assertRegexp(attr, regex) 97 | for attr in ["1a"]: 98 | self.assertNotRegexp(attr, regex) 99 | # Tests based on introspection 100 | for o in get_subclasses(object): 101 | try: 102 | real_attrs = dir(o) 103 | except AttributeError: 104 | real_attrs = set() 105 | for attr in real_attrs: 106 | # Ignore weird case like spec_for_test.test_distutils from 107 | if attr.startswith("spec_for_test."): 108 | continue 109 | # Ignore weird case like עִברִית from 110 | if ".unicode." in str(o): 111 | continue 112 | self.assertRegexp(attr, regex, "for {0} from {1}".format(attr, str(o))) 113 | 114 | def test_type_name(self): 115 | """Test TYPE_NAME.""" 116 | regex = r"^" + re.TYPE_NAME + r"$" 117 | real_types = set(c.__name__ for c in get_subclasses(object)) 118 | types = [ 119 | 'str', 120 | 'int', 121 | 'method-wrapper', 122 | 'builtin_function', 123 | 'builtin_function_or_method', 124 | '_io.TextIOWrapper' 125 | ] + list(real_types) 126 | for type_ in types: 127 | if type_ not in ('symtable entry', 'builtin method', 'Counter optimizer'): 128 | self.assertRegexp(type_, regex) 129 | 130 | def test_func_name(self): 131 | """Test FUNC_NAME.""" 132 | regex = r"^" + re.FUNC_NAME + r"$" 133 | real_funcs = [lambda x:x, range, dir, dict.get, 134 | list.index, classmethod] # TODO 135 | real_func_names = [f.__name__ for f in real_funcs] 136 | more_func_names = ['get', 'range', '', 'print'] 137 | for func in real_func_names + more_func_names: 138 | self.assertRegexp(func, regex) 139 | 140 | def test_qual_func_name(self): 141 | """Test QUAL_FUNC_NAME.""" 142 | regex = r"^" + re.QUAL_FUNC_NAME + r"$" 143 | real_funcs = [lambda x:x, range, dir, dict.get, 144 | list.index, classmethod] # TODO 145 | real_func_names = [f.__qualname__ for f in real_funcs 146 | if hasattr(f, "__qualname__")] 147 | more_func_names = ['struct.pack', 'deque.index', 'Struct.pack'] 148 | for func in real_func_names + more_func_names: 149 | self.assertRegexp(func, regex) 150 | 151 | def test_module_name(self): 152 | """Test MODULE_NAME.""" 153 | regex = r"^" + re.MODULE_NAME + r"$" 154 | real_modules = set(sys.modules.keys()) 155 | modules = ['sys', 'unittest.runner'] + list(real_modules) 156 | for mod in modules: 157 | if not mod.startswith('$coverage'): 158 | self.assertRegexp(mod, regex) 159 | 160 | def test_unbound_assignment(self): 161 | """Test VARREFBEFOREASSIGN_RE.""" 162 | msgs = [ 163 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/../3.10/PyPy/PyPy3 164 | "local variable 'some_var' referenced before assignment", 165 | "free variable 'some_var' referenced before assignment " 166 | "in enclosing scope", 167 | # Python 3.11 168 | "cannot access free variable 'some_var' where it is not " 169 | "associated with a value in enclosing scope", 170 | "cannot access local variable 'some_var' where it is not " 171 | "associated with a value", 172 | ] 173 | groups = ('some_var',) 174 | named_groups = {'name': 'some_var'} 175 | results = (groups, named_groups) 176 | for msg in msgs: 177 | self.re_matches(msg, re.VARREFBEFOREASSIGN_RE, results) 178 | 179 | def test_name_not_defined(self): 180 | """Test NAMENOTDEFINED_RE.""" 181 | msgs = [ 182 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy3 183 | "name 'some_name' is not defined", 184 | # Python 2.6/2.7/3.2/3.3/PyPy/PyPy3 185 | "global name 'some_name' is not defined", 186 | ] 187 | groups = ('some_name',) 188 | named_groups = {'name': 'some_name'} 189 | results = (groups, named_groups) 190 | for msg in msgs: 191 | self.re_matches(msg, re.NAMENOTDEFINED_RE, results) 192 | 193 | def test_attribute_error(self): 194 | """Test ATTRIBUTEERROR_RE.""" 195 | group_msg = { 196 | ('some.class', 'attri'): [ 197 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy/PyPy3 198 | "'some.class' object has no attribute 'attri'", 199 | ], 200 | ('SomeClass', 'attri'): [ 201 | # Python 2.6/2.7/PyPy 202 | "SomeClass instance has no attribute 'attri'", 203 | # Python 2.6/2.7 204 | "class SomeClass has no attribute 'attri'", 205 | # Python 3.2/3.3/3.4/3.5 206 | "type object 'SomeClass' has no attribute 'attri'", 207 | ], 208 | } 209 | for groups, msgs in group_msg.items(): 210 | _, attr = groups 211 | named_groups = {'attr': attr} 212 | results = (groups, named_groups) 213 | for msg in msgs: 214 | self.re_matches(msg, re.ATTRIBUTEERROR_RE, results) 215 | 216 | def test_module_attribute_error(self): 217 | """Test MODULEHASNOATTRIBUTE_RE.""" 218 | # Python 3.5 219 | msg = "module 'some_module' has no attribute 'attri'" 220 | groups = ('some_module', 'attri') 221 | _, attr = groups 222 | named_groups = {'attr': attr} 223 | results = (groups, named_groups) 224 | self.re_matches(msg, re.MODULEHASNOATTRIBUTE_RE, results) 225 | 226 | def test_cannot_import(self): 227 | """Test CANNOTIMPORT_RE.""" 228 | msgs = [ 229 | # Python 2.6/2.7/3.2/3.3 230 | "cannot import name pie", 231 | # Python 3.4/3.5/PyPy/PyPy3 232 | "cannot import name 'pie'", 233 | # Python 3.7 234 | "cannot import name 'pie' from 'math' (/some/path)", 235 | "cannot import name 'pie' from 'math' (unknown location)" 236 | ] 237 | name = 'pie' 238 | groups = (name, ) 239 | named_groups = {'name': name} 240 | results = (groups, named_groups) 241 | for msg in msgs: 242 | self.re_matches(msg, re.CANNOTIMPORT_RE, results) 243 | 244 | def test_no_module_named(self): 245 | """Test NOMODULE_RE.""" 246 | msgs = [ 247 | # Python 2.6/2.7/3.2/PyPy/PyPy3 248 | "No module named fake_module", 249 | # Python 3.3/3.4/3.5 250 | "No module named 'fake_module'", 251 | ] 252 | groups = ('fake_module',) 253 | results = (groups, dict()) 254 | for msg in msgs: 255 | self.re_matches(msg, re.NOMODULE_RE, results) 256 | 257 | def test_index_out_of_range(self): 258 | """Test INDEXOUTOFRANGE_RE.""" 259 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy/PyPy3 260 | msg = "list index out of range" 261 | self.re_matches(msg, re.INDEXOUTOFRANGE_RE, NO_GROUP) 262 | 263 | def test_unsubscriptable(self): 264 | """Test UNSUBSCRIPTABLE_RE.""" 265 | msgs = [ 266 | # Python 2.6 267 | "'function' object is unsubscriptable", 268 | # Python 3.2/3.3/3.4/3.5/PyPy/PyPy3 269 | "'function' object is not subscriptable", 270 | # PyPy3.6 271 | "'function' object is not subscriptable (key 0)", 272 | ] 273 | groups = ('function',) 274 | results = (groups, dict()) 275 | for msg in msgs: 276 | self.re_matches(msg, re.UNSUBSCRIPTABLE_RE, results) 277 | 278 | def test_unexpected_kw_arg(self): 279 | """Test UNEXPECTED_KEYWORDARG_RE.""" 280 | msgs = [ 281 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy/PyPy3 282 | ("some_func() got an unexpected keyword argument 'a'", 283 | ('some_func', 'a')), 284 | ("() got an unexpected keyword argument 'a'", 285 | ('', 'a')), 286 | # Python 3.10 287 | ("MyClass.func() got an unexpected keyword argument 'a'", 288 | ('MyClass.func', 'a')), 289 | ] 290 | for msg, groups in msgs: 291 | func, kw_arg = groups 292 | named_groups = {'arg': kw_arg, 'func': func} 293 | results = (groups, named_groups) 294 | self.re_matches(msg, re.UNEXPECTED_KEYWORDARG_RE, results) 295 | 296 | def test_unexpected_kw_arg_sugg(self): 297 | """Test UNEXPECTED_KEYWORDARG_SUGG_RE.""" 298 | msgs = [ 299 | # Python 3.13 300 | ("MyClass.func() got an unexpected keyword argument 'abcdf'. Did you mean 'abcdef'?", 301 | ('MyClass.func', 'abcdf', 'abcdef')), 302 | ] 303 | for msg, groups in msgs: 304 | func, kw_arg, sugg = groups 305 | named_groups = {'arg': kw_arg, 'func': func, 'sugg': sugg} 306 | results = (groups, named_groups) 307 | self.re_matches(msg, re.UNEXPECTED_KEYWORDARG_SUGG_RE, results) 308 | 309 | def test_unexpected_kw_arg2(self): 310 | """Test UNEXPECTED_KEYWORDARG2_RE.""" 311 | # Python 2.6/2.7/3.2/3.3/3.4/3.5 312 | msg = "'this_doesnt_exist' is an invalid " \ 313 | "keyword argument for this function" 314 | groups = ('this_doesnt_exist', ) 315 | kw_arg, = groups 316 | named_groups = {'arg': kw_arg} 317 | results = (groups, named_groups) 318 | self.re_matches(msg, re.UNEXPECTED_KEYWORDARG2_RE, results) 319 | 320 | def test_unexpected_kw_arg3(self): 321 | """Test UNEXPECTED_KEYWORDARG3_RE.""" 322 | # PyPy/PyPy3 323 | msg = "invalid keyword arguments to print()" 324 | func = 'print' 325 | groups = (func, ) 326 | named_groups = {'func': func} 327 | results = (groups, named_groups) 328 | self.re_matches(msg, re.UNEXPECTED_KEYWORDARG3_RE, results) 329 | 330 | def test_unexpected_kw_arg4(self): 331 | """Test UNEXPECTED_KEYWORDARG4_RE.""" 332 | # Python 3.7 333 | msgs = [ 334 | ("'this_doesnt_exist' is an invalid keyword argument for int()", 335 | ('this_doesnt_exist', 'int')), 336 | ("'end_' is an invalid keyword argument for print()", 337 | ('end_', 'print')), 338 | ("'cmp' is an invalid keyword argument for sort()", 339 | ('cmp', 'sort')), 340 | ] 341 | for msg, groups in msgs: 342 | kw_arg, func = groups 343 | named_groups = {'arg': kw_arg, 'func': func} 344 | results = (groups, named_groups) 345 | self.re_matches(msg, re.UNEXPECTED_KEYWORDARG4_RE, results) 346 | 347 | def test_func_takes_no_kwarg(self): 348 | """Test FUNC_TAKES_NO_KEYWORDARG_RE.""" 349 | msgs = [ 350 | # CPython : most versions 351 | ("get", "get() takes no keyword arguments"), 352 | # CPython nightly (as of 21 January 2017) - Python 3.7 353 | ("get", "get does not take keyword arguments"), 354 | # CPython nightly (as of 7 March 2017) - Python 3.7 355 | ("get", "get() does not take keyword arguments"), 356 | # CPython - Python 3.9 357 | ("dict.get", "dict.get() takes no keyword arguments"), 358 | ] 359 | for func, msg in msgs: 360 | groups = (func, ) 361 | named_groups = {'func': func} 362 | results = (groups, named_groups) 363 | self.re_matches(msg, re.FUNC_TAKES_NO_KEYWORDARG_RE, results) 364 | 365 | def test_zero_length_field(self): 366 | """Test ZERO_LEN_FIELD_RE.""" 367 | # Python 2.6 368 | msg = "zero length field name in format" 369 | self.re_matches(msg, re.ZERO_LEN_FIELD_RE, NO_GROUP) 370 | 371 | def test_math_domain_error(self): 372 | """Test MATH_DOMAIN_ERROR_RE.""" 373 | msgs = [ 374 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy/PyPy3 375 | "math domain error", 376 | "expected a positive input, got -1", 377 | # Python 3.14.0.b 378 | "expected a positive input", 379 | ] 380 | for msg in msgs: 381 | self.re_matches(msg, re.MATH_DOMAIN_ERROR_RE, NO_GROUP) 382 | 383 | def test_too_many_values(self): 384 | """Test TOO_MANY_VALUES_UNPACK_RE.""" 385 | msgs = [ 386 | # Python 2.6/2.7 387 | "too many values to unpack", 388 | # Python 3.2/3.3/3.4/3.5/PyPy3 389 | "too many values to unpack (expected 3)", 390 | # Python 3.14.1 391 | "too many values to unpack (expected 3, got 4)" 392 | ] 393 | for msg in msgs: 394 | self.re_matches(msg, re.TOO_MANY_VALUES_UNPACK_RE, NO_GROUP) 395 | 396 | def test_unhashable_type(self): 397 | """Test UNHASHABLE_RE.""" 398 | msgs = [ 399 | # Python 2.6/2.7/3.2/3.3/3.4/3.5 400 | "unhashable type: 'list'", 401 | # Python 3.14.0.b 402 | "cannot use 'list' as a set element (unhashable type: 'list')", 403 | # PyPy/PyPy3 404 | "'list' objects are unhashable", 405 | ] 406 | groups = ('list',) 407 | results = (groups, dict()) 408 | for msg in msgs: 409 | self.re_matches(msg, re.UNHASHABLE_RE, results) 410 | 411 | def test_cannot_be_interpreted_as_integer(self): 412 | """Test CANNOT_BE_INTERPRETED_INT_RE.""" 413 | msgs = { 414 | "'str' object cannot be interpreted as an integer": 'str', 415 | "'list' object cannot be interpreted as an integer": 'list', 416 | } 417 | for msg, typ in msgs.items(): 418 | results = ((typ,), dict()) 419 | self.re_matches(msg, re.CANNOT_BE_INTERPRETED_INT_RE, results) 420 | 421 | def test_int_expected_got(self): 422 | """Test INTEGER_EXPECTED_GOT_RE.""" 423 | msgs = { 424 | "expected integer, got str object": 'str', 425 | "range() integer end argument expected, got list.": 'list', 426 | "range() integer start argument expected, got list.": 'list', 427 | } 428 | for msg, typ in msgs.items(): 429 | results = ((typ,), dict()) 430 | self.re_matches(msg, re.INTEGER_EXPECTED_GOT_RE, results) 431 | 432 | def test_indices_must_be_int(self): 433 | """Test INDICES_MUST_BE_INT_RE.""" 434 | msgs = { 435 | # Python 2.6, 2.7, 3.2, 3.3, 3.4 436 | "list indices must be integers, not str": "str", 437 | "list indices must be integers or slices, not str": "str", 438 | # Python 3.5 439 | "tuple indices must be integers or slices, not str": "str", 440 | # PyPy 441 | "list index must be an integer, not str": "str", 442 | } 443 | for msg, typ in msgs.items(): 444 | results = ((typ,), dict()) 445 | self.re_matches(msg, re.INDICES_MUST_BE_INT_RE, results) 446 | 447 | def test_outside_function(self): 448 | """Test OUTSIDE_FUNCTION_RE.""" 449 | msgs = [ 450 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy/PyPy3 451 | "'return' outside function", 452 | # PyPy/PyPy3 453 | "return outside function", 454 | ] 455 | groups = ('return',) 456 | results = (groups, dict()) 457 | for msg in msgs: 458 | self.re_matches(msg, re.OUTSIDE_FUNCTION_RE, results) 459 | 460 | def test_nb_positional_argument(self): 461 | """Test NB_ARG_RE.""" 462 | msgs = [ 463 | # Python 2.6/2.7/PyPy/PyPy3 464 | ("some_func() takes exactly 1 argument (2 given)", 465 | 'some_func', '1', '2'), 466 | ("some_func() takes exactly 3 arguments (1 given)", 467 | 'some_func', '3', '1'), 468 | ("some_func() takes no arguments (1 given)", 469 | 'some_func', 'no', '1'), 470 | ("some_func() takes at least 2 non-keyword arguments (0 given)", 471 | 'some_func', '2', '0'), 472 | # Python 3.2 473 | ("some_func() takes exactly 1 positional argument (2 given)", 474 | 'some_func', '1', '2'), 475 | # Python 3.3/3.4/3.5 476 | ("some_func() takes 1 positional argument but 2 were given", 477 | 'some_func', '1', '2'), 478 | ("some_func() takes 0 positional arguments but 1 was given", 479 | 'some_func', '0', '1'), 480 | # Python 3.10 481 | ("MyClass.method() takes 0 positional arguments but 1 was given", 482 | 'MyClass.method', '0', '1'), 483 | # Pypy 3 484 | ("get() takes from 2 to 3 positional arguments but 4 were given", 485 | 'get', '2 to 3', '4'), 486 | # PyPy adds suggestions sometimes: 487 | ("some_func() takes no arguments (1 given)" 488 | ". Did you forget 'self' in the function definition?", 489 | 'some_func', 'no', '1'), 490 | # More!!! 491 | ("get expected at least 1 arguments, got 0", 492 | 'get', '1', '0'), 493 | ("get expected at most 2 arguments, got 3", 494 | 'get', '2', '3'), 495 | ] 496 | for msg, func, exp, nb in msgs: 497 | groups = (func, exp, nb) 498 | named_groups = {'func': func, 'expected': exp, 'actual': nb} 499 | results = (groups, named_groups) 500 | self.re_matches(msg, re.NB_ARG_RE, results) 501 | 502 | def test_missing_positional_arg(self): 503 | """Test MISSING_POS_ARG_RE.""" 504 | msgs = [ 505 | # Python 3.3/3.4/3.5 506 | ("some_func() missing 2 required positional arguments: " 507 | "'much' and 'args'", "some_func"), 508 | ("some_func() missing 1 required positional argument: " 509 | "'much'", "some_func"), 510 | # Python 3.10 511 | ("MyClass.some_method() missing 2 required positional " 512 | "arguments: 'much' and 'args'", "MyClass.some_method"), 513 | ] 514 | for msg, func in msgs: 515 | groups = (func,) 516 | named_groups = {'func': func} 517 | results = (groups, named_groups) 518 | self.re_matches(msg, re.MISSING_POS_ARG_RE, results) 519 | 520 | def test_need_more_values_to_unpack(self): 521 | """Test NEED_MORE_VALUES_RE.""" 522 | msgs = [ 523 | # Python 2.6/2.7/3.2/3.3/3.4/3.5(?)/PyPy3 524 | "need more than 2 values to unpack", 525 | # Python 3.5 526 | "not enough values to unpack (expected 3, got 2)", 527 | ] 528 | for msg in msgs: 529 | self.re_matches(msg, re.NEED_MORE_VALUES_RE, NO_GROUP) 530 | 531 | def test_missing_parentheses(self): 532 | """Test MISSING_PARENT_RE.""" 533 | msgs = [ 534 | # Python 3.4/3.5 (?) 535 | "Missing parentheses in call to 'exec'", 536 | # Python 3.10 537 | "Missing parentheses in call to 'exec'. Did you mean print(...)?", 538 | ] 539 | func = 'exec' 540 | groups = (func,) 541 | named_groups = {'func': func} 542 | results = (groups, named_groups) 543 | for msg in msgs: 544 | self.re_matches(msg, re.MISSING_PARENT_RE, results) 545 | 546 | def test_invalid_literal(self): 547 | """Test INVALID_LITERAL_RE.""" 548 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy/PyPy3 549 | msg = "invalid literal for int() with base 10: 'toto'" 550 | groups = ('int', 'toto') 551 | results = (groups, dict()) 552 | self.re_matches(msg, re.INVALID_LITERAL_RE, results) 553 | 554 | def test_invalid_syntax(self): 555 | """Test INVALID_SYNTAX_RE.""" 556 | msgs = [ 557 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy3 558 | "invalid syntax", 559 | # PyPy 560 | "invalid syntax (expected ':')", 561 | # Python 3.10 562 | "expected ':'", 563 | "invalid syntax. Maybe you meant '==' or ':=' instead of '='?", 564 | ] 565 | for msg in msgs: 566 | self.re_matches(msg, re.INVALID_SYNTAX_RE, NO_GROUP) 567 | 568 | def test_func_param_cannot_be_parenthesized(self): 569 | """Test FUNC_PARAM_CANNOT_BE_PARENTH_RE.""" 570 | # Python 3.11.0 alpha 3 571 | msg = "Function parameters cannot be parenthesized" 572 | self.re_matches(msg, re.FUNC_PARAM_CANNOT_BE_PARENTH_RE, NO_GROUP) 573 | 574 | def test_invalid_comp(self): 575 | """Test INVALID_COMP_RE.""" 576 | # PyPy3 577 | msg = "invalid comparison" 578 | self.re_matches(msg, re.INVALID_COMP_RE, NO_GROUP) 579 | 580 | def test_expected_length(self): 581 | """Test EXPECTED_LENGTH_RE.""" 582 | # PyPy 583 | msg = "expected length 3, got 2" 584 | groups = ('3', '2') 585 | results = (groups, dict()) 586 | self.re_matches(msg, re.EXPECTED_LENGTH_RE, results) 587 | 588 | def test_future_first(self): 589 | """Test FUTURE_FIRST_RE.""" 590 | msgs = [ 591 | # Python 2.6/2.7/3.2/3.3/3.4/3.5 592 | "from __future__ imports must occur at the beginning of the file", 593 | # PyPy/PyPy3 594 | "__future__ statements must appear at beginning of file", 595 | ] 596 | for msg in msgs: 597 | self.re_matches(msg, re.FUTURE_FIRST_RE, NO_GROUP) 598 | 599 | def test_future_feature_not_def(self): 600 | """Test FUTURE_FEATURE_NOT_DEF_RE.""" 601 | # Python 2.6/2.7/3.2/3.3/3.4/3.5/PyPy/PyPy3 602 | msg = "future feature divisio is not defined" 603 | groups = ('divisio',) 604 | results = (groups, dict()) 605 | self.re_matches(msg, re.FUTURE_FEATURE_NOT_DEF_RE, results) 606 | 607 | def test_result_has_too_many_items(self): 608 | """Test RESULT_TOO_MANY_ITEMS_RE.""" 609 | # Python 2.6 610 | msg = "range() result has too many items" 611 | func = 'range' 612 | groups = (func, ) 613 | named_groups = {'func': func} 614 | results = (groups, named_groups) 615 | self.re_matches(msg, re.RESULT_TOO_MANY_ITEMS_RE, results) 616 | 617 | def test_unqualified_exec(self): 618 | """Test UNQUALIFIED_EXEC_RE.""" 619 | msgs = [ 620 | # Python 2.6 621 | "unqualified exec is not allowed in function 'func_name' " 622 | "it is a nested function", 623 | # Python 2.7 624 | "unqualified exec is not allowed in function 'func_name' " 625 | "because it is a nested function", 626 | # Python 2.6 627 | "unqualified exec is not allowed in function 'func_name' " 628 | "it contains a nested function with free variables", 629 | # Python 2.7 630 | "unqualified exec is not allowed in function 'func_name' " 631 | "because it contains a nested function with free variables", 632 | ] 633 | for msg in msgs: 634 | self.re_matches(msg, re.UNQUALIFIED_EXEC_RE, NO_GROUP) 635 | 636 | def test_import_star(self): 637 | """Test IMPORTSTAR_RE.""" 638 | msgs = [ 639 | # Python 2.6 640 | "import * is not allowed in function 'func_name' because it " 641 | "is contains a nested function with free variables", 642 | # Python 2.7 643 | "import * is not allowed in function 'func_name' because it " 644 | "contains a nested function with free variables", 645 | # Python 2.6 646 | "import * is not allowed in function 'func_name' because it " 647 | "is is a nested function", 648 | # Python 2.7 649 | "import * is not allowed in function 'func_name' because it " 650 | "is a nested function", 651 | # Python 3 652 | "import * only allowed at module level" 653 | ] 654 | for msg in msgs: 655 | self.re_matches(msg, re.IMPORTSTAR_RE, NO_GROUP) 656 | 657 | def test_does_not_support(self): 658 | """Test OBJ_DOES_NOT_SUPPORT_RE.""" 659 | msgs = [ 660 | ("'range' object does not support item assignment", 661 | ("range", "item assignment")), 662 | ("'str' object doesn't support item deletion", 663 | ("str", "item deletion")), 664 | ("'set' object does not support indexing", 665 | ("set", "indexing")), 666 | ] 667 | for msg, groups in msgs: 668 | results = (groups, dict()) 669 | self.re_matches(msg, re.OBJ_DOES_NOT_SUPPORT_RE, results) 670 | 671 | def test_cant_convert(self): 672 | """Test CANT_CONVERT_RE.""" 673 | msg = "Can't convert 'int' object to str implicitly" 674 | groups = ('int', 'str') 675 | results = (groups, dict()) 676 | self.re_matches(msg, re.CANT_CONVERT_RE, results) 677 | 678 | def test_must_be_type1_not_type2(self): 679 | """Test MUST_BE_TYPE1_NOT_TYPE2_RE.""" 680 | msg = "must be str, not int" 681 | groups = ('str', 'int') 682 | results = (groups, dict()) 683 | self.re_matches(msg, re.MUST_BE_TYPE1_NOT_TYPE2_RE, results) 684 | 685 | def test_cannot_concat(self): 686 | """Test CANNOT_CONCAT_RE.""" 687 | msg = "cannot concatenate 'str' and 'int' objects" 688 | groups = ('str', 'int') 689 | results = (groups, dict()) 690 | self.re_matches(msg, re.CANNOT_CONCAT_RE, results) 691 | 692 | def test_only_concat(self): 693 | """Test ONLY_CONCAT_RE.""" 694 | msg = 'can only concatenate list (not "set") to list' 695 | self.re_matches(msg, re.ONLY_CONCAT_RE, NO_GROUP) 696 | 697 | def test_unsupported_operand(self): 698 | """Test UNSUPPORTED_OP_RE.""" 699 | msgs = [ 700 | ("unsupported operand type(s) for +: 'int' and 'str'", 701 | '+', 702 | 'int', 703 | 'str'), 704 | ("unsupported operand type(s) for -: 'builtin_function' and 'int'", 705 | '-', 706 | 'builtin_function', 707 | 'int'), 708 | # Python 3.14.0.b 709 | ("unsupported operand type(s) for >>: 'builtin_function_or_method' and '_io.TextIOWrapper'", 710 | '>>', 711 | 'builtin_function_or_method', 712 | '_io.TextIOWrapper'), 713 | ] 714 | for msg, op, t1, t2 in msgs: 715 | groups = op, t1, t2 716 | named_groups = {'op': op, 't1': t1, 't2': t2} 717 | results = (groups, named_groups) 718 | self.re_matches(msg, re.UNSUPPORTED_OP_RE, results) 719 | 720 | def test_unsupported_operand_sugg(self): 721 | """Test UNSUPPORTED_OP_SUGG_RE.""" 722 | msgs = [ 723 | ("unsupported operand type(s) for >>: " 724 | "'builtin_function_or_method' and 'int'. " 725 | "Did you mean \"print(, file=)\"?", 726 | '>>', 727 | 'builtin_function_or_method', 728 | 'int', 729 | 'print(, file=)'), 730 | ("unsupported operand type(s) for -: " 731 | "'builtin_function' and 'int'. " 732 | "Did you mean \"print(<-number>)\"?", 733 | "-", 734 | "builtin_function", 735 | "int", 736 | "print(<-number>)"), 737 | ] 738 | for msg, op, t1, t2, sugg in msgs: 739 | groups = op, t1, t2, sugg 740 | named_groups = {'op': op, 't1': t1, 't2': t2, 'sugg': sugg} 741 | results = (groups, named_groups) 742 | self.re_matches(msg, re.UNSUPPORTED_OP_SUGG_RE, results) 743 | 744 | def test_bad_operand_unary(self): 745 | """Test BAD_OPERAND_UNARY_RE.""" 746 | msgs = [ 747 | ("bad operand type for unary ~: 'set'", ('~', 'set')), 748 | ("bad operand type for abs(): 'set'", ('abs()', 'set')), 749 | ("unsupported operand type for unary neg: 'Foobar'", 750 | ('neg', 'Foobar')), 751 | ] 752 | for msg, groups in msgs: 753 | results = (groups, dict()) 754 | self.re_matches(msg, re.BAD_OPERAND_UNARY_RE, results) 755 | 756 | def test_not_callable(self): 757 | """Test NOT_CALLABLE_RE.""" 758 | msg = "'list' object is not callable" 759 | groups = ('list',) 760 | results = (groups, dict()) 761 | self.re_matches(msg, re.NOT_CALLABLE_RE, results) 762 | 763 | def test_descriptor_requires(self): 764 | """Test DESCRIPT_REQUIRES_TYPE_RE.""" 765 | msgs = [ 766 | "descriptor 'add' requires a 'set' object but received a 'int'", 767 | # Python 3.7 used with coverage 768 | "descriptor 'add' for 'set' objects " 769 | "doesn't apply to 'int' object", 770 | # Python 3.8 771 | "descriptor 'add' for 'set' objects " 772 | "doesn't apply to a 'int' object", 773 | ] 774 | for msg in msgs: 775 | groups = ('add', 'set', 'int') 776 | results = (groups, dict()) 777 | self.re_matches(msg, re.DESCRIPT_REQUIRES_TYPE_RE, results) 778 | 779 | def test_argument_not_iterable(self): 780 | """Test ARG_NOT_ITERABLE_RE.""" 781 | msgs = [ 782 | # Python 2.6/2.7/3.2/3.3/3.4/3.5 783 | "argument of type 'type' is not iterable", 784 | # Python 3.14 785 | "argument of type 'type' is not a container or iterable", 786 | # PyPy/PyPy3 787 | "'type' object is not iterable" 788 | ] 789 | groups = ('type',) 790 | results = (groups, dict()) 791 | for msg in msgs: 792 | self.re_matches(msg, re.ARG_NOT_ITERABLE_RE, results) 793 | 794 | def test_must_be_called_with_instance(self): 795 | """Test MUST_BE_CALLED_WITH_INST_RE.""" 796 | msg = "unbound method add() must be called with set " \ 797 | "instance as first argument (got int instance instead)" 798 | groups = ('add', 'set', 'int') 799 | results = (groups, dict()) 800 | self.re_matches(msg, re.MUST_BE_CALLED_WITH_INST_RE, results) 801 | 802 | def test_object_has_no(self): 803 | """Test OBJECT_HAS_NO_FUNC_RE.""" 804 | msgs = { 805 | # Python 2.6/2.7/3.2/3.3/3.4/3.5 806 | 'len': "object of type 'generator' has no len()", 807 | # PyPy/PyPy3 808 | 'length': "'generator' has no length", 809 | } 810 | for name, msg in msgs.items(): 811 | groups = ('generator', name) 812 | results = (groups, dict()) 813 | self.re_matches(msg, re.OBJECT_HAS_NO_FUNC_RE, results) 814 | 815 | def test_instance_has_no_meth(self): 816 | """Test INSTANCE_HAS_NO_METH_RE.""" 817 | # Python 2.6/2.7 818 | msg = "CustomClass instance has no __call__ method" 819 | class_, method = 'CustomClass', '__call__' 820 | groups = (class_, method) 821 | results = (groups, dict()) 822 | self.re_matches(msg, re.INSTANCE_HAS_NO_METH_RE, results) 823 | 824 | def test_nobinding_nonlocal(self): 825 | """Test NO_BINDING_NONLOCAL_RE.""" 826 | msg = "no binding for nonlocal 'foo' found" 827 | groups = ('foo',) 828 | results = (groups, dict()) 829 | self.re_matches(msg, re.NO_BINDING_NONLOCAL_RE, results) 830 | 831 | def test_nonlocal_at_module_level(self): 832 | """Test NONLOCAL_AT_MODULE_RE.""" 833 | msg = "nonlocal declaration not allowed at module level" 834 | self.re_matches(msg, re.NONLOCAL_AT_MODULE_RE, NO_GROUP) 835 | 836 | def test_unexpected_eof(self): 837 | """Test UNEXPECTED_EOF_RE.""" 838 | msg = "unexpected EOF while parsing" 839 | self.re_matches(msg, re.UNEXPECTED_EOF_RE, NO_GROUP) 840 | 841 | def test_nosuchfile(self): 842 | """Test NO_SUCH_FILE_RE.""" 843 | msg = "No such file or directory" 844 | self.re_matches(msg, re.NO_SUCH_FILE_RE, NO_GROUP) 845 | 846 | def test_timedata_does_not_match_format(self): 847 | """Test TIME_DATA_DOES_NOT_MATCH_FORMAT_RE.""" 848 | msg = "time data '%d %b %y' does not match format '30 Nov 00'" 849 | # 'time data "%d \'%b %y" does not match format \'30 Nov 00\'' 850 | groups = ("'%d %b %y'", "'30 Nov 00'") 851 | named_groups = {'format': "'30 Nov 00'", 'timedata': "'%d %b %y'"} 852 | results = (groups, named_groups) 853 | self.re_matches(msg, re.TIME_DATA_DOES_NOT_MATCH_FORMAT_RE, results) 854 | 855 | def test_invalid_token(self): 856 | """Test INVALID_TOKEN_RE.""" 857 | msg = 'invalid token' 858 | self.re_matches(msg, re.INVALID_TOKEN_RE, NO_GROUP) 859 | 860 | def test_leading_zeros(self): 861 | """Test LEADING_ZEROS_RE.""" 862 | msg = "leading zeros in decimal integer literals are not permitted; " \ 863 | "use an 0o prefix for octal integers" 864 | self.re_matches(msg, re.LEADING_ZEROS_RE, NO_GROUP) 865 | 866 | def test_exception_group_parenthesized(self): 867 | """Test EXC_GROUP_PARENTH_RE.""" 868 | msgs = [ 869 | # Python 3.10 alpha & beta.1 870 | "exception group must be parenthesized", 871 | # Python after 3.10-beta.2 872 | "multiple exception types must be parenthesized", 873 | ] 874 | for msg in msgs: 875 | self.re_matches(msg, re.EXC_GROUP_PARENTH_RE, NO_GROUP) 876 | 877 | def test_exc_must_derive_from(self): 878 | """Test EXC_MUST_DERIVE_FROM_RE.""" 879 | msgs = [ 880 | # Python 2.7 881 | "exceptions must be old-style classes or derived from " 882 | "BaseException, not NoneType", 883 | # Python 3.3 / 3.4 884 | "exceptions must derive from BaseException", 885 | ] 886 | for msg in msgs: 887 | self.re_matches(msg, re.EXC_MUST_DERIVE_FROM_RE, NO_GROUP) 888 | 889 | def test_unorderable_types(self): 890 | """Test UNORDERABLE_TYPES_RE.""" 891 | msgs = [ 892 | # Python 3.2 to 3.5 893 | "unorderable types: str() > int()", 894 | "unorderable types: FoobarClass() <= int()", 895 | # PyPy 896 | "unorderable types: FoobarClass > FoobarClass", 897 | ] 898 | for msg in msgs: 899 | self.re_matches(msg, re.UNORDERABLE_TYPES_RE, NO_GROUP) 900 | 901 | def test_op_not_supported_between_instances(self): 902 | """Test OP_NOT_SUPP_BETWEEN_INSTANCES_RE.""" 903 | msgs = [ 904 | # Python 3.6 905 | "'<' not supported between instances of 'int' and 'NoneType'", 906 | "'>' not supported between instances of 'Foo' and 'Foo'", 907 | ] 908 | for msg in msgs: 909 | self.re_matches(msg, re.OP_NOT_SUPP_BETWEEN_INSTANCES_RE, NO_GROUP) 910 | 911 | def test_max_recursion_depth(self): 912 | """Test MAX_RECURSION_DEPTH_RE.""" 913 | msgs = [ 914 | # Most versions 915 | 'maximum recursion depth exceeded', 916 | # Python 3.11.0a7 when used with coverage - https://github.com/nedbat/coveragepy/issues/1396 917 | 'maximum recursion depth exceeded in comparison', 918 | ] 919 | for msg in msgs: 920 | self.re_matches(msg, re.MAX_RECURSION_DEPTH_RE, NO_GROUP) 921 | 922 | def test_size_changed_during_iter(self): 923 | """Test SIZE_CHANGED_DURING_ITER_RE.""" 924 | msgs = { 925 | "Set": "Set changed size during iteration", 926 | "dictionary": "dictionary changed size during iteration", 927 | } 928 | for name, msg in msgs.items(): 929 | groups = (name, ) 930 | results = (groups, dict()) 931 | self.re_matches(msg, re.SIZE_CHANGED_DURING_ITER_RE, results) 932 | 933 | 934 | if __name__ == '__main__': 935 | print(sys.version_info) 936 | unittest_module.main() 937 | -------------------------------------------------------------------------------- /didyoumean/readme_examples.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 2 | """Code to generate examples in README.md.""" 3 | from didyoumean_internal import add_suggestions_to_exception 4 | import didyoumean_common_tests as common 5 | import datetime 6 | import os 7 | import sys 8 | import traceback 9 | from test.support import captured_stderr 10 | import io 11 | import contextlib 12 | 13 | 14 | def standardise(string): 15 | """Standardise string by removing elements from the environment. 16 | 17 | Replace strings from the environment by the name of the environment 18 | variable. 19 | """ 20 | for var in ["USER"]: 21 | val = os.environ.get(var) 22 | if val is not None: 23 | string = string.replace(val, var.lower()) 24 | return string 25 | 26 | 27 | # Functions to try to get the string representation for an exception 28 | 29 | 30 | def get_except_hook_result_as_str(type_, value, traceback): 31 | # Inspired from "get_message_lines" in Lib/idlelib/run.py 32 | redirect_stderr = getattr(contextlib, 'redirect_stderr', None) 33 | if redirect_stderr is None: 34 | return "redirect_stderr not supported" 35 | err = io.StringIO() 36 | with redirect_stderr(err): 37 | sys.__excepthook__(type_, value, traceback) 38 | return err.getvalue().split("\n")[-2] 39 | 40 | 41 | def get_print_exception_result_as_str(type_, value, traceback_): 42 | # Trying with traceback 43 | with captured_stderr() as output: 44 | traceback.print_exception(type_, value, traceback_) 45 | return output.getvalue().splitlines()[-1] 46 | 47 | 48 | def get_exc_value_with_str(type_, value, traceback_): 49 | return str(value) 50 | 51 | 52 | def get_exc_value_with_repr(type_, value, traceback_): 53 | return repr(value) 54 | 55 | 56 | # Different examples : 57 | # Code examples are grouped by error type then by suggestion type 58 | # Numbers have been added in dict keys just to be able to iterate 59 | # over them and have the result in the wanted order. 60 | EXAMPLES = { 61 | (1, NameError): { 62 | ( 63 | 1, 64 | "Fuzzy matches on existing names " 65 | "(local, builtin, keywords, modules, etc)", 66 | ): [ 67 | "def my_func(foo, bar):\n\treturn foob\n\nmy_func(1, 2)", 68 | "leng([0])", 69 | "import math\nmaths.pi", 70 | "passs", 71 | "def my_func():\n\tfoo = 1\n\tfoob +=1\n\nmy_func()", 72 | ], 73 | (2, "Checking if name is the attribute of a defined object"): [ 74 | "class Duck():\n\tdef __init__(self):\n\t\tquack()" 75 | "\n\tdef quack(self):\n\t\tpass\nd = Duck()", 76 | "import math\npi", 77 | ], 78 | (3, "Looking for missing imports"): [ 79 | "string.ascii_lowercase", 80 | ], 81 | (4, "Looking in missing imports"): [ 82 | "choice", 83 | ], 84 | (5, "Special cases"): [ 85 | "assert j ** 2 == -1", 86 | ], 87 | }, 88 | (2, AttributeError): { 89 | (1, "Fuzzy matches on existing attributes"): [ 90 | "lst = [1, 2, 3]\nlst.appendh(4)", 91 | "import math\nmath.pie", 92 | ], 93 | (2, "Trying to find method with similar meaning (hardcoded)"): [ 94 | "lst = [1, 2, 3]\nlst.add(4)", 95 | "lst = [1, 2, 3]\nlst.get(5, None)", 96 | ], 97 | (3, "Detection of mis-used builtins"): [ 98 | "lst = [1, 2, 3]\nlst.max()", 99 | ], 100 | (4, "Period used instead of comma"): [ 101 | "a, b = 1, 2\nmax(a. b)", 102 | ], 103 | }, 104 | (3, ImportError): { 105 | (1, "Fuzzy matches on existing modules"): [ 106 | "from maths import pi", 107 | ], 108 | (2, "Fuzzy matches on elements of the module"): [ 109 | "from math import pie", 110 | ], 111 | (3, "Looking for import from wrong module"): [ 112 | "from itertools import pi", 113 | ], 114 | }, 115 | (4, TypeError): { 116 | (1, "Fuzzy matches on keyword arguments"): [ 117 | "def my_func(abcde):\n\tpass\n\nmy_func(abcdf=1)", 118 | ], 119 | (2, "Confusion between brackets and parenthesis"): [ 120 | "lst = [1, 2, 3]\nlst(0)", 121 | "def my_func(a):\n\tpass\n\nmy_func[1]", 122 | ], 123 | }, 124 | (5, ValueError): { 125 | (1, "Special cases"): [ 126 | "'Foo{}'.format('bar')", 127 | "import datetime\n" 'datetime.datetime.strptime("%d %b %y", "30 Nov 00")', 128 | ], 129 | }, 130 | (6, SyntaxError): { 131 | (1, "Fuzzy matches when importing from __future__"): [ 132 | "from __future__ import divisio", 133 | ], 134 | (2, "Various"): [ 135 | "return", 136 | ], 137 | }, 138 | (7, MemoryError): { 139 | (1, "Search for a memory-efficient equivalent"): [ 140 | "range(999999999999999)", 141 | ], 142 | }, 143 | (8, OverflowError): { 144 | (1, "Search for a memory-efficient equivalent"): [ 145 | "range(999999999999999)", 146 | ], 147 | }, 148 | (9, (OSError, IOError)): { 149 | (1, "Suggestion for tilde/variable expansions"): [ 150 | "import os\nos.listdir('~')", 151 | ] 152 | }, 153 | (10, RuntimeError): { 154 | (1, "Suggestion to avoid reaching maximum recursion depth"): [ 155 | "global rec\ndef rec(n): return rec(n-1)\nrec(0)" 156 | ], 157 | }, 158 | } 159 | 160 | 161 | def get_code_with_exc_before_and_after(code, exc_types, exception_to_str_func): 162 | exc = common.get_exception(code) 163 | if exc is None: 164 | before = after = "No exception thrown on this version of Python" 165 | else: 166 | type_, value, traceback = exc 167 | if not issubclass(type_, exc_types): 168 | msg = "Wrong exception thrown on this version of Python (%s != %s)" % ( 169 | type_, 170 | exc_types, 171 | ) 172 | before = after = msg 173 | else: 174 | before = exception_to_str_func(type_, value, traceback) 175 | add_suggestions_to_exception(type_, value, traceback) 176 | after = exception_to_str_func(type_, value, traceback) 177 | if before == after: 178 | after += " (unchanged on this version of Python)" 179 | return """```python 180 | {0} 181 | #>>> Before: {1} 182 | #>>> After: {2} 183 | ```""".format( 184 | code, before, after 185 | ) 186 | 187 | 188 | def main(exception_to_str_func): 189 | """Main.""" 190 | print(datetime.datetime.now()) 191 | print( 192 | "## Exception on Python {0} printed with {1}".format( 193 | sys.version, exception_to_str_func.__name__ 194 | ) 195 | ) 196 | for (_, exc_types), exc_examples in sorted(EXAMPLES.items()): 197 | if not isinstance(exc_types, tuple): 198 | exc_types = (exc_types,) 199 | print("### {0}\n".format("/".join(e.__name__ for e in exc_types))) 200 | for (_, desc), codes in sorted(exc_examples.items()): 201 | print("##### {0}\n".format(desc)) 202 | for code in codes: 203 | print( 204 | standardise( 205 | get_code_with_exc_before_and_after( 206 | code, exc_types, exception_to_str_func 207 | ) 208 | ) 209 | ) 210 | 211 | 212 | if __name__ == "__main__": 213 | # Use various output formats to pick to most interesting based on usage 214 | str_functions = [ 215 | get_exc_value_with_str, 216 | get_exc_value_with_repr, 217 | get_except_hook_result_as_str, 218 | get_print_exception_result_as_str, 219 | ] 220 | for exception_to_str_func in str_functions: 221 | main(exception_to_str_func) 222 | -------------------------------------------------------------------------------- /didyoumean/readme_examples_cpython3.11.md: -------------------------------------------------------------------------------- 1 | Tests performed with a recent version of Python to check which errors leads to which type of suggestion. 2 | 3 | Unfortunately, the implemented suggestions are not easy to retrieve programmatically as they are only computed and added when the error is uncaught and displayed to the user. 4 | 5 | The logic happens from the following places: 6 | - Python/pythonrun.c:1212:print_exception(struct exception_print_context *ctx, PyObject *value) 7 | - Python/pythonrun.c:1104:print_exception_suggestions(struct exception_print_context *ctx, ...) 8 | - Python/suggestions.c:269:_Py_Offer_Suggestions(PyObject *exception) 9 | 10 | Using some inspiration from Lib/idlelib/idle_test/test_run.py , a workaround was found. 11 | 12 | Here is the corresponding output with Python 3.11.0b3 (main, Jun 1 2022, 23:51:17) [GCC 9.4.0]. 13 | 14 | 3.11.0b3 (main, Jun 1 2022, 23:51:17) [GCC 9.4.0] 15 | ### NameError 16 | 17 | ##### Fuzzy matches on existing names (local, builtin, keywords, modules, etc) 18 | 19 | ```python 20 | def my_func(foo, bar): 21 | return foob 22 | 23 | my_func(1, 2) 24 | #>>> Before: NameError: name 'foob' is not defined. Did you mean: 'foo'? 25 | #>>> After: NameError: name 'foob' is not defined. Did you mean 'foo' (local)?. Did you mean: 'foo'? 26 | # My comment: the suggestion looks good! (My suggestion is not relevant anymore now) 27 | ``` 28 | ```python 29 | leng([0]) 30 | #>>> Before: NameError: name 'leng' is not defined. Did you mean: 'len'? 31 | #>>> After: NameError: name 'leng' is not defined. Did you mean 'len' (builtin)?. Did you mean: 'len'? 32 | # My comment: the suggestion looks good! (My suggestion is not relevant anymore now) 33 | ``` 34 | ```python 35 | import math 36 | maths.pi 37 | #>>> Before: NameError: name 'maths' is not defined 38 | #>>> After: NameError: name 'maths' is not defined. Did you mean 'math' (local)? 39 | # My comment: the suggestion looks good! (My suggestion is not relevant anymore now) 40 | ``` 41 | ```python 42 | passs 43 | #>>> Before: NameError: name 'passs' is not defined 44 | #>>> After: NameError: name 'passs' is not defined. Did you mean 'pass' (keyword)? 45 | # My comment: the suggestion could include keywords 46 | ``` 47 | ```python 48 | def my_func(): 49 | foo = 1 50 | foob +=1 51 | 52 | my_func() 53 | #>>> Before: UnboundLocalError: cannot access local variable 'foob' where it is not associated with a value 54 | #>>> After: UnboundLocalError: cannot access local variable 'foob' where it is not associated with a value. Did you mean 'foo' (local)? 55 | # My comment: the suggestion could be added for UnboundLocalError 56 | ``` 57 | ##### Checking if name is the attribute of a defined object 58 | 59 | ```python 60 | class Duck(): 61 | def __init__(self): 62 | quack() 63 | def quack(self): 64 | pass 65 | d = Duck() 66 | #>>> Before: NameError: name 'quack' is not defined 67 | #>>> After: NameError: name 'quack' is not defined. Did you mean 'self.quack'? 68 | # My comment: the suggestion looks okay but we can do better! 69 | ``` 70 | ```python 71 | import math 72 | pi 73 | #>>> Before: NameError: name 'pi' is not defined 74 | #>>> After: NameError: name 'pi' is not defined. Did you mean 'math.pi'? 75 | ``` 76 | ##### Looking for missing imports 77 | 78 | ```python 79 | string.ascii_lowercase 80 | #>>> Before: NameError: name 'string' is not defined 81 | #>>> After: NameError: name 'string' is not defined. Did you mean to import string first? 82 | ``` 83 | ##### Looking in missing imports 84 | 85 | ```python 86 | choice 87 | #>>> Before: NameError: name 'choice' is not defined 88 | #>>> After: NameError: name 'choice' is not defined. Did you mean 'choice' from random (not imported)? 89 | ``` 90 | ##### Special cases 91 | 92 | ```python 93 | assert j ** 2 == -1 94 | #>>> Before: NameError: name 'j' is not defined 95 | #>>> After: NameError: name 'j' is not defined. Did you mean '1j' (imaginary unit)? 96 | ``` 97 | ### AttributeError 98 | 99 | ##### Fuzzy matches on existing attributes 100 | 101 | ```python 102 | lst = [1, 2, 3] 103 | lst.appendh(4) 104 | #>>> Before: AttributeError: 'list' object has no attribute 'appendh'. Did you mean: 'append'? 105 | #>>> After: AttributeError: 'list' object has no attribute 'appendh'. Did you mean 'append'?. Did you mean: 'append'? 106 | # My comment: the suggestion looks good! (My suggestion is not relevant anymore now) 107 | ``` 108 | ```python 109 | import math 110 | math.pie 111 | #>>> Before: AttributeError: module 'math' has no attribute 'pie'. Did you mean: 'pi'? 112 | #>>> After: AttributeError: module 'math' has no attribute 'pie'. Did you mean 'pi'?. Did you mean: 'pi'? 113 | # My comment: the suggestion looks good! (My suggestion is not relevant anymore now) 114 | ``` 115 | ##### Trying to find method with similar meaning (hardcoded) 116 | 117 | ```python 118 | lst = [1, 2, 3] 119 | lst.add(4) 120 | #>>> Before: AttributeError: 'list' object has no attribute 'add' 121 | #>>> After: AttributeError: 'list' object has no attribute 'add'. Did you mean 'append'? 122 | ``` 123 | ```python 124 | lst = [1, 2, 3] 125 | lst.get(5, None) 126 | #>>> Before: AttributeError: 'list' object has no attribute 'get' 127 | #>>> After: AttributeError: 'list' object has no attribute 'get'. Did you mean 'obj[key]' with a len() check or try: except: KeyError or IndexError? 128 | ``` 129 | ##### Detection of mis-used builtins 130 | 131 | ```python 132 | lst = [1, 2, 3] 133 | lst.max() 134 | #>>> Before: AttributeError: 'list' object has no attribute 'max' 135 | #>>> After: AttributeError: 'list' object has no attribute 'max'. Did you mean 'max(list)'? 136 | ``` 137 | ##### Period used instead of comma 138 | 139 | ```python 140 | a, b = 1, 2 141 | max(a. b) 142 | #>>> Before: AttributeError: 'int' object has no attribute 'b' 143 | #>>> After: AttributeError: 'int' object has no attribute 'b'. Did you mean to use a comma instead of a period? 144 | ``` 145 | ### ImportError 146 | 147 | ##### Fuzzy matches on existing modules 148 | 149 | ```python 150 | from maths import pi 151 | #>>> Before: ModuleNotFoundError: No module named 'maths' 152 | #>>> After: ModuleNotFoundError: No module named 'maths'. Did you mean 'math'? 153 | ``` 154 | ##### Fuzzy matches on elements of the module 155 | 156 | ```python 157 | from math import pie 158 | #>>> Before: ImportError: cannot import name 'pie' from 'math' (unknown location) 159 | #>>> After: ImportError: cannot import name 'pie' from 'math' (unknown location). Did you mean 'pi'? 160 | ``` 161 | ##### Looking for import from wrong module 162 | 163 | ```python 164 | from itertools import pi 165 | #>>> Before: ImportError: cannot import name 'pi' from 'itertools' (unknown location) 166 | #>>> After: ImportError: cannot import name 'pi' from 'itertools' (unknown location). Did you mean 'from math import pi'? 167 | ``` 168 | ### TypeError 169 | 170 | ##### Fuzzy matches on keyword arguments 171 | 172 | ```python 173 | def my_func(abcde): 174 | pass 175 | 176 | my_func(abcdf=1) 177 | #>>> Before: TypeError: my_func() got an unexpected keyword argument 'abcdf' 178 | #>>> After: TypeError: my_func() got an unexpected keyword argument 'abcdf'. Did you mean 'abcde'? 179 | ``` 180 | ##### Confusion between brackets and parenthesis 181 | 182 | ```python 183 | lst = [1, 2, 3] 184 | lst(0) 185 | #>>> Before: TypeError: 'list' object is not callable 186 | #>>> After: TypeError: 'list' object is not callable. Did you mean 'list[value]'? 187 | ``` 188 | ```python 189 | def my_func(a): 190 | pass 191 | 192 | my_func[1] 193 | #>>> Before: TypeError: 'function' object is not subscriptable 194 | #>>> After: TypeError: 'function' object is not subscriptable. Did you mean 'function(value)'? 195 | ``` 196 | ### ValueError 197 | 198 | ##### Special cases 199 | 200 | ```python 201 | 'Foo{}'.format('bar') 202 | #>>> Before: No exception thrown on this version of Python 203 | #>>> After: No exception thrown on this version of Python 204 | ``` 205 | ```python 206 | import datetime 207 | datetime.datetime.strptime("%d %b %y", "30 Nov 00") 208 | #>>> Before: ValueError: time data '%d %b %y' does not match format '30 Nov 00' 209 | #>>> After: ValueError: time data '%d %b %y' does not match format '30 Nov 00'. Did you mean to swap value and format parameters? 210 | ``` 211 | ### SyntaxError 212 | 213 | ##### Fuzzy matches when importing from __future__ 214 | 215 | ```python 216 | from __future__ import divisio 217 | #>>> Before: SyntaxError: future feature divisio is not defined 218 | #>>> After: SyntaxError: future feature divisio is not defined. Did you mean 'division'? 219 | ``` 220 | ##### Various 221 | 222 | ```python 223 | return 224 | #>>> Before: SyntaxError: 'return' outside function 225 | #>>> After: SyntaxError: 'return' outside function. Did you mean to indent it, 'sys.exit([arg])'? 226 | ``` 227 | ### MemoryError 228 | 229 | ##### Search for a memory-efficient equivalent 230 | 231 | ```python 232 | range(999999999999999) 233 | #>>> Before: No exception thrown on this version of Python 234 | #>>> After: No exception thrown on this version of Python 235 | ``` 236 | ### OverflowError 237 | 238 | ##### Search for a memory-efficient equivalent 239 | 240 | ```python 241 | range(999999999999999) 242 | #>>> Before: No exception thrown on this version of Python 243 | #>>> After: No exception thrown on this version of Python 244 | ``` 245 | ### OSError/OSError 246 | 247 | ##### Suggestion for tilde/variable expansions 248 | 249 | ```python 250 | import os 251 | os.listdir('~') 252 | #>>> Before: FileNotFoundError: [Errno 2] No such file or directory: '~' 253 | #>>> After: FileNotFoundError: [Errno 2] No such file or directory. Did you mean '/home/user' (calling os.path.expanduser)?: '~' 254 | ``` 255 | ### RuntimeError 256 | 257 | ##### Suggestion to avoid reaching maximum recursion depth 258 | 259 | ```python 260 | global rec 261 | def rec(n): return rec(n-1) 262 | rec(0) 263 | #>>> Before: RecursionError: maximum recursion depth exceeded 264 | #>>> After: RecursionError: maximum recursion depth exceeded. Did you mean to avoid recursion (cf http://neopythonic.blogspot.fr/2009/04/tail-recursion-elimination.html), increase the limit with `sys.setrecursionlimit(limit)` (current value is 1000)? 265 | ``` 266 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [metadata] 2 | description-file = README.md 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """Setup.""" 2 | 3 | try: 4 | from setuptools import setup 5 | except ImportError: 6 | from distutils.core import setup 7 | # with the help from http://peterdowns.com/posts/first-time-with-pypi.html 8 | # http://www.scotttorborg.com/python-packaging/minimal.html 9 | setup( 10 | name='BetterErrorMessages', 11 | packages=['didyoumean'], 12 | version='0.4', 13 | description=('Logic to have suggestions in case of errors ' 14 | '(NameError, AttributeError, ImportError, TypeError, etc).'), 15 | author='Sylvain Desodt', 16 | author_email='sylvain.desodt+didyoumean@gmail.com', 17 | url='https://github.com/SylvainDe/DidYouMean-Python', 18 | download_url='https://github.com/SylvainDe/DidYouMean-Python/tarball/0.1', 19 | keywords=[ 20 | 'didyoumean', 21 | 'exception', 22 | 'error', 23 | 'suggestion', 24 | 'excepthook', 25 | 'decorator', 26 | 'contextmanager', 27 | 'typo'], 28 | classifiers=[], 29 | ) 30 | --------------------------------------------------------------------------------