├── .cruft.json ├── .github ├── ISSUE_TEMPLATE │ ├── bug.md │ ├── feature_request.md │ └── question.md ├── PULL_REQUEST_TEMPLATE.md ├── SECURITY.md └── workflows │ ├── build.yml │ ├── install.yml │ ├── tests.yml │ └── update.yml ├── .gitignore ├── .markdownlint.json ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docs ├── adr │ ├── 001-high_level_problem_analysis.md │ ├── 002-initial_program_design.md │ └── adr.md ├── contributing.md ├── editor_integration.md ├── index.md ├── reference.md ├── stylesheets │ ├── extra.css │ └── links.css └── theme │ └── assets │ └── images │ ├── amazon.svg │ ├── archive.svg │ ├── audio.svg │ ├── bluebook.2.svg │ ├── bluebook.bmp │ ├── bluebook.svg │ ├── chi-dna.svg │ ├── code.svg │ ├── csv.svg │ ├── deepmind.svg │ ├── dropbox.svg │ ├── erowid.svg │ ├── gitea.svg │ ├── github.svg │ ├── google-scholar.svg │ ├── hn.svg │ ├── image.svg │ ├── internetarchive.svg │ ├── kubernetes.svg │ ├── mega.svg │ ├── miri.svg │ ├── misc.svg │ ├── newyorktimes.svg │ ├── nlm-ncbi.svg │ ├── openai.svg │ ├── patreon.svg │ ├── plos.svg │ ├── pydo.svg │ ├── reddit.svg │ ├── spreadsheet.svg │ ├── stackexchange.svg │ ├── theguardian.svg │ ├── thenewyorker.svg │ ├── twitter.svg │ ├── txt.svg │ ├── uptontea.svg │ ├── video.svg │ ├── washingtonpost.svg │ ├── wired.svg │ └── worddoc.svg ├── mkdocs.yml ├── pdm.lock ├── pyproject.toml ├── src └── autoimport │ ├── __init__.py │ ├── __main__.py │ ├── entrypoints │ ├── __init__.py │ └── cli.py │ ├── model.py │ ├── py.typed │ ├── services.py │ └── version.py └── tests ├── __init__.py ├── conftest.py ├── e2e ├── __init__.py └── test_cli.py └── unit ├── __init__.py ├── test_entrypoints.py ├── test_extract_package.py ├── test_services.py └── test_version.py /.cruft.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "git@github.com:lyz-code/cookiecutter-python-project.git", 3 | "commit": "68a4cf28a19c6518974b71ecd61b8313c291bd2f", 4 | "context": { 5 | "cookiecutter": { 6 | "project_name": "Autoimport", 7 | "project_slug": "autoimport", 8 | "project_description": "Autoimport missing python libraries.", 9 | "requirements": "autoflake", 10 | "configure_command_line": "True", 11 | "read_configuration_from_yaml": "True", 12 | "github_user": "lyz-code", 13 | "github_token_pass_path": "internet/github.lyz-code.api_token", 14 | "pypi_token_pass_path": "internet/pypi.token", 15 | "test_pypi_token_pass_path": "internet/test.pypi.token", 16 | "author": "Lyz", 17 | "author_email": "lyz-code-security-advisories@riseup.net", 18 | "security_advisories_email": "lyz-code-security-advisories@riseup.net", 19 | "project_underscore_slug": "autoimport", 20 | "_template": "git@github.com:lyz-code/cookiecutter-python-project.git" 21 | } 22 | }, 23 | "directory": null, 24 | "checkout": null 25 | } 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug 3 | about: Create a bug report to help us improve autoimport 4 | labels: bug 5 | --- 6 | 7 | ## Description 8 | 9 | 10 | ## Steps to reproduce 11 | 15 | 16 | ## Current behavior 17 | 18 | 19 | ## Desired behavior 20 | 26 | 27 | ## Environment 28 | 35 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature Request 3 | about: Suggest a new feature or change to autoimport 4 | labels: feature request 5 | --- 6 | 7 | ## Description 8 | 9 | 10 | ## Possible Solution 11 | 12 | 13 | ## Additional context 14 | 15 | 16 | ## Related Issue 17 | 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question 3 | about: Ask a question about how to use autoimport 4 | labels: question 5 | --- 6 | 7 | 14 | 15 | ## Question 16 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | ## Checklist 8 | 9 | * [ ] Add test cases to all the changes you introduce 10 | * [ ] Update the documentation for the changes 11 | -------------------------------------------------------------------------------- /.github/SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We will endeavour to support: 6 | 7 | * The most recent minor release with bug fixes. 8 | * The latest minor release from the last major version for 6 months after a new 9 | major version is released with critical bug fixes. 10 | * All versions if a security vulnerability is found provided: 11 | * Upgrading to a later version is non-trivial. 12 | * Sufficient people are using that version to make support worthwhile. 13 | 14 | ## Reporting a Vulnerability 15 | 16 | If you find what you think might be a security vulnerability with 17 | autoimport, please do not create an issue on github. Instead please 18 | email lyz-code-security-advisories@riseup.net I'll reply to your email promptly 19 | and try to get a patch out ASAP. 20 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Build 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: 7 | - main 8 | workflow_dispatch: 9 | 10 | jobs: 11 | PyPI: 12 | name: Build and publish Python distributions to TestPyPI 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@master 16 | - uses: pdm-project/setup-pdm@main 17 | name: Setup Python and PDM 18 | with: 19 | python-version: 3.9 20 | - name: Build package 21 | run: make build-package 22 | - name: Publish distribution to Test PyPI 23 | uses: pypa/gh-action-pypi-publish@master 24 | with: 25 | password: ${{ secrets.test_pypi_password }} 26 | repository_url: https://test.pypi.org/legacy/ 27 | skip_existing: true 28 | Documentation: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v2 33 | with: 34 | # Number of commits to fetch. 0 indicates all history. 35 | # Default: 1 36 | fetch-depth: 0 37 | - uses: pdm-project/setup-pdm@main 38 | name: Setup Python and PDM 39 | with: 40 | python-version: 3.9 41 | - name: Install dependencies 42 | run: make install 43 | - name: Build the Documentation 44 | run: make build-docs 45 | - name: Deploy 46 | uses: peaceiris/actions-gh-pages@v3 47 | with: 48 | deploy_key: ${{ secrets.ACTIONS_DEPLOY_KEY }} 49 | publish_dir: ./site 50 | -------------------------------------------------------------------------------- /.github/workflows/install.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Install 3 | on: # yamllint disable-line rule:truthy 4 | schedule: 5 | - cron: 21 08 * * * 6 | workflow_dispatch: 7 | 8 | jobs: 9 | Install: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | max-parallel: 4 13 | matrix: 14 | python-version: [3.8, 3.9, '3.10', '3.11'] 15 | steps: 16 | - uses: actions/checkout@v1 17 | - name: Set up Python ${{ matrix.python-version }} 18 | uses: actions/setup-python@v1 19 | with: 20 | python-version: ${{ matrix.python-version }} 21 | - name: Install the program 22 | run: pip install autoimport 23 | - name: Test the program works 24 | run: autoimport --version 25 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Tests 3 | 4 | on: # yamllint disable-line rule:truthy 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | types: [opened, synchronize] 10 | workflow_dispatch: 11 | jobs: 12 | Tests: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | max-parallel: 4 16 | matrix: 17 | python-version: [3.8, 3.9, '3.10', '3.11'] 18 | steps: 19 | - uses: actions/checkout@v1 20 | - name: Set up Python ${{ matrix.python-version }} 21 | uses: actions/setup-python@v1 22 | with: 23 | python-version: ${{ matrix.python-version }} 24 | - name: Install dependencies 25 | run: | 26 | # All these steps are required so that mypy behaves equally than the 27 | # local environment, once mypy supports __pypackages__ try to use the 28 | # github action 29 | pip install virtualenv pdm 30 | virtualenv .venv 31 | source .venv/bin/activate 32 | pdm config python.use_venv True 33 | make install 34 | - name: Test linters 35 | run: make lint 36 | - name: Test type checkers 37 | run: make mypy 38 | - name: Test security 39 | run: make security 40 | - name: Test with pytest 41 | run: make test 42 | - name: Upload Coverage 43 | run: | 44 | pip3 install 'coveralls[toml]' 45 | coveralls --service=github 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 48 | COVERALLS_FLAG_NAME: ${{ matrix.test-name }} 49 | COVERALLS_PARALLEL: true 50 | - name: Test documentation 51 | run: make build-docs 52 | - name: Build the package 53 | run: make build-package 54 | Coveralls: 55 | name: Finish Coveralls 56 | needs: Tests 57 | runs-on: ubuntu-latest 58 | container: python:3-slim 59 | steps: 60 | - name: Finished 61 | run: | 62 | pip3 install coveralls 63 | coveralls --service=github --finish 64 | env: 65 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 66 | -------------------------------------------------------------------------------- /.github/workflows/update.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update 3 | on: # yamllint disable-line rule:truthy 4 | schedule: 5 | - cron: 11 7 * * * 6 | workflow_dispatch: 7 | 8 | jobs: 9 | Update: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@master 13 | with: 14 | persist-credentials: false 15 | fetch-depth: 0 16 | - name: Set up Python ${{ matrix.python-version }} 17 | uses: actions/setup-python@v1 18 | with: 19 | python-version: ${{ matrix.python-version }} 20 | - name: Install dependencies 21 | run: | 22 | # All these steps are required so that mypy behaves equally than the 23 | # local environment, once mypy supports __pypackages__ try to use the 24 | # github action 25 | pip install virtualenv pdm 26 | virtualenv .venv 27 | source .venv/bin/activate 28 | pdm config python.use_venv True 29 | - name: Update requirements 30 | run: make update-production 31 | - name: Install the program 32 | run: make install 33 | - name: Run tests 34 | run: make all 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pdm-python 2 | 3 | # Created by https://www.toptal.com/developers/gitignore/api/python 4 | # Edit at https://www.toptal.com/developers/gitignore?templates=python 5 | 6 | ### Python ### 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | wheels/ 29 | pip-wheel-metadata/ 30 | share/python-wheels/ 31 | *.egg-info/ 32 | .installed.cfg 33 | *.egg 34 | MANIFEST 35 | 36 | # PyInstaller 37 | # Usually these files are written by a python script from a template 38 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 39 | *.manifest 40 | *.spec 41 | 42 | # Installer logs 43 | pip-log.txt 44 | pip-delete-this-directory.txt 45 | 46 | # Unit test / coverage reports 47 | htmlcov/ 48 | .tox/ 49 | .nox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | *.py,cover 57 | .hypothesis/ 58 | .pytest_cache/ 59 | pytestdebug.log 60 | 61 | # Translations 62 | *.mo 63 | *.pot 64 | 65 | # Django stuff: 66 | *.log 67 | local_settings.py 68 | db.sqlite3 69 | db.sqlite3-journal 70 | 71 | # Flask stuff: 72 | instance/ 73 | .webassets-cache 74 | 75 | # Scrapy stuff: 76 | .scrapy 77 | 78 | # Sphinx documentation 79 | docs/_build/ 80 | doc/_build/ 81 | 82 | # PyBuilder 83 | target/ 84 | 85 | # Jupyter Notebook 86 | .ipynb_checkpoints 87 | 88 | # IPython 89 | profile_default/ 90 | ipython_config.py 91 | 92 | # pyenv 93 | .python-version 94 | 95 | # pipenv 96 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 97 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 98 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 99 | # install all needed dependencies. 100 | #Pipfile.lock 101 | 102 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 103 | __pypackages__/ 104 | 105 | # Celery stuff 106 | celerybeat-schedule 107 | celerybeat.pid 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .env 114 | .venv 115 | env/ 116 | venv/ 117 | ENV/ 118 | env.bak/ 119 | venv.bak/ 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # pytype static type analyzer 140 | .pytype/ 141 | 142 | # End of https://www.toptal.com/developers/gitignore/api/python 143 | .flakeheaven_cache 144 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "default": true, 3 | "MD003": { 4 | "style": "atx" 5 | }, 6 | "MD013": { 7 | "line_length": 180 8 | }, 9 | "MD004": { 10 | "style": "asterisk" 11 | }, 12 | "MD007": { 13 | "indent": 4 14 | }, 15 | "MD025": false, 16 | "MD030": false, 17 | "MD035": { 18 | "style": "---" 19 | }, 20 | "MD041": false, 21 | "MD046": false 22 | } 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v3.1.0 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: check-added-large-files 8 | - id: check-docstring-first 9 | - id: check-merge-conflict 10 | - id: end-of-file-fixer 11 | - repo: https://github.com/ambv/black 12 | rev: 21.12b0 13 | hooks: 14 | - id: black 15 | language_version: python3.7 16 | - repo: https://github.com/pre-commit/mirrors-mypy 17 | rev: v0.910-1 18 | hooks: 19 | - name: Run mypy static analysis tool 20 | id: mypy 21 | args: [--no-warn-unused-ignores, --ignore-missing-imports] 22 | files: src 23 | # - repo: https://github.com/flakehell/flakehell/ 24 | # rev: v.0.8.0 25 | # hooks: 26 | # - name: Run flakehell static analysis tool 27 | # id: flakehell 28 | # exclude: index.md 29 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: autoimport 2 | name: autoimport 3 | entry: autoimport 4 | require_serial: true 5 | language: python 6 | language_version: python3 7 | types_or: [cython, pyi, python] 8 | args: [] 9 | minimum_pre_commit_version: '2.9.0' 10 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.6.0 (2024-07-11) 2 | 3 | ## 1.5.0 (2024-05-10) 4 | 5 | ## 1.4.0 (2023-11-23) 6 | 7 | ### Feat 8 | 9 | - support Python 3.11 10 | 11 | ### Fix 12 | 13 | - drop support for Python 3.7 14 | - switch from pdm-pep517 to pdm-backend 15 | 16 | ## 1.3.3 (2023-02-08) 17 | 18 | ### Fix 19 | 20 | - handle well comments in from import statements 21 | - remove dependency with py 22 | 23 | ## 1.3.2 (2022-11-14) 24 | 25 | ### Fix 26 | 27 | - type_checking bug 28 | 29 | ## 1.3.1 (2022-10-14) 30 | 31 | ### Fix 32 | 33 | - Respect two newlines between code and imports. 34 | - allow imports with dots 35 | 36 | ## 1.3.0 (2022-10-14) 37 | 38 | ### Feat 39 | 40 | - added --ignore-init-modules flag (fixes #226) 41 | - added __main__.py file (fixes #210) 42 | 43 | ### Fix 44 | 45 | - ignore `if __name__ == "__main__":` lines for coverage 46 | - ignore incorrect no-value-for-parameter lint 47 | - fixed typing issue 48 | 49 | ## 1.2.3 (2022-09-15) 50 | 51 | ## 1.2.2 (2022-02-16) 52 | 53 | ### Perf 54 | 55 | - remove the uppercap on python 3.11 56 | 57 | ## 1.2.1 (2022-02-11) 58 | 59 | ### Fix 60 | 61 | - correct pdm local installation so mypy works 62 | 63 | ## 1.2.0 (2022-02-09) 64 | 65 | ## 1.1.1 (2022-02-04) 66 | 67 | ### Fix 68 | 69 | - correct the package building 70 | - correct the package building 71 | 72 | ## 1.1.0 (2022-02-03) 73 | 74 | ### Feat 75 | 76 | - use pdm to manage dependencies 77 | - use pdm to manage dependencies 78 | - add support for python 3.10 79 | 80 | ## 1.0.5 (2022-02-03) 81 | 82 | ## 1.0.4 (2022-01-25) 83 | 84 | ## 1.0.3 (2022-01-24) 85 | 86 | ## 1.0.2 (2022-01-21) 87 | 88 | ### Fix 89 | 90 | - don't touch files that are not going to be changed 91 | 92 | ## 1.0.1 (2022-01-21) 93 | 94 | ### Fix 95 | 96 | - remove empty multiline import statements 97 | 98 | ## 1.0.0 (2022-01-18) 99 | 100 | ## 0.11.0 (2021-12-21) 101 | 102 | ### Feat 103 | 104 | - support multi paragraph sections inside the TYPE_CHECKING block 105 | 106 | ## 0.10.0 (2021-12-08) 107 | 108 | ### Feat 109 | 110 | - cli `--config-file` option 111 | 112 | ## 0.9.1 (2021-11-30) 113 | 114 | ### Fix 115 | 116 | - support the removal of from x import y as z statements 117 | 118 | ## 0.9.0 (2021-11-30) 119 | 120 | ### Feat 121 | 122 | - give priority to the common statements over the rest of sources 123 | - added datetime and ModelFactory to the default imports 124 | 125 | ### feat 126 | 127 | - support comments in simple import statementsgn 128 | 129 | ## 0.8.0 (2021-11-23) 130 | 131 | ### Feat 132 | 133 | - extend common statements 134 | 135 | ## 0.7.5 (2021-10-22) 136 | 137 | ### Fix 138 | 139 | - ignore unused import statements with noqa autoimport line 140 | 141 | ## 0.7.4 (2021-10-15) 142 | 143 | ### Fix 144 | 145 | - deprecate python 3.6 146 | 147 | ## 0.7.3 (2021-08-31) 148 | 149 | ### Fix 150 | 151 | - let autoimport handle empty files 152 | 153 | ## 0.7.2 (2021-08-20) 154 | 155 | ### Fix 156 | 157 | - avoid RecursionError when searching for packages inside the project 158 | 159 | ## 0.7.1 (2021-08-20) 160 | 161 | ### Fix 162 | 163 | - prevent autoimport from importing same package many times 164 | 165 | ## 0.7.0 (2021-04-23) 166 | 167 | ### Fix 168 | 169 | - install wheel in the build pipeline 170 | 171 | ### feat 172 | 173 | - add a ci job to test that the program is installable 174 | 175 | ### Perf 176 | 177 | - add new Enum import alias 178 | 179 | ## 0.6.1 (2021-02-02) 180 | 181 | ### Fix 182 | 183 | - respect newlines in the header section 184 | 185 | ### fix 186 | 187 | - respect document trailing newline 188 | 189 | ## 0.6.0 (2021-02-01) 190 | 191 | ### Fix 192 | 193 | - respect shebang and leading comments 194 | 195 | ### feat 196 | 197 | - add BaseModel and Faker to common statements 198 | 199 | ## 0.5.0 (2021-01-25) 200 | 201 | ### Feat 202 | 203 | - add FrozenDateTimeFactory and suppress import statements 204 | 205 | ## 0.4.3 (2020-12-29) 206 | 207 | ### Fix 208 | 209 | - respect try except statements in imports 210 | 211 | ### fix 212 | 213 | - remove autoimport from development dependencies 214 | 215 | ## 0.4.2 (2020-12-29) 216 | 217 | ### Fix 218 | 219 | - wrong import indentation when moving up the imports. 220 | 221 | ### perf 222 | 223 | - common import statements are now run sooner 224 | 225 | ## 0.4.1 (2020-12-29) 226 | 227 | ### Fix 228 | 229 | - make fix_code respect if TYPE_CHECKING statements 230 | 231 | ## 0.4.0 (2020-12-17) 232 | 233 | ### Refactor 234 | 235 | - make _find_package_in_our_project a statimethod 236 | 237 | ### Fix 238 | 239 | - import developing package objects when not in src directory 240 | 241 | ### feat 242 | 243 | - import objects defined in the __all__ special variable 244 | 245 | ## 0.3.0 (2020-12-17) 246 | 247 | ### Feat 248 | 249 | - Add imports of the local package objects 250 | - make autoimport manage the commonly used imports 251 | 252 | ### Refactor 253 | 254 | - move the business logic to the SourceCode entity 255 | 256 | ### fix 257 | 258 | - remove all unused imports instead of just one 259 | 260 | ## 0.2.2 (2020-12-11) 261 | 262 | ### Fix 263 | 264 | - remove unused imported objects in multiline from import statements 265 | 266 | ## 0.2.1 (2020-12-10) 267 | 268 | ### Fix 269 | 270 | - support multiline import statements and multiline strings 271 | 272 | ## 0.2.0 (2020-11-12) 273 | 274 | ### Feat 275 | 276 | - move import statements to the top 277 | 278 | ### Fix 279 | 280 | - **setup.py**: extract the version from source without exec statement 281 | 282 | ## 0.1.1 (2020-10-27) 283 | 284 | ### Fix 285 | 286 | - correct the formatting of single line module docstrings. 287 | - add newline between module docstring, import statements and code. 288 | 289 | ## 0.1.0 (2020-10-23) 290 | 291 | ### Feat 292 | 293 | - create first version of the program (#3) 294 | - create initial project structure 295 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := test 2 | isort = pdm run isort --skip tests/assets src tests setup.py 3 | black = pdm run black --exclude assets --target-version py39 src tests 4 | 5 | .PHONY: install 6 | install: 7 | pdm install --dev 8 | pdm run pre-commit install 9 | 10 | .PHONY: update 11 | update: 12 | @echo "-------------------------" 13 | @echo "- Updating dependencies -" 14 | @echo "-------------------------" 15 | 16 | pdm update --no-sync --update-eager 17 | pdm sync --clean 18 | 19 | @echo "\a" 20 | 21 | .PHONY: update-production 22 | update-production: 23 | @echo "------------------------------------" 24 | @echo "- Updating production dependencies -" 25 | @echo "------------------------------------" 26 | 27 | pdm update --production --no-sync --update-eager 28 | pdm sync --clean 29 | 30 | @echo "\a" 31 | 32 | .PHONY: outdated 33 | outdated: 34 | @echo "-------------------------" 35 | @echo "- Outdated dependencies -" 36 | @echo "-------------------------" 37 | 38 | pdm update --dry-run --unconstrained 39 | 40 | @echo "\a" 41 | 42 | .PHONY: format 43 | format: 44 | @echo "----------------------" 45 | @echo "- Formating the code -" 46 | @echo "----------------------" 47 | 48 | $(isort) 49 | $(black) 50 | 51 | @echo "" 52 | 53 | .PHONY: lint 54 | lint: 55 | @echo "--------------------" 56 | @echo "- Testing the lint -" 57 | @echo "--------------------" 58 | 59 | pdm run flakeheaven lint --exclude assets src/ tests/ setup.py 60 | $(isort) --check-only --df 61 | $(black) --check --diff 62 | 63 | @echo "" 64 | 65 | .PHONY: mypy 66 | mypy: 67 | @echo "----------------" 68 | @echo "- Testing mypy -" 69 | @echo "----------------" 70 | 71 | pdm run mypy src tests 72 | 73 | @echo "" 74 | 75 | .PHONY: test 76 | test: test-code test-examples 77 | 78 | @echo "\a" 79 | 80 | .PHONY: test-code 81 | test-code: 82 | @echo "----------------" 83 | @echo "- Testing code -" 84 | @echo "----------------" 85 | 86 | pdm run pytest --cov-report term-missing --cov src tests ${ARGS} 87 | 88 | @echo "" 89 | 90 | .PHONY: test-examples 91 | test-examples: 92 | @echo "--------------------" 93 | @echo "- Testing examples -" 94 | @echo "--------------------" 95 | 96 | # @find docs/examples -type f -name '*.py' | xargs -I'{}' sh -c 'echo {}; pdm run python {} >/dev/null 2>&1 || (echo "{} failed" ; exit 1)' 97 | @echo "" 98 | 99 | # pdm run pytest docs/examples/* 100 | 101 | @echo "" 102 | 103 | .PHONY: all 104 | all: lint mypy test security build-docs 105 | 106 | @echo "\a" 107 | 108 | .PHONY: clean 109 | clean: 110 | @echo "---------------------------" 111 | @echo "- Cleaning unwanted files -" 112 | @echo "---------------------------" 113 | 114 | rm -rf `find . -name __pycache__` 115 | rm -f `find . -type f -name '*.py[co]' ` 116 | rm -f `find . -type f -name '*.rej' ` 117 | rm -rf `find . -type d -name '*.egg-info' ` 118 | rm -rf `find . -type d -name '.mypy_cache' ` 119 | rm -f `find . -type f -name '*~' ` 120 | rm -f `find . -type f -name '.*~' ` 121 | rm -rf .cache 122 | rm -rf .pytest_cache 123 | rm -rf .mypy_cache 124 | rm -rf htmlcov 125 | rm -f .coverage 126 | rm -f .coverage.* 127 | rm -rf build 128 | rm -rf dist 129 | rm -f src/*.c pydantic/*.so 130 | rm -rf site 131 | rm -rf docs/_build 132 | rm -rf docs/.changelog.md docs/.version.md docs/.tmp_schema_mappings.html 133 | rm -rf codecov.sh 134 | rm -rf coverage.xml 135 | 136 | @echo "" 137 | 138 | .PHONY: docs 139 | docs: test-examples 140 | @echo "-------------------------" 141 | @echo "- Serving documentation -" 142 | @echo "-------------------------" 143 | 144 | pdm run mkdocs serve 145 | 146 | @echo "" 147 | 148 | .PHONY: bump 149 | bump: pull-main bump-version build-package upload-pypi clean 150 | 151 | @echo "\a" 152 | 153 | 154 | .PHONY: pull-main 155 | pull-main: 156 | @echo "------------------------" 157 | @echo "- Updating repository -" 158 | @echo "------------------------" 159 | 160 | git checkout main 161 | git pull 162 | 163 | @echo "" 164 | 165 | .PHONY: build-package 166 | build-package: clean 167 | @echo "------------------------" 168 | @echo "- Building the package -" 169 | @echo "------------------------" 170 | 171 | pdm build 172 | 173 | @echo "" 174 | 175 | .PHONY: build-docs 176 | build-docs: 177 | @echo "--------------------------" 178 | @echo "- Building documentation -" 179 | @echo "--------------------------" 180 | 181 | pdm run mkdocs build --strict 182 | 183 | @echo "" 184 | 185 | .PHONY: upload-pypi 186 | upload-pypi: 187 | @echo "-----------------------------" 188 | @echo "- Uploading package to pypi -" 189 | @echo "-----------------------------" 190 | 191 | twine upload -r pypi dist/* 192 | 193 | @echo "" 194 | 195 | .PHONY: upload-testing-pypi 196 | upload-testing-pypi: 197 | @echo "-------------------------------------" 198 | @echo "- Uploading package to pypi testing -" 199 | @echo "-------------------------------------" 200 | 201 | twine upload -r testpypi dist/* 202 | 203 | @echo "" 204 | 205 | .PHONY: bump-version 206 | bump-version: 207 | @echo "---------------------------" 208 | @echo "- Bumping program version -" 209 | @echo "---------------------------" 210 | 211 | cz bump --changelog --no-verify 212 | git push --tags 213 | git push 214 | 215 | @echo "" 216 | 217 | .PHONY: version 218 | version: 219 | @python -c "import repository_pattern.version; print(repository_pattern.version.version_info())" 220 | 221 | .PHONY: security 222 | security: 223 | @echo "--------------------" 224 | @echo "- Testing security -" 225 | @echo "--------------------" 226 | 227 | # Ignoring 70612 (CVE-2019-8341). It is disputed and no fix is apparent, and 228 | # the related dependencies are only used at dev time so do not present as 229 | # great a risk to users of autoimport. 230 | pdm run safety check --ignore 70612 231 | @echo "" 232 | pdm run bandit -r src 233 | 234 | @echo "" 235 | 236 | .PHONY: release 237 | release: 238 | @echo "----------------------" 239 | @echo "- Generating Release -" 240 | @echo "----------------------" 241 | 242 | pdm run cz bump --changelog 243 | 244 | @echo "" 245 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Autoimport 2 | 3 | [![Actions Status](https://github.com/lyz-code/autoimport/workflows/Tests/badge.svg)](https://github.com/lyz-code/autoimport/actions) 4 | [![Actions Status](https://github.com/lyz-code/autoimport/workflows/Build/badge.svg)](https://github.com/lyz-code/autoimport/actions) 5 | [![Coverage Status](https://coveralls.io/repos/github/lyz-code/autoimport/badge.svg?branch=main)](https://coveralls.io/github/lyz-code/autoimport?branch=main) 6 | 7 | Autoimport missing python libraries. 8 | 9 | Throughout the development of a python program you continuously need to manage 10 | the python import statements either because you need one new object or because 11 | you no longer need it. This means that you need to stop writing whatever you 12 | were writing, go to the top of the file, create or remove the import statement 13 | and then resume coding. 14 | 15 | This workflow break is annoying and almost always unnecessary. `autoimport` 16 | solves this problem if you execute it whenever you have an import error, for 17 | example by configuring your editor to run it when saving the file. 18 | 19 | ## Help 20 | 21 | See [documentation](https://lyz-code.github.io/autoimport) for more details. 22 | 23 | ## Installing 24 | 25 | ```bash 26 | pip install autoimport 27 | ``` 28 | 29 | ## Contributing 30 | 31 | For guidance on setting up a development environment, and how to make 32 | a contribution to *autoimport*, see [Contributing to 33 | autoimport](https://lyz-code.github.io/autoimport/contributing). 34 | 35 | ## License 36 | 37 | GPLv3 38 | -------------------------------------------------------------------------------- /docs/adr/001-high_level_problem_analysis.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyz-code/autoimport/842554d5feb8e6629beecb4596249d0e68621451/docs/adr/001-high_level_problem_analysis.md -------------------------------------------------------------------------------- /docs/adr/002-initial_program_design.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyz-code/autoimport/842554d5feb8e6629beecb4596249d0e68621451/docs/adr/002-initial_program_design.md -------------------------------------------------------------------------------- /docs/adr/adr.md: -------------------------------------------------------------------------------- 1 | [ADR](https://lyz-code.github.io/blue-book/adr/) are short text documents that 2 | captures an important architectural decision made along with its context and 3 | consequences. 4 | 5 | ```mermaid 6 | graph TD 7 | 001[001: High level analysis] 8 | 002[002: Initial Program design] 9 | 10 | 001 -- Extended --> 002 11 | 12 | click 001 "https://lyz-code.github.io/autoimport/adr/001-high_level_problem_analysis" _blank 13 | click 002 "https://lyz-code.github.io/autoimport/adr/002-initial_program_design" _blank 14 | 15 | 001:::draft 16 | 002:::draft 17 | 18 | classDef draft fill:#CDBFEA; 19 | classDef proposed fill:#B1CCE8; 20 | classDef accepted fill:#B1E8BA; 21 | classDef rejected fill:#E8B1B1; 22 | classDef deprecated fill:#E8B1B1; 23 | classDef superseeded fill:#E8E5B1; 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | So you've started using `autoimport` and want to show your gratitude to the project, 2 | depending on your programming skills there are different ways to do so. 3 | 4 | # I don't know how to program 5 | 6 | There are several ways you can contribute: 7 | 8 | * [Open an issue](https://github.com/lyz-code/autoimport/issues/new) if you encounter 9 | any bug or to let us know if you want a new feature to be implemented. 10 | * Spread the word about the program. 11 | * Review the [documentation](https://lyz-code.github.io/autoimport) and try to improve 12 | it. 13 | 14 | # I know how to program in Python 15 | 16 | If you have some python knowledge there are some additional ways to contribute. 17 | We've ordered the [issues](https://github.com/lyz-code/autoimport/issues) in 18 | [milestones](https://github.com/lyz-code/autoimport/milestones), check the issues in 19 | the smaller one, as it's where we'll be spending most of our efforts. Try the 20 | [good first 21 | issues](https://github.com/lyz-code/autoimport/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22), 22 | as they are expected to be easier to get into the project. 23 | 24 | We develop the program with 25 | [TDD](https://en.wikipedia.org/wiki/Test-driven_development), so we expect any 26 | contribution to have it's associated tests. We also try to maintain an updated 27 | [documentation](https://lyz-code.github.io/autoimport) of the project, so think if 28 | your contribution needs to update it. 29 | 30 | We know that the expected code quality is above average. Therefore it might 31 | be challenging to get the initial grasp of the project structure, know how to make the 32 | tests, update the documentation or use all the project technology stack. but please 33 | don't let this fact discourage you from contributing: 34 | 35 | * If you want to develop a new feature, explain how you'd like to do it in the related issue. 36 | * If you don't know how to test your code, do the pull request without the tests 37 | and we'll try to do them for you. 38 | 39 | # Issues 40 | 41 | Questions, feature requests and bug reports are all welcome as issues. 42 | **To report a security vulnerability, please see our [security 43 | policy](https://github.com/lyz-code/autoimport/security/policy) instead.** 44 | 45 | To make it as simple as possible for us to help you, please include the output 46 | of the following call in your issue: 47 | 48 | ```bash 49 | python -c "import autoimport.version; print(autoimport.version.version_info())" 50 | ``` 51 | 52 | or if you have `make` installed, you can use `make version`. 53 | 54 | Please try to always include the above unless you're unable to install `autoimport` or know it's not relevant to your question or 55 | feature request. 56 | 57 | # Pull Requests 58 | 59 | *autoimport* is released regularly so you should see your 60 | improvements release in a matter of days or weeks. 61 | 62 | !!! note 63 | Unless your change is trivial (typo, docs tweak etc.), please create an 64 | issue to discuss the change before creating a pull request. 65 | 66 | If you're looking for something to get your teeth into, check out the ["help 67 | wanted"](https://github.com/lyz-code/autoimport/issues?q=is%3Aopen+is%3Aissue+label%3A%22help+wanted%22) 68 | label on github. 69 | 70 | # Development facilities 71 | 72 | To make contributing as easy and fast as possible, you'll want to run tests and 73 | linting locally. 74 | 75 | !!! note "" 76 | **tl;dr**: use `make format` to fix formatting, `make` to run tests and linting & `make docs` 77 | to build the docs. 78 | 79 | You'll need to have python 3.6, 3.7, or 3.8, virtualenv, git, and make installed. 80 | 81 | * Clone your fork and go into the repository directory: 82 | 83 | ```bash 84 | git clone git@github.com:/autoimport.git 85 | cd autoimport 86 | ``` 87 | 88 | * Set up the virtualenv for running tests: 89 | 90 | ```bash 91 | virtualenv -p `which python3.7` env 92 | source env/bin/activate 93 | ``` 94 | 95 | * Install autoimport, dependencies and configure the 96 | pre-commits: 97 | 98 | ```bash 99 | make install 100 | ``` 101 | 102 | * Checkout a new branch and make your changes: 103 | 104 | ```bash 105 | git checkout -b my-new-feature-branch 106 | ``` 107 | 108 | * Fix formatting and imports: autoimport uses 109 | [black](https://github.com/ambv/black) to enforce formatting and 110 | [isort](https://github.com/timothycrosley/isort) to fix imports. 111 | 112 | ```bash 113 | make format 114 | ``` 115 | 116 | * Run tests and linting: 117 | 118 | ```bash 119 | make 120 | ``` 121 | 122 | There are more sub-commands in Makefile like `test-code`, `test-examples`, 123 | `mypy` or `security` which you might want to use, but generally `make` 124 | should be all you need. 125 | 126 | If you need to pass specific arguments to pytest use the `ARGS` variable, 127 | for example `make test ARGS='-k test_markdownlint_passes'`. 128 | 129 | * Build documentation: If you have changed the documentation, make sure it 130 | builds the static site. Once built it will serve the documentation at 131 | `localhost:8000`: 132 | 133 | ```bash 134 | make docs 135 | ``` 136 | 137 | * Commit, push, and create your pull request. 138 | 139 | We'd love you to contribute to *autoimport*! 140 | -------------------------------------------------------------------------------- /docs/editor_integration.md: -------------------------------------------------------------------------------- 1 | For a smoother experience, you can run `autoimport` automatically each 2 | time you save your file in your editor or when you run `git commit`. 3 | 4 | # Vim 5 | 6 | To integrate `autoimport` into Vim, I recommend using the [ale 7 | plugin](https://github.com/dense-analysis/ale). 8 | 9 | !!! note "" 10 | 11 | If you are new to ALE, check [this 12 | post](https://lyz-code.github.io/blue-book/linux/vim/vim_plugins/#ale). 13 | 14 | `ale` is configured to run `autoimport` automatically by default. 15 | 16 | # [pre-commit](https://pre-commit.com/) 17 | 18 | You can run `autoimport` before we do a commit using the 19 | [pre-commit](https://pre-commit.com/) framework. If you don't know how to use 20 | it, follow [these 21 | guidelines](https://lyz-code.github.io/blue-book/devops/ci/#configuring-pre-commit). 22 | 23 | You'll need to add the following lines to your project's 24 | `.pre-commit-config.yaml` file. 25 | 26 | ```yaml 27 | repos: 28 | - repo: https://github.com/lyz-code/autoimport/ 29 | rev: master 30 | hooks: 31 | - id: autoimport 32 | ``` 33 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | [![Actions Status](https://github.com/lyz-code/autoimport/workflows/Tests/badge.svg)](https://github.com/lyz-code/autoimport/actions) 2 | [![Actions Status](https://github.com/lyz-code/autoimport/workflows/Build/badge.svg)](https://github.com/lyz-code/autoimport/actions) 3 | [![Coverage Status](https://coveralls.io/repos/github/lyz-code/autoimport/badge.svg?branch=main)](https://coveralls.io/github/lyz-code/autoimport?branch=main) 4 | 5 | Autoimport missing python libraries. 6 | 7 | Throughout the development of a python program you continuously need to manage 8 | the python import statements either because you need one new object or because 9 | you no longer need it. This means that you need to stop writing whatever you 10 | were writing, go to the top of the file, create or remove the import statement 11 | and then resume coding. 12 | 13 | This workflow break is annoying and almost always unnecessary. `autoimport` 14 | solves this problem if you execute it whenever you have an import error, for 15 | example by configuring your editor to run it when saving the file. 16 | 17 | # Installing 18 | 19 | ```bash 20 | pip install autoimport 21 | ``` 22 | 23 | # Usage 24 | 25 | Imagine we've got the following source code: 26 | 27 | ```python 28 | import requests 29 | 30 | 31 | def hello(names: Tuple[str]) -> None: 32 | for name in names: 33 | print(f"Hi {name}!") 34 | 35 | 36 | os.getcwd() 37 | ``` 38 | 39 | It has the following import errors: 40 | 41 | - `requests` is imported but unused. 42 | - `os` and `Tuple` are needed but not imported. 43 | 44 | After running `autoimport` the resulting source code will be: 45 | 46 | ```python 47 | import os 48 | from typing import Tuple 49 | 50 | 51 | def hello(names: Tuple[str]) -> None: 52 | for name in names: 53 | print(f"Hi {name}!") 54 | 55 | 56 | os.getcwd() 57 | ``` 58 | 59 | `autoimport` can be used both as command line tool and as a library. 60 | 61 | It can be parsed either an array of files and/or a directory. 62 | 63 | A parsed directory will have `autoimport` be executed on all recursively found 64 | python files in said directory. 65 | 66 | - As a command line tool: 67 | 68 | ```bash 69 | $: autoimport file.py 70 | $: autoimport dir/ 71 | ``` 72 | 73 | - As a library: 74 | 75 | ```python 76 | from autoimport import fix_files 77 | 78 | fix_files(["file.py", "dir/"]) 79 | ``` 80 | 81 | Warning: `autoimport` will add all dependencies at the top of the file, we 82 | suggest using [isort](https://pycqa.github.io/isort) and 83 | [black](https://black.readthedocs.io/en/stable/) afterwards to clean the file. 84 | 85 | # Features 86 | 87 | ## Add missing imports 88 | 89 | `autoimport` matches each of the missing import statements against the following 90 | objects: 91 | 92 | - The modules referenced in `PYTHONPATH`. 93 | 94 | - The `typing` library objects. 95 | 96 | - The common statements. Where some of the common statements are: 97 | 98 | - `BeautifulSoup` -> `from bs4 import BeautifulSoup` 99 | - `call` -> `from unittest.mock import call` 100 | - `CaptureFixture` -> `from _pytest.capture import CaptureFixture` 101 | - `CliRunner` -> `from click.testing import CliRunner` 102 | - `copyfile` -> `from shutil import copyfile` 103 | - `dedent` -> `from textwrap import dedent` 104 | - `LocalPath` -> `from py._path.local import LocalPath` 105 | - `LogCaptureFixture` -> `from _pytest.logging import LogCaptureFixture` 106 | - `Mock` -> `from unittest.mock import Mock` 107 | - `patch` -> `from unittest.mock import patch` 108 | - `StringIO` -> `from io import StringIO` 109 | - `YAMLError` -> `from yaml import YAMLError` 110 | 111 | - The objects of the Python project you are developing, assuming you are 112 | executing the program in a directory of the project and you can import it. 113 | 114 | Warning: It may not work if you use `pip install -e .`. Given that you execute 115 | `autoimport` inside a virtualenv where the package is installed with 116 | `pip install -e .`, when there is an import error in a file that is indexed in 117 | the package, `autoimport` won't be able to read the package contents as the 118 | `import` statement will fail. So it's a good idea to run autoimport from a 119 | virtualenv that has a stable version of the package we are developing. 120 | 121 | ## Remove unused import statements 122 | 123 | If an object is imported but unused, `autoimport` will remove the import 124 | statement. 125 | 126 | This can be problematic when run in `__init__.py` files, which often contain 127 | "unused" imports. To tell `autoimport` to not run on these files, you can use 128 | the `--ignore-init-modules` flag, which will filter away any passed 129 | `__init__.py` files before processing. 130 | 131 | There may be import statements that are being used dynamically, 132 | to autoimport it would look like those are not being used but actually they may have some usages. 133 | 134 | In such cases where you want to retain the unused imports across any file, you can use the `--keep-unused-imports` flag, 135 | which will prevent `autoimport` from removing any import statements. 136 | 137 | **Note:** If there are not many cases where you intend to keep the unused imports, prefer placing 138 | `#noqa: autoimport` on the concerned import line/s, over using the `--keep-unused-imports` flag. 139 | 140 | ## Moving the imports to the top 141 | 142 | There are going to be import cases that may not work, if you find one, please 143 | [open an issue](https://github.com/lyz-code/autoimport/issues/new?labels=bug&template=bug.md). 144 | 145 | While we fix it you can write the import statement wherever you are in the file 146 | and the next time you run `autoimport` it will get moved to the top. 147 | 148 | If you don't want a specific line to go to the top, add the `# noqa: autoimport` 149 | or `# fmt: skip` at the end. For example: 150 | 151 | ```python 152 | a = 1 153 | 154 | from os import getcwd # noqa: autoimport 155 | 156 | getcwd() 157 | ``` 158 | 159 | # Configuration 160 | 161 | `autoimport` uses the `maison` library to discover and read your project-local 162 | `pyproject.toml` file (if it exists). This file can be used to configure 163 | `autoimport`'s behavior: the `tool.autoimport.common_statements` table in that 164 | file can be used to define a custom set of "common statements", overriding the 165 | default set of common statements mentioned above. For example: 166 | 167 | ```toml 168 | # pyproject.toml 169 | 170 | [tool.autoimport.common_statements] 171 | "np" = "import numpy as np" 172 | "FooBar" = "from baz_qux import FooBar" 173 | ``` 174 | 175 | It is also possible to specify a different path for this config file: 176 | 177 | ```bash 178 | $: autoimport --config-file ~/.autoimport.toml file.py 179 | ``` 180 | 181 | If using the `--config-file` flag to specify a file that is named something 182 | other than `pyproject.toml`, the autoimport settings should not be nested under 183 | toplevel `tool.autoimport` keys. 184 | 185 | ```toml 186 | # .autoimport.toml 187 | 188 | [common_statements] 189 | "np" = "import numpy as np" 190 | "FooBar" = "from baz_qux import FooBar" 191 | ``` 192 | 193 | Furthermore, `autoimport` supports the use of a global configuration file, 194 | located at `autoimport/config.toml` under the xdg config home folder. For most 195 | users, this means that the file `~/.config/autoimport/config.toml`, if it 196 | exists, will be loaded and used as configuration for `autoimport`. As before, do 197 | not write `tool.autoimport` at the toplevel; just specify your global 198 | `autoimport` settings directly. 199 | 200 | The settings defined in the local `pyproject.toml` file (if found) or in the 201 | file specified by the `--config-file` flag (if given) will override the settings 202 | defined in the global `autoimport/config.toml` file. 203 | 204 | ## Disabling Move To Top 205 | 206 | While discouraged in favor of proper refactoring to eliminate cyclical 207 | dependencies, it is possible to disable autoimport from moving import statements 208 | to the tops of the files. 209 | 210 | To do so, set `disable_move_to_top` to `true`. Here is how that might look in a 211 | `pyproject.toml` configuration file. 212 | 213 | ```toml 214 | [tool.autoimport] 215 | disable_move_to_top = true 216 | ``` 217 | 218 | # References 219 | 220 | As most open sourced programs, `autoimport` is standing on the shoulders of 221 | giants, namely: 222 | 223 | - [autoflake](https://pypi.org/project/autoflake/): Inspiration of `autoimport`. 224 | Also used their code to interact with 225 | - [pyflakes](https://pypi.org/project/pyflakes/). 226 | - [Click](https://click.palletsprojects.com/): Used to create the command line 227 | interface. 228 | - [Pytest](https://docs.pytest.org/en/latest): Testing framework, enhanced by 229 | the awesome [pytest-cases](https://smarie.github.io/python-pytest-cases/) 230 | library that made the parametrization of the tests a lovely experience. 231 | - [Mypy](https://mypy.readthedocs.io/en/stable/): Python static type checker. 232 | - [Flakeheaven](https://github.com/flakeheaven/flakeheaven): Python linter with 233 | [lots of checks](https://lyz-code.github.io/blue-book/devops/flakeheaven#plugins). 234 | - [Black](https://black.readthedocs.io/en/stable/): Python formatter to keep a 235 | nice style without effort. 236 | - [Autoimport](https://lyz-code.github.io/autoimport): Python formatter to 237 | automatically fix wrong import statements. 238 | - [isort](https://github.com/timothycrosley/isort): Python formatter to order 239 | the import statements. 240 | - [PDM](https://pdm.fming.dev/): Command line tool to manage the dependencies. 241 | - [Mkdocs](https://www.mkdocs.org/): To build this documentation site, with the 242 | - [Material theme](https://squidfunk.github.io/mkdocs-material). 243 | - [Safety](https://github.com/pyupio/safety): To check the installed 244 | dependencies for known security vulnerabilities. 245 | - [Bandit](https://bandit.readthedocs.io/en/latest/): To finds common security 246 | issues in Python code. 247 | - [Yamlfix](https://github.com/lyz-code/yamlfix): YAML fixer. 248 | 249 | # Alternatives 250 | 251 | If you like the idea but not how we solved the problem, take a look at this 252 | other solutions: 253 | 254 | - [smart-imports](https://github.com/Tiendil/smart-imports) 255 | 256 | # Contributing 257 | 258 | For guidance on setting up a development environment, and how to make a 259 | contribution to *autoimport*, see 260 | [Contributing to autoimport](https://lyz-code.github.io/autoimport/contributing). 261 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | ::: autoimport.services 2 | 3 | ::: autoimport.entrypoints 4 | 5 | ::: autoimport.version 6 | -------------------------------------------------------------------------------- /docs/stylesheets/extra.css: -------------------------------------------------------------------------------- 1 | .md-content a:link { 2 | text-decoration:underline; 3 | } 4 | 5 | .md-typeset a:hover { 6 | color: #abb9c1; 7 | text-decoration:underline; 8 | } 9 | 10 | .md-typeset h1 { 11 | font-size: 32px; 12 | font-family: Content-font, Roboto, sans-serif; 13 | font-weight: 500; 14 | line-height: 1.5; 15 | color: #28292d; 16 | } 17 | 18 | .md-typeset h1::after { 19 | width:93%; 20 | height:2px; 21 | background: #283551; 22 | content:""; 23 | display: block; 24 | margin-top: 1px; 25 | opacity: 0.3; 26 | } 27 | 28 | .md-typeset h2 { 29 | font-size: 24px; 30 | font-family: Content-font, Roboto, sans-serif; 31 | font-weight: 700; 32 | line-height: 1.5; 33 | color: #28292d; 34 | } 35 | 36 | .md-typeset h2::after { 37 | width:100%; 38 | height:1px; 39 | background: #283551; 40 | content:""; 41 | display: block; 42 | margin-top: -5px; 43 | opacity: 0.2; 44 | } 45 | -------------------------------------------------------------------------------- /docs/theme/assets/images/amazon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/archive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/audio.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/bluebook.2.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 314 | 315 | -------------------------------------------------------------------------------- /docs/theme/assets/images/bluebook.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyz-code/autoimport/842554d5feb8e6629beecb4596249d0e68621451/docs/theme/assets/images/bluebook.bmp -------------------------------------------------------------------------------- /docs/theme/assets/images/bluebook.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | 10 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /docs/theme/assets/images/chi-dna.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/code.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/csv.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/deepmind.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | image/svg+xml 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/theme/assets/images/dropbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/erowid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/gitea.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/theme/assets/images/github.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/google-scholar.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/hn.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/theme/assets/images/image.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/internetarchive.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/kubernetes.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/theme/assets/images/mega.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/miri.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/misc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/newyorktimes.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/nlm-ncbi.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/openai.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/patreon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/plos.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/pydo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/theme/assets/images/reddit.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/spreadsheet.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/stackexchange.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/theguardian.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/thenewyorker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/twitter.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/txt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/uptontea.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/video.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/washingtonpost.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/wired.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /docs/theme/assets/images/worddoc.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | --- 2 | site_name: Autoimport 3 | site_author: Lyz 4 | site_url: https://lyz-code.github.io/autoimport 5 | nav: 6 | - Autoimport: index.md 7 | - Editor integration: editor_integration.md 8 | - Reference: reference.md 9 | - Contributing: contributing.md 10 | 11 | plugins: 12 | - search 13 | - mkdocstrings: 14 | handlers: 15 | python: 16 | rendering: 17 | show_root_heading: true 18 | heading_level: 1 19 | - autolinks 20 | - git-revision-date-localized: 21 | type: timeago 22 | fallback_to_build_date: true 23 | - minify: 24 | minify_html: true 25 | - section-index 26 | 27 | watch: 28 | - src 29 | 30 | markdown_extensions: 31 | - abbr 32 | - def_list 33 | - admonition 34 | # We need the markdown-include to inject files into other files 35 | - markdown_include.include: 36 | base_path: docs 37 | - meta 38 | - toc: 39 | permalink: true 40 | baselevel: 2 41 | - pymdownx.arithmatex 42 | - pymdownx.betterem: 43 | smart_enable: all 44 | - pymdownx.caret 45 | - pymdownx.critic 46 | - pymdownx.details 47 | - pymdownx.emoji: 48 | emoji_generator: '!!python/name:pymdownx.emoji.to_svg' 49 | - pymdownx.inlinehilite 50 | - pymdownx.magiclink 51 | - pymdownx.mark 52 | - pymdownx.smartsymbols 53 | - pymdownx.superfences: 54 | custom_fences: 55 | - name: mermaid 56 | class: mermaid 57 | format: !!python/name:pymdownx.superfences.fence_code_format 58 | - pymdownx.tabbed: 59 | alternate_style: true 60 | - pymdownx.tasklist: 61 | custom_checkbox: true 62 | - pymdownx.tilde 63 | 64 | theme: 65 | name: material 66 | custom_dir: docs/theme 67 | # logo: 'images/logo.bmp' 68 | features: 69 | - navigation.instant 70 | - navigation.top 71 | - content.code.annotate 72 | 73 | palette: 74 | primary: blue grey 75 | accent: light blue 76 | 77 | extra_css: 78 | - stylesheets/extra.css 79 | - stylesheets/links.css 80 | 81 | repo_name: lyz-code/autoimport 82 | repo_url: https://github.com/lyz-code/autoimport 83 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # --------- Commitizen ------------- 2 | 3 | [tool.commitizen] 4 | name = "cz_conventional_commits" 5 | version = "1.6.0" 6 | tag_format = "$version" 7 | version_files = [ 8 | "pyproject.toml:version", 9 | "src/autoimport/version.py", 10 | ] 11 | 12 | # --------- Autoimport ------------- 13 | 14 | [tool.autoimport.common_statements] 15 | "factories" = "from tests import factories" 16 | 17 | # --------- PDM ------------- 18 | 19 | [project] 20 | # PEP 621 project metadata 21 | # See https://www.python.org/dev/peps/pep-0621/ 22 | dynamic = ["version"] 23 | authors = [ 24 | {name = "Lyz", email = "lyz@riseup.net"}, 25 | ] 26 | license = {text = "GPL-3.0-only"} 27 | requires-python = ">=3.8" 28 | dependencies = [ 29 | "click>=8.1.3", 30 | "autoflake>=1.4", 31 | "pyprojroot>=0.2.0", 32 | "sh>=1.14.2", 33 | "maison>=1.4.0,<2.0.0", 34 | "xdg>=6.0.0", 35 | ] 36 | name = "autoimport" 37 | description = "Autoimport missing python libraries." 38 | readme = "README.md" 39 | classifiers=[ 40 | "Development Status :: 5 - Production/Stable", 41 | "Intended Audience :: Developers", 42 | "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", 43 | "Operating System :: Unix", 44 | "Operating System :: POSIX", 45 | "Programming Language :: Python", 46 | "Programming Language :: Python :: 3", 47 | "Programming Language :: Python :: 3.8", 48 | "Programming Language :: Python :: 3.9", 49 | "Programming Language :: Python :: 3.10", 50 | "Programming Language :: Python :: 3.11", 51 | "Topic :: Utilities", 52 | "Natural Language :: English", 53 | ] 54 | 55 | [project.urls] 56 | Issues = "https://github.com/lyz-code/autoimport/issues" 57 | homepage = "https://github.com/lyz-code/autoimport" 58 | documentation = "https://lyz-code.github.io/autoimport" 59 | 60 | [project.scripts] 61 | autoimport = "autoimport.entrypoints.cli:cli" 62 | 63 | [project.optional-dependencies] 64 | 65 | [tool.pdm] 66 | allow_prereleases = true 67 | 68 | [tool.pdm.build] 69 | editable-backend = "path" 70 | package-dir = "src" 71 | source-includes = ["tests/"] 72 | 73 | [tool.pdm.version] 74 | source = "file" 75 | path = "src/autoimport/version.py" 76 | 77 | [tool.pdm.overrides] 78 | # To be removed once https://github.com/flakeheaven/flakeheaven/issues/132 is solved 79 | "importlib-metadata" = ">=3.10" 80 | 81 | [tool.pdm.dev-dependencies] 82 | lint = [ 83 | "yamllint>=1.27.1", 84 | "flake8-aaa>=0.12.2", 85 | "flake8-annotations>=2.9.1", 86 | "flake8-annotations-complexity>=0.0.7", 87 | "flake8-typing-imports>=0.12.0,!=1.13.0", 88 | "flake8-bugbear>=22.8.23", 89 | "flake8-debugger>=4.1.2", 90 | "flake8-fixme>=1.1.1", 91 | "flake8-markdown>=0.3.0", 92 | "flake8-mutable>=1.2.0", 93 | "flake8-pytest>=1.4", 94 | "flake8-pytest-style>=1.6.0", 95 | "flake8-simplify>=0.19.3", 96 | "flake8-variables-names>=0.0.5", 97 | "flake8-comprehensions>=3.10.0", 98 | "flake8-expression-complexity>=0.0.11", 99 | "flake8-use-fstring>=1.4", 100 | "flake8-eradicate>=1.3.0", 101 | "flake8-docstrings>=1.6.0", 102 | "pep8-naming>=0.13.2", 103 | "dlint>=0.13.0", 104 | "pylint>=2.15.2", 105 | "flake8>=4.0.1", 106 | "flakeheaven>=3.0.0", 107 | ] 108 | test = [ 109 | "pytest>=7.1.3", 110 | "pytest-cov>=3.0.0", 111 | "pytest-xdist>=3.0.2", 112 | "pytest-freezegun>=0.4.2", 113 | "pydantic-factories>=1.6.1", 114 | ] 115 | doc = [ 116 | "mkdocs>=1.6.0", 117 | "mkdocs-git-revision-date-localized-plugin>=1.1.0", 118 | "mkdocs-htmlproofer-plugin>=0.8.0", 119 | "mkdocs-minify-plugin>=0.5.0", 120 | "mkdocs-autolinks-plugin>=0.6.0", 121 | "mkdocs-material>=8.4.2", 122 | "mkdocstrings[python]>=0.18", 123 | "markdown-include>=0.7.0", 124 | "mkdocs-section-index>=0.3.4", 125 | ] 126 | security = [ 127 | "safety>=2.3.1", 128 | "bandit>=1.7.3", 129 | ] 130 | fixers = [ 131 | "black>=22.8.0", 132 | "isort>=5.10.1", 133 | "yamlfix>=1.0.1", 134 | ] 135 | typing = [ 136 | "mypy>=0.971", 137 | "types-toml>=0.10.3", 138 | ] 139 | dev = [ 140 | "pre-commit>=2.20.0", 141 | "twine>=4.0.1", 142 | "commitizen>=3.27.0", 143 | ] 144 | # The next ones are required to manually solve the dependencies issues 145 | dependencies = [ 146 | # Until flakeheaven supports flake8 5.x 147 | # https://github.com/flakeheaven/flakeheaven/issues/132 148 | "flake8>=4.0.1,<5.0.0", 149 | "pyflakes<2.5.0", 150 | ] 151 | 152 | [build-system] 153 | requires = ["pdm-backend"] 154 | build-backend = "pdm.backend" 155 | 156 | # --------- Black ------------- 157 | 158 | [tool.black] 159 | line-length = 88 160 | target-version = ['py37', 'py38', 'py39', 'py310'] 161 | include = '\.pyi?$' 162 | exclude = ''' 163 | /( 164 | \.eggs 165 | | \.git 166 | | \.hg 167 | | \.mypy_cache 168 | | \.tox 169 | | \.venv 170 | | _build 171 | | buck-out 172 | | build 173 | | dist 174 | # The following are specific to Black, you probably don't want those. 175 | | blib2to3 176 | | tests/data 177 | | profiling 178 | )/ 179 | ''' 180 | 181 | # --------- Pytest ------------- 182 | 183 | [tool.pytest.ini_options] 184 | minversion = "6.0" 185 | addopts = "-vv --tb=short -n auto" 186 | norecursedirs = [ 187 | ".tox", 188 | ".git", 189 | "*/migrations/*", 190 | "*/static/*", 191 | "docs", 192 | "venv", 193 | "*/Autoimport/*", 194 | "*/deepdiff/*" 195 | ] 196 | markers = [ 197 | "slow: marks tests as slow (deselect with '-m \"not slow\"')", 198 | "secondary: mark tests that use functionality tested in the same file (deselect with '-m \"not secondary\"')" 199 | ] 200 | 201 | filterwarnings = [ 202 | "error", 203 | # Until https://github.com/ktosiek/pytest-freezegun/issues/35 is merged 204 | "ignore::DeprecationWarning:pytest_freezegun.*", 205 | ] 206 | 207 | # --------- Coverage ------------- 208 | 209 | [tool.coverage.report] 210 | exclude_lines = [ 211 | # Have to re-enable the standard pragma 212 | 'pragma: no cover', 213 | 214 | # Type checking can not be tested 215 | 'if TYPE_CHECKING:', 216 | 217 | # Ignore the Abstract classes definition 218 | 'raise NotImplementedError', 219 | 220 | # Ignore entrypoints 221 | 'if __name__ == "__main__":', 222 | ] 223 | 224 | # --------- Isort ------------- 225 | 226 | [tool.isort] 227 | profile = "black" 228 | src_paths = ["src", "test"] 229 | 230 | # --------- Flakeheaven ------------- 231 | 232 | [tool.flakeheaven] 233 | format = "grouped" 234 | max_line_length = 88 235 | show_source = true 236 | docstring-convention = "google" 237 | 238 | [tool.flakeheaven.plugins] 239 | flake8-aaa = ["+*"] 240 | flake8-annotations = [ 241 | "+*", 242 | "-ANN101", # There is usually no need to type the self argument of class methods. 243 | "-ANN102", # There is usually no need to type the cls argument of class methods. 244 | ] 245 | flake8-annotations-complexity = ["+*"] 246 | flake8-bugbear = ["+*"] 247 | flake8-comprehensions = ["+*"] 248 | flake8-debugger = ["+*"] 249 | flake8-docstrings = [ 250 | "+*", 251 | "-D101", # Missing docstring, already covered by C0115 of pylint 252 | ] 253 | flake8-eradicate = ["+*"] 254 | flake8-expression-complexity = ["+*"] 255 | flake8-fixme = ["+*"] 256 | flake8-markdown = ["+*"] 257 | flake8-mutable = ["+*"] 258 | flake8-pytest = ["+*"] 259 | flake8-pytest-style = ["+*"] 260 | flake8-simplify = ["+*"] 261 | flake8-use-fstring = [ 262 | "+*", 263 | '-FS003' # f-string missing prefix 264 | ] 265 | flake8-typing-imports = [ 266 | "+*", 267 | "-TYP001", # guard import by `if False: # TYPE_CHECKING`: TYPE_CHECKING (not in 268 | # 3.5.0, 3.5.1). We don't support Python 3.5 269 | "-TYP002", # @overload is broken in <3.5.2, but we don't support Python 3.5 270 | ] 271 | flake8-variables-names = ["+*"] 272 | dlint = ["+*"] 273 | pylint = [ 274 | "+*", 275 | "-C0411", # %s should be placed before %s, 276 | # see https://github.com/PyCQA/pylint/issues/2175 and https://github.com/PyCQA/pylint/issues/1797 277 | "-R0903", # Too few methods warning, but if you define an interface with just one 278 | # method that's fine 279 | "-W1203", # Use %s formatting in logging functions. Deprecated rule in favor of 280 | # f-strings. 281 | "-W1201", # Use lazy % formatting in logging functions. Deprecated rule in favor of 282 | # f-strings. 283 | "-C0301", # Lines too long. Already covered by E501. 284 | ] 285 | mccabe = ["+*"] 286 | pep8-naming = ["+*"] 287 | pycodestyle = [ 288 | "+*", 289 | "-W503", # No longer applies, incompatible with newer version of PEP8 290 | # see https://github.com/PyCQA/pycodestyle/issues/197 291 | # and https://github.com/psf/black/issues/113 292 | ] 293 | pyflakes = [ 294 | "+*", 295 | "-F841", # Unused variable, already covered by W0612 of pylint 296 | "-F821", # Undefined variable, already covered by E0602 of pylint 297 | ] 298 | 299 | [tool.flakeheaven.exceptions."tests/"] 300 | flake8-docstrings = [ 301 | "-D205", # 1 blank line required between summary line and description 302 | "-D212", # Multi-line docstring summary should start at the first line 303 | "-D400", # First line should end with a period 304 | "-D415", # First line should end with a period, question mark, or exclamation point 305 | ] 306 | flake8-annotations = [ 307 | "-ANN001", 308 | "-ANN401", # Dynamically typed expressions (typing.Any) are disallowed 309 | ] 310 | pylint = [ 311 | "-R0201", # Method could be a function. Raised because the methods of a test class 312 | # don't use the self object, which is not wrong. 313 | "-W0613", # Unused argument in function, but in tests there are fixtures that even 314 | # though they are not used in the function directly, they are used to 315 | # configure the test. 316 | "-C0302", # Too many lines in module, this means that the test files are long, but 317 | # Refactoring into more files can be confusing and less ergonomic. 318 | ] 319 | 320 | [tool.flakeheaven.exceptions."tests/factories.py"] 321 | pylint = [ 322 | "-R0903", # Too few methods warning, but is the way to define factoryboy factories. 323 | ] 324 | 325 | [tool.flakeheaven.exceptions."tests/unit/test_views.py"] 326 | pycodestyle = [ 327 | "-E501", # lines too long. As we are testing the output of the terminal, the test is 328 | # cleaner if we show the actual result without splitting long lines. 329 | ] 330 | 331 | # --------- Pylint ------------- 332 | [tool.pylint.'TYPECHECK'] 333 | generated-members = "sh" 334 | 335 | [tool.pylint.'MESSAGES CONTROL'] 336 | extension-pkg-whitelist = "pydantic" 337 | 338 | # --------- Mypy ------------- 339 | 340 | [tool.mypy] 341 | python_version = 3.9 342 | show_error_codes = true 343 | follow_imports = "silent" 344 | ignore_missing_imports = false 345 | strict_optional = true 346 | warn_redundant_casts = true 347 | warn_unused_ignores = true 348 | disallow_any_generics = true 349 | check_untyped_defs = true 350 | no_implicit_reexport = true 351 | warn_unused_configs = true 352 | disallow_subclassing_any = true 353 | disallow_incomplete_defs = true 354 | disallow_untyped_decorators = true 355 | disallow_untyped_calls = true 356 | disallow_untyped_defs = true 357 | plugins = [ 358 | "pydantic.mypy" 359 | ] 360 | 361 | [tool.pydantic-mypy] 362 | init_forbid_extra = true 363 | init_typed = true 364 | warn_required_dynamic_aliases = true 365 | warn_untyped_fields = true 366 | 367 | [[tool.mypy.overrides]] 368 | module = "tests.*" 369 | # Required to not have error: Untyped decorator makes function on fixtures and 370 | # parametrize decorators 371 | disallow_untyped_decorators = false 372 | 373 | [[tool.mypy.overrides]] 374 | module = [ 375 | "goodconf", 376 | "pytest", 377 | "autoflake", 378 | "pyflakes.*", 379 | "isort", 380 | "_io", 381 | "pyprojroot", 382 | "sh", 383 | "virtualenv", 384 | "xdg", 385 | ] 386 | ignore_missing_imports = true 387 | -------------------------------------------------------------------------------- /src/autoimport/__init__.py: -------------------------------------------------------------------------------- 1 | """Python program to automatically import missing python libraries. 2 | 3 | Functions: 4 | fix_code: Fix python source code to correct missed or unused import statements. 5 | fix_files: Fix the python source code of a list of files. 6 | """ 7 | 8 | from typing import List 9 | 10 | from .services import fix_code, fix_files 11 | 12 | __all__: List[str] = ["fix_code", "fix_files"] 13 | -------------------------------------------------------------------------------- /src/autoimport/__main__.py: -------------------------------------------------------------------------------- 1 | """Entrypoint to allow running as `python3 -m autoimport`.""" 2 | 3 | from autoimport.entrypoints.cli import cli 4 | 5 | if __name__ == "__main__": 6 | # this needs no-value-for-parameter exclusion because pylint 7 | # isn't smart enough to realize that click decorator handles it (but mypy is) 8 | cli() # pylint: disable=no-value-for-parameter 9 | -------------------------------------------------------------------------------- /src/autoimport/entrypoints/__init__.py: -------------------------------------------------------------------------------- 1 | """Define the different ways to expose the program functionality. 2 | 3 | Functions: 4 | load_logger: Configure the Logging logger. 5 | """ 6 | 7 | import logging 8 | import sys 9 | 10 | log = logging.getLogger(__name__) 11 | 12 | 13 | # I have no idea how to test this function :(. If you do, please send a PR. 14 | def load_logger(verbose: bool = False) -> None: # pragma no cover 15 | """Configure the Logging logger. 16 | 17 | Args: 18 | verbose: Set the logging level to Debug. 19 | """ 20 | logging.addLevelName(logging.INFO, "[\033[36m+\033[0m]") 21 | logging.addLevelName(logging.ERROR, "[\033[31m+\033[0m]") 22 | logging.addLevelName(logging.DEBUG, "[\033[32m+\033[0m]") 23 | logging.addLevelName(logging.WARNING, "[\033[33m+\033[0m]") 24 | if verbose: 25 | logging.basicConfig( 26 | stream=sys.stderr, level=logging.DEBUG, format=" %(levelname)s %(message)s" 27 | ) 28 | else: 29 | logging.basicConfig( 30 | stream=sys.stderr, level=logging.INFO, format=" %(levelname)s %(message)s" 31 | ) 32 | -------------------------------------------------------------------------------- /src/autoimport/entrypoints/cli.py: -------------------------------------------------------------------------------- 1 | """Command line interface definition.""" 2 | 3 | import logging 4 | from pathlib import Path 5 | from typing import IO, Any, List, Optional, Sequence, Tuple, Union 6 | 7 | import click 8 | 9 | # Migrate away from xdg to xdg-base-dirs once only Python >= 3.10 is supported 10 | # https://github.com/lyz-code/autoimport/issues/239 11 | import xdg 12 | from maison.config import ProjectConfig 13 | 14 | from autoimport import services, version 15 | 16 | log = logging.getLogger(__name__) 17 | 18 | 19 | def get_files(source_path: str) -> List[IO[Any]]: 20 | """Get all files recursively from the given source path.""" 21 | files = [] 22 | for py_file in Path(source_path).glob("**/*.py"): 23 | if py_file.is_file(): 24 | files.append(click.File("r+").convert(py_file, None, None)) 25 | return files 26 | 27 | 28 | def flatten(seq: Sequence[Any]) -> Tuple[Any, ...]: 29 | """Flatten nested sequences.""" 30 | flattened = [] 31 | for items in seq: 32 | if isinstance(items, (tuple, list)): 33 | for item in items: 34 | flattened.append(item) 35 | else: 36 | item = items 37 | flattened.append(item) 38 | return tuple(flattened) 39 | 40 | 41 | class FileOrDir(click.ParamType): 42 | """Custom parameter type that accepts either a directory or file.""" 43 | 44 | def convert( 45 | self, 46 | value: Union[str, Path], 47 | param: Optional[click.Parameter], 48 | ctx: Optional[click.Context], 49 | ) -> List[IO[Any]]: 50 | """Convert the value to the correct type.""" 51 | try: 52 | return [click.File("r+").convert(value, param, ctx)] 53 | except click.BadParameter: 54 | path = click.Path(exists=True).convert(value, param, ctx) 55 | return get_files(str(path)) 56 | 57 | 58 | @click.command() 59 | @click.version_option(version="", message=version.version_info()) 60 | @click.option("--config-file", default=None) 61 | @click.option("--ignore-init-modules", is_flag=True, help="Ignore __init__.py files.") 62 | @click.option( 63 | "--keep-unused-imports", 64 | is_flag=True, 65 | help="If True, retains unused imports.", 66 | default=False, 67 | ) 68 | @click.argument("files", type=FileOrDir(), nargs=-1) 69 | def cli( 70 | files: List[IO[Any]], 71 | config_file: Optional[str] = None, 72 | ignore_init_modules: bool = False, 73 | keep_unused_imports: bool = False, 74 | ) -> None: 75 | """Corrects the source code of the specified files.""" 76 | # Compose configuration 77 | config_files: List[str] = [] 78 | 79 | global_config_path = xdg.xdg_config_home() / "autoimport" / "config.toml" 80 | if global_config_path.is_file(): 81 | config_files.append(str(global_config_path)) 82 | 83 | config_files.append("pyproject.toml") 84 | 85 | if config_file is not None: 86 | config_files.append(config_file) 87 | 88 | config = ProjectConfig( 89 | project_name="autoimport", source_files=config_files, merge_configs=True 90 | ).to_dict() 91 | 92 | # Process inputs 93 | flattened_files = flatten(files) 94 | if ignore_init_modules: 95 | flattened_files = tuple( 96 | file for file in flattened_files if "__init__.py" not in file.name 97 | ) 98 | 99 | try: 100 | fixed_code = services.fix_files(flattened_files, config, keep_unused_imports) 101 | except FileNotFoundError as error: 102 | log.error(error) 103 | 104 | if fixed_code is not None: 105 | print(fixed_code, end="") 106 | 107 | 108 | if __name__ == "__main__": # pragma: no cover 109 | cli() # pylint: disable=E1120 110 | -------------------------------------------------------------------------------- /src/autoimport/model.py: -------------------------------------------------------------------------------- 1 | """Define the entities.""" 2 | 3 | import importlib.util 4 | import inspect 5 | import os 6 | import re 7 | from typing import Any, Dict, List, Optional, Tuple 8 | 9 | import autoflake 10 | from pyflakes.messages import UndefinedExport, UndefinedName, UnusedImport 11 | from pyprojroot import here 12 | 13 | common_statements: Dict[str, str] = { 14 | "ABC": "from abc import ABC", 15 | "abstractmethod": "from abc import abstractmethod", 16 | "BaseModel": "from pydantic import BaseModel # noqa: E0611", 17 | "BeautifulSoup": "from bs4 import BeautifulSoup", 18 | "call": "from unittest.mock import call", 19 | "CaptureFixture": "from _pytest.capture import CaptureFixture", 20 | "CliRunner": "from click.testing import CliRunner", 21 | "copyfile": "from shutil import copyfile", 22 | "datetime": "from datetime import datetime", 23 | "dedent": "from textwrap import dedent", 24 | "Enum": "from enum import Enum", 25 | "Faker": "from faker import Faker", 26 | "FrozenDateTimeFactory": "from freezegun.api import FrozenDateTimeFactory", 27 | "LocalPath": "from py._path.local import LocalPath", 28 | "LogCaptureFixture": "from _pytest.logging import LogCaptureFixture", 29 | "Mock": "from unittest.mock import Mock", 30 | "ModelFactory": "from pydantic_factories import ModelFactory", 31 | "Path": "from pathlib import Path", 32 | "patch": "from unittest.mock import patch", 33 | "StringIO": "from io import StringIO", 34 | "suppress": "from contextlib import suppress", 35 | "tz": "from dateutil import tz", 36 | "YAMLError": "from yaml import YAMLError", 37 | } 38 | 39 | 40 | # R0903: Too few public methods (1/2). We don't need more, but using the class instead 41 | # of passing the data between function calls is useful. 42 | class SourceCode: # noqa: R090 43 | """Python source code entity.""" 44 | 45 | def __init__( 46 | self, 47 | source_code: str, 48 | config: Optional[Dict[str, Any]] = None, 49 | keep_unused_imports: bool = False, 50 | ) -> None: 51 | """Initialize the object.""" 52 | self.header: List[str] = [] 53 | self.imports: List[str] = [] 54 | self.typing: List[str] = [] 55 | self.code: List[str] = [] 56 | self.config: Dict[str, Any] = config if config else {} 57 | self._trailing_newline = False 58 | self._split_code(source_code) 59 | self.keep_unused_imports = keep_unused_imports 60 | 61 | def fix(self) -> str: 62 | """Fix python source code to correct import statements. 63 | 64 | It corrects these errors: 65 | 66 | * Add missed import statements. 67 | * Remove unused import statements. 68 | * Move import statements to the top. 69 | """ 70 | self._move_imports_to_top() 71 | self._fix_flake_import_errors() 72 | 73 | return self._join_code() 74 | 75 | def _split_code(self, source_code: str) -> None: 76 | """Split the source code in the different sections. 77 | 78 | * Module Docstring 79 | * Import statements 80 | * Typing statements 81 | * Code. 82 | 83 | Args: 84 | source_code: Source code to be corrected. 85 | """ 86 | source_code_lines = source_code.splitlines() 87 | 88 | self._extract_header(source_code_lines) 89 | self._extract_import_statements(source_code_lines) 90 | self._extract_typing_statements(source_code_lines) 91 | self._extract_code(source_code_lines) 92 | if source_code.endswith("\n"): 93 | self._trailing_newline = True 94 | 95 | def _extract_header(self, source_lines: List[str]) -> None: 96 | """Save the module leading comments and docstring from the source code. 97 | 98 | Save them into self.header. 99 | 100 | Args: 101 | source_lines: A list containing all code lines. 102 | """ 103 | docstring_type: Optional[str] = None 104 | 105 | for line in source_lines: 106 | if re.match(r'"{3}.*"{3}', line): 107 | # Match single line docstrings. 108 | self.header.append(line) 109 | break 110 | 111 | if docstring_type == "start_multiple_lines" and re.match(r'""" ?', line): 112 | # Match end of multiple line docstrings 113 | docstring_type = "multiple_lines" 114 | elif re.match(r'"{3}.*', line): 115 | # Match multiple line docstrings start 116 | docstring_type = "start_multiple_lines" 117 | elif re.match(r"#.*", line) or line == "": 118 | # Match leading comments and empty lines 119 | pass 120 | elif docstring_type in [None, "multiple_lines"]: 121 | break 122 | self.header.append(line) 123 | 124 | def _extract_import_statements(self, source_lines: List[str]) -> None: 125 | """Save the import statements from the source code into self.imports. 126 | 127 | Args: 128 | source_lines: A list containing all code lines. 129 | """ 130 | import_start_line = len(self.header) 131 | multiline_import = False 132 | try_line: Optional[str] = None 133 | 134 | for line in source_lines[import_start_line:]: 135 | if re.match(r"^if TYPE_CHECKING:$", line): 136 | break 137 | if re.match(r"^(try|except.*):$", line): 138 | try_line = line 139 | elif ( 140 | re.match(r"^\s*(from .*)?import.[^\'\"]*$", line) 141 | or line == "" 142 | or multiline_import 143 | ): 144 | # Process multiline import statements 145 | if "(" in line: 146 | multiline_import = True 147 | elif ")" in line: 148 | multiline_import = False 149 | 150 | if try_line: 151 | self.imports.append(try_line) 152 | try_line = None 153 | 154 | self.imports.append(line) 155 | else: 156 | break 157 | 158 | def _extract_typing_statements(self, source_lines: List[str]) -> None: 159 | """Save the typing statements from the source code into self.typing. 160 | 161 | Args: 162 | source_lines: A list containing all code lines. 163 | """ 164 | typing_start_line = len(self.header) + len(self.imports) 165 | 166 | if typing_start_line < len(source_lines) and re.match( 167 | r"^if TYPE_CHECKING:$", source_lines[typing_start_line] 168 | ): 169 | self.typing.append(source_lines[typing_start_line]) 170 | typing_start_line += 1 171 | for line in source_lines[typing_start_line:]: 172 | if not re.match(r"^\s+.*", line) and line != "": 173 | break 174 | self.typing.append(line) 175 | 176 | def _extract_code(self, source_lines: List[str]) -> None: 177 | """Save the code from the source code into self.code. 178 | 179 | Args: 180 | source_lines: A list containing all code lines. 181 | """ 182 | # Extract the code lines 183 | code_start_line = len(self.header) + len(self.imports) + len(self.typing) 184 | self.code = source_lines[code_start_line:] 185 | 186 | def _join_code(self) -> str: 187 | """Join the source code from docstring, import statements and code lines. 188 | 189 | Make sure that an empty line splits them. 190 | 191 | Returns: 192 | source_code: Source code to be corrected. 193 | """ 194 | source_code = "" 195 | for section, new_lines in [ 196 | ("header", 0), 197 | ("imports", 2), 198 | ("typing", 2), 199 | ("code", 3), 200 | ]: 201 | source_code = self._append_section(source_code, section, new_lines) 202 | 203 | # Remove possible new lines at the start of the document 204 | source_code = source_code.strip() 205 | 206 | # Respect the trailing newline 207 | if self._trailing_newline: 208 | source_code += "\n" 209 | 210 | return source_code 211 | 212 | def _append_section( 213 | self, source_code: str, section_name: str, empty_lines: int = 1 214 | ) -> str: 215 | """Append a section to the existent source code. 216 | 217 | Args: 218 | source_code: existing source code to append the new section. 219 | section_name: the source code section to append 220 | empty_lines: number of empty lines to append at the start. 221 | """ 222 | section = getattr(self, section_name) 223 | 224 | if len(section) == 0 or section == [""]: 225 | return source_code 226 | 227 | source_code += "\n" * empty_lines + "\n".join(section).strip() 228 | 229 | return source_code 230 | 231 | @staticmethod 232 | def _should_ignore_line(line: str) -> bool: 233 | """Determine whether a line should be ignored by autoimport or not.""" 234 | return any( 235 | [ 236 | re.match(r".*?# ?fmt:.*?skip.*", line), 237 | re.match(r".*?# ?noqa:.*?autoimport.*", line), 238 | ] 239 | ) 240 | 241 | def _move_imports_to_top(self) -> None: 242 | """Fix python source code to move import statements to the top of the file. 243 | 244 | Ignore the lines that contain the # noqa: autoimport string. 245 | """ 246 | if self._get_disable_move_to_top(): 247 | return 248 | multiline_import = False 249 | multiline_string = False 250 | code_lines_to_remove = [] 251 | 252 | for line_num, line in enumerate(self.code): 253 | # Process multiline strings, taking care not to catch single line strings 254 | # defined with three quotes. 255 | if re.match(r"^.*?(\"|\'){3}.*?(?!\1{3})$", line) and not re.match( 256 | r"^.*?(\"|\'){3}.*?\1{3}", line 257 | ): 258 | multiline_string = not multiline_string 259 | continue 260 | 261 | # Process import lines 262 | if ( 263 | "=" not in line 264 | and not multiline_string 265 | and re.match(r"^\s*(?:from .*)?import .[^\'\"]*$", line) 266 | ) or multiline_import: 267 | if self._should_ignore_line(line): 268 | continue 269 | 270 | # process lines using separation markers 271 | if ";" in line: 272 | import_line, next_line = self._split_separation_line(line) 273 | self.imports.append(import_line.strip()) 274 | self.code[line_num] = next_line 275 | continue 276 | 277 | # Process multiline import statements 278 | if "(" in line: 279 | multiline_import = True 280 | elif ")" in line: 281 | multiline_import = False 282 | 283 | code_lines_to_remove.append(line) 284 | if not multiline_import: 285 | line = line.strip() 286 | 287 | self.imports.append(line) 288 | 289 | for line in code_lines_to_remove: 290 | self.code.remove(line) 291 | 292 | @staticmethod 293 | def _split_separation_line(line: str) -> Tuple[str, str]: 294 | """Split separation lines into two and return both lines back.""" 295 | first_line, next_line = line.split(";") 296 | # add correct number of leading spaces 297 | num_lspaces = len(first_line) - len(first_line.lstrip()) 298 | next_line = f"{' ' * num_lspaces}{next_line.lstrip()}" 299 | return first_line, next_line 300 | 301 | def _fix_flake_import_errors(self) -> None: 302 | """Fix python source code to correct missed or unused import statements.""" 303 | error_messages = autoflake.check(self._join_code()) 304 | fixed_packages = [] 305 | 306 | for message in error_messages: 307 | if isinstance(message, (UndefinedName, UndefinedExport)): 308 | object_name = message.message_args[0] 309 | if object_name not in fixed_packages: 310 | self._add_package(object_name) 311 | fixed_packages.append(object_name) 312 | elif isinstance(message, UnusedImport) and not self.keep_unused_imports: 313 | import_name = message.message_args[0] 314 | self._remove_unused_imports(import_name) 315 | 316 | def _add_package(self, object_name: str) -> None: 317 | """Add a package to the source code. 318 | 319 | Args: 320 | object_name: Object name to search. 321 | """ 322 | import_string = self._find_package(object_name) 323 | 324 | if import_string is not None: 325 | self.imports.append(import_string) 326 | 327 | def _find_package(self, name: str) -> Optional[str]: 328 | """Search package by an object's name. 329 | 330 | It will search in these places: 331 | 332 | * In the package we are developing. 333 | * Modules in PYTHONPATH. 334 | * Typing library. 335 | * Common statements. 336 | 337 | Args: 338 | name: Object name to search. 339 | 340 | Returns: 341 | import_string: String required to import the package. 342 | """ 343 | for check in [ 344 | "_find_package_in_common_statements", 345 | "_find_package_in_modules", 346 | "_find_package_in_typing", 347 | "_find_package_in_our_project", 348 | ]: 349 | package = getattr(self, check)(name) 350 | if package is not None: 351 | return package 352 | return None 353 | 354 | @staticmethod 355 | def _find_package_in_our_project(name: str) -> Optional[str]: 356 | """Search the name in the objects of the package we are developing. 357 | 358 | Args: 359 | name: package name 360 | 361 | Returns: 362 | import_string: String required to import the package. 363 | """ 364 | # Find the package name 365 | try: 366 | project_package = os.path.basename(here()).replace("-", "_") 367 | except RuntimeError: # pragma: no cover 368 | # I don't know how to make a test that raises it :( 369 | # To manually reproduce, follow the steps of 370 | # https://github.com/lyz-code/autoimport/issues/131 371 | return None 372 | package_objects = extract_package_objects(project_package) 373 | 374 | # nocover: as the tests are run inside the autoimport virtualenv, it will 375 | # always find the objects on that package 376 | if package_objects is None: # pragma: nocover 377 | return None 378 | try: 379 | return package_objects[name] 380 | except KeyError: 381 | return None 382 | 383 | @staticmethod 384 | def _find_package_in_modules(name: str) -> Optional[str]: 385 | """Search in the PYTHONPATH modules if object is a package. 386 | 387 | Args: 388 | name: package name 389 | 390 | Returns: 391 | import_string: String required to import the package. 392 | """ 393 | package_specs = importlib.util.find_spec(name) 394 | 395 | try: 396 | importlib.util.module_from_spec(package_specs) # type: ignore 397 | except AttributeError: 398 | return None 399 | 400 | return f"import {name}" 401 | 402 | @staticmethod 403 | def _find_package_in_typing(name: str) -> Optional[str]: 404 | """Search in the typing library the object name. 405 | 406 | Args: 407 | name: package name 408 | 409 | Returns: 410 | import_string: Python 3.7 type checking compatible import string. 411 | """ 412 | typing_objects = extract_package_objects("typing") 413 | 414 | try: 415 | return typing_objects[name] 416 | except KeyError: 417 | return None 418 | 419 | def _get_disable_move_to_top(self) -> bool: 420 | """Fetch the disable_move_to_top configuration value.""" 421 | # When parsing to the cli via --config-file the config becomes nested. 422 | disable_move_to_top = self.config.get("disable_move_to_top") 423 | if disable_move_to_top is not None: 424 | return disable_move_to_top 425 | return ( 426 | self.config.get("tool", {}) 427 | .get("autoimport", {}) 428 | .get("disable_move_to_top", False) 429 | ) 430 | 431 | def _get_additional_statements(self) -> Dict[str, str]: 432 | """Fetch the common_statements configuration value.""" 433 | # When parsing to the cli via --config-file the config becomes nested. 434 | config_statements = self.config.get("common_statements") 435 | if config_statements: 436 | return config_statements 437 | return ( 438 | self.config.get("tool", {}).get("autoimport", {}).get("common_statements") 439 | ) 440 | 441 | def _find_package_in_common_statements(self, name: str) -> Optional[str]: 442 | """Search in the common statements the object name. 443 | 444 | Args: 445 | name: package name 446 | 447 | Returns: 448 | import_string 449 | """ 450 | local_common_statements = common_statements.copy() 451 | additional_statements = self._get_additional_statements() 452 | if additional_statements: 453 | local_common_statements.update(additional_statements) 454 | 455 | if name in local_common_statements: 456 | return local_common_statements[name] 457 | 458 | return None 459 | 460 | def _remove_unused_imports(self, import_name: str) -> None: 461 | """Remove unused import statements. 462 | 463 | Args: 464 | import_name: Name of the imported object to remove. 465 | """ 466 | package_name = ".".join(import_name.split(".")[:-1]) 467 | object_name = import_name.split(".")[-1] 468 | 469 | for line in self.imports: 470 | if self._should_ignore_line(line): 471 | continue 472 | 473 | # If it's the only line, remove it 474 | if re.match( 475 | rf"(from {package_name} )?import ({package_name}\.)?{object_name}" 476 | rf"( *as [a-z]+)?( *#.*)?$", 477 | line, 478 | ): 479 | self.imports.remove(line) 480 | return 481 | # If it shares the line with other objects, just remove the unused one. 482 | if re.match(rf"from {package_name} import .*?{object_name}", line): 483 | # fmt: off 484 | # Format is required until there is no more need of the 485 | # experimental-string-processing flag of the Black formatter. 486 | match = re.match( 487 | fr"(?Pfrom {package_name} import) " 488 | fr"(?P[^#]*)(?P#.*)?", 489 | line, 490 | ) 491 | # fmt: on 492 | if match is not None: 493 | line_number = self.imports.index(line) 494 | imports = [ 495 | import_.strip() for import_ in match["imports"].split(", ") 496 | ] 497 | imports.remove(object_name) 498 | new_imports = ", ".join(imports) 499 | if match["comment"]: 500 | new_imports += f' {match["comment"]}' 501 | self.imports[line_number] = f"{match['from']} {new_imports}" 502 | return 503 | # If it's a multiline import statement 504 | elif re.match( 505 | rf"from {package_name} import .*?\($", 506 | line, 507 | ): 508 | line_number = self.imports.index(line) 509 | # Remove the object name from the multiline imports 510 | while line_number + 1 < len(self.imports): 511 | line_number += 1 512 | if re.match(rf"\s*?{object_name},?", self.imports[line_number]): 513 | self.imports.pop(line_number) 514 | break 515 | 516 | # Remove the whole import if there is no other object loaded 517 | if ( 518 | re.match(r"\s*from .* import", self.imports[line_number - 1]) 519 | and self.imports[line_number] == ")" 520 | ): 521 | self.imports.pop(line_number) 522 | self.imports.pop(line_number - 1) 523 | 524 | return 525 | 526 | 527 | def extract_package_objects(name: str) -> Dict[str, str]: 528 | """Extract the package objects and their import string. 529 | 530 | Returns: 531 | objects: A dictionary with the object name as a key and the import string 532 | as the value. 533 | """ 534 | package_objects: Dict[str, str] = {} 535 | 536 | # Get the modules of the desired package 537 | try: 538 | package_modules = [__import__(name)] 539 | 540 | except ModuleNotFoundError: 541 | return package_objects 542 | package_modules.extend( 543 | [ 544 | module[1] 545 | for module in inspect.getmembers(package_modules[0], inspect.ismodule) 546 | ] 547 | ) 548 | 549 | # Get objects of the package 550 | for module in package_modules: 551 | for package_object_tuple in inspect.getmembers(module): 552 | object_name = package_object_tuple[0] 553 | package_object = package_object_tuple[1] 554 | # If the object is a function or a class 555 | if inspect.isfunction(package_object) or inspect.isclass(package_object): 556 | if ( 557 | object_name not in package_objects 558 | and name in package_object.__module__ 559 | ): 560 | # Try to load the object from the module instead of the 561 | # submodules. 562 | if hasattr(module, "__all__") and object_name in module.__all__: 563 | package_objects[object_name] = ( 564 | f"from {module.__name__} import {object_name}" 565 | ) 566 | else: 567 | package_objects[object_name] = ( 568 | f"from {package_object.__module__} import {object_name}" 569 | ) 570 | 571 | elif not re.match(r"^_.*", object_name): 572 | # The rest of objects 573 | package_objects[object_name] = ( 574 | f"from {module.__name__} import {object_name}" 575 | ) 576 | return package_objects 577 | -------------------------------------------------------------------------------- /src/autoimport/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyz-code/autoimport/842554d5feb8e6629beecb4596249d0e68621451/src/autoimport/py.typed -------------------------------------------------------------------------------- /src/autoimport/services.py: -------------------------------------------------------------------------------- 1 | """Define all the orchestration functionality required by the program to work. 2 | 3 | Classes and functions that connect the different domain model objects with the adapters 4 | and handlers to achieve the program's purpose. 5 | """ 6 | 7 | from typing import Any, Dict, Optional, Tuple 8 | 9 | from _io import TextIOWrapper 10 | 11 | from autoimport.model import SourceCode 12 | 13 | 14 | def fix_files( 15 | files: Tuple[TextIOWrapper, ...], 16 | config: Optional[Dict[str, Any]] = None, 17 | keep_unused_imports: bool = False, 18 | ) -> Optional[str]: 19 | """Fix the python source code of a list of files. 20 | 21 | If the input is taken from stdin, it will output the value to stdout. 22 | 23 | Args: 24 | files: List of files to fix. 25 | 26 | Returns: 27 | Fixed code retrieved from stdin or None. 28 | """ 29 | for file_wrapper in files: 30 | source = file_wrapper.read() 31 | fixed_source = fix_code(source, config, keep_unused_imports) 32 | 33 | if fixed_source == source and file_wrapper.name != "": 34 | continue 35 | 36 | try: 37 | # Click testing runner doesn't simulate correctly the reading from stdin 38 | # instead of setting the name attribute to `` it gives an 39 | # AttributeError. But when you use it outside testing, no AttributeError 40 | # is raised and name has the value . So there is no way of testing 41 | # this behaviour. 42 | if file_wrapper.name == "": # pragma no cover 43 | output = "output" 44 | else: 45 | output = "file" 46 | except AttributeError: 47 | output = "output" 48 | 49 | if output == "file": 50 | file_wrapper.seek(0) 51 | file_wrapper.write(fixed_source) 52 | file_wrapper.truncate() 53 | file_wrapper.close() 54 | else: 55 | return fixed_source 56 | 57 | return None 58 | 59 | 60 | def fix_code( 61 | original_source_code: str, 62 | config: Optional[Dict[str, Any]] = None, 63 | keep_unused_imports: bool = False, 64 | ) -> str: 65 | """Fix python source code to correct import statements. 66 | 67 | It corrects these errors: 68 | 69 | * Add missed import statements. 70 | * Remove unused import statements. 71 | * Move import statements to the top. 72 | 73 | Args: 74 | original_source_code: Source code to be corrected. 75 | keep_unused_imports: If true, unused imports are retained. 76 | 77 | Returns: 78 | Corrected source code. 79 | """ 80 | return SourceCode( 81 | original_source_code, config=config, keep_unused_imports=keep_unused_imports 82 | ).fix() 83 | -------------------------------------------------------------------------------- /src/autoimport/version.py: -------------------------------------------------------------------------------- 1 | """Utilities to retrieve the information of the program version.""" 2 | 3 | import platform 4 | import sys 5 | from textwrap import dedent 6 | 7 | # Do not edit the version manually, let `make bump` do it. 8 | 9 | __version__ = "1.6.0" 10 | 11 | 12 | def version_info() -> str: 13 | """Display the version of the program, python and the platform.""" 14 | return dedent( 15 | f"""\ 16 | ------------------------------------------------------------------ 17 | autoimport: {__version__} 18 | Python: {sys.version.split(" ", maxsplit=1)[0]} 19 | Platform: {platform.platform()} 20 | ------------------------------------------------------------------""" 21 | ) 22 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyz-code/autoimport/842554d5feb8e6629beecb4596249d0e68621451/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | """Store the classes and fixtures used throughout the tests.""" 2 | 3 | import pathlib 4 | from pathlib import Path 5 | 6 | import pytest 7 | 8 | 9 | @pytest.fixture() 10 | def test_dir(tmp_path: Path) -> pathlib.Path: 11 | """Creates test directory and files and returns root test file directory.""" 12 | file_contents = "os.getcwd()" 13 | 14 | test_dirs = tmp_path / "test_files" 15 | subdir = test_dirs / "subdir" 16 | 17 | subdir.mkdir(parents=True) 18 | 19 | file1 = test_dirs / "test_file1.py" 20 | with file1.open("w", encoding="UTF8") as file_descriptor: 21 | file_descriptor.write(file_contents) 22 | 23 | file2 = subdir / "test_file2.py" 24 | with file2.open("w", encoding="UTF8") as file_descriptor: 25 | file_descriptor.write(file_contents) 26 | 27 | return test_dirs 28 | -------------------------------------------------------------------------------- /tests/e2e/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyz-code/autoimport/842554d5feb8e6629beecb4596249d0e68621451/tests/e2e/__init__.py -------------------------------------------------------------------------------- /tests/e2e/test_cli.py: -------------------------------------------------------------------------------- 1 | """Test the command line interface.""" 2 | 3 | import os 4 | import re 5 | from pathlib import Path 6 | from textwrap import dedent 7 | from typing import Dict, List, Optional 8 | 9 | import pytest 10 | from click.testing import CliRunner 11 | 12 | from autoimport.entrypoints.cli import cli 13 | from autoimport.version import __version__ 14 | 15 | 16 | @pytest.fixture(name="runner") 17 | def fixture_runner() -> CliRunner: 18 | """Configure the Click cli test runner.""" 19 | return CliRunner(mix_stderr=False, env={"XDG_CONFIG_HOME": "/dev/null"}) 20 | 21 | 22 | def test_version(runner: CliRunner) -> None: 23 | """Prints program version when called with --version.""" 24 | result = runner.invoke(cli, ["--version"]) 25 | 26 | assert result.exit_code == 0 27 | assert re.search( 28 | rf" *autoimport: {__version__}\n *Python: .*\n *Platform: .*", 29 | result.stdout, 30 | ) 31 | 32 | 33 | def test_corrects_one_file(runner: CliRunner, tmp_path: Path) -> None: 34 | """Correct the source code of a file.""" 35 | test_file = tmp_path / "source.py" 36 | test_file.write_text("os.getcwd()") 37 | fixed_source = dedent( 38 | """\ 39 | import os 40 | 41 | 42 | os.getcwd()""" 43 | ) 44 | 45 | result = runner.invoke(cli, [str(test_file)]) 46 | 47 | assert result.exit_code == 0 48 | assert test_file.read_text() == fixed_source 49 | 50 | 51 | @pytest.mark.secondary() 52 | def test_corrects_three_files(runner: CliRunner, tmp_path: Path) -> None: 53 | """Correct the source code of multiple files.""" 54 | test_files = [] 55 | for file_number in range(3): 56 | test_file = tmp_path / f"source_{file_number}.py" 57 | test_file.write_text("os.getcwd()") 58 | test_files.append(test_file) 59 | fixed_source = dedent( 60 | """\ 61 | import os 62 | 63 | 64 | os.getcwd()""" 65 | ) 66 | 67 | result = runner.invoke(cli, [str(test_file) for test_file in test_files]) 68 | 69 | assert result.exit_code == 0 70 | for test_file in test_files: 71 | assert test_file.read_text() == fixed_source 72 | 73 | 74 | def test_correct_all_files_in_dir_recursively( 75 | runner: CliRunner, test_dir: Path 76 | ) -> None: 77 | """Ensure files and dirs can be parsed and fixes associated files.""" 78 | result = runner.invoke(cli, [str(test_dir)]) 79 | 80 | assert result.exit_code == 0 81 | fixed_source = "import os\n\n\nos.getcwd()" 82 | assert (test_dir / "test_file1.py").read_text() == fixed_source 83 | assert (test_dir / "subdir/test_file2.py").read_text() == fixed_source 84 | 85 | 86 | def test_correct_mix_dir_and_files( 87 | runner: CliRunner, test_dir: Path, tmp_path: Path 88 | ) -> None: 89 | """Ensure all files in a given directory get fixed by autoimport.""" 90 | test_file = tmp_path / "source.py" 91 | test_file.write_text("os.getcwd()") 92 | 93 | result = runner.invoke(cli, [str(test_dir), str(test_file)]) 94 | 95 | assert result.exit_code == 0 96 | fixed_source = "import os\n\n\nos.getcwd()" 97 | assert (test_dir / "test_file1.py").read_text() == fixed_source 98 | assert (test_dir / "subdir/test_file2.py").read_text() == fixed_source 99 | assert test_file.read_text() == fixed_source 100 | 101 | 102 | def test_corrects_code_from_stdin(runner: CliRunner) -> None: 103 | """Correct the source code passed as stdin.""" 104 | source = "os.getcwd()" 105 | fixed_source = dedent( 106 | """\ 107 | import os 108 | 109 | 110 | os.getcwd()""" 111 | ) 112 | 113 | result = runner.invoke(cli, ["-"], input=source) 114 | 115 | assert result.exit_code == 0 116 | assert result.stdout == fixed_source 117 | 118 | 119 | def test_pyproject_common_statements(runner: CliRunner, tmp_path: Path) -> None: 120 | """Allow common_statements to be defined in pyproject.toml""" 121 | pyproject_toml = tmp_path / "pyproject.toml" 122 | pyproject_toml.write_text( 123 | dedent( 124 | """\ 125 | [tool.autoimport] 126 | common_statements = { "FooBar" = "from baz.qux import FooBar" } 127 | """ 128 | ) 129 | ) 130 | test_file = tmp_path / "source.py" 131 | test_file.write_text("FooBar\n") 132 | # AAA03: Until https://github.com/jamescooke/flake8-aaa/issues/196 is fixed 133 | with runner.isolated_filesystem(temp_dir=tmp_path): 134 | result = runner.invoke(cli, [str(test_file)]) # noqa: AAA03 135 | 136 | assert result.exit_code == 0 137 | assert test_file.read_text() == dedent( 138 | """\ 139 | from baz.qux import FooBar 140 | 141 | 142 | FooBar 143 | """ 144 | ) 145 | 146 | 147 | @pytest.mark.skip("Until https://github.com/dbatten5/maison/issues/141 is fixed") 148 | def test_config_path_argument(runner: CliRunner, tmp_path: Path) -> None: 149 | """Allow common_statements to be defined in pyproject.toml""" 150 | config_dir = tmp_path / "config" 151 | config_dir.mkdir() 152 | pyproject_toml = config_dir / "pyproject.toml" 153 | pyproject_toml.write_text( 154 | dedent( 155 | """\ 156 | [tool.autoimport] 157 | common_statements = { "FooBar" = "from baz.qux import FooBar" } 158 | """ 159 | ) 160 | ) 161 | code_dir = tmp_path / "code" 162 | code_dir.mkdir() 163 | test_file = code_dir / "source.py" 164 | test_file.write_text("FooBar\n") 165 | 166 | result = runner.invoke(cli, ["--config-file", str(pyproject_toml), str(test_file)]) 167 | 168 | assert result.exit_code == 0 169 | assert test_file.read_text() == dedent( 170 | """\ 171 | from baz.qux import FooBar 172 | 173 | 174 | FooBar 175 | """ 176 | ) 177 | 178 | 179 | @pytest.mark.parametrize( 180 | ("create_global_conf", "use_local_conf", "create_pyproject", "expected_imports"), 181 | [ 182 | pytest.param(True, False, False, "from g import G", id="global"), 183 | pytest.param(False, True, False, "from r import R", id="local"), 184 | pytest.param(False, False, True, "from p import P", id="pyproject"), 185 | pytest.param( 186 | True, True, False, "from g import G\nfrom r import R", id="global-and-local" 187 | ), 188 | pytest.param( 189 | True, 190 | False, 191 | True, 192 | "from g import G\nfrom p import P", 193 | id="global-and-pyproject", 194 | ), 195 | pytest.param( 196 | False, 197 | True, 198 | True, 199 | "from r import R\nfrom p import P", 200 | id="local-and-pyproject", 201 | ), 202 | pytest.param( 203 | True, 204 | True, 205 | True, 206 | "from g import G\nfrom r import R\nfrom p import P", 207 | id="global-and-local-and-pyproject", 208 | ), # noqa: R0913, R0914 209 | ], 210 | ) 211 | # R0913: Too many arguments (6/5): We need to refactor this test in many more 212 | # R0914: Too many local variables (16/15): We need to refactor this test in many more 213 | def test_global_and_local_config( # noqa: R0913, R0914 214 | runner: CliRunner, 215 | tmp_path: Path, 216 | create_global_conf: bool, 217 | use_local_conf: bool, 218 | create_pyproject: bool, 219 | expected_imports: str, 220 | ) -> None: 221 | """ 222 | Test interaction between the following: 223 | - presence of the global config file $XDG_CONFIG_HOME/autoimport/config.toml 224 | - use of the --config-file flag to specify a local config file 225 | - presence of a pyproject.toml file 226 | """ 227 | config = { 228 | "global": '[common_statements]\n"G" = "from g import G"', 229 | "local": '[common_statements]\n"R" = "from r import R"', 230 | "pyproject": '[tool.autoimport.common_statements]\n"P" = "from p import P"', 231 | } 232 | code_path = tmp_path / "code.py" 233 | original_code = dedent( 234 | """ 235 | G 236 | R 237 | P 238 | """ 239 | ) 240 | code_path.write_text(original_code) 241 | args: List[str] = [str(code_path)] 242 | env: Dict[str, Optional[str]] = {} 243 | if create_global_conf: 244 | xdg_home = (tmp_path / "xdg_home").resolve() # must be absolute path 245 | env["XDG_CONFIG_HOME"] = str(xdg_home) 246 | global_conf_path = xdg_home / "autoimport" / "config.toml" 247 | global_conf_path.parent.mkdir(parents=True) 248 | global_conf_path.write_text(config["global"]) 249 | if use_local_conf: 250 | local_conf_path = tmp_path / "cfg" / "local.toml" 251 | local_conf_path.parent.mkdir(parents=True) 252 | local_conf_path.write_text(config["local"]) 253 | args.extend(["--config-file", str(local_conf_path)]) 254 | if create_pyproject: 255 | pyproject_path = tmp_path / "pyproject.toml" 256 | pyproject_path.write_text(config["pyproject"]) 257 | # AAA03: Until https://github.com/jamescooke/flake8-aaa/issues/196 is fixed 258 | with runner.isolated_filesystem(temp_dir=tmp_path): 259 | result = runner.invoke(cli, args, env=env) # noqa: AAA03 260 | 261 | assert result.exit_code == 0 262 | assert code_path.read_text() == expected_imports + "\n\n" + original_code 263 | 264 | 265 | def test_global_and_local_config_precedence(runner: CliRunner, tmp_path: Path) -> None: 266 | """ 267 | Test precedence of configuration specified in the global config vs 268 | pyproject.toml vs --config-file. From low to high priority: 269 | - global config file 270 | - project-local pyproject.toml file 271 | - file specified by the --config-file flag, if any 272 | """ 273 | config = { 274 | "global": dedent( 275 | """ 276 | [common_statements] 277 | "G" = "from g import G" 278 | "A" = "from ga import A" 279 | "B" = "from gb import B" 280 | "C" = "from gc import C" 281 | """ 282 | ), 283 | "pyproject": dedent( 284 | """ 285 | [tool.autoimport.common_statements] 286 | "A" = "from pa import A" 287 | "C" = "from pc import C" 288 | "D" = "from pd import D" 289 | """ 290 | ), 291 | "local": dedent( 292 | """ 293 | [common_statements] 294 | "B" = "from lb import B" 295 | "C" = "from lc import C" 296 | "D" = "from ld import D" 297 | """ 298 | ), 299 | } 300 | code_path = tmp_path / "code.py" 301 | original_code = dedent( 302 | """ 303 | A 304 | B 305 | C 306 | D 307 | G 308 | """ 309 | ) 310 | expected_imports = dedent( 311 | """\ 312 | from pa import A 313 | from lb import B 314 | from lc import C 315 | from ld import D 316 | from g import G 317 | """ 318 | ) 319 | code_path.write_text(original_code) 320 | args: List[str] = [str(code_path)] 321 | env: Dict[str, Optional[str]] = {} 322 | # create_global_conf: 323 | xdg_home = (tmp_path / "xdg_home").resolve() # must be absolute path 324 | env["XDG_CONFIG_HOME"] = str(xdg_home) 325 | global_conf_path = xdg_home / "autoimport" / "config.toml" 326 | global_conf_path.parent.mkdir(parents=True) 327 | global_conf_path.write_text(config["global"]) 328 | # create: 329 | local_conf_path = tmp_path / "cfg" / "local.toml" 330 | local_conf_path.parent.mkdir(parents=True) 331 | local_conf_path.write_text(config["local"]) 332 | args.extend(["--config-file", str(local_conf_path)]) 333 | # create_pyproject: 334 | pyproject_path = tmp_path / "pyproject.toml" 335 | pyproject_path.write_text(config["pyproject"]) 336 | # AAA03: Until https://github.com/jamescooke/flake8-aaa/issues/196 is fixed 337 | with runner.isolated_filesystem(temp_dir=tmp_path): 338 | result = runner.invoke(cli, args, env=env) # noqa: AAA03 339 | 340 | assert result.exit_code == 0 341 | assert code_path.read_text() == expected_imports + "\n" + original_code 342 | 343 | 344 | def test_fix_files_doesnt_touch_the_file_if_its_not_going_to_change_it( 345 | runner: CliRunner, tmp_path: Path 346 | ) -> None: 347 | """ 348 | Given: A file that doesn't need any change 349 | When: fix files is run 350 | Then: The file is untouched 351 | """ 352 | test_file = tmp_path / "source.py" 353 | test_file.write_text("a = 1") 354 | modified_time = os.path.getmtime(test_file) 355 | 356 | result = runner.invoke(cli, [str(test_file)]) 357 | 358 | assert result.exit_code == 0 359 | assert os.path.getmtime(test_file) == modified_time 360 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lyz-code/autoimport/842554d5feb8e6629beecb4596249d0e68621451/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/test_entrypoints.py: -------------------------------------------------------------------------------- 1 | """Tests for all entrypints modules.""" 2 | 3 | import re 4 | from io import TextIOWrapper 5 | from pathlib import Path 6 | from typing import Any, Sequence 7 | 8 | import click 9 | import pytest 10 | 11 | from autoimport.entrypoints.cli import FileOrDir, flatten, get_files 12 | 13 | 14 | @pytest.mark.parametrize( 15 | ("sequence", "expected"), 16 | [ 17 | ((1, (2, 3, 4), 5), (1, 2, 3, 4, 5)), 18 | ([1, 2, 3, [4, 5, 6]], (1, 2, 3, 4, 5, 6)), 19 | ([["a", "b", "c"], "d"], ("a", "b", "c", "d")), 20 | ], 21 | ) 22 | def test_flatten(sequence: Sequence[Any], expected: Sequence[Any]) -> None: 23 | """Test the flatten function works.""" 24 | result = flatten(sequence) 25 | 26 | assert result == expected 27 | 28 | 29 | def test_custom_param_type_works_with_dir(test_dir: Path) -> None: 30 | """Ensure the custom param type can be parsed a directory.""" 31 | param_type = FileOrDir() 32 | 33 | result = param_type.convert(test_dir, None, None) 34 | 35 | for file_ in result: 36 | assert isinstance(file_, TextIOWrapper) 37 | assert re.match(r".*file[1-2].py", file_.name) 38 | file_.close() 39 | 40 | 41 | def test_custom_param_type_works_with_file(test_dir: Path) -> None: 42 | """Ensure the custom param type can be parsed a file.""" 43 | param_type = FileOrDir() 44 | 45 | result = param_type.convert(test_dir / "test_file1.py", None, None) 46 | 47 | assert re.match(r".*file[1-2].py", result[0].name) 48 | result[0].close() 49 | 50 | 51 | @pytest.mark.parametrize("filename", ["h.py", "new_dir"]) 52 | def test_custom_param_type_with_non_existing_files( 53 | test_dir: Path, filename: str 54 | ) -> None: 55 | """Ensure an error occurs when a non existing file or dir is parsed.""" 56 | param_type = FileOrDir() 57 | 58 | with pytest.raises(click.BadParameter) as error: 59 | param_type.convert(test_dir / filename, None, None) 60 | 61 | assert f"{filename}' does not exist" in error.value.args[0] 62 | 63 | 64 | def test_get_files(test_dir: Path) -> None: 65 | """Ensure we can get all files recursively from a given directory.""" 66 | result = get_files(str(test_dir)) 67 | 68 | assert all(re.match(r".*file[1-2].py", file.name) for file in result) 69 | for file_path in result: 70 | file_path.close() 71 | -------------------------------------------------------------------------------- /tests/unit/test_extract_package.py: -------------------------------------------------------------------------------- 1 | """Test the extraction of package objects.""" 2 | 3 | from autoimport.model import extract_package_objects 4 | 5 | 6 | def test_extraction_returns_the_package_functions() -> None: 7 | """ 8 | Given: A package with functions. 9 | When: extract package objects is called 10 | Then: All the functions are extracted 11 | """ 12 | result = extract_package_objects("autoimport") 13 | 14 | desired_objects = { 15 | "fix_code": "from autoimport import fix_code", 16 | "fix_files": "from autoimport import fix_files", 17 | "extract_package_objects": ( 18 | "from autoimport.model import extract_package_objects" 19 | ), 20 | } 21 | for object_name, object_import_string in desired_objects.items(): 22 | assert result[object_name] == object_import_string 23 | 24 | 25 | def test_extraction_returns_the_package_classes() -> None: 26 | """ 27 | Given: A package with classes. 28 | When: extract package objects is called. 29 | Then: All the classes are extracted. 30 | """ 31 | result = extract_package_objects("autoimport") 32 | 33 | desired_objects = { 34 | "SourceCode": "from autoimport.model import SourceCode", 35 | } 36 | for object_name, object_import_string in desired_objects.items(): 37 | assert result[object_name] == object_import_string 38 | 39 | 40 | def test_extraction_returns_the_package_dictionaries() -> None: 41 | """ 42 | Given: A package with dictionaries. 43 | When: extract package objects is called. 44 | Then: All the dictionaries are extracted. 45 | """ 46 | result = extract_package_objects("autoimport") 47 | 48 | desired_objects = { 49 | "common_statements": "from autoimport.model import common_statements", 50 | } 51 | for object_name, object_import_string in desired_objects.items(): 52 | assert result[object_name] == object_import_string 53 | 54 | 55 | def test_extraction_returns_empty_dict_if_package_is_not_importable() -> None: 56 | """ 57 | Given: Autoimport can't import the package. 58 | When: the extract package objects is called. 59 | Then: An empty directory is returned 60 | """ 61 | result = extract_package_objects("inexistent") 62 | 63 | assert not result 64 | -------------------------------------------------------------------------------- /tests/unit/test_services.py: -------------------------------------------------------------------------------- 1 | """Tests the service layer.""" 2 | 3 | from textwrap import dedent 4 | 5 | import pytest 6 | 7 | from autoimport.model import common_statements 8 | from autoimport.services import fix_code 9 | 10 | 11 | def test_fix_code_adds_missing_import() -> None: 12 | """Understands that os is a package and add it to the top of the file.""" 13 | source = "os.getcwd()" 14 | fixed_source = dedent( 15 | """\ 16 | import os 17 | 18 | 19 | os.getcwd()""" 20 | ) 21 | 22 | result = fix_code(source) 23 | 24 | assert result == fixed_source 25 | 26 | 27 | def test_fix_doesnt_change_source_if_package_doesnt_exist() -> None: 28 | """As foo is not found, nothing is changed.""" 29 | source = "foo" 30 | 31 | result = fix_code(source) 32 | 33 | assert result == source 34 | 35 | 36 | def test_fix_imports_packages_below_docstring() -> None: 37 | """Imports are located below the module docstrings.""" 38 | source = dedent( 39 | '''\ 40 | """Module docstring. 41 | 42 | """ 43 | import pytest 44 | os.getcwd()''' 45 | ) 46 | fixed_source = dedent( 47 | '''\ 48 | """Module docstring. 49 | 50 | """ 51 | 52 | import os 53 | 54 | 55 | os.getcwd()''' 56 | ) 57 | 58 | result = fix_code(source) 59 | 60 | assert result == fixed_source 61 | 62 | 63 | def test_fix_imports_packages_below_single_line_docstring() -> None: 64 | """Imports are located below the module docstrings when they only take one line.""" 65 | source = dedent( 66 | '''\ 67 | """Module docstring.""" 68 | 69 | import pytest 70 | os.getcwd()''' 71 | ) 72 | fixed_source = dedent( 73 | '''\ 74 | """Module docstring.""" 75 | 76 | import os 77 | 78 | 79 | os.getcwd()''' 80 | ) 81 | 82 | result = fix_code(source) 83 | 84 | assert result == fixed_source 85 | 86 | 87 | def test_fix_imports_type_hints() -> None: 88 | """Typing objects are initialized with their required header.""" 89 | source = dedent( 90 | """\ 91 | def function(dictionary: Dict) -> None: 92 | pass""" 93 | ) 94 | fixed_source = dedent( 95 | """\ 96 | from typing import Dict 97 | 98 | 99 | def function(dictionary: Dict) -> None: 100 | pass""" 101 | ) 102 | 103 | result = fix_code(source) 104 | 105 | assert result == fixed_source 106 | 107 | 108 | def test_fix_removes_unneeded_imports() -> None: 109 | """If there is an import statement of an unused package it should be removed.""" 110 | source = dedent( 111 | """\ 112 | import requests 113 | foo = 1""" 114 | ) 115 | fixed_source = "foo = 1" 116 | 117 | result = fix_code(source) 118 | 119 | assert result == fixed_source 120 | 121 | 122 | def test_fix_removes_multiple_unneeded_imports() -> None: 123 | """ 124 | Given: A source code with multiple unused import statements. 125 | When: fix_code is run. 126 | Then: The unused import statements are deleted. 127 | """ 128 | source = dedent( 129 | """\ 130 | import requests 131 | from textwrap import dedent 132 | 133 | from yaml import YAMLError 134 | foo = 1""" 135 | ) 136 | fixed_source = "foo = 1" 137 | 138 | result = fix_code(source) 139 | 140 | assert result == fixed_source 141 | 142 | 143 | def test_fix_removes_unneeded_imports_in_from_statements() -> None: 144 | """Remove `from package import` statement of an unused packages.""" 145 | source = dedent( 146 | """\ 147 | from os import path 148 | foo = 1""" 149 | ) 150 | fixed_source = "foo = 1" 151 | 152 | result = fix_code(source) 153 | 154 | assert result == fixed_source 155 | 156 | 157 | def test_fix_removes_unused_imports_in_multiline_from_statements() -> None: 158 | """ 159 | Given: A source code with multiline import from statements. 160 | When: fix_code is run 161 | Then: Unused import statements are deleted 162 | """ 163 | source = dedent( 164 | """\ 165 | from os import ( 166 | getcwd, 167 | path, 168 | ) 169 | 170 | getcwd()""" 171 | ) 172 | fixed_source = dedent( 173 | """\ 174 | from os import ( 175 | getcwd, 176 | ) 177 | 178 | 179 | getcwd()""" 180 | ) 181 | 182 | result = fix_code(source) 183 | 184 | assert result == fixed_source 185 | 186 | 187 | def test_fix_removes_unneeded_imports_in_beginning_of_from_statements() -> None: 188 | """Remove unused `object_name` in `from package import object_name, other_object` 189 | statements. 190 | """ 191 | source = dedent( 192 | """\ 193 | from os import path, getcwd 194 | 195 | getcwd()""" 196 | ) 197 | fixed_source = dedent( 198 | """\ 199 | from os import getcwd 200 | 201 | 202 | getcwd()""" 203 | ) 204 | 205 | result = fix_code(source) 206 | 207 | assert result == fixed_source 208 | 209 | 210 | def test_fix_removes_unneeded_imports_in_middle_of_from_statements() -> None: 211 | """Remove unused `object_name` in 212 | `from package import other_object, object_name, other_used_object` statements. 213 | """ 214 | source = dedent( 215 | """\ 216 | from os import getcwd, path, mkdir 217 | 218 | getcwd() 219 | mkdir()""" 220 | ) 221 | fixed_source = dedent( 222 | """\ 223 | from os import getcwd, mkdir 224 | 225 | 226 | getcwd() 227 | mkdir()""" 228 | ) 229 | 230 | result = fix_code(source) 231 | 232 | assert result == fixed_source 233 | 234 | 235 | def test_fix_removes_unneeded_imports_in_end_of_from_statements() -> None: 236 | """Remove unused `object_name` in `from package import other_object, object_name` 237 | statements. 238 | """ 239 | source = dedent( 240 | """\ 241 | from os import getcwd, path 242 | 243 | getcwd()""" 244 | ) 245 | fixed_source = dedent( 246 | """\ 247 | from os import getcwd 248 | 249 | 250 | getcwd()""" 251 | ) 252 | 253 | result = fix_code(source) 254 | 255 | assert result == fixed_source 256 | 257 | 258 | def test_fix_respects_multiple_from_import_lines() -> None: 259 | """ 260 | Given: Multiple from X import Y lines. 261 | When: Fix code is run 262 | Then: The import statements aren't broken 263 | """ 264 | source = dedent( 265 | """\ 266 | from os import getcwd 267 | 268 | from re import match 269 | 270 | 271 | getcwd() 272 | match(r'a', 'a')""" 273 | ) 274 | 275 | result = fix_code(source) 276 | 277 | assert result == source 278 | 279 | 280 | def test_fix_respects_multiple_from_import_lines_in_multiple_lines() -> None: 281 | """ 282 | Given: Multiple from X import Y lines, some with multiline format. 283 | When: Fix code is run 284 | Then: The import statements aren't broken 285 | """ 286 | source = dedent( 287 | """\ 288 | from os import ( 289 | getcwd, 290 | ) 291 | 292 | from re import match 293 | 294 | 295 | getcwd() 296 | match(r'a', 'a')""" 297 | ) 298 | 299 | result = fix_code(source) 300 | 301 | assert result == source 302 | 303 | 304 | def test_fix_respects_import_lines_in_multiple_line_strings() -> None: 305 | """ 306 | Given: Import lines in several multiline strings. 307 | When: Fix code is run. 308 | Then: The import statements inside the string are not moved to the top. 309 | """ 310 | source = dedent( 311 | """\ 312 | from textwrap import dedent 313 | 314 | source = dedent( 315 | \"\"\"\\ 316 | from re import match 317 | 318 | match(r'a', 'a')\"\"\" 319 | ) 320 | 321 | source = dedent( 322 | \"\"\"\\ 323 | from os import ( 324 | getcwd, 325 | ) 326 | 327 | getcwd()\"\"\" 328 | )""" 329 | ) 330 | fixed_source = dedent( 331 | """\ 332 | from textwrap import dedent 333 | 334 | 335 | source = dedent( 336 | \"\"\"\\ 337 | from re import match 338 | 339 | match(r'a', 'a')\"\"\" 340 | ) 341 | 342 | source = dedent( 343 | \"\"\"\\ 344 | from os import ( 345 | getcwd, 346 | ) 347 | 348 | getcwd()\"\"\" 349 | )""" 350 | ) 351 | 352 | result = fix_code(source) 353 | 354 | assert result == fixed_source 355 | 356 | 357 | def test_fix_moves_import_statements_to_the_top() -> None: 358 | """Move import statements present in the source code to the top of the file""" 359 | source = dedent( 360 | """\ 361 | a = 3 362 | 363 | import os 364 | os.getcwd()""" 365 | ) 366 | fixed_source = dedent( 367 | """\ 368 | import os 369 | 370 | 371 | a = 3 372 | 373 | os.getcwd()""" 374 | ) 375 | 376 | result = fix_code(source) 377 | 378 | assert result == fixed_source 379 | 380 | 381 | def test_fix_moves_import_statements_in_indented_code_to_the_top() -> None: 382 | """Move import statements present indented in the source code 383 | to the top of the file 384 | """ 385 | source = dedent( 386 | """\ 387 | import requests 388 | 389 | requests.get('hi') 390 | 391 | def test(): 392 | import os 393 | os.getcwd()""" 394 | ) 395 | fixed_source = dedent( 396 | """\ 397 | import requests 398 | 399 | import os 400 | 401 | 402 | requests.get('hi') 403 | 404 | def test(): 405 | os.getcwd()""" 406 | ) 407 | 408 | result = fix_code(source) 409 | 410 | assert result == fixed_source 411 | 412 | 413 | def test_fix_skips_moves_to_the_top_when_disabled_is_true() -> None: 414 | """Skip moving import statements when disable_move_to_top config is true.""" 415 | source = dedent( 416 | """\ 417 | import requests 418 | 419 | 420 | requests.get('hi') 421 | 422 | def test(): 423 | import os 424 | os.getcwd()""" 425 | ) 426 | config = {"disable_move_to_top": True} 427 | 428 | result = fix_code(source, config) 429 | 430 | assert result == source 431 | 432 | 433 | def test_fix_skips_moves_to_the_top_when_disabled_is_false() -> None: 434 | """Moving import statements should still occur when disable_move_to_top 435 | config is false. 436 | """ 437 | source = dedent( 438 | """\ 439 | import requests 440 | 441 | requests.get('hi') 442 | 443 | def test(): 444 | import os 445 | os.getcwd()""" 446 | ) 447 | fixed_source = dedent( 448 | """\ 449 | import requests 450 | 451 | import os 452 | 453 | 454 | requests.get('hi') 455 | 456 | def test(): 457 | os.getcwd()""" 458 | ) 459 | config = {"disable_move_to_top": False} 460 | 461 | result = fix_code(source, config) 462 | 463 | assert result == fixed_source 464 | 465 | 466 | def test_fix_moves_from_import_statements_to_the_top() -> None: 467 | """Move from import statements present in the source code to the top of the file""" 468 | source = dedent( 469 | """\ 470 | a = 3 471 | 472 | from os import getcwd 473 | getcwd()""" 474 | ) 475 | fixed_source = dedent( 476 | """\ 477 | from os import getcwd 478 | 479 | 480 | a = 3 481 | 482 | getcwd()""" 483 | ) 484 | 485 | result = fix_code(source) 486 | 487 | assert result == fixed_source 488 | 489 | 490 | def test_fix_moves_multiline_import_statements_to_the_top() -> None: 491 | """ 492 | Given: Multiple from X import Y lines. 493 | When: Fix code is run. 494 | Then: The import statements are moved to the top. 495 | """ 496 | source = dedent( 497 | """\ 498 | from os import getcwd 499 | 500 | getcwd() 501 | 502 | from re import ( 503 | match, 504 | ) 505 | match(r'a', 'a')""" 506 | ) 507 | fixed_source = dedent( 508 | """\ 509 | from os import getcwd 510 | 511 | from re import ( 512 | match, 513 | ) 514 | 515 | 516 | getcwd() 517 | 518 | match(r'a', 'a')""" 519 | ) 520 | 521 | result = fix_code(source) 522 | 523 | assert result == fixed_source 524 | 525 | 526 | def test_fix_doesnt_break_objects_with_import_in_their_names() -> None: 527 | """Objects that have the import name in their name should not be changed.""" 528 | source = dedent( 529 | """\ 530 | def import_code(): 531 | pass 532 | 533 | def code_import(): 534 | pass 535 | 536 | def import(): 537 | pass 538 | 539 | import_string = 'a'""" 540 | ) 541 | 542 | result = fix_code(source) 543 | 544 | assert result == source 545 | 546 | 547 | def test_fix_doesnt_move_import_statements_with_noqa_to_the_top() -> None: 548 | """Ignore lines that have # noqa: autoimport.""" 549 | source = dedent( 550 | """\ 551 | a = 3 552 | 553 | from os import getcwd # noqa: autoimport 554 | getcwd()""" 555 | ) 556 | 557 | result = fix_code(source) 558 | 559 | assert result == source 560 | 561 | 562 | def test_fix_doesnt_fail_on_noqa_lines_on_unused_import() -> None: 563 | """Ignore lines that have # noqa: autoimport.""" 564 | source = dedent( 565 | """\ 566 | from os import getcwd # noqa: autoimport""" 567 | ) 568 | 569 | result = fix_code(source) 570 | 571 | assert result == source 572 | 573 | 574 | def test_fix_respects_fmt_skip_lines() -> None: 575 | """Ignore lines that have # fmt: skip.""" 576 | source = dedent( 577 | """ 578 | def why(): 579 | import pdb;pdb.set_trace() # fmt: skip 580 | return 'dunno' 581 | """ 582 | ).replace("\n", "", 1) 583 | 584 | result = fix_code(source) 585 | 586 | assert result == source 587 | 588 | 589 | def test_fix_respects_noqa_in_from_import_lines_in_multiple_lines() -> None: 590 | """ 591 | Given: Multiple from X import Y lines, some with multiline format with noqa 592 | statement. 593 | When: Fix code is run. 594 | Then: The import statements aren't broken. 595 | """ 596 | source = dedent( 597 | """\ 598 | from os import getcwd 599 | 600 | 601 | getcwd() 602 | 603 | from re import ( # noqa: autoimport 604 | match, 605 | ) 606 | 607 | match(r'a', 'a')""" 608 | ) 609 | 610 | result = fix_code(source) 611 | 612 | assert result == source 613 | 614 | 615 | def test_fix_respects_strings_with_import_statements() -> None: 616 | """ 617 | Given: Code with a string that has import statements structure. 618 | When: Fix code is run. 619 | Then: The string is respected 620 | """ 621 | source = dedent( 622 | """\ 623 | import_string = 'import requests' 624 | from_import_string = "from re import match" 625 | multiline string = dedent( 626 | \"\"\"\\ 627 | import requests 628 | from re import match 629 | \"\"\" 630 | ) 631 | multiline single_quote_string = dedent( 632 | \'\'\'\\ 633 | import requests 634 | from re import match 635 | \'\'\' 636 | ) 637 | import os""" 638 | ) 639 | fixed_source = dedent( 640 | """\ 641 | import os 642 | 643 | 644 | import_string = 'import requests' 645 | from_import_string = "from re import match" 646 | multiline string = dedent( 647 | \"\"\"\\ 648 | import requests 649 | from re import match 650 | \"\"\" 651 | ) 652 | multiline single_quote_string = dedent( 653 | \'\'\'\\ 654 | import requests 655 | from re import match 656 | \'\'\' 657 | )""" 658 | ) 659 | 660 | result = fix_code(source) 661 | 662 | assert result == fixed_source 663 | 664 | 665 | def test_fix_doesnt_mistake_docstrings_with_multiline_string() -> None: 666 | """ 667 | Given: A function with a docstring. 668 | When: Fix code is run. 669 | Then: The rest of the file is not mistaken for a long multiline string 670 | """ 671 | source = dedent( 672 | """\ 673 | def function_1(): 674 | \"\"\"Function docstring\"\"\" 675 | import os 676 | os.getcwd()""" 677 | ) 678 | fixed_source = dedent( 679 | """\ 680 | import os 681 | 682 | 683 | def function_1(): 684 | \"\"\"Function docstring\"\"\" 685 | os.getcwd()""" 686 | ) 687 | 688 | result = fix_code(source) 689 | 690 | assert result == fixed_source 691 | 692 | 693 | @pytest.mark.parametrize( 694 | ("import_key", "import_statement"), 695 | ((key, value) for key, value in common_statements.items()), 696 | ids=list(common_statements.keys()), 697 | ) 698 | def test_fix_autoimports_common_imports(import_key: str, import_statement: str) -> None: 699 | """ 700 | Given: Code with missing import statements that match the common list. 701 | When: Fix code is run. 702 | Then: The imports are done 703 | """ 704 | source = dedent( 705 | f"""\ 706 | import os 707 | 708 | os.getcwd 709 | 710 | variable = {import_key}""" 711 | ) 712 | fixed_source = dedent( 713 | f"""\ 714 | import os 715 | 716 | {import_statement} 717 | 718 | 719 | os.getcwd 720 | 721 | variable = {import_key}""" 722 | ) 723 | 724 | result = fix_code(source) 725 | 726 | assert result == fixed_source 727 | 728 | 729 | def test_fix_autoimports_objects_defined_in_the_root_of_the_package() -> None: 730 | """ 731 | Given: 732 | The fix code is run from a directory that belongs to a python project package. 733 | And a source code with an object that needs an import statement. 734 | And that object belongs to the root of the python package. 735 | When: Fix code is run. 736 | Then: The import is done 737 | """ 738 | source = dedent( 739 | """\ 740 | fix_code()""" 741 | ) 742 | fixed_source = dedent( 743 | """\ 744 | from autoimport import fix_code 745 | 746 | 747 | fix_code()""" 748 | ) 749 | 750 | result = fix_code(source) 751 | 752 | assert result == fixed_source 753 | 754 | 755 | def test_fix_autoimports_objects_defined_in___all__special_variable() -> None: 756 | """ 757 | Given: Some missing packages in the __all__ variable 758 | When: Fix code is run. 759 | Then: The import is done 760 | """ 761 | source = dedent( 762 | """\ 763 | __all__ = ['fix_code']""" 764 | ) 765 | fixed_source = dedent( 766 | """\ 767 | from autoimport import fix_code 768 | 769 | 770 | __all__ = ['fix_code']""" 771 | ) 772 | 773 | result = fix_code(source) 774 | 775 | assert result == fixed_source 776 | 777 | 778 | @pytest.mark.parametrize( 779 | "source", 780 | [ 781 | dedent( 782 | """\ 783 | import os 784 | from typing import TYPE_CHECKING 785 | 786 | if TYPE_CHECKING: 787 | from .model import Book 788 | 789 | 790 | os.getcwd() 791 | 792 | 793 | def read_book(book: Book): 794 | pass""" 795 | ), 796 | dedent( 797 | """\ 798 | from typing import TYPE_CHECKING 799 | 800 | if TYPE_CHECKING: 801 | foo = "bar" 802 | """ 803 | ), 804 | ], 805 | ) 806 | def test_fix_respects_type_checking_import_statements(source: str) -> None: 807 | """ 808 | Given: Code with if TYPE_CHECKING imports 809 | When: Fix code is run. 810 | Then: The imports are not moved above the if statement. 811 | 812 | Related to https://github.com/lyz-code/autoimport/issues/231 813 | """ 814 | result = fix_code(source) 815 | 816 | assert result == source 817 | 818 | 819 | def test_fix_respects_multiparagraph_type_checking_import_statements() -> None: 820 | """ 821 | Given: Code with two paragraphs of imports inside an if TYPE_CHECKING block 822 | When: Fix code is run. 823 | Then: The imports are not moved above the if statement. 824 | """ 825 | source = dedent( 826 | """\ 827 | import os 828 | from typing import TYPE_CHECKING 829 | 830 | if TYPE_CHECKING: 831 | from .model import Book 832 | 833 | from other import Other 834 | 835 | 836 | os.getcwd() 837 | 838 | 839 | def read_book(book: Book, other: Other): 840 | pass""" 841 | ) 842 | 843 | result = fix_code(source) 844 | 845 | assert result == source 846 | 847 | 848 | def test_fix_creates_the_typing_import() -> None: 849 | """ 850 | Given: Code with no TYPE_CHECKING import statement 851 | When: Fix code is run. 852 | Then: The import is created. 853 | 854 | Related to https://github.com/lyz-code/autoimport/issues/231 855 | """ 856 | source = dedent( 857 | """\ 858 | if TYPE_CHECKING: 859 | foo = 'bar'""" 860 | ) 861 | fixed_source = dedent( 862 | """\ 863 | from typing import TYPE_CHECKING 864 | 865 | if TYPE_CHECKING: 866 | foo = 'bar'""" 867 | ) 868 | 869 | result = fix_code(source) 870 | 871 | assert result == fixed_source 872 | 873 | 874 | def test_fix_respects_try_except_in_import_statements() -> None: 875 | """ 876 | Given: Code with try except statements in the imports. 877 | When: Fix code is run 878 | Then: The try except statements are respected 879 | """ 880 | source = dedent( 881 | """\ 882 | import os 883 | 884 | try: 885 | from typing import TypedDict # noqa 886 | except ImportError: 887 | from mypy_extensions import TypedDict # <=3.7 888 | 889 | 890 | os.getcwd() 891 | Movie = TypedDict('Movie', {'name': str, 'year': int})""" 892 | ) 893 | 894 | result = fix_code(source) 895 | 896 | assert result == source 897 | 898 | 899 | def test_fix_respects_leading_comments() -> None: 900 | """ 901 | Given: Code with initial comments like shebang and editor configuration. 902 | When: Fix code is run 903 | Then: The comment statements are respected 904 | """ 905 | source = dedent( 906 | '''\ 907 | #!/usr/bin/env python3 908 | # -*- coding: latin-1 -*- 909 | """docstring""" 910 | print(os.path.exists("."))''' 911 | ) 912 | desired_source = dedent( 913 | '''\ 914 | #!/usr/bin/env python3 915 | # -*- coding: latin-1 -*- 916 | """docstring""" 917 | 918 | import os 919 | 920 | 921 | print(os.path.exists("."))''' 922 | ) 923 | 924 | result = fix_code(source) 925 | 926 | assert result == desired_source 927 | 928 | 929 | def test_fix_respects_leading_comments_with_new_lines() -> None: 930 | """ 931 | Given: Code with initial comments with new lines and a trailing newline. 932 | When: Fix code is run. 933 | Then: The comment statements and trailing newline are respected. 934 | """ 935 | source = dedent( 936 | '''\ 937 | #!/usr/bin/env python3 938 | # -*- coding: latin-1 -*- 939 | 940 | # pylint: disable=foobar 941 | 942 | """ 943 | 944 | This is the docstring. 945 | 946 | """ 947 | 948 | import sys 949 | 950 | print(os.path.exists(sys.argv[1])) 951 | ''' 952 | ) 953 | desired_source = dedent( 954 | '''\ 955 | #!/usr/bin/env python3 956 | # -*- coding: latin-1 -*- 957 | 958 | # pylint: disable=foobar 959 | 960 | """ 961 | 962 | This is the docstring. 963 | 964 | """ 965 | 966 | import sys 967 | 968 | import os 969 | 970 | 971 | print(os.path.exists(sys.argv[1])) 972 | ''' 973 | ) 974 | 975 | result = fix_code(source) 976 | 977 | assert result == desired_source 978 | 979 | 980 | def test_fix_imports_dependency_only_once() -> None: 981 | """ 982 | Given: Code with a line that uses a package three times. 983 | When: Fix code is run. 984 | Then: The dependency is imported only once 985 | """ 986 | source = dedent( 987 | """\ 988 | def f(x): 989 | return os.getcwd() + os.getcwd() + os.getcwd() 990 | """ 991 | ) 992 | desired_source = dedent( 993 | """\ 994 | import os 995 | 996 | 997 | def f(x): 998 | return os.getcwd() + os.getcwd() + os.getcwd() 999 | """ 1000 | ) 1001 | 1002 | result = fix_code(source) 1003 | 1004 | assert result == desired_source 1005 | 1006 | 1007 | def test_fix_doesnt_fail_on_empty_file() -> None: 1008 | """ 1009 | Given: An empty file 1010 | When: Fix code is run. 1011 | Then: The output doesn't change 1012 | """ 1013 | source = "" 1014 | 1015 | result = fix_code(source) 1016 | 1017 | assert result == source 1018 | 1019 | 1020 | def test_fix_not_remove_unused_imports() -> None: 1021 | """ 1022 | Given: Code with imports, few being used, others not being used. 1023 | When: Fix code is run. 1024 | Then: Missing imports added, unused imports not removed. 1025 | """ 1026 | source = dedent( 1027 | """\ 1028 | import gzip 1029 | import hashlib 1030 | 1031 | csv_writer = csv.DictWriter(filename, fieldnames=["name", "age"]) 1032 | gzip.open(filename, 'wb') 1033 | """ 1034 | ) 1035 | desired_source = dedent( 1036 | """\ 1037 | import gzip 1038 | import hashlib 1039 | 1040 | import csv 1041 | 1042 | 1043 | csv_writer = csv.DictWriter(filename, fieldnames=["name", "age"]) 1044 | gzip.open(filename, 'wb') 1045 | """ 1046 | ) 1047 | 1048 | result = fix_code(source, keep_unused_imports=True) 1049 | 1050 | assert result == desired_source 1051 | 1052 | 1053 | def test_file_that_only_has_unused_imports() -> None: 1054 | """ 1055 | Given: A file that only has unused imports. 1056 | When: Fix code is run. 1057 | Then: The output should be a single empty line. 1058 | """ 1059 | source = dedent( 1060 | """\ 1061 | import os 1062 | import sys 1063 | """ 1064 | ) 1065 | 1066 | result = fix_code(source) 1067 | 1068 | assert result == "\n" 1069 | 1070 | 1071 | def test_file_with_common_statement() -> None: 1072 | """ 1073 | Given: Code with a commonly-used object. 1074 | When: Fix code is run. 1075 | Then: The appropriate import statement from the common_statements dict is added. 1076 | """ 1077 | source = dedent( 1078 | """\ 1079 | BeautifulSoup 1080 | """ 1081 | ) 1082 | desired_source = dedent( 1083 | """\ 1084 | from bs4 import BeautifulSoup 1085 | 1086 | 1087 | BeautifulSoup 1088 | """ 1089 | ) 1090 | 1091 | result = fix_code(source) 1092 | 1093 | assert result == desired_source 1094 | 1095 | 1096 | def test_file_with_custom_common_statement() -> None: 1097 | """ 1098 | Given: Code that uses an undefined object called `FooBar`. 1099 | When: 1100 | Fix code is run and a `config` dict is passed specifying `FooBar` as a common 1101 | statement. 1102 | Then: 1103 | The appropriate import statement from the common_statements dict is added. 1104 | """ 1105 | source = dedent( 1106 | """\ 1107 | FooBar 1108 | """ 1109 | ) 1110 | custom_config = {"common_statements": {"FooBar": "from baz_qux import FooBar"}} 1111 | desired_source = dedent( 1112 | """\ 1113 | from baz_qux import FooBar 1114 | 1115 | 1116 | FooBar 1117 | """ 1118 | ) 1119 | 1120 | result = fix_code(source, config=custom_config) 1121 | 1122 | assert result == desired_source 1123 | 1124 | 1125 | def test_file_with_comment_in_import() -> None: 1126 | """ 1127 | Given: Code with a comment on two import statements 1128 | When: Fix code is run. 1129 | Then: The unused import line is removed with it's comment 1130 | """ 1131 | source = dedent( 1132 | """\ 1133 | import os # comment 1 1134 | import sys # comment 2 1135 | 1136 | os.getcwd() 1137 | """ 1138 | ) 1139 | desired_source = dedent( 1140 | """\ 1141 | import os # comment 1 1142 | 1143 | 1144 | os.getcwd() 1145 | """ 1146 | ) 1147 | 1148 | result = fix_code(source) 1149 | 1150 | assert result == desired_source 1151 | 1152 | 1153 | def test_file_with_comment_in_from_import() -> None: 1154 | """ 1155 | Given: Code with a comment on two import statements 1156 | When: Fix code is run. 1157 | Then: The unused import line is removed with it's comment 1158 | """ 1159 | source = dedent( 1160 | """\ 1161 | import os # comment 1 1162 | from textwrap import dedent # comment 2 1163 | 1164 | os.getcwd() 1165 | """ 1166 | ) 1167 | desired_source = dedent( 1168 | """\ 1169 | import os # comment 1 1170 | 1171 | 1172 | os.getcwd() 1173 | """ 1174 | ) 1175 | 1176 | result = fix_code(source) 1177 | 1178 | assert result == desired_source 1179 | 1180 | 1181 | def test_file_with_comment_in_from_import_partial_remove() -> None: 1182 | """ 1183 | Given: Code with a comment on an from import statement 1184 | When: Fix code is run. 1185 | Then: The unused dependency is removed but the comment is respected 1186 | """ 1187 | source = dedent( 1188 | """\ 1189 | from os import getcwd, chmod # noqa: E0611 1190 | 1191 | getcwd() 1192 | """ 1193 | ) 1194 | desired_source = dedent( 1195 | """\ 1196 | from os import getcwd # noqa: E0611 1197 | 1198 | 1199 | getcwd() 1200 | """ 1201 | ) 1202 | 1203 | result = fix_code(source) 1204 | 1205 | assert result == desired_source 1206 | 1207 | 1208 | def test_file_with_comment_in_from_import_that_will_dissapear() -> None: 1209 | """ 1210 | Given: Code with a comment on an from import statement that is to be deleted 1211 | When: Fix code is run. 1212 | Then: Everything is deleted 1213 | """ 1214 | source = dedent( 1215 | """\ 1216 | from os import getcwd, chmod # noqa: E0611 1217 | 1218 | a = 1 1219 | """ 1220 | ) 1221 | desired_source = dedent( 1222 | """\ 1223 | a = 1 1224 | """ 1225 | ) 1226 | 1227 | result = fix_code(source) 1228 | 1229 | assert result == desired_source 1230 | 1231 | 1232 | def test_file_with_import_as() -> None: 1233 | """ 1234 | Given: Code with an from x import y as z import statement 1235 | When: Fix code is run. 1236 | Then: The unused import line is removed 1237 | """ 1238 | source = dedent( 1239 | """\ 1240 | from subprocess import run as run 1241 | """ 1242 | ) 1243 | 1244 | result = fix_code(source) 1245 | 1246 | assert result == "\n" 1247 | 1248 | 1249 | def test_file_with_non_used_multiline_import() -> None: 1250 | """ 1251 | Given: Code with a multiline from import where no one is used. 1252 | When: Fix code is run. 1253 | Then: The unused import line is removed 1254 | """ 1255 | source = dedent( 1256 | """\ 1257 | from foo import ( 1258 | bar, 1259 | baz, 1260 | ) 1261 | """ 1262 | ) 1263 | 1264 | result = fix_code(source) 1265 | 1266 | assert result == "\n" 1267 | 1268 | 1269 | def test_file_with_import_and_seperator() -> None: 1270 | """Ensure import lines with seperators are fixed correctly.""" 1271 | source = dedent( 1272 | """ 1273 | a = 1 1274 | import pdb;pdb.set_trace() 1275 | b = 2 1276 | """ 1277 | ) 1278 | expected = dedent( 1279 | """ 1280 | import pdb 1281 | 1282 | 1283 | a = 1 1284 | pdb.set_trace() 1285 | b = 2 1286 | """ 1287 | ).replace("\n", "", 1) 1288 | 1289 | result = fix_code(source) 1290 | 1291 | assert result == expected 1292 | 1293 | 1294 | def test_file_with_import_and_seperator_indentation() -> None: 1295 | """Ensure import lines with seperators are fixed correctly when indented.""" 1296 | source = dedent( 1297 | """ 1298 | Class Person: 1299 | import pdb; pdb.set_trace() 1300 | def say_hi(self): 1301 | print('hi') 1302 | """ 1303 | ) 1304 | expected = dedent( 1305 | """ 1306 | import pdb 1307 | 1308 | 1309 | Class Person: 1310 | pdb.set_trace() 1311 | def say_hi(self): 1312 | print('hi') 1313 | """ 1314 | ).replace("\n", "", 1) 1315 | 1316 | result = fix_code(source) 1317 | 1318 | assert result == expected 1319 | 1320 | 1321 | def test_import_module_with_dot() -> None: 1322 | """ 1323 | Given: An import file with an import with a dot 1324 | When: running autoimport on the file 1325 | Then: ValueError exception is not raised 1326 | 1327 | Tests https://github.com/lyz-code/autoimport/issues/225 1328 | """ 1329 | source = dedent( 1330 | """ 1331 | import my_module.m 1332 | """ 1333 | ) 1334 | 1335 | result = fix_code(source) 1336 | 1337 | assert result == "\n" 1338 | 1339 | 1340 | def test_respect_new_lines_between_imports_and_code() -> None: 1341 | r""" 1342 | Given: A file with two \n between imports and the code 1343 | When: running autoimport on the file 1344 | Then: the file is untouched 1345 | 1346 | For more info check https://github.com/lyz-code/autoimport/issues/219 1347 | """ 1348 | source = dedent( 1349 | """\ 1350 | import random 1351 | 1352 | 1353 | def foo(): 1354 | print(random.random()) 1355 | """ 1356 | ) 1357 | 1358 | result = fix_code(source) 1359 | 1360 | assert result == source 1361 | -------------------------------------------------------------------------------- /tests/unit/test_version.py: -------------------------------------------------------------------------------- 1 | """Test the version message""" 2 | 3 | import platform 4 | import sys 5 | 6 | from autoimport.version import __version__, version_info 7 | 8 | 9 | def test_version() -> None: 10 | """ 11 | Given: Nothing 12 | When: version_info is called 13 | Then: the expected output is given 14 | """ 15 | result = version_info() 16 | 17 | assert sys.version.split(" ", maxsplit=1)[0] in result 18 | assert platform.platform() in result 19 | assert __version__ in result 20 | --------------------------------------------------------------------------------