├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── pull_request_template.md ├── renovate.json5 ├── workflows │ ├── main.yml │ ├── release.yml │ ├── validate-codecov-config.yml │ └── validate-renovate-config.yml └── zizmor.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .pre-commit-hooks.yaml ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── codecov.yaml ├── docs ├── CHANGELOG.md ├── CNAME ├── contributing.md ├── index.md ├── rules-violations.md ├── static │ └── deptry_Logo-01.svg ├── supported-dependency-managers.md └── usage.md ├── mkdocs.yml ├── pyproject.toml ├── python └── deptry │ ├── __init__.py │ ├── __main__.py │ ├── cli.py │ ├── config.py │ ├── core.py │ ├── dependency.py │ ├── dependency_getter │ ├── __init__.py │ ├── base.py │ ├── builder.py │ ├── pep621 │ │ ├── __init__.py │ │ ├── base.py │ │ ├── pdm.py │ │ ├── poetry.py │ │ └── uv.py │ └── requirements_files.py │ ├── exceptions.py │ ├── imports │ ├── __init__.py │ ├── extract.py │ └── location.py │ ├── module.py │ ├── python_file_finder.py │ ├── reporters │ ├── __init__.py │ ├── base.py │ ├── json.py │ └── text.py │ ├── rust.pyi │ ├── stdlibs.py │ ├── utils.py │ └── violations │ ├── __init__.py │ ├── base.py │ ├── dep001_missing │ ├── __init__.py │ ├── finder.py │ └── violation.py │ ├── dep002_unused │ ├── __init__.py │ ├── finder.py │ └── violation.py │ ├── dep003_transitive │ ├── __init__.py │ ├── finder.py │ └── violation.py │ ├── dep004_misplaced_dev │ ├── __init__.py │ ├── finder.py │ └── violation.py │ ├── dep005_standard_library │ ├── __init__.py │ ├── finder.py │ └── violation.py │ └── finder.py ├── rust-toolchain.toml ├── scripts └── generate_stdlibs.py ├── src ├── file_utils.rs ├── imports │ ├── ipynb.rs │ ├── mod.rs │ ├── py.rs │ └── shared.rs ├── lib.rs ├── location.rs ├── python_file_finder.rs └── visitor.rs ├── tests ├── __init__.py ├── fixtures │ ├── example_project │ │ ├── poetry.toml │ │ ├── pyproject.toml │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── pep_621_project │ │ ├── .ignore │ │ ├── pyproject.toml │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── project_using_namespace │ │ ├── .ignore │ │ ├── foo │ │ │ ├── api │ │ │ │ └── http.py │ │ │ └── database │ │ │ │ └── bar.py │ │ └── pyproject.toml │ ├── project_with_gitignore │ │ ├── .gitignore │ │ ├── pyproject.toml │ │ └── src │ │ │ ├── .gitignore │ │ │ ├── bar.py │ │ │ ├── barfoo.py │ │ │ ├── baz.py │ │ │ ├── foo.py │ │ │ ├── foobar.py │ │ │ └── notebook.ipynb │ ├── project_with_multiple_source_directories │ │ ├── another_directory │ │ │ ├── __init__.py │ │ │ └── foo.py │ │ ├── pyproject.toml │ │ ├── src │ │ │ ├── __init__.py │ │ │ ├── foo.py │ │ │ └── foobar.py │ │ └── worker │ │ │ ├── __init__.py │ │ │ ├── foo.py │ │ │ └── foobaz.py │ ├── project_with_pdm │ │ ├── pyproject.toml │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── project_with_poetry │ │ ├── poetry.toml │ │ ├── pyproject.toml │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── project_with_poetry_pep_621 │ │ ├── poetry.toml │ │ ├── pyproject.toml │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── project_with_pyproject_different_directory │ │ ├── a_sub_directory │ │ │ └── pyproject.toml │ │ └── src │ │ │ └── project_with_src_directory │ │ │ ├── __init__.py │ │ │ ├── bar.py │ │ │ ├── foo.py │ │ │ └── notebook.ipynb │ ├── project_with_requirements_in │ │ ├── pyproject.toml │ │ ├── requirements-dev.txt │ │ ├── requirements.in │ │ ├── requirements.txt │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── project_with_requirements_txt │ │ ├── pyproject.toml │ │ ├── requirements-2.txt │ │ ├── requirements-dev.txt │ │ ├── requirements-from-other.txt │ │ ├── requirements-typing.txt │ │ ├── requirements.txt │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── project_with_setuptools_dynamic_dependencies │ │ ├── cli-requirements.txt │ │ ├── dev-requirements.txt │ │ ├── pyproject.toml │ │ ├── requirements-2.txt │ │ ├── requirements.txt │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── project_with_src_directory │ │ ├── .ignore │ │ ├── pyproject.toml │ │ └── src │ │ │ ├── foobar.py │ │ │ ├── project_with_src_directory │ │ │ ├── __init__.py │ │ │ ├── bar.py │ │ │ ├── foo.py │ │ │ └── notebook.ipynb │ │ │ └── this_file_is_ignored.py │ ├── project_with_uv │ │ ├── pyproject.toml │ │ └── src │ │ │ ├── main.py │ │ │ └── notebook.ipynb │ ├── some_imports.ipynb │ └── some_imports.py ├── functional │ ├── __init__.py │ ├── cli │ │ ├── __init__.py │ │ ├── test_cli.py │ │ ├── test_cli_gitignore.py │ │ ├── test_cli_multiple_source_directories.py │ │ ├── test_cli_namespace.py │ │ ├── test_cli_pdm.py │ │ ├── test_cli_pep_621.py │ │ ├── test_cli_poetry.py │ │ ├── test_cli_poetry_pep_621.py │ │ ├── test_cli_pyproject_different_directory.py │ │ ├── test_cli_requirements_in.py │ │ ├── test_cli_requirements_txt.py │ │ ├── test_cli_setuptools_dynamic_dependencies.py │ │ ├── test_cli_src_directory.py │ │ └── test_cli_uv.py │ ├── conftest.py │ ├── types.py │ └── utils.py ├── unit │ ├── __init__.py │ ├── dependency_getter │ │ ├── __init__.py │ │ ├── test_builder.py │ │ ├── test_pdm.py │ │ ├── test_pep_621.py │ │ ├── test_poetry.py │ │ ├── test_requirements_txt.py │ │ └── test_uv.py │ ├── imports │ │ ├── __init__.py │ │ └── test_extract.py │ ├── reporters │ │ ├── __init__.py │ │ ├── test_json.py │ │ └── test_text.py │ ├── test_cli.py │ ├── test_config.py │ ├── test_core.py │ ├── test_dependency.py │ ├── test_module.py │ ├── test_python_file_finder.py │ ├── test_utils.py │ └── violations │ │ ├── __init__.py │ │ ├── dep001_missing │ │ ├── __init__.py │ │ └── test_finder.py │ │ ├── dep002_unused │ │ ├── __init__.py │ │ └── test_finder.py │ │ ├── dep003_transitive │ │ ├── __init__.py │ │ └── test_finder.py │ │ ├── dep004_misplaced_dev │ │ ├── __init__.py │ │ └── test_finder.py │ │ ├── dep005_standard_library │ │ ├── __init__.py │ │ └── test_finder.py │ │ └── test_finder.py └── utils.py ├── tox.ini └── uv.lock /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report an issue on the project 4 | labels: "bug" 5 | --- 6 | 7 | **Environment** 8 | 9 | - **deptry version**: 10 | - **Python version**: 11 | - **Operating system** (e.g. Ubuntu 22.04, Windows 11): 12 | 13 | **Describe the issue** 14 | 15 | 16 | 17 | **Minimal way to reproduce the issue** 18 | 19 | 20 | 21 | **Expected behavior** 22 | 23 | 24 | 25 | **Additional context** 26 | 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature 4 | labels: "enhancement" 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | 9 | 10 | 11 | **Describe the solution you would like** 12 | 13 | 14 | 15 | **Additional context** 16 | 17 | 18 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **PR Checklist** 2 | 3 | - [ ] A description of the changes is added to the description of this PR. 4 | - [ ] If there is a related issue, make sure it is linked to this PR. 5 | - [ ] If you've fixed a bug or added code that should be tested, add tests! 6 | - [ ] If you've added or modified a feature, documentation in `docs` is updated 7 | 8 | **Description of changes** 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/renovate.json5: -------------------------------------------------------------------------------- 1 | { 2 | $schema: "https://docs.renovatebot.com/renovate-schema.json", 3 | 4 | // https://docs.renovatebot.com/configuration-options/#extends 5 | extends: [ 6 | // https://docs.renovatebot.com/presets-config/#configbase 7 | "config:recommended", 8 | 9 | // https://docs.renovatebot.com/presets-default/#automergepatch 10 | ":automergePatch", 11 | 12 | // https://docs.renovatebot.com/presets-default/#enableprecommit 13 | ":enablePreCommit", 14 | 15 | // https://docs.renovatebot.com/presets-default/#prhourlylimitnone 16 | ":prHourlyLimitNone", 17 | 18 | // https://docs.renovatebot.com/presets-default/#rebasestaleprs 19 | ":rebaseStalePrs", 20 | 21 | // https://docs.renovatebot.com/presets-customManagers/#custommanagersgithubactionsversions 22 | "customManagers:githubActionsVersions", 23 | ], 24 | 25 | // https://docs.renovatebot.com/configuration-options/#ignorepresets 26 | ignorePresets: [ 27 | // https://docs.renovatebot.com/presets-default/#ignoremodulesandtests 28 | // By default, some directories, including "tests", are ignored. Since we do want Renovate to pick up dependencies 29 | // in "tests" directory, we explicitly opt out from this preset. 30 | ":ignoreModulesAndTests", 31 | ], 32 | 33 | // https://docs.renovatebot.com/configuration-options/#labels 34 | labels: ["dependencies"], 35 | 36 | // https://docs.renovatebot.com/configuration-options/#schedule 37 | schedule: ["before 5am on saturday"], 38 | 39 | // https://docs.renovatebot.com/configuration-options/#rangestrategy 40 | rangeStrategy: "replace", 41 | 42 | // https://docs.renovatebot.com/configuration-options/#platformautomerge 43 | platformAutomerge: true, 44 | 45 | // https://docs.renovatebot.com/configuration-options/#lockfilemaintenance 46 | lockFileMaintenance: { 47 | enabled: true, 48 | schedule: ["before 5am on saturday"], 49 | }, 50 | 51 | // https://docs.renovatebot.com/configuration-options/#packagerules 52 | packageRules: [ 53 | { 54 | // Create dedicated branch to update dependencies in tests. 55 | matchFileNames: ["tests/**"], 56 | commitMessageTopic: "dependencies in tests", 57 | semanticCommitType: "test", 58 | semanticCommitScope: null, 59 | additionalBranchPrefix: "tests-", 60 | groupName: "all test dependencies", 61 | groupSlug: "all-test-dependencies", 62 | separateMajorMinor: false, 63 | separateMinorPatch: false, 64 | lockFileMaintenance: { 65 | enabled: false, 66 | }, 67 | automerge: true, 68 | }, 69 | { 70 | matchPackageNames: ["uv", "astral-sh/uv-pre-commit"], 71 | groupName: "uv-version", 72 | }, 73 | ], 74 | } 75 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | push: 7 | branches: [main] 8 | 9 | concurrency: 10 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 11 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} 12 | 13 | env: 14 | PYTHON_VERSION: '3.13' 15 | # renovate: datasource=pypi depName=uv 16 | UV_VERSION: '0.7.9' 17 | 18 | permissions: {} 19 | 20 | jobs: 21 | quality: 22 | runs-on: ubuntu-24.04 23 | steps: 24 | - name: Check out 25 | uses: actions/checkout@v4 26 | with: 27 | persist-credentials: false 28 | 29 | - name: Install uv 30 | uses: astral-sh/setup-uv@v6 31 | with: 32 | version: ${{ env.UV_VERSION }} 33 | enable-cache: true 34 | cache-dependency-glob: "uv.lock" 35 | 36 | - name: Install Python 37 | uses: actions/setup-python@v5 38 | with: 39 | python-version: ${{ env.PYTHON_VERSION }} 40 | 41 | - name: Install Python dependencies 42 | run: uv sync --frozen 43 | 44 | - name: Setup Rust toolchain 45 | run: rustup component add clippy rustfmt 46 | 47 | - name: Load pre-commit cache 48 | uses: actions/cache@v4 49 | with: 50 | path: ~/.cache/pre-commit 51 | key: pre-commit-${{ env.PYTHON_VERSION }}-${{ hashFiles('.pre-commit-config.yaml') }} 52 | 53 | - name: Run pre-commit 54 | run: uvx pre-commit@${PRE_COMMIT_VERSION} run --all-files --show-diff-on-failure 55 | env: 56 | # renovate: datasource=pypi depName=pre-commit 57 | PRE_COMMIT_VERSION: '4.2.0' 58 | 59 | - name: Inspect dependencies with deptry 60 | run: uv run deptry python 61 | 62 | tests: 63 | strategy: 64 | matrix: 65 | os: 66 | - name: linux 67 | image: ubuntu-24.04 68 | - name: macos 69 | image: macos-15 70 | - name: windows 71 | image: windows-2025 72 | python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', 'pypy3.10'] 73 | fail-fast: false 74 | runs-on: ${{ matrix.os.image }} 75 | name: ${{ matrix.os.name }} (${{ matrix.python-version }}) 76 | steps: 77 | - name: Check out 78 | uses: actions/checkout@v4 79 | with: 80 | persist-credentials: false 81 | 82 | - name: Install uv 83 | uses: astral-sh/setup-uv@v6 84 | with: 85 | version: ${{ env.UV_VERSION }} 86 | enable-cache: true 87 | cache-dependency-glob: "uv.lock" 88 | cache-suffix: ${{ matrix.python-version }} 89 | 90 | - name: Install Python 91 | uses: actions/setup-python@v5 92 | with: 93 | python-version: ${{ matrix.python-version }} 94 | 95 | - name: Install Python dependencies 96 | run: uv sync --frozen --group test --group typing 97 | 98 | - name: Check typing 99 | run: uv run mypy 100 | if: ${{ matrix.os.name == 'linux' }} 101 | 102 | - name: Run unit tests 103 | run: uv run pytest tests/unit --cov --cov-config=pyproject.toml --cov-report=xml 104 | 105 | - name: Run functional tests 106 | run: uv run pytest tests/functional -n auto --dist loadgroup 107 | 108 | - name: Upload coverage reports to Codecov 109 | uses: codecov/codecov-action@v5 110 | env: 111 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 112 | if: ${{ matrix.os.name == 'linux' && matrix.python-version == env.PYTHON_VERSION }} 113 | 114 | check-docs: 115 | runs-on: ubuntu-24.04 116 | steps: 117 | - name: Check out 118 | uses: actions/checkout@v4 119 | with: 120 | persist-credentials: false 121 | 122 | - name: Install uv 123 | uses: astral-sh/setup-uv@v6 124 | with: 125 | version: ${{ env.UV_VERSION }} 126 | enable-cache: true 127 | cache-dependency-glob: "uv.lock" 128 | 129 | - name: Install Python 130 | uses: actions/setup-python@v5 131 | with: 132 | python-version: ${{ env.PYTHON_VERSION }} 133 | - name: Check if documentation can be built 134 | run: uv run --only-group docs mkdocs build --strict 135 | -------------------------------------------------------------------------------- /.github/workflows/validate-codecov-config.yml: -------------------------------------------------------------------------------- 1 | name: validate-codecov-config 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - ".github/workflows/validate-codecov-config.yml" 7 | - "codecov.yaml" 8 | push: 9 | branches: [main] 10 | 11 | permissions: {} 12 | 13 | jobs: 14 | validate-codecov-config: 15 | runs-on: ubuntu-24.04 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | persist-credentials: false 20 | - name: Validate codecov configuration 21 | run: curl -sSL --fail-with-body --data-binary @codecov.yaml https://codecov.io/validate 22 | -------------------------------------------------------------------------------- /.github/workflows/validate-renovate-config.yml: -------------------------------------------------------------------------------- 1 | name: validate-renovate-config 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - ".github/workflows/validate-renovate-config.yml" 7 | - ".github/renovate.json5" 8 | push: 9 | branches: [main] 10 | 11 | env: 12 | # renovate: datasource=node depName=node versioning=node 13 | NODE_VERSION: "22" 14 | 15 | permissions: {} 16 | 17 | jobs: 18 | validate-renovate-config: 19 | runs-on: ubuntu-24.04 20 | steps: 21 | - uses: actions/checkout@v4 22 | with: 23 | persist-credentials: false 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: ${{ env.NODE_VERSION }} 27 | - run: npx -p renovate renovate-config-validator 28 | -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | artipacked: 3 | ignore: 4 | # Required for publishing documentation to `gh-pages` branch. 5 | - release.yml:222 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docs/source 2 | deptry.json 3 | 4 | .DS_Store 5 | .vscode 6 | 7 | # From https://raw.githubusercontent.com/github/gitignore/main/Python.gitignore 8 | 9 | Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | *.so 16 | 17 | # Distribution / packaging 18 | .Python 19 | build/ 20 | develop-eggs/ 21 | dist/ 22 | downloads/ 23 | eggs/ 24 | .eggs/ 25 | lib/ 26 | lib64/ 27 | parts/ 28 | sdist/ 29 | var/ 30 | wheels/ 31 | share/python-wheels/ 32 | *.egg-info/ 33 | .installed.cfg 34 | *.egg 35 | MANIFEST 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .nox/ 51 | .coverage 52 | .coverage.* 53 | .cache 54 | nosetests.xml 55 | coverage.xml 56 | *.cover 57 | *.py,cover 58 | .hypothesis/ 59 | .pytest_cache/ 60 | cover/ 61 | 62 | # Translations 63 | *.mo 64 | *.pot 65 | 66 | # Django stuff: 67 | *.log 68 | local_settings.py 69 | db.sqlite3 70 | db.sqlite3-journal 71 | 72 | # Flask stuff: 73 | instance/ 74 | .webassets-cache 75 | 76 | # Scrapy stuff: 77 | .scrapy 78 | 79 | # Sphinx documentation 80 | docs/_build/ 81 | 82 | # PyBuilder 83 | .pybuilder/ 84 | target/ 85 | 86 | # Jupyter Notebook 87 | .ipynb_checkpoints 88 | 89 | # IPython 90 | profile_default/ 91 | ipython_config.py 92 | 93 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 94 | __pypackages__/ 95 | 96 | # Celery stuff 97 | celerybeat-schedule 98 | celerybeat.pid 99 | 100 | # SageMath parsed files 101 | *.sage.py 102 | 103 | # Environments 104 | .env 105 | .venv 106 | env/ 107 | venv/ 108 | ENV/ 109 | env.bak/ 110 | venv.bak/ 111 | 112 | # Spyder project settings 113 | .spyderproject 114 | .spyproject 115 | 116 | # Rope project settings 117 | .ropeproject 118 | 119 | # mkdocs documentation 120 | /site 121 | 122 | # mypy 123 | .mypy_cache/ 124 | .dmypy.json 125 | dmypy.json 126 | 127 | # Pyre type checker 128 | .pyre/ 129 | 130 | # pytype static type analyzer 131 | .pytype/ 132 | 133 | # Cython debug symbols 134 | cython_debug/ 135 | 136 | # PyCharm 137 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 138 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 139 | # and can be added to the global gitignore or merged into this file. For a more nuclear 140 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 141 | #.idea/ 142 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: "v5.0.0" 4 | hooks: 5 | - id: check-case-conflict 6 | - id: check-merge-conflict 7 | - id: check-toml 8 | - id: check-yaml 9 | - id: end-of-file-fixer 10 | - id: trailing-whitespace 11 | 12 | - repo: https://github.com/astral-sh/ruff-pre-commit 13 | rev: "v0.11.12" 14 | hooks: 15 | - id: ruff 16 | args: [--exit-non-zero-on-fix] 17 | - id: ruff-format 18 | 19 | - repo: https://github.com/astral-sh/uv-pre-commit 20 | rev: "0.7.9" 21 | hooks: 22 | - id: uv-lock 23 | name: check uv lock file consistency 24 | args: ["--locked"] 25 | 26 | - repo: local 27 | hooks: 28 | - id: cargo-check-lock 29 | name: check cargo lock file consistency 30 | entry: cargo check 31 | args: ["--locked", "--all-targets", "--all-features"] 32 | language: system 33 | pass_filenames: false 34 | files: ^Cargo\.toml$ 35 | 36 | - repo: local 37 | hooks: 38 | - id: cargo-fmt 39 | name: cargo fmt 40 | entry: cargo fmt -- 41 | language: system 42 | types: [rust] 43 | pass_filenames: false 44 | 45 | - repo: local 46 | hooks: 47 | - id: cargo-clippy 48 | name: cargo clippy 49 | entry: cargo clippy 50 | args: ["--all-targets", "--all-features", "--", "-D", "warnings"] 51 | language: system 52 | types: [rust] 53 | pass_filenames: false 54 | 55 | - repo: https://github.com/woodruffw/zizmor-pre-commit 56 | rev: "v1.5.2" 57 | hooks: 58 | - id: zizmor 59 | -------------------------------------------------------------------------------- /.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: deptry 2 | name: deptry 3 | description: deptry is a command line tool to check for issues with dependencies in a Python project, such as unused or missing dependencies. 4 | entry: deptry . 5 | language: system 6 | always_run: true 7 | pass_filenames: false 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "deptryrs" 3 | version = "0.1.0" 4 | edition = "2024" 5 | 6 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 7 | [lib] 8 | name = "deptry" 9 | crate-type = ["cdylib"] 10 | 11 | [dependencies] 12 | chardetng = "=0.1.17" 13 | encoding_rs = "=0.8.35" 14 | ignore = "=0.4.23" 15 | log = "=0.4.27" 16 | path-slash = "=0.2.1" 17 | pyo3 = { version = "=0.25.0", features = ["abi3-py39", "generate-import-lib"] } 18 | pyo3-log = "=0.12.4" 19 | rayon = "=1.10.0" 20 | regex = "=1.11.1" 21 | ruff_python_ast = { git = "https://github.com/astral-sh/ruff", tag = "0.9.3" } 22 | ruff_python_parser = { git = "https://github.com/astral-sh/ruff", tag = "0.9.3" } 23 | ruff_source_file = { git = "https://github.com/astral-sh/ruff", tag = "0.9.3" } 24 | ruff_text_size = { git = "https://github.com/astral-sh/ruff", tag = "0.9.3" } 25 | serde_json = "=1.0.140" 26 | 27 | [profile.release] 28 | lto = true 29 | codegen-units = 1 30 | panic = "abort" 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022, Florian Maas 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: install 2 | install: ## Install the uv environment. 3 | @echo "🚀 Creating virtual environment using uv" 4 | @uv sync 5 | 6 | .PHONY: check 7 | check: ## Run code quality tools. 8 | @echo "🚀 Linting code: Running pre-commit" 9 | @pre-commit run -a 10 | @echo "🚀 Static type checking: Running mypy" 11 | @uv run mypy 12 | @echo "🚀 Checking for dependency issues: Running deptry" 13 | @uv run deptry python 14 | 15 | .PHONY: test 16 | test: test-unit test-functional 17 | 18 | .PHONY: test-unit 19 | test-unit: ## Run unit tests. 20 | @echo "🚀 Running unit tests" 21 | @uv run pytest tests/unit 22 | 23 | .PHONY: test-functional 24 | test-functional: ## Run functional tests. 25 | @echo "🚀 Running functional tests" 26 | @uv run pytest tests/functional -n auto --dist loadgroup 27 | 28 | .PHONY: build 29 | build: ## Build wheel and sdist files using maturin. 30 | @echo "🚀 Creating wheel and sdist files" 31 | @maturin build 32 | 33 | .PHONY: docs-test 34 | docs-test: ## Test if documentation can be built without warnings or errors. 35 | @uv run mkdocs build -s 36 | 37 | .PHONY: docs 38 | docs: ## Build and serve the documentation. 39 | @uv run mkdocs serve 40 | 41 | .PHONY: help 42 | help: ## Show help for the commands. 43 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}' 44 | 45 | .DEFAULT_GOAL := help 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | deptry logo 3 |

4 | 5 | [![Release](https://img.shields.io/github/v/release/fpgmaas/deptry)](https://pypi.org/project/deptry/) 6 | [![Build status](https://github.com/fpgmaas/deptry/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/fpgmaas/deptry/actions/workflows/main.yml) 7 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/deptry)](https://pypi.org/project/deptry/) 8 | [![codecov](https://codecov.io/gh/fpgmaas/deptry/branch/main/graph/badge.svg)](https://codecov.io/gh/fpgmaas/deptry) 9 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/deptry)](https://pypistats.org/packages/deptry) 10 | [![License](https://img.shields.io/github/license/fpgmaas/deptry)](https://img.shields.io/github/license/fpgmaas/deptry) 11 | 12 | _deptry_ is a command line tool to check for issues with dependencies in a Python project, such as unused or missing 13 | dependencies. It supports projects 14 | using [Poetry](https://python-poetry.org/), [pip](https://pip.pypa.io/), [PDM](https://pdm-project.org/), [uv](https://docs.astral.sh/uv/), 15 | and more generally any project supporting [PEP 621](https://peps.python.org/pep-0621/) specification. 16 | 17 | Dependency issues are detected by scanning for imported modules within all Python files in a directory and its 18 | subdirectories, and comparing those to the dependencies listed in the project's requirements. 19 | 20 | --- 21 |

22 | Documentation - Contributing 23 |

24 | 25 | --- 26 | 27 | ## Quickstart 28 | 29 | ### Installation 30 | 31 | To add _deptry_ to your project, run one of the following commands: 32 | 33 | ```shell 34 | # Install with poetry 35 | poetry add --group dev deptry 36 | 37 | # Install with pip 38 | pip install deptry 39 | ``` 40 | 41 | > **Warning**: When using pip to install _deptry_, make sure you install it within the virtual environment of your project. Installing _deptry_ globally will not work, since it needs to have access to the metadata of the packages in the virtual environment. 42 | 43 | ### Prerequisites 44 | 45 | _deptry_ should be run within the root directory of the project to be scanned, and the project should be running in its own dedicated virtual environment. 46 | 47 | ### Usage 48 | 49 | To scan your project for dependency issues, run: 50 | 51 | ```shell 52 | deptry . 53 | ``` 54 | 55 | Example output could look as follows: 56 | 57 | ```console 58 | Scanning 2 files... 59 | 60 | foo/bar.py:1:0: DEP004 'numpy' imported but declared as a dev dependency 61 | foo/bar.py:2:0: DEP001 'matplotlib' imported but missing from the dependency definitions 62 | pyproject.toml: DEP002 'pandas' defined as a dependency but not used in the codebase 63 | Found 3 dependency issues. 64 | ``` 65 | 66 | ### Configuration 67 | 68 | _deptry_ can be configured by using additional command line arguments, or by adding a `[tool.deptry]` section in _pyproject.toml_. For more information, see the [Usage and Configuration](https://deptry.com/usage/) section of the documentation.. 69 | 70 | --- 71 | 72 | Repository initiated with [fpgmaas/cookiecutter-poetry](https://github.com/fpgmaas/cookiecutter-poetry). 73 | -------------------------------------------------------------------------------- /codecov.yaml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 70..100 3 | round: down 4 | precision: 1 5 | status: 6 | project: 7 | default: 8 | target: 90% 9 | threshold: 0.5% 10 | -------------------------------------------------------------------------------- /docs/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | deptry.com 2 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are welcome, and they are greatly appreciated! Every little bit helps, and credit will always be given. 4 | 5 | You can contribute in many ways. 6 | 7 | ## Types of contributions 8 | 9 | ### Reporting bugs 10 | 11 | Report bugs at https://github.com/fpgmaas/deptry/issues. 12 | 13 | If you are reporting a bug, please include: 14 | 15 | * Your operating system name and version. 16 | * Any details about your local setup that might be helpful in troubleshooting. 17 | * Detailed steps to reproduce the bug. 18 | 19 | ### Fixing bugs 20 | 21 | Look through the GitHub issues for bugs. Anything tagged with `bug` and `help wanted` is open to whoever wants to implement a fix for it. 22 | 23 | ### Implementing features 24 | 25 | Look through the GitHub issues for features. Anything tagged with `enhancement` and `help wanted` is open to whoever wants to implement it. 26 | 27 | ### Writing documentation 28 | 29 | _deptry_ could always use more documentation, whether as part of the official documentation, in docstrings, or even on the web in blog posts, articles, and such. 30 | 31 | ### Submitting feedback 32 | 33 | The best way to send feedback is to file an issue at https://github.com/fpgmaas/deptry/issues. 34 | 35 | If you are proposing a new feature: 36 | 37 | * Explain in detail how it would work. 38 | * Keep the scope as narrow as possible, to make it easier to implement. 39 | * Remember that this is a volunteer-driven project, and that contributions are welcome :) 40 | 41 | ## Get started! 42 | 43 | Ready to contribute? Here's how to set up _deptry_ for local development. Please note this documentation assumes you 44 | already have [uv](https://docs.astral.sh/uv/), [Git](https://git-scm.com/) and [pre-commit](https://pre-commit.com/) 45 | installed and ready to go. 46 | 47 | 1. [Fork](https://github.com/fpgmaas/deptry/fork) the _deptry_ repository on GitHub. 48 | 49 | 2. Clone your fork locally: 50 | ```bash 51 | cd 52 | git clone git@github.com:YOUR_NAME/deptry.git 53 | ``` 54 | 55 | 3. Now you need to set up your local environment. Navigate into the directory: 56 | ```bash 57 | cd deptry 58 | ``` 59 | 60 | Then, install the virtual environment with: 61 | ```bash 62 | uv sync 63 | ``` 64 | 65 | 4. Install `pre-commit` hooks to run linters/formatters at commit time: 66 | ```bash 67 | pre-commit install 68 | ``` 69 | 70 | 5. Create a branch for local development: 71 | ```bash 72 | git checkout -b name-of-your-bugfix-or-feature 73 | ``` 74 | 75 | Now you can make your changes locally. 76 | 77 | 6. If you are adding a feature or fixing a bug, make sure to add tests in the `tests` directory. 78 | 79 | 7. Once you're done, validate that all unit and functional tests are passing: 80 | ```bash 81 | make test 82 | ``` 83 | 84 | 8. Before submitting a pull request, you should also run [tox](https://tox.wiki/en/latest/). This will run the tests across all the Python versions that _deptry_ supports: 85 | ```bash 86 | tox 87 | ``` 88 | 89 | This requires you to have multiple versions of Python installed. 90 | This step is also triggered in the CI pipeline, so you could also choose to skip this step locally. 91 | 92 | 9. Commit your changes and push your branch to GitHub: 93 | ```bash 94 | git add . 95 | git commit -m "Your detailed description of your changes." 96 | git push origin name-of-your-bugfix-or-feature 97 | ``` 98 | 99 | 10. Submit a pull request through GitHub. 100 | 101 | ## Pull request guidelines 102 | 103 | Before you submit a pull request, ensure that it meets the following guidelines: 104 | 105 | 1. If the pull request adds a functionality or fixes a bug, the pull request should include tests. 106 | 2. If the pull request adds a functionality, the documentation in `docs` directory should probably be updated. 107 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 | ![Image title](static/deptry_Logo-01.svg){ width="460" } 9 |
10 | 11 | --- 12 | 13 | [![Release](https://img.shields.io/github/v/release/fpgmaas/deptry)](https://pypi.org/project/deptry/) 14 | [![Build status](https://github.com/fpgmaas/deptry/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/fpgmaas/deptry/actions/workflows/main.yml) 15 | [![Supported Python versions](https://img.shields.io/pypi/pyversions/deptry)](https://pypi.org/project/deptry/) 16 | [![codecov](https://codecov.io/gh/fpgmaas/deptry/branch/main/graph/badge.svg)](https://codecov.io/gh/fpgmaas/deptry) 17 | [![PyPI - Downloads](https://img.shields.io/pypi/dm/deptry)](https://pypistats.org/packages/deptry) 18 | [![License](https://img.shields.io/github/license/fpgmaas/deptry)](https://img.shields.io/github/license/fpgmaas/deptry) 19 | 20 | _deptry_ is a command line tool to check for issues with dependencies in a Python project, such as unused or missing 21 | dependencies. It supports projects 22 | using [Poetry](https://python-poetry.org/), [pip](https://pip.pypa.io/), [PDM](https://pdm-project.org/), [uv](https://docs.astral.sh/uv/), 23 | and more generally any project supporting [PEP 621](https://peps.python.org/pep-0621/) specification. 24 | 25 | Dependency issues are detected by scanning for imported modules within all Python files in a directory and its subdirectories, and comparing those to the dependencies listed in the project's requirements. 26 | 27 | --- 28 | 29 | ## Quickstart 30 | 31 | ### Installation 32 | 33 | To add _deptry_ to your project, run one of the following commands: 34 | 35 | ```shell 36 | # Install with poetry 37 | poetry add --group dev deptry 38 | 39 | # Install with pip 40 | pip install deptry 41 | ``` 42 | 43 | !!! important 44 | 45 | When using pip to install _deptry_, make sure you install it within the virtual environment of your project. Installing _deptry_ globally will not work, since it needs to have access to the metadata of the packages in the virtual environment. 46 | 47 | ### Prerequisites 48 | 49 | _deptry_ should be run within the root directory of the project to be scanned, and the project should be running in its own dedicated virtual environment. 50 | 51 | ### Usage 52 | 53 | To scan your project for dependency issues, run 54 | 55 | ```shell 56 | deptry . 57 | ``` 58 | 59 | Example output could look as follows: 60 | 61 | ```console 62 | Scanning 2 files... 63 | 64 | foo/bar.py:1:0: DEP004 'numpy' imported but declared as a dev dependency 65 | foo/bar.py:2:0: DEP001 'matplotlib' imported but missing from the dependency definitions 66 | pyproject.toml: DEP002 'pandas' defined as a dependency but not used in the codebase 67 | Found 3 dependency issues. 68 | ``` 69 | 70 | ### Configuration 71 | 72 | _deptry_ can be configured by using additional command line arguments, or 73 | by adding a `[tool.deptry]` section in `pyproject.toml`. For more information, see [Usage and Configuration](./usage.md) 74 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: deptry 2 | edit_uri: edit/main/docs/ 3 | repo_name: fpgmaas/deptry 4 | repo_url: https://github.com/fpgmaas/deptry 5 | site_url: https://deptry.com 6 | site_description: A command line tool to check for unused dependencies in a poetry managed python project. 7 | site_author: Florian Maas 8 | copyright: Maintained by Florian. 9 | 10 | nav: 11 | - Home: index.md 12 | - Usage and Configuration: usage.md 13 | - Rules and Violations: rules-violations.md 14 | - Supported dependency managers: supported-dependency-managers.md 15 | - Changelog: CHANGELOG.md 16 | - Contributing: contributing.md 17 | 18 | plugins: 19 | - search 20 | 21 | theme: 22 | name: material 23 | features: 24 | - content.action.edit 25 | - content.code.copy 26 | - navigation.footer 27 | palette: 28 | - media: "(prefers-color-scheme)" 29 | toggle: 30 | icon: material/brightness-auto 31 | name: Switch to light mode 32 | - media: "(prefers-color-scheme: light)" 33 | scheme: default 34 | primary: cyan 35 | accent: deep orange 36 | toggle: 37 | icon: material/brightness-7 38 | name: Switch to dark mode 39 | - media: "(prefers-color-scheme: dark)" 40 | scheme: slate 41 | primary: cyan 42 | accent: deep orange 43 | toggle: 44 | icon: material/brightness-4 45 | name: Switch to system preferences 46 | icon: 47 | repo: fontawesome/brands/github 48 | 49 | extra: 50 | social: 51 | - icon: fontawesome/brands/github 52 | link: https://github.com/fpgmaas/deptry 53 | - icon: fontawesome/brands/python 54 | link: https://pypi.org/project/deptry/ 55 | 56 | markdown_extensions: 57 | - admonition 58 | - attr_list 59 | - md_in_html 60 | - pymdownx.details 61 | - pymdownx.superfences 62 | - toc: 63 | permalink: true 64 | - pymdownx.arithmatex: 65 | generic: true 66 | 67 | validation: 68 | omitted_files: warn 69 | absolute_links: warn 70 | unrecognized_links: warn 71 | anchors: warn 72 | -------------------------------------------------------------------------------- /python/deptry/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | logging.getLogger("nbconvert").setLevel(logging.WARNING) 6 | -------------------------------------------------------------------------------- /python/deptry/__main__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from deptry.cli import deptry 4 | 5 | deptry() 6 | -------------------------------------------------------------------------------- /python/deptry/config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import TYPE_CHECKING, Any 5 | 6 | from deptry.exceptions import InvalidPyprojectTOMLOptionsError 7 | from deptry.utils import load_pyproject_toml 8 | 9 | if TYPE_CHECKING: 10 | from pathlib import Path 11 | 12 | import click 13 | 14 | 15 | def _get_invalid_pyproject_toml_keys(ctx: click.Context, deptry_toml_config_keys: set[str]) -> list[str]: 16 | """Returns the list of options set in `pyproject.toml` that do not exist as CLI parameters.""" 17 | existing_cli_params = {param.name for param in ctx.command.params} 18 | 19 | return sorted(deptry_toml_config_keys.difference(existing_cli_params)) 20 | 21 | 22 | def read_configuration_from_pyproject_toml(ctx: click.Context, _param: click.Parameter, value: Path) -> Path | None: 23 | """ 24 | Callback that, given a click context, overrides the default values with configuration options set in a 25 | pyproject.toml file. 26 | Using a callback ensures that the following order is respected for setting an option: 27 | 1. Default value is set 28 | 2. Value is overrode by the one set from pyproject.toml, if any 29 | 3. Value is overrode by the one set from the command line, if any 30 | """ 31 | 32 | try: 33 | pyproject_data = load_pyproject_toml(value) 34 | except FileNotFoundError: 35 | logging.debug("No pyproject.toml file to read configuration from.") 36 | return value 37 | 38 | try: 39 | deptry_toml_config: dict[str, Any] = pyproject_data["tool"]["deptry"] 40 | except KeyError: 41 | logging.debug("No configuration for deptry was found in pyproject.toml.") 42 | return value 43 | 44 | invalid_pyproject_toml_keys = _get_invalid_pyproject_toml_keys(ctx, set(deptry_toml_config)) 45 | if invalid_pyproject_toml_keys: 46 | raise InvalidPyprojectTOMLOptionsError(invalid_pyproject_toml_keys) 47 | 48 | click_default_map: dict[str, Any] = {} 49 | 50 | if ctx.default_map: 51 | click_default_map.update(ctx.default_map) 52 | 53 | click_default_map.update(deptry_toml_config) 54 | 55 | ctx.default_map = click_default_map 56 | 57 | return value 58 | -------------------------------------------------------------------------------- /python/deptry/dependency_getter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/python/deptry/dependency_getter/__init__.py -------------------------------------------------------------------------------- /python/deptry/dependency_getter/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass, field 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from collections.abc import Mapping, Sequence 9 | from pathlib import Path 10 | 11 | from deptry.dependency import Dependency 12 | 13 | 14 | @dataclass 15 | class DependenciesExtract: 16 | dependencies: list[Dependency] 17 | dev_dependencies: list[Dependency] 18 | 19 | 20 | @dataclass 21 | class DependencyGetter(ABC): 22 | """Base class for all classes that extract a list of project's dependencies from a file. 23 | 24 | Args: 25 | config: The path to a configuration file that contains the project's dependencies. 26 | package_module_name_map: A mapping of package names to their corresponding module names that may not be found 27 | otherwise from the package's metadata. The keys in the mapping should be package names, and the values should 28 | be sequences of module names associated with the package. 29 | """ 30 | 31 | config: Path 32 | package_module_name_map: Mapping[str, Sequence[str]] = field(default_factory=dict) 33 | 34 | @abstractmethod 35 | def get(self) -> DependenciesExtract: 36 | """Get extracted dependencies and dev dependencies.""" 37 | raise NotImplementedError() 38 | -------------------------------------------------------------------------------- /python/deptry/dependency_getter/pep621/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/python/deptry/dependency_getter/pep621/__init__.py -------------------------------------------------------------------------------- /python/deptry/dependency_getter/pep621/pdm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING 6 | 7 | from deptry.dependency_getter.pep621.base import PEP621DependencyGetter 8 | from deptry.utils import load_pyproject_toml 9 | 10 | if TYPE_CHECKING: 11 | from deptry.dependency import Dependency 12 | 13 | 14 | @dataclass 15 | class PDMDependencyGetter(PEP621DependencyGetter): 16 | """ 17 | Class to get dependencies that are specified according to PEP 621 from a `pyproject.toml` file for a project that 18 | uses PDM for its dependency management. 19 | """ 20 | 21 | def _get_dev_dependencies( 22 | self, 23 | dependency_groups_dependencies: dict[str, list[Dependency]], 24 | dev_dependencies_from_optional: list[Dependency], 25 | ) -> list[Dependency]: 26 | """ 27 | Retrieve dev dependencies from pyproject.toml, which in PDM are specified as: 28 | 29 | [tool.pdm.dev-dependencies] 30 | test = [ 31 | "pytest", 32 | "pytest-cov", 33 | ] 34 | tox = [ 35 | "tox", 36 | "tox-pdm>=0.5", 37 | ] 38 | """ 39 | dev_dependencies = super()._get_dev_dependencies(dependency_groups_dependencies, dev_dependencies_from_optional) 40 | 41 | pyproject_data = load_pyproject_toml(self.config) 42 | 43 | dev_dependency_strings: list[str] = [] 44 | try: 45 | dev_dependencies_dict: dict[str, str] = pyproject_data["tool"]["pdm"]["dev-dependencies"] 46 | for deps in dev_dependencies_dict.values(): 47 | dev_dependency_strings += deps 48 | except KeyError: 49 | logging.debug("No section [tool.pdm.dev-dependencies] found in pyproject.toml") 50 | 51 | return [*dev_dependencies, *self._extract_pep_508_dependencies(dev_dependency_strings)] 52 | -------------------------------------------------------------------------------- /python/deptry/dependency_getter/pep621/poetry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import contextlib 4 | from dataclasses import dataclass 5 | from typing import Any 6 | 7 | from deptry.dependency import Dependency 8 | from deptry.dependency_getter.pep621.base import PEP621DependencyGetter 9 | from deptry.utils import load_pyproject_toml 10 | 11 | 12 | @dataclass 13 | class PoetryDependencyGetter(PEP621DependencyGetter): 14 | """ 15 | Class that retrieves dependencies from a project that uses Poetry, either through PEP 621 syntax, Poetry specific 16 | syntax, or a mix of both. 17 | """ 18 | 19 | def _get_dependencies(self) -> list[Dependency]: 20 | """ 21 | Retrieve dependencies from either: 22 | - `[project.dependencies]` defined by PEP 621 23 | - `[tool.poetry.dependencies]` which is specific to Poetry 24 | 25 | If dependencies are set in `[project.dependencies]`, then assume that the project uses PEP 621 format to define 26 | dependencies. Even if `[tool.poetry.dependencies]` is populated, having entries in `[project.dependencies]` 27 | means that `[tool.poetry.dependencies]` is only used to enrich existing dependencies, and cannot be used to 28 | define additional ones. 29 | 30 | If no dependencies are found in `[project.dependencies]`, then extract dependencies present in 31 | `[tool.poetry.dependencies]`. 32 | """ 33 | if dependencies := super()._get_dependencies(): 34 | return dependencies 35 | 36 | pyproject_data = load_pyproject_toml(self.config) 37 | return self._extract_poetry_dependencies(pyproject_data["tool"]["poetry"].get("dependencies", {})) 38 | 39 | def _get_dev_dependencies( 40 | self, 41 | dependency_groups_dependencies: dict[str, list[Dependency]], 42 | dev_dependencies_from_optional: list[Dependency], 43 | ) -> list[Dependency]: 44 | """ 45 | Poetry's development dependencies can be specified under either, or both: 46 | - [tool.poetry.dev-dependencies] 47 | - [tool.poetry.group..dependencies] 48 | """ 49 | dev_dependencies = super()._get_dev_dependencies(dependency_groups_dependencies, dev_dependencies_from_optional) 50 | 51 | pyproject_data = load_pyproject_toml(self.config) 52 | poetry_dev_dependencies: dict[str, str] = {} 53 | 54 | with contextlib.suppress(KeyError): 55 | poetry_dev_dependencies = { 56 | **poetry_dev_dependencies, 57 | **pyproject_data["tool"]["poetry"]["dev-dependencies"], 58 | } 59 | 60 | try: 61 | dependency_groups = pyproject_data["tool"]["poetry"]["group"] 62 | except KeyError: 63 | dependency_groups = {} 64 | 65 | for group_values in dependency_groups.values(): 66 | with contextlib.suppress(KeyError): 67 | poetry_dev_dependencies = {**poetry_dev_dependencies, **group_values["dependencies"]} 68 | 69 | return [*dev_dependencies, *self._extract_poetry_dependencies(poetry_dev_dependencies)] 70 | 71 | def _extract_poetry_dependencies(self, poetry_dependencies: dict[str, Any]) -> list[Dependency]: 72 | return [ 73 | Dependency(dep, self.config, module_names=self.package_module_name_map.get(dep)) 74 | for dep in poetry_dependencies 75 | if dep != "python" 76 | ] 77 | -------------------------------------------------------------------------------- /python/deptry/dependency_getter/pep621/uv.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING 6 | 7 | from deptry.dependency_getter.pep621.base import PEP621DependencyGetter 8 | from deptry.utils import load_pyproject_toml 9 | 10 | if TYPE_CHECKING: 11 | from deptry.dependency import Dependency 12 | 13 | 14 | @dataclass 15 | class UvDependencyGetter(PEP621DependencyGetter): 16 | """ 17 | Class to get dependencies that are specified according to PEP 621 from a `pyproject.toml` file for a project that 18 | uses uv for its dependency management. 19 | """ 20 | 21 | def _get_dev_dependencies( 22 | self, 23 | dependency_groups_dependencies: dict[str, list[Dependency]], 24 | dev_dependencies_from_optional: list[Dependency], 25 | ) -> list[Dependency]: 26 | """ 27 | Retrieve dev dependencies from pyproject.toml, which in uv are specified as: 28 | 29 | [tool.uv] 30 | dev-dependencies = [ 31 | "pytest==8.3.2", 32 | "pytest-cov==5.0.0", 33 | "tox", 34 | ] 35 | 36 | Dev dependencies marked as such from optional dependencies are also added to the list of dev dependencies found. 37 | """ 38 | dev_dependencies = super()._get_dev_dependencies(dependency_groups_dependencies, dev_dependencies_from_optional) 39 | 40 | pyproject_data = load_pyproject_toml(self.config) 41 | 42 | dev_dependency_strings: list[str] = [] 43 | try: 44 | dev_dependency_strings = pyproject_data["tool"]["uv"]["dev-dependencies"] 45 | except KeyError: 46 | logging.debug("No section [tool.uv.dev-dependencies] found in pyproject.toml") 47 | 48 | return [*dev_dependencies, *self._extract_pep_508_dependencies(dev_dependency_strings)] 49 | -------------------------------------------------------------------------------- /python/deptry/dependency_getter/requirements_files.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import itertools 4 | import logging 5 | import re 6 | from dataclasses import dataclass 7 | from pathlib import Path 8 | from typing import TYPE_CHECKING 9 | 10 | import requirements 11 | 12 | from deptry.dependency import Dependency 13 | from deptry.dependency_getter.base import DependenciesExtract, DependencyGetter 14 | 15 | if TYPE_CHECKING: 16 | from collections.abc import Mapping, Sequence 17 | 18 | from requirements.requirement import Requirement 19 | 20 | 21 | @dataclass 22 | class RequirementsTxtDependencyGetter(DependencyGetter): 23 | """Extract dependencies from requirements.txt files.""" 24 | 25 | requirements_files: tuple[str, ...] = ("requirements.txt",) 26 | requirements_files_dev: tuple[str, ...] = ("dev-requirements.txt", "requirements-dev.txt") 27 | 28 | def get(self) -> DependenciesExtract: 29 | return DependenciesExtract( 30 | get_dependencies_from_requirements_files(self.requirements_files, self.package_module_name_map), 31 | get_dependencies_from_requirements_files( 32 | self._scan_for_dev_requirements_files(), self.package_module_name_map 33 | ), 34 | ) 35 | 36 | def _scan_for_dev_requirements_files(self) -> list[str]: 37 | """ 38 | Check if any of the files passed as requirements_files_dev exist, and if so; return them. 39 | """ 40 | project_path = Path() 41 | 42 | dev_requirements_files = [ 43 | file_name for file_name in self.requirements_files_dev if (project_path / file_name).exists() 44 | ] 45 | 46 | if dev_requirements_files: 47 | logging.debug("Found files with development requirements! %s", dev_requirements_files) 48 | 49 | return dev_requirements_files 50 | 51 | 52 | def get_dependencies_from_requirements_files( 53 | file_names: Sequence[str], package_module_name_map: Mapping[str, Sequence[str]], is_dev: bool = False 54 | ) -> list[Dependency]: 55 | return list( 56 | itertools.chain( 57 | *( 58 | get_dependencies_from_requirements_file(file_name, package_module_name_map, is_dev) 59 | for file_name in file_names 60 | ) 61 | ) 62 | ) 63 | 64 | 65 | def get_dependencies_from_requirements_file( 66 | file_name: str, package_module_name_map: Mapping[str, Sequence[str]], is_dev: bool = False 67 | ) -> list[Dependency]: 68 | logging.debug("Scanning %s for %s", file_name, "dev dependencies" if is_dev else "dependencies") 69 | 70 | dependencies = [] 71 | requirements_file = Path(file_name) 72 | 73 | with requirements_file.open() as requirements_file_content: 74 | for requirement in requirements.parse(requirements_file_content): 75 | if ( 76 | dependency := _build_dependency_from_requirement( 77 | requirement, requirements_file, package_module_name_map 78 | ) 79 | ) is not None: 80 | dependencies.append(dependency) 81 | 82 | return dependencies 83 | 84 | 85 | def _build_dependency_from_requirement( 86 | requirement: Requirement, requirements_file: Path, package_module_name_map: Mapping[str, Sequence[str]] 87 | ) -> Dependency | None: 88 | """ 89 | Build a dependency from an extracted requirement. 90 | """ 91 | # Explicitly set types, as "name" and "uri" default to `None` in `Requirement`, and are not typed, so `mypy` always 92 | # assume that they both will be `None`. 93 | dependency_name: str | None = requirement.name 94 | dependency_uri: str | None = requirement.uri 95 | 96 | # If the dependency name could not be guessed, and we have a URI, try to guess it from the URI. 97 | if not dependency_name and dependency_uri: 98 | dependency_name = _extract_name_from_url(dependency_uri) 99 | 100 | if dependency_name is None: 101 | return None 102 | 103 | return Dependency( 104 | name=dependency_name, 105 | definition_file=requirements_file, 106 | module_names=package_module_name_map.get(dependency_name), 107 | ) 108 | 109 | 110 | def _extract_name_from_url(line: str) -> str | None: 111 | # Try to find egg, for url like git+https://github.com/xxxxx/package@xxxxx#egg=package 112 | match = re.search("egg=([a-zA-Z0-9-_]*)", line) 113 | if match: 114 | return match.group(1) 115 | 116 | # for url like git+https://github.com/name/python-module.git@0d6dc38d58 117 | match = re.search(r"\/((?:(?!\/).)*?)\.git", line) 118 | if match: 119 | return match.group(1) 120 | 121 | # for url like https://github.com/urllib3/urllib3/archive/refs/tags/1.26.8.zip 122 | match = re.search(r"\/((?:(?!\/).)*?)\/archive\/", line) 123 | if match: 124 | return match.group(1) 125 | 126 | logging.warning("Could not parse dependency name from url %s", line) 127 | return None 128 | -------------------------------------------------------------------------------- /python/deptry/exceptions.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING 4 | 5 | from click import UsageError 6 | 7 | if TYPE_CHECKING: 8 | from pathlib import Path 9 | 10 | 11 | class DependencySpecificationNotFoundError(FileNotFoundError): 12 | def __init__(self, requirements_files: tuple[str, ...]) -> None: 13 | super().__init__( 14 | "No file called 'pyproject.toml' with a [tool.poetry.dependencies], [tool.pdm] or [project] section or" 15 | f" file(s) called '{', '.join(requirements_files)}' found. Exiting." 16 | ) 17 | 18 | 19 | class PyprojectFileNotFoundError(FileNotFoundError): 20 | def __init__(self, directory: Path) -> None: 21 | super().__init__(f"No file `pyproject.toml` found in directory {directory}") 22 | 23 | 24 | class UnsupportedPythonVersionError(ValueError): 25 | def __init__(self, version: tuple[int, int]) -> None: 26 | super().__init__( 27 | f"Python version {version[0]}.{version[1]} is not supported. Only versions >= 3.9 are supported." 28 | ) 29 | 30 | 31 | class InvalidPyprojectTOMLOptionsError(UsageError): 32 | def __init__(self, invalid_options: list[str]) -> None: 33 | super().__init__( 34 | f"'[tool.deptry]' section in 'pyproject.toml' contains invalid configuration options: {invalid_options}." 35 | ) 36 | -------------------------------------------------------------------------------- /python/deptry/imports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/python/deptry/imports/__init__.py -------------------------------------------------------------------------------- /python/deptry/imports/extract.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | import logging 5 | from collections import OrderedDict, defaultdict 6 | from typing import TYPE_CHECKING 7 | 8 | from deptry.rust import get_imports_from_ipynb_files, get_imports_from_py_files 9 | 10 | if TYPE_CHECKING: 11 | from pathlib import Path 12 | 13 | from deptry.rust import Location as RustLocation 14 | 15 | from deptry.imports.location import Location 16 | 17 | 18 | def get_imported_modules_from_list_of_files(list_of_files: list[Path]) -> dict[str, list[Location]]: 19 | logging.info("Scanning %d %s...", len(list_of_files), "files" if len(list_of_files) > 1 else "file") 20 | 21 | py_files = [str(file) for file in list_of_files if file.suffix == ".py"] 22 | ipynb_files = [str(file) for file in list_of_files if file.suffix == ".ipynb"] 23 | 24 | modules: dict[str, list[Location]] = defaultdict(list) 25 | 26 | # Process all .py files in parallel using Rust 27 | if py_files: 28 | rust_result = get_imports_from_py_files(py_files) 29 | for module, locations in _convert_rust_locations_to_python_locations(rust_result).items(): 30 | modules[module].extend(locations) 31 | 32 | # Process all .ipynb files in parallel using Rust 33 | if ipynb_files: 34 | rust_result = get_imports_from_ipynb_files(ipynb_files) 35 | for module, locations in _convert_rust_locations_to_python_locations(rust_result).items(): 36 | modules[module].extend(locations) 37 | 38 | sorted_modules = OrderedDict(sorted(modules.items())) 39 | _log_modules_with_locations(sorted_modules) 40 | return sorted_modules 41 | 42 | 43 | def _log_modules_with_locations(modules: dict[str, list[Location]]) -> None: 44 | modules_dict = { 45 | module_name: [str(location) for location in locations] for module_name, locations in modules.items() 46 | } 47 | modules_json = json.dumps(modules_dict, indent=2) 48 | logging.debug("All imported modules and their locations:\n%s", modules_json) 49 | 50 | 51 | def _convert_rust_locations_to_python_locations( 52 | imported_modules: dict[str, list[RustLocation]], 53 | ) -> dict[str, list[Location]]: 54 | converted_modules: dict[str, list[Location]] = {} 55 | for module, locations in imported_modules.items(): 56 | converted_modules[module] = [Location.from_rust_location_object(loc) for loc in locations] 57 | return converted_modules 58 | -------------------------------------------------------------------------------- /python/deptry/imports/location.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from deptry.rust import Location as RustLocation 9 | 10 | 11 | @dataclass(frozen=True) 12 | class Location: 13 | file: Path 14 | line: int | None = None 15 | column: int | None = None 16 | 17 | @classmethod 18 | def from_rust_location_object(cls, location: RustLocation) -> Location: 19 | return cls(file=Path(location.file), line=location.line, column=location.column) 20 | -------------------------------------------------------------------------------- /python/deptry/python_file_finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from deptry.rust import find_python_files 6 | 7 | 8 | def get_all_python_files_in( 9 | directories: tuple[Path, ...], 10 | exclude: tuple[str, ...], 11 | extend_exclude: tuple[str, ...], 12 | using_default_exclude: bool, 13 | ignore_notebooks: bool = False, 14 | ) -> list[Path]: 15 | return [ 16 | Path(f) 17 | for f in find_python_files(directories, exclude, extend_exclude, using_default_exclude, ignore_notebooks) 18 | ] 19 | -------------------------------------------------------------------------------- /python/deptry/reporters/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from deptry.reporters.json import JSONReporter 4 | from deptry.reporters.text import TextReporter 5 | 6 | __all__ = ("JSONReporter", "TextReporter") 7 | -------------------------------------------------------------------------------- /python/deptry/reporters/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from deptry.violations import Violation 9 | 10 | 11 | @dataclass 12 | class Reporter(ABC): 13 | """Base class for all violation reporters.""" 14 | 15 | violations: list[Violation] 16 | 17 | @abstractmethod 18 | def report(self) -> None: 19 | raise NotImplementedError() 20 | -------------------------------------------------------------------------------- /python/deptry/reporters/json.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from dataclasses import dataclass 5 | from pathlib import Path 6 | from typing import TYPE_CHECKING 7 | 8 | from deptry.reporters.base import Reporter 9 | 10 | if TYPE_CHECKING: 11 | from typing import Any 12 | 13 | 14 | @dataclass 15 | class JSONReporter(Reporter): 16 | json_output: str 17 | 18 | def report(self) -> None: 19 | output: list[dict[str, str | dict[str, Any]]] = [] 20 | 21 | for violation in self.violations: 22 | output.append( 23 | { 24 | "error": { 25 | "code": violation.error_code, 26 | "message": violation.get_error_message(), 27 | }, 28 | "module": violation.issue.name, 29 | "location": { 30 | "file": str(violation.location.file), 31 | "line": violation.location.line, 32 | "column": violation.location.column, 33 | }, 34 | }, 35 | ) 36 | 37 | with Path(self.json_output).open("w", encoding="utf-8") as f: 38 | json.dump(output, f, ensure_ascii=False, indent=4) 39 | -------------------------------------------------------------------------------- /python/deptry/reporters/text.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING, Any 6 | 7 | from deptry.reporters.base import Reporter 8 | 9 | if TYPE_CHECKING: 10 | from deptry.imports.location import Location 11 | from deptry.violations import Violation 12 | 13 | 14 | COLORS = { 15 | "BOLD": "\033[1m", 16 | "CYAN": "\033[36m", 17 | "GREEN": "\033[32m", 18 | "RED": "\033[31m", 19 | "RESET": "\033[m", 20 | } 21 | COLORS_NOOP = dict.fromkeys(COLORS, "") 22 | 23 | 24 | @dataclass 25 | class TextReporter(Reporter): 26 | use_ansi: bool = True 27 | 28 | def report(self) -> None: 29 | self._log_and_exit() 30 | 31 | def _log_and_exit(self) -> None: 32 | self._log_violations(self.violations) 33 | 34 | self._log_total_number_of_violations_found(self.violations) 35 | 36 | def _log_total_number_of_violations_found(self, violations: list[Violation]) -> None: 37 | if violations: 38 | logging.info( 39 | self._stylize( 40 | "{BOLD}{RED}Found {total} dependency {issue_word}.{RESET}", 41 | total=len(violations), 42 | issue_word="issues" if len(violations) > 1 else "issue", 43 | ) 44 | ) 45 | logging.info("\nFor more information, see the documentation: https://deptry.com/") 46 | else: 47 | logging.info(self._stylize("{BOLD}{GREEN}Success! No dependency issues found.{RESET}")) 48 | 49 | def _log_violations(self, violations: list[Violation]) -> None: 50 | logging.info("") 51 | 52 | for violation in violations: 53 | logging.info(self._format_error(violation)) 54 | 55 | def _format_error(self, violation: Violation) -> str: 56 | return self._stylize( 57 | "{location}{CYAN}:{RESET} {BOLD}{RED}{error_code}{RESET} {error_message}", 58 | location=self._format_location(violation.location), 59 | error_code=violation.error_code, 60 | error_message=violation.get_error_message(), 61 | ) 62 | 63 | def _format_location(self, location: Location) -> str: 64 | if location.line is not None and location.column is not None: 65 | return self._stylize( 66 | "{BOLD}{file}{RESET}{CYAN}:{RESET}{line}{CYAN}:{RESET}{column}", 67 | file=location.file, 68 | line=location.line, 69 | column=location.column, 70 | ) 71 | return self._stylize("{BOLD}{file}{RESET}", file=location.file) 72 | 73 | def _stylize(self, text: str, **kwargs: Any) -> str: 74 | return text.format(**kwargs, **self._get_colors()) 75 | 76 | def _get_colors(self) -> dict[str, str]: 77 | return COLORS if self.use_ansi else COLORS_NOOP 78 | -------------------------------------------------------------------------------- /python/deptry/rust.pyi: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | from .rust import Location as RustLocation 4 | 5 | def get_imports_from_py_files(file_paths: list[str]) -> dict[str, list[RustLocation]]: ... 6 | def get_imports_from_ipynb_files(file_paths: list[str]) -> dict[str, list[RustLocation]]: ... 7 | def find_python_files( 8 | directories: tuple[Path, ...], 9 | exclude: tuple[str, ...], 10 | extend_exclude: tuple[str, ...], 11 | using_default_exclude: bool, 12 | ignore_notebooks: bool = False, 13 | ) -> list[str]: ... 14 | 15 | class Location: 16 | file: str 17 | line: int 18 | column: int 19 | def __init__(self, file: str, line: int, column: int) -> None: ... 20 | -------------------------------------------------------------------------------- /python/deptry/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sys 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | if TYPE_CHECKING: 8 | from typing import Any 9 | 10 | 11 | from deptry.exceptions import PyprojectFileNotFoundError 12 | 13 | if sys.version_info >= (3, 11): 14 | import tomllib 15 | else: 16 | import tomli as tomllib 17 | 18 | 19 | def load_pyproject_toml(config: Path) -> dict[str, Any]: 20 | try: 21 | with config.open("rb") as pyproject_file: 22 | return tomllib.load(pyproject_file) 23 | except FileNotFoundError: 24 | raise PyprojectFileNotFoundError(Path.cwd()) from None 25 | -------------------------------------------------------------------------------- /python/deptry/violations/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from deptry.violations.base import Violation, ViolationsFinder 4 | from deptry.violations.dep001_missing.finder import DEP001MissingDependenciesFinder 5 | from deptry.violations.dep001_missing.violation import DEP001MissingDependencyViolation 6 | from deptry.violations.dep002_unused.finder import DEP002UnusedDependenciesFinder 7 | from deptry.violations.dep002_unused.violation import DEP002UnusedDependencyViolation 8 | from deptry.violations.dep003_transitive.finder import DEP003TransitiveDependenciesFinder 9 | from deptry.violations.dep003_transitive.violation import DEP003TransitiveDependencyViolation 10 | from deptry.violations.dep004_misplaced_dev.finder import DEP004MisplacedDevDependenciesFinder 11 | from deptry.violations.dep004_misplaced_dev.violation import DEP004MisplacedDevDependencyViolation 12 | from deptry.violations.dep005_standard_library.finder import DEP005StandardLibraryDependenciesFinder 13 | from deptry.violations.dep005_standard_library.violation import DEP005StandardLibraryDependencyViolation 14 | 15 | __all__ = ( 16 | "DEP001MissingDependenciesFinder", 17 | "DEP001MissingDependencyViolation", 18 | "DEP002UnusedDependenciesFinder", 19 | "DEP002UnusedDependencyViolation", 20 | "DEP003TransitiveDependenciesFinder", 21 | "DEP003TransitiveDependencyViolation", 22 | "DEP004MisplacedDevDependenciesFinder", 23 | "DEP004MisplacedDevDependencyViolation", 24 | "DEP005StandardLibraryDependenciesFinder", 25 | "DEP005StandardLibraryDependencyViolation", 26 | "Violation", 27 | "ViolationsFinder", 28 | ) 29 | -------------------------------------------------------------------------------- /python/deptry/violations/base.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC, abstractmethod 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING, ClassVar 6 | 7 | if TYPE_CHECKING: 8 | from deptry.dependency import Dependency 9 | from deptry.imports.location import Location 10 | from deptry.module import Module, ModuleLocations 11 | 12 | 13 | @dataclass 14 | class ViolationsFinder(ABC): 15 | """Base class for all issues finders. 16 | 17 | This abstract class provides a common interface for classes that find issues related to project dependencies. 18 | Subclasses must implement the 'find' method, which returns a list of Violation objects representing the issues found. 19 | 20 | Attributes: 21 | imported_modules_with_locations: A list of ModuleLocations objects representing the 22 | modules imported by the project and their locations. 23 | dependencies: A list of Dependency objects representing the project's dependencies. 24 | ignored_modules: A tuple of module names to ignore when scanning for issues. Defaults to an 25 | empty tuple. 26 | standard_library_modules: A set of modules that are part of the standard library 27 | """ 28 | 29 | violation: ClassVar[type[Violation]] 30 | imported_modules_with_locations: list[ModuleLocations] 31 | dependencies: list[Dependency] 32 | standard_library_modules: frozenset[str] 33 | ignored_modules: tuple[str, ...] = () 34 | 35 | @abstractmethod 36 | def find(self) -> list[Violation]: 37 | """Find issues about dependencies.""" 38 | raise NotImplementedError() 39 | 40 | 41 | @dataclass 42 | class Violation(ABC): 43 | """ 44 | An abstract base class representing a violation found in the project's dependencies. 45 | 46 | Attributes: 47 | error_code: A class variable representing the error code associated with the violation. e.g. 48 | `DEP001`. 49 | error_template: A class variable representing a string template used to generate an error 50 | message for the violation. 51 | issue: An attribute representing the module or dependency where the violation 52 | occurred. 53 | location: An attribute representing the location in the code where the violation occurred. 54 | """ 55 | 56 | error_code: ClassVar[str] = "" 57 | error_template: ClassVar[str] = "" 58 | issue: Dependency | Module 59 | location: Location 60 | 61 | @abstractmethod 62 | def get_error_message(self) -> str: 63 | raise NotImplementedError() 64 | -------------------------------------------------------------------------------- /python/deptry/violations/dep001_missing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/python/deptry/violations/dep001_missing/__init__.py -------------------------------------------------------------------------------- /python/deptry/violations/dep001_missing/finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING 6 | 7 | from deptry.violations.base import ViolationsFinder 8 | from deptry.violations.dep001_missing.violation import DEP001MissingDependencyViolation 9 | 10 | if TYPE_CHECKING: 11 | from deptry.module import Module 12 | from deptry.violations.base import Violation 13 | 14 | 15 | @dataclass 16 | class DEP001MissingDependenciesFinder(ViolationsFinder): 17 | """ 18 | Given a list of imported modules and a list of project dependencies, determine which ones are missing. 19 | """ 20 | 21 | violation = DEP001MissingDependencyViolation 22 | 23 | def find(self) -> list[Violation]: 24 | logging.debug("\nScanning for missing dependencies...") 25 | missing_dependencies: list[Violation] = [] 26 | 27 | for module_with_locations in self.imported_modules_with_locations: 28 | module = module_with_locations.module 29 | 30 | if module.standard_library: 31 | continue 32 | 33 | logging.debug("Scanning module %s...", module.name) 34 | 35 | if self._is_missing(module): 36 | for location in module_with_locations.locations: 37 | missing_dependencies.append(self.violation(module, location)) 38 | 39 | return missing_dependencies 40 | 41 | def _is_missing(self, module: Module) -> bool: 42 | if any([ 43 | module.package is not None, 44 | module.is_provided_by_dependency, 45 | module.is_provided_by_dev_dependency, 46 | module.local_module, 47 | ]): 48 | return False 49 | 50 | if module.name in self.ignored_modules: 51 | logging.debug("Identified module '%s' as a missing dependency, but ignoring.", module.name) 52 | return False 53 | 54 | logging.debug("No package found to import module '%s' from. Marked as a missing dependency.", module.name) 55 | return True 56 | -------------------------------------------------------------------------------- /python/deptry/violations/dep001_missing/violation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import ClassVar 5 | 6 | from deptry.violations.base import Violation 7 | 8 | 9 | @dataclass 10 | class DEP001MissingDependencyViolation(Violation): 11 | error_code: ClassVar[str] = "DEP001" 12 | error_template: ClassVar[str] = "'{name}' imported but missing from the dependency definitions" 13 | 14 | def get_error_message(self) -> str: 15 | return self.error_template.format(name=self.issue.name) 16 | -------------------------------------------------------------------------------- /python/deptry/violations/dep002_unused/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/python/deptry/violations/dep002_unused/__init__.py -------------------------------------------------------------------------------- /python/deptry/violations/dep002_unused/finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING 6 | 7 | from deptry.imports.location import Location 8 | from deptry.violations.base import ViolationsFinder 9 | from deptry.violations.dep002_unused.violation import DEP002UnusedDependencyViolation 10 | 11 | if TYPE_CHECKING: 12 | from deptry.dependency import Dependency 13 | from deptry.violations import Violation 14 | 15 | 16 | @dataclass 17 | class DEP002UnusedDependenciesFinder(ViolationsFinder): 18 | """ 19 | Finds unused dependencies by comparing a list of imported modules to a list of project dependencies. 20 | 21 | A dependency is considered unused if none of the following conditions hold: 22 | - A module with the exact name of the dependency is imported. 23 | - Any of the top-level modules of the dependency are imported. 24 | 25 | For example, 'matplotlib' has top-levels ['matplotlib', 'mpl_toolkits', 'pylab']. `mpl_toolkits` does not have 26 | any associated metadata, but if this is imported the associated dependency `matplotlib` is not unused, 27 | even if `matplotlib` itself is not imported anywhere. 28 | """ 29 | 30 | violation = DEP002UnusedDependencyViolation 31 | 32 | def find(self) -> list[Violation]: 33 | logging.debug("\nScanning for unused dependencies...") 34 | unused_dependencies: list[Violation] = [] 35 | 36 | for dependency in self.dependencies: 37 | logging.debug("Scanning module %s...", dependency.name) 38 | 39 | if self._is_unused(dependency): 40 | unused_dependencies.append(self.violation(dependency, Location(dependency.definition_file))) 41 | 42 | return unused_dependencies 43 | 44 | def _is_unused(self, dependency: Dependency) -> bool: 45 | if self._dependency_found_in_imported_modules(dependency) or self._any_of_the_top_levels_imported(dependency): 46 | return False 47 | 48 | if dependency.name in self.ignored_modules: 49 | logging.debug("Dependency '%s' found to be unused, but ignoring.", dependency.name) 50 | return False 51 | 52 | logging.debug("Dependency '%s' does not seem to be used.", dependency.name) 53 | return True 54 | 55 | def _dependency_found_in_imported_modules(self, dependency: Dependency) -> bool: 56 | return any( 57 | module_with_locations.module.package == dependency.name 58 | for module_with_locations in self.imported_modules_with_locations 59 | ) 60 | 61 | def _any_of_the_top_levels_imported(self, dependency: Dependency) -> bool: 62 | if not dependency.top_levels: 63 | return False 64 | 65 | return any( 66 | any( 67 | module_with_locations.module.name == top_level 68 | for module_with_locations in self.imported_modules_with_locations 69 | ) 70 | for top_level in dependency.top_levels 71 | ) 72 | -------------------------------------------------------------------------------- /python/deptry/violations/dep002_unused/violation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import ClassVar 5 | 6 | from deptry.violations.base import Violation 7 | 8 | 9 | @dataclass 10 | class DEP002UnusedDependencyViolation(Violation): 11 | error_code: ClassVar[str] = "DEP002" 12 | error_template: ClassVar[str] = "'{name}' defined as a dependency but not used in the codebase" 13 | 14 | def get_error_message(self) -> str: 15 | return self.error_template.format(name=self.issue.name) 16 | -------------------------------------------------------------------------------- /python/deptry/violations/dep003_transitive/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/python/deptry/violations/dep003_transitive/__init__.py -------------------------------------------------------------------------------- /python/deptry/violations/dep003_transitive/finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING 6 | 7 | from deptry.violations.base import ViolationsFinder 8 | from deptry.violations.dep003_transitive.violation import DEP003TransitiveDependencyViolation 9 | 10 | if TYPE_CHECKING: 11 | from deptry.module import Module 12 | from deptry.violations import Violation 13 | 14 | 15 | @dataclass 16 | class DEP003TransitiveDependenciesFinder(ViolationsFinder): 17 | """ 18 | Given a list of imported modules and a list of project dependencies, determine which ones are transitive. 19 | This is done by elimination; if a module uses an installed package but the package is; 20 | - not a local directory 21 | - not in standard library 22 | - not a dependency 23 | - not a dev dependency 24 | 25 | Then it must be a transitive dependency. 26 | """ 27 | 28 | violation = DEP003TransitiveDependencyViolation 29 | 30 | def find(self) -> list[Violation]: 31 | logging.debug("\nScanning for transitive dependencies...") 32 | transitive_dependencies: list[Violation] = [] 33 | 34 | for module_with_locations in self.imported_modules_with_locations: 35 | module = module_with_locations.module 36 | 37 | if module.standard_library: 38 | continue 39 | 40 | logging.debug("Scanning module %s...", module.name) 41 | 42 | if self._is_transitive(module): 43 | # `self._is_transitive` only returns `True` if the package is not None. 44 | for location in module_with_locations.locations: 45 | transitive_dependencies.append(self.violation(module, location)) 46 | 47 | return transitive_dependencies 48 | 49 | def _is_transitive(self, module: Module) -> bool: 50 | if any([ 51 | module.package is None, 52 | module.is_provided_by_dependency, 53 | module.is_provided_by_dev_dependency, 54 | module.local_module, 55 | ]): 56 | return False 57 | 58 | if module.name in self.ignored_modules: 59 | logging.debug("Dependency '%s' found to be a transitive dependency, but ignoring.", module.package) 60 | return False 61 | 62 | logging.debug("Dependency '%s' marked as a transitive dependency.", module.package) 63 | return True 64 | -------------------------------------------------------------------------------- /python/deptry/violations/dep003_transitive/violation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import ClassVar 5 | 6 | from deptry.violations.base import Violation 7 | 8 | 9 | @dataclass 10 | class DEP003TransitiveDependencyViolation(Violation): 11 | error_code: ClassVar[str] = "DEP003" 12 | error_template: ClassVar[str] = "'{name}' imported but it is a transitive dependency" 13 | 14 | def get_error_message(self) -> str: 15 | return self.error_template.format(name=self.issue.name) 16 | -------------------------------------------------------------------------------- /python/deptry/violations/dep004_misplaced_dev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/python/deptry/violations/dep004_misplaced_dev/__init__.py -------------------------------------------------------------------------------- /python/deptry/violations/dep004_misplaced_dev/finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from typing import TYPE_CHECKING 5 | 6 | from deptry.violations.base import ViolationsFinder 7 | from deptry.violations.dep004_misplaced_dev.violation import DEP004MisplacedDevDependencyViolation 8 | 9 | if TYPE_CHECKING: 10 | from deptry.module import Module 11 | from deptry.violations import Violation 12 | 13 | from dataclasses import dataclass 14 | 15 | 16 | @dataclass 17 | class DEP004MisplacedDevDependenciesFinder(ViolationsFinder): 18 | """ 19 | Given a list of imported modules and a list of project dependencies, determine which development dependencies 20 | should actually be regular dependencies. 21 | 22 | This is the case for any development dependency encountered, since files solely used for development purposes should be excluded from scanning. 23 | """ 24 | 25 | violation = DEP004MisplacedDevDependencyViolation 26 | 27 | def find(self) -> list[Violation]: 28 | """ 29 | In this function, we use 'corresponding_package_name' instead of module.package, since it can happen that a 30 | development dependency is not installed, but it's still found to be used in the codebase, due to simple name 31 | matching. In that case, it's added under module.dev_top_levels. _get_package_name is added for these edge-cases. 32 | """ 33 | logging.debug("\nScanning for incorrect development dependencies...") 34 | misplaced_dev_dependencies: list[Violation] = [] 35 | 36 | for module_with_locations in self.imported_modules_with_locations: 37 | module = module_with_locations.module 38 | 39 | if module.standard_library: 40 | continue 41 | 42 | logging.debug("Scanning module %s...", module.name) 43 | corresponding_package_name = self._get_package_name(module) 44 | 45 | if corresponding_package_name and self._is_development_dependency(module, corresponding_package_name): 46 | for location in module_with_locations.locations: 47 | misplaced_dev_dependencies.append(self.violation(module, location)) 48 | 49 | return misplaced_dev_dependencies 50 | 51 | def _is_development_dependency(self, module: Module, corresponding_package_name: str) -> bool: 52 | # Module can be provided both by a regular and by a development dependency. 53 | # Only continue if module is ONLY provided by a dev dependency. 54 | if not module.is_provided_by_dev_dependency or module.is_provided_by_dependency: 55 | return False 56 | 57 | if module.name in self.ignored_modules: 58 | logging.debug( 59 | "Dependency '%s' found to be a misplaced development dependency, but ignoring.", 60 | corresponding_package_name, 61 | ) 62 | return False 63 | 64 | logging.debug("Dependency '%s' marked as a misplaced development dependency.", corresponding_package_name) 65 | return True 66 | 67 | def _get_package_name(self, module: Module) -> str | None: 68 | if module.package: 69 | return module.package 70 | if module.dev_top_levels: 71 | if len(module.dev_top_levels) > 1: 72 | logging.debug( 73 | "Module %s is found in the top-level module names of multiple development dependencies. Skipping.", 74 | module.name, 75 | ) 76 | elif len(module.dev_top_levels) == 0: 77 | logging.debug( 78 | "Module %s has no metadata and it is not found in any top-level module names. Skipping.", 79 | module.name, 80 | ) 81 | else: 82 | return module.dev_top_levels[0] 83 | return None 84 | -------------------------------------------------------------------------------- /python/deptry/violations/dep004_misplaced_dev/violation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import ClassVar 5 | 6 | from deptry.violations.base import Violation 7 | 8 | 9 | @dataclass 10 | class DEP004MisplacedDevDependencyViolation(Violation): 11 | error_code: ClassVar[str] = "DEP004" 12 | error_template: ClassVar[str] = "'{name}' imported but declared as a dev dependency" 13 | 14 | def get_error_message(self) -> str: 15 | return self.error_template.format(name=self.issue.name) 16 | -------------------------------------------------------------------------------- /python/deptry/violations/dep005_standard_library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/python/deptry/violations/dep005_standard_library/__init__.py -------------------------------------------------------------------------------- /python/deptry/violations/dep005_standard_library/finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from dataclasses import dataclass 5 | from typing import TYPE_CHECKING 6 | 7 | from deptry.imports.location import Location 8 | from deptry.violations.base import ViolationsFinder 9 | from deptry.violations.dep005_standard_library.violation import DEP005StandardLibraryDependencyViolation 10 | 11 | if TYPE_CHECKING: 12 | from deptry.violations import Violation 13 | 14 | 15 | @dataclass 16 | class DEP005StandardLibraryDependenciesFinder(ViolationsFinder): 17 | """ 18 | Finds dependencies that are part of the standard library but are defined as dependencies. 19 | """ 20 | 21 | violation = DEP005StandardLibraryDependencyViolation 22 | 23 | def find(self) -> list[Violation]: 24 | logging.debug("\nScanning for dependencies that are part of the standard library...") 25 | stdlib_violations: list[Violation] = [] 26 | 27 | for dependency in self.dependencies: 28 | logging.debug("Scanning module %s...", dependency.name) 29 | 30 | if dependency.name in self.standard_library_modules: 31 | if dependency.name in self.ignored_modules: 32 | logging.debug( 33 | "Dependency '%s' found to be a dependency that is part of the standard library, but ignoring.", 34 | dependency.name, 35 | ) 36 | continue 37 | 38 | logging.debug( 39 | "Dependency '%s' marked as a dependency that is part of the standard library.", dependency.name 40 | ) 41 | stdlib_violations.append(self.violation(dependency, Location(dependency.definition_file))) 42 | 43 | return stdlib_violations 44 | -------------------------------------------------------------------------------- /python/deptry/violations/dep005_standard_library/violation.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from dataclasses import dataclass 4 | from typing import ClassVar 5 | 6 | from deptry.violations.base import Violation 7 | 8 | 9 | @dataclass 10 | class DEP005StandardLibraryDependencyViolation(Violation): 11 | error_code: ClassVar[str] = "DEP005" 12 | error_template: ClassVar[str] = ( 13 | "'{name}' is defined as a dependency but it is included in the Python standard library." 14 | ) 15 | 16 | def get_error_message(self) -> str: 17 | return self.error_template.format(name=self.issue.name) 18 | -------------------------------------------------------------------------------- /python/deptry/violations/finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import operator 4 | from typing import TYPE_CHECKING 5 | 6 | from deptry.violations import ( 7 | DEP001MissingDependenciesFinder, 8 | DEP002UnusedDependenciesFinder, 9 | DEP003TransitiveDependenciesFinder, 10 | DEP004MisplacedDevDependenciesFinder, 11 | DEP005StandardLibraryDependenciesFinder, 12 | ) 13 | 14 | if TYPE_CHECKING: 15 | from collections.abc import Mapping 16 | 17 | from deptry.dependency import Dependency 18 | from deptry.module import ModuleLocations 19 | from deptry.violations import Violation, ViolationsFinder 20 | 21 | 22 | _VIOLATIONS_FINDERS: tuple[type[ViolationsFinder], ...] = ( 23 | DEP001MissingDependenciesFinder, 24 | DEP002UnusedDependenciesFinder, 25 | DEP003TransitiveDependenciesFinder, 26 | DEP004MisplacedDevDependenciesFinder, 27 | DEP005StandardLibraryDependenciesFinder, 28 | ) 29 | 30 | 31 | def find_violations( 32 | imported_modules_with_locations: list[ModuleLocations], 33 | dependencies: list[Dependency], 34 | ignore: tuple[str, ...], 35 | per_rule_ignores: Mapping[str, tuple[str, ...]], 36 | standard_library_modules: frozenset[str], 37 | ) -> list[Violation]: 38 | violations = [] 39 | 40 | for violation_finder in _VIOLATIONS_FINDERS: 41 | if violation_finder.violation.error_code not in ignore: 42 | violations.extend( 43 | violation_finder( 44 | imported_modules_with_locations=imported_modules_with_locations, 45 | dependencies=dependencies, 46 | ignored_modules=per_rule_ignores.get(violation_finder.violation.error_code, ()), 47 | standard_library_modules=standard_library_modules, 48 | ).find() 49 | ) 50 | return _get_sorted_violations(violations) 51 | 52 | 53 | def _get_sorted_violations(violations: list[Violation]) -> list[Violation]: 54 | return sorted( 55 | violations, key=operator.attrgetter("location.file", "location.line", "location.column", "error_code") 56 | ) 57 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.86" 3 | -------------------------------------------------------------------------------- /scripts/generate_stdlibs.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # This script is inspired by isort: https://github.com/PyCQA/isort/blob/4ccbd1eddf564d2c9e79c59d59c1fc06a7e35f94/scripts/mkstdlibs.py. 4 | 5 | from __future__ import annotations 6 | 7 | import ast 8 | import urllib.request 9 | from html.parser import HTMLParser 10 | from pathlib import Path 11 | 12 | OUTPUT_PATH = Path("python/deptry/stdlibs.py") 13 | STDLIB_MODULES_URL = "https://docs.python.org/{}.{}/py-modindex.html" 14 | 15 | # Starting from Python 3.10, https://docs.python.org/3/library/sys.html#sys.stdlib_module_names is available. 16 | PYTHON_VERSIONS = ((3, 9),) 17 | 18 | # Modules that are in stdlib, but undocumented. 19 | EXTRA_STDLIBS_MODULES = ("_ast", "ntpath", "posixpath", "sre", "sre_constants", "sre_compile", "sre_parse") 20 | 21 | DOCSTRING_GENERATED_FILES = """ 22 | DO NOT EDIT THIS FILE MANUALLY. 23 | It is generated from `scripts/generate_stdlibs.py` script and contains the stdlib modules for Python versions that does 24 | not support https://docs.python.org/3/library/sys.html#sys.stdlib_module_names (< 3.10). 25 | The file can be generated again using `python scripts/generate_stdlibs.py`. 26 | """ 27 | 28 | 29 | class PythonStdlibHTMLParser(HTMLParser): 30 | def __init__(self) -> None: 31 | super().__init__() 32 | self._is_in_code_tag = False 33 | self.modules: list[str] = [] 34 | 35 | def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: 36 | if tag == "code": 37 | self._is_in_code_tag = True 38 | 39 | def handle_endtag(self, tag: str) -> None: 40 | if tag == "code": 41 | self._is_in_code_tag = False 42 | 43 | def handle_data(self, data: str) -> None: 44 | if self._is_in_code_tag: 45 | self.modules.append(data) 46 | 47 | 48 | def get_standard_library_modules_for_python_version(python_version: tuple[int, int]) -> list[str]: 49 | with urllib.request.urlopen( # noqa: S310 50 | STDLIB_MODULES_URL.format(python_version[0], python_version[1]) 51 | ) as response: 52 | html_content = response.read().decode() 53 | 54 | parser = PythonStdlibHTMLParser() 55 | parser.feed(html_content) 56 | 57 | modules = {module.split(".")[0] for module in parser.modules}.union(EXTRA_STDLIBS_MODULES) 58 | modules.remove("__main__") 59 | 60 | return sorted(modules) 61 | 62 | 63 | def get_standard_library_modules() -> dict[str, list[str]]: 64 | return { 65 | f"{python_version[0]}{python_version[1]}": get_standard_library_modules_for_python_version(python_version) 66 | for python_version in PYTHON_VERSIONS 67 | } 68 | 69 | 70 | def write_stdlibs_file(stdlib_python: dict[str, list[str]]) -> None: 71 | node = ast.Module( 72 | body=[ 73 | ast.Expr(ast.Constant(DOCSTRING_GENERATED_FILES)), 74 | ast.Assign( 75 | targets=[ast.Name("STDLIBS_PYTHON")], 76 | value=ast.Dict( 77 | keys=[ast.Constant(python_version) for python_version in stdlib_python], 78 | values=[ 79 | ast.Call( 80 | func=ast.Name(id="frozenset"), 81 | args=[ast.Set(elts=[ast.Constant(module) for module in python_standard_library_modules])], 82 | keywords=[], 83 | ) 84 | for python_standard_library_modules in stdlib_python.values() 85 | ], 86 | ), 87 | lineno=0, 88 | ), 89 | ], 90 | type_ignores=[], 91 | ) 92 | 93 | with OUTPUT_PATH.open("w+") as stdlib_file: 94 | stdlib_file.write(ast.unparse(node)) # type: ignore[attr-defined, unused-ignore] 95 | 96 | 97 | if __name__ == "__main__": 98 | write_stdlibs_file(get_standard_library_modules()) 99 | -------------------------------------------------------------------------------- /src/file_utils.rs: -------------------------------------------------------------------------------- 1 | use chardetng::EncodingDetector; 2 | use encoding_rs::Encoding; 3 | use pyo3::exceptions::{PyFileNotFoundError, PyIOError}; 4 | use pyo3::prelude::*; 5 | use regex::Regex; 6 | use std::fs; 7 | use std::fs::File; 8 | use std::io::{BufReader, ErrorKind, Read}; 9 | use std::path::Path; 10 | 11 | /// Reads a Python file's content as a `String`. It first attempts to read the file as UTF-8. 12 | /// If reading fails due to an encoding issue, it tries to determine the file's encoding 13 | /// from a Python encoding declaration or by guessing and then reads the file using the detected encoding 14 | pub fn read_file(file_path: &str) -> PyResult { 15 | let path = Path::new(file_path); 16 | 17 | match fs::read_to_string(path) { 18 | Ok(content) => Ok(content), 19 | Err(e) => match e.kind() { 20 | ErrorKind::NotFound => Err(PyFileNotFoundError::new_err(format!( 21 | "File not found: '{file_path}'", 22 | ))), 23 | ErrorKind::InvalidData => { 24 | let file = File::open(path).unwrap(); 25 | let mut buffer = Vec::new(); 26 | BufReader::new(file).read_to_end(&mut buffer)?; 27 | 28 | let encoding = detect_python_file_encoding_from_regex(&buffer) 29 | .unwrap_or_else(|| guess_encoding(&buffer)); 30 | read_with_encoding(&buffer, encoding) 31 | } 32 | _ => Err(PyIOError::new_err(format!("An error occurred: '{e}'"))), 33 | }, 34 | } 35 | } 36 | 37 | /// Detects the encoding declared in the first or second line of a Python file according to PEP 263. 38 | /// Returns the detected encoding if found; otherwise, returns None. 39 | fn detect_python_file_encoding_from_regex(buffer: &[u8]) -> Option<&'static Encoding> { 40 | let content = String::from_utf8_lossy(buffer); 41 | let re = Regex::new(r"^[ \t\f]*#.*?coding[:=][ \t]*([-_.a-zA-Z0-9]+)").unwrap(); 42 | 43 | for line in content.lines().take(2) { 44 | if let Some(caps) = re.captures(line) { 45 | if let Some(m) = caps.get(1) { 46 | return Encoding::for_label(m.as_str().as_bytes()); 47 | } 48 | } 49 | } 50 | 51 | None 52 | } 53 | 54 | /// Reads the content of a buffer using the specified encoding and returns the content as a `String`. 55 | /// If decoding fails, it returns an error indicating that decoding was unsuccessful. 56 | fn read_with_encoding(buffer: &[u8], encoding: &'static Encoding) -> PyResult { 57 | let (cow, _encoding_used, had_errors) = encoding.decode(buffer); 58 | if had_errors { 59 | return Err(PyIOError::new_err( 60 | "Failed to decode file content with the detected encoding.", 61 | )); 62 | } 63 | Ok(cow.into_owned()) 64 | } 65 | 66 | /// Uses the `EncodingDetector` crate to guess the encoding of a given byte array. 67 | /// Returns the guessed encoding, defaulting to UTF-8 if no conclusive guess can be made. 68 | fn guess_encoding(bytes: &[u8]) -> &'static Encoding { 69 | let mut detector = EncodingDetector::new(); 70 | detector.feed(bytes, true); 71 | detector.guess(None, true) 72 | } 73 | -------------------------------------------------------------------------------- /src/imports/ipynb.rs: -------------------------------------------------------------------------------- 1 | use crate::file_utils; 2 | use crate::location; 3 | 4 | use super::shared; 5 | use file_utils::read_file; 6 | use location::Location; 7 | use pyo3::exceptions::PySyntaxError; 8 | use pyo3::prelude::*; 9 | use pyo3::types::PyDict; 10 | use rayon::prelude::*; 11 | use std::collections::HashMap; 12 | 13 | /// Processes multiple Python files in parallel to extract import statements and their locations. 14 | /// Accepts a list of file paths and returns a dictionary mapping module names to their import locations. 15 | #[pyfunction] 16 | pub fn get_imports_from_ipynb_files(py: Python, file_paths: Vec) -> Bound<'_, PyDict> { 17 | let results: Vec<_> = file_paths 18 | .par_iter() 19 | .map(|path_str| { 20 | let result = get_imports_from_ipynb_file(path_str); 21 | shared::ThreadResult { 22 | file: path_str.to_string(), 23 | result, 24 | } 25 | }) 26 | .collect(); 27 | 28 | let (all_imports, errors) = shared::merge_results_from_threads(results); 29 | shared::log_python_errors_as_warnings(&errors); 30 | 31 | all_imports.into_pyobject(py).unwrap() 32 | } 33 | 34 | /// Core helper function that extracts import statements and their locations from a single .ipynb file. 35 | /// Ensures robust error handling and provides clearer, more detailed comments. 36 | fn get_imports_from_ipynb_file(path_str: &str) -> PyResult>> { 37 | let file_content = read_file(path_str)?; 38 | let notebook: serde_json::Value = 39 | serde_json::from_str(&file_content).map_err(|e| PySyntaxError::new_err(e.to_string()))?; 40 | let cells = notebook["cells"] 41 | .as_array() 42 | .ok_or_else(|| PySyntaxError::new_err("Expected 'cells' to be an array"))?; 43 | let python_code = extract_code_from_notebook_cells(cells); 44 | 45 | let parsed = shared::parse_file_content(&python_code)?; 46 | let imported_modules = shared::extract_imports_from_parsed_file_content(parsed); 47 | 48 | Ok(shared::convert_imports_with_textranges_to_location_objects( 49 | imported_modules, 50 | path_str, 51 | &python_code, 52 | )) 53 | } 54 | 55 | /// Extracts and concatenates code from notebook code cells. 56 | fn extract_code_from_notebook_cells(cells: &[serde_json::Value]) -> String { 57 | let code_lines: Vec = cells 58 | .iter() 59 | .filter(|cell| cell["cell_type"] == "code") 60 | .filter_map(|cell| cell["source"].as_array()) 61 | .flatten() 62 | .filter_map(|line| line.as_str()) 63 | .map(|line| { 64 | // https://ipython.readthedocs.io/en/stable/interactive/magics.html 65 | // We want to skip lines using magics, as they are not valid Python. We replace the 66 | // lines with empty strings instead of completely removing them, to ensure that the 67 | // violation reporters show the correct line location for imports made below those 68 | // lines. 69 | if line.starts_with('%') || line.starts_with('!') { 70 | "" 71 | } else { 72 | line 73 | } 74 | }) 75 | .map(|s| s.strip_suffix('\n').unwrap_or(s)) 76 | .map(str::to_owned) 77 | .collect(); 78 | 79 | code_lines.join("\n") 80 | } 81 | -------------------------------------------------------------------------------- /src/imports/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ipynb; 2 | pub mod py; 3 | pub mod shared; 4 | -------------------------------------------------------------------------------- /src/imports/py.rs: -------------------------------------------------------------------------------- 1 | use crate::file_utils; 2 | use crate::location; 3 | 4 | use super::shared; 5 | use file_utils::read_file; 6 | use location::Location; 7 | use pyo3::prelude::*; 8 | use pyo3::types::PyDict; 9 | use rayon::prelude::*; 10 | use std::collections::HashMap; 11 | 12 | /// Processes multiple Python files in parallel to extract import statements and their locations. 13 | /// Accepts a list of file paths and returns a dictionary mapping module names to their import locations. 14 | #[pyfunction] 15 | pub fn get_imports_from_py_files(py: Python, file_paths: Vec) -> Bound<'_, PyDict> { 16 | let results: Vec<_> = file_paths 17 | .par_iter() 18 | .map(|path_str| { 19 | let result = get_imports_from_py_file(path_str); 20 | shared::ThreadResult { 21 | file: path_str.to_string(), 22 | result, 23 | } 24 | }) 25 | .collect(); 26 | 27 | let (all_imports, errors) = shared::merge_results_from_threads(results); 28 | shared::log_python_errors_as_warnings(&errors); 29 | 30 | all_imports.into_pyobject(py).unwrap() 31 | } 32 | 33 | /// Core helper function that extracts import statements and their locations from the content of a single Python file. 34 | /// Used internally by both parallel and single file processing functions. 35 | fn get_imports_from_py_file(path_str: &str) -> PyResult>> { 36 | let file_content = read_file(path_str)?; 37 | let ast = shared::parse_file_content(&file_content)?; 38 | let imported_modules = shared::extract_imports_from_parsed_file_content(ast); 39 | Ok(shared::convert_imports_with_textranges_to_location_objects( 40 | imported_modules, 41 | path_str, 42 | &file_content, 43 | )) 44 | } 45 | -------------------------------------------------------------------------------- /src/imports/shared.rs: -------------------------------------------------------------------------------- 1 | use crate::location; 2 | use crate::visitor; 3 | 4 | use location::Location; 5 | use pyo3::exceptions::PySyntaxError; 6 | use pyo3::prelude::*; 7 | use ruff_python_ast::visitor::Visitor; 8 | use ruff_python_ast::{Mod, ModModule}; 9 | use ruff_python_parser::{Mode, Parsed, parse}; 10 | use ruff_source_file::LineIndex; 11 | use ruff_text_size::TextRange; 12 | use std::collections::HashMap; 13 | use visitor::ImportVisitor; 14 | 15 | pub type FileToImportsMap = HashMap>; 16 | pub type ErrorList = Vec<(String, PyErr)>; 17 | 18 | pub struct ThreadResult { 19 | pub file: String, 20 | pub result: PyResult, 21 | } 22 | 23 | /// Parses the content of a Python file into a parsed source code. 24 | pub fn parse_file_content(file_content: &str) -> PyResult> { 25 | let parsed = 26 | parse(file_content, Mode::Module).map_err(|e| PySyntaxError::new_err(e.to_string()))?; 27 | Ok(parsed) 28 | } 29 | 30 | /// Iterates through a parsed source code to identify and collect import statements, and returns them 31 | /// together with their respective `TextRange` for each occurrence. 32 | pub fn extract_imports_from_parsed_file_content( 33 | parsed: Parsed, 34 | ) -> HashMap> { 35 | let mut visitor = ImportVisitor::new(); 36 | 37 | if let Mod::Module(ModModule { body, .. }) = parsed.into_syntax() { 38 | for stmt in body { 39 | visitor.visit_stmt(&stmt); 40 | } 41 | } 42 | 43 | visitor.get_imports() 44 | } 45 | 46 | /// Converts textual ranges of import statements into structured location objects. 47 | /// Facilitates the mapping of imports to detailed, file-specific location data (file, line, column). 48 | pub fn convert_imports_with_textranges_to_location_objects( 49 | imports: HashMap>, 50 | file_path: &str, 51 | source_code: &str, 52 | ) -> FileToImportsMap { 53 | let line_index = LineIndex::from_source_text(source_code); 54 | let mut imports_with_locations = HashMap::>::new(); 55 | 56 | for (module, ranges) in imports { 57 | let locations: Vec = ranges 58 | .iter() 59 | .map(|range| { 60 | let start_line = line_index.line_index(range.start()).get(); 61 | let start_col = line_index 62 | .source_location(range.start(), source_code) 63 | .column 64 | .get(); 65 | Location { 66 | file: file_path.to_owned(), 67 | line: Some(start_line), 68 | column: Some(start_col), 69 | } 70 | }) 71 | .collect(); 72 | imports_with_locations.insert(module, locations); 73 | } 74 | imports_with_locations 75 | } 76 | 77 | // Shared logic for merging results from different threads. 78 | pub fn merge_results_from_threads(results: Vec) -> (FileToImportsMap, ErrorList) { 79 | let mut all_imports = HashMap::new(); 80 | let mut errors = Vec::new(); 81 | 82 | for thread_result in results { 83 | match thread_result.result { 84 | Ok(file_result) => { 85 | for (module, locations) in file_result { 86 | all_imports 87 | .entry(module) 88 | .or_insert_with(Vec::new) 89 | .extend(locations); 90 | } 91 | } 92 | Err(e) => errors.push((thread_result.file, e)), 93 | } 94 | } 95 | 96 | (all_imports, errors) 97 | } 98 | 99 | // Shared logic for logging errors. 100 | pub fn log_python_errors_as_warnings(errors: &[(String, PyErr)]) { 101 | for (path, error) in errors { 102 | log::warn!( 103 | "Warning: Skipping processing of {} because of the following error: \"{}\".", 104 | path, 105 | error 106 | ); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | mod file_utils; 4 | mod imports; 5 | mod location; 6 | mod python_file_finder; 7 | mod visitor; 8 | 9 | use location::Location; 10 | 11 | #[pymodule] 12 | fn rust(_py: Python, m: &Bound<'_, PyModule>) -> PyResult<()> { 13 | pyo3_log::init(); // Initialize logging to forward to Python's logger 14 | 15 | m.add_function(wrap_pyfunction!(imports::py::get_imports_from_py_files, m)?)?; 16 | m.add_function(wrap_pyfunction!( 17 | imports::ipynb::get_imports_from_ipynb_files, 18 | m 19 | )?)?; 20 | m.add_function(wrap_pyfunction!(python_file_finder::find_python_files, m)?)?; 21 | m.add_class::()?; 22 | Ok(()) 23 | } 24 | -------------------------------------------------------------------------------- /src/location.rs: -------------------------------------------------------------------------------- 1 | use pyo3::prelude::*; 2 | 3 | #[pyclass] 4 | #[derive(Clone, Debug)] 5 | pub struct Location { 6 | #[pyo3(get, set)] 7 | pub file: String, 8 | #[pyo3(get, set)] 9 | pub line: Option, 10 | #[pyo3(get, set)] 11 | pub column: Option, 12 | } 13 | 14 | #[pymethods] 15 | impl Location { 16 | #[new] 17 | #[pyo3(signature = (file, line=None, column=None))] 18 | fn new(file: String, line: Option, column: Option) -> Self { 19 | Self { file, line, column } 20 | } 21 | 22 | fn __repr__(&self) -> String { 23 | format!( 24 | "Location(file='{}', line={:?}, column={:?})", 25 | self.file, self.line, self.column 26 | ) 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/python_file_finder.rs: -------------------------------------------------------------------------------- 1 | use ignore::types::{Types, TypesBuilder}; 2 | use ignore::{DirEntry, Walk, WalkBuilder}; 3 | use path_slash::PathExt; 4 | use pyo3::{Bound, IntoPyObject, PyAny, Python, pyfunction}; 5 | use regex::Regex; 6 | use std::path::PathBuf; 7 | 8 | #[pyfunction] 9 | #[pyo3(signature = (directories, exclude, extend_exclude, using_default_exclude, ignore_notebooks=false))] 10 | pub fn find_python_files( 11 | py: Python, 12 | directories: Vec, 13 | exclude: Vec, 14 | extend_exclude: Vec, 15 | using_default_exclude: bool, 16 | ignore_notebooks: bool, 17 | ) -> Bound<'_, PyAny> { 18 | let mut unique_directories = directories; 19 | unique_directories.dedup(); 20 | 21 | let python_files: Vec<_> = build_walker( 22 | unique_directories.as_ref(), 23 | [exclude, extend_exclude].concat().as_ref(), 24 | using_default_exclude, 25 | ignore_notebooks, 26 | ) 27 | .flatten() 28 | .filter(|entry| entry.path().is_file()) 29 | .map(|entry| { 30 | entry 31 | .path() 32 | .to_string_lossy() 33 | .strip_prefix("./") 34 | .unwrap_or(&entry.path().to_string_lossy()) 35 | .to_owned() 36 | }) 37 | .collect(); 38 | 39 | python_files.into_pyobject(py).unwrap() 40 | } 41 | 42 | fn build_walker( 43 | directories: &[PathBuf], 44 | excluded_patterns: &[String], 45 | use_git_ignore: bool, 46 | ignore_notebooks: bool, 47 | ) -> Walk { 48 | let (first_directory, additional_directories) = directories.split_first().unwrap(); 49 | 50 | let mut walk_builder = WalkBuilder::new(first_directory); 51 | for path in additional_directories { 52 | walk_builder.add(path); 53 | } 54 | 55 | let re: Option = if excluded_patterns.is_empty() { 56 | None 57 | } else { 58 | Some(Regex::new(format!(r"^({})", excluded_patterns.join("|")).as_str()).unwrap()) 59 | }; 60 | 61 | walk_builder 62 | .types(build_types(ignore_notebooks).unwrap()) 63 | .standard_filters(use_git_ignore) 64 | .hidden(false) 65 | .filter_entry(move |entry| entry_satisfies_predicate(entry, re.as_ref())) 66 | .build() 67 | } 68 | 69 | fn build_types(ignore_notebooks: bool) -> Result { 70 | let mut types_builder = TypesBuilder::new(); 71 | types_builder.add("python", "*.py").unwrap(); 72 | types_builder.select("python"); 73 | 74 | if !ignore_notebooks { 75 | types_builder.add("jupyter", "*.ipynb").unwrap(); 76 | types_builder.select("jupyter"); 77 | } 78 | 79 | types_builder.build() 80 | } 81 | 82 | fn entry_satisfies_predicate(entry: &DirEntry, regex: Option<&Regex>) -> bool { 83 | if regex.is_none() { 84 | return true; 85 | } 86 | 87 | let path_str = entry.path().to_slash_lossy(); 88 | !regex 89 | .unwrap() 90 | .is_match(path_str.strip_prefix("./").unwrap_or(&path_str).as_ref()) 91 | } 92 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/example_project/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /tests/fixtures/example_project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "foo" 3 | version = "0.0.1" 4 | description = "" 5 | authors = [] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.9" 9 | arrow = "1.3.0" 10 | click = "8.1.8" 11 | isort = "6.0.1" 12 | pkginfo = "1.12.1.2" 13 | requests = "2.32.3" 14 | urllib3 = "2.4.0" 15 | 16 | [tool.poetry.dev-dependencies] 17 | black = "25.1.0" 18 | 19 | [tool.deptry.per_rule_ignores] 20 | DEP002 = ["pkginfo"] 21 | -------------------------------------------------------------------------------- /tests/fixtures/example_project/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import click 6 | import white as w 7 | from urllib3 import contrib 8 | -------------------------------------------------------------------------------- /tests/fixtures/example_project/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "!ls\n", 11 | "%timeit\n", 12 | "%%timeit\n", 13 | "import click\n", 14 | "from urllib3 import contrib\n", 15 | "import arrow\n", 16 | "1 +\\\n", 17 | " 2" 18 | ] 19 | } 20 | ], 21 | "metadata": { 22 | "kernelspec": { 23 | "display_name": "Python 3 (ipykernel)", 24 | "language": "python", 25 | "name": "python3" 26 | }, 27 | "language_info": { 28 | "codemirror_mode": { 29 | "name": "ipython", 30 | "version": 3 31 | }, 32 | "file_extension": ".py", 33 | "mimetype": "text/x-python", 34 | "name": "python", 35 | "nbconvert_exporter": "python", 36 | "pygments_lexer": "ipython3", 37 | "version": "3.9.11" 38 | } 39 | }, 40 | "nbformat": 4, 41 | "nbformat_minor": 5 42 | } 43 | -------------------------------------------------------------------------------- /tests/fixtures/pep_621_project/.ignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tests/fixtures/pep_621_project/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | requires-python = ">=3.9" 5 | dependencies = [ 6 | "arrow==1.3.0", 7 | "asyncio==3.4.3", 8 | "click==8.1.8", 9 | "isort==6.0.1", 10 | "pkginfo==1.12.1.2", 11 | "requests==2.32.3", 12 | "urllib3==2.4.0", 13 | ] 14 | 15 | [project.optional-dependencies] 16 | dev = [ 17 | "black==25.1.0", 18 | "mypy==1.15.0", 19 | ] 20 | test = ["pytest==8.3.5"] 21 | plot = ["matplotlib==3.10.1"] 22 | 23 | [dependency-groups] 24 | foo = [ 25 | "certifi==2025.1.31", 26 | "idna==3.10", 27 | ] 28 | all = [{include-group = "foo"}, "packaging==25.0"] 29 | 30 | [build-system] 31 | requires = ["setuptools>=61.0.0"] 32 | build-backend = "setuptools.build_meta" 33 | 34 | [tool.deptry] 35 | pep621_dev_dependency_groups = ["dev"] 36 | 37 | [tool.deptry.per_rule_ignores] 38 | DEP002 = ["pkginfo"] 39 | -------------------------------------------------------------------------------- /tests/fixtures/pep_621_project/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import asyncio 5 | import black 6 | import certifi 7 | import click 8 | import idna 9 | import packaging 10 | import white as w 11 | from urllib3 import contrib 12 | -------------------------------------------------------------------------------- /tests/fixtures/pep_621_project/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_using_namespace/.ignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_using_namespace/foo/api/http.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/fixtures/project_using_namespace/foo/api/http.py -------------------------------------------------------------------------------- /tests/fixtures/project_using_namespace/foo/database/bar.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import flake8 5 | import white as w 6 | 7 | from foo import api 8 | -------------------------------------------------------------------------------- /tests/fixtures/project_using_namespace/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | requires-python = ">=3.9" 5 | dependencies = ["arrow==1.3.0"] 6 | 7 | [project.optional-dependencies] 8 | dev = ["flake8==7.2.0"] 9 | 10 | [build-system] 11 | requires = ["setuptools>=61.0.0"] 12 | build-backend = "setuptools.build_meta" 13 | 14 | [tool.deptry] 15 | pep621_dev_dependency_groups = ["dev"] 16 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | foobar.py 3 | /src/barfoo.py 4 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | requires-python = ">=3.9" 5 | dependencies = [ 6 | "arrow==1.3.0", 7 | "click==8.1.8", 8 | "isort==6.0.1", 9 | "pkginfo==1.12.1.2", 10 | "requests==2.32.3", 11 | "urllib3==2.4.0", 12 | ] 13 | 14 | [project.optional-dependencies] 15 | dev = [ 16 | "black==25.1.0", 17 | "mypy==1.15.0", 18 | ] 19 | test = ["pytest==8.3.5"] 20 | 21 | [build-system] 22 | requires = ["setuptools>=61.0.0"] 23 | build-backend = "setuptools.build_meta" 24 | 25 | [tool.deptry.per_rule_ignores] 26 | DEP002 = ["pkginfo"] 27 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/src/.gitignore: -------------------------------------------------------------------------------- 1 | /baz.py 2 | /src/bar.py 3 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/src/bar.py: -------------------------------------------------------------------------------- 1 | import isort 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/src/barfoo.py: -------------------------------------------------------------------------------- 1 | import hello 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/src/baz.py: -------------------------------------------------------------------------------- 1 | import hej 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/src/foo.py: -------------------------------------------------------------------------------- 1 | import black 2 | import click 3 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/src/foobar.py: -------------------------------------------------------------------------------- 1 | import mypy 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_gitignore/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/another_directory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/fixtures/project_with_multiple_source_directories/another_directory/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/another_directory/foo.py: -------------------------------------------------------------------------------- 1 | import a_non_existing_dependency 2 | import toml 3 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | requires-python = ">=3.9" 5 | dependencies = ["arrow==1.3.0"] 6 | 7 | [build-system] 8 | requires = ["setuptools>=61.0.0"] 9 | build-backend = "setuptools.build_meta" 10 | 11 | [tool.deptry.per_rule_ignores] 12 | DEP002 = ["pkginfo"] 13 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/fixtures/project_with_multiple_source_directories/src/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/src/foo.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | from foobar import a_local_method 4 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/src/foobar.py: -------------------------------------------------------------------------------- 1 | def a_local_method(): ... 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/worker/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/fixtures/project_with_multiple_source_directories/worker/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/worker/foo.py: -------------------------------------------------------------------------------- 1 | import celery 2 | 3 | from foobaz import a_local_method 4 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_multiple_source_directories/worker/foobaz.py: -------------------------------------------------------------------------------- 1 | def a_local_method(): ... 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_pdm/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | requires-python = ">=3.9" 5 | dependencies = [ 6 | "arrow==1.3.0", 7 | "pkginfo==1.12.1.2", 8 | "urllib3==2.4.0", 9 | ] 10 | 11 | [project.optional-dependencies] 12 | baz = [ 13 | "click==8.1.8", 14 | "isort==6.0.1", 15 | ] 16 | bar = ["requests==2.32.3"] 17 | 18 | [tool.pdm] 19 | version = {source = "scm"} 20 | 21 | [dependency-groups] 22 | foo = [ 23 | "certifi==2025.1.31", 24 | "idna==3.10", 25 | ] 26 | all = [{include-group = "foo"}, "packaging==25.0"] 27 | 28 | [tool.pdm.dev-dependencies] 29 | lint = [ 30 | "black==25.1.0", 31 | "mypy==1.15.0", 32 | ] 33 | test = [ 34 | "pytest==8.3.5", 35 | "pytest-cov==6.1.1", 36 | ] 37 | 38 | [tool.deptry] 39 | pep621_dev_dependency_groups = ["bar"] 40 | 41 | [tool.deptry.per_rule_ignores] 42 | DEP002 = ["pkginfo"] 43 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_pdm/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import certifi 6 | import click 7 | import idna 8 | import mypy 9 | import packaging 10 | import pytest 11 | import pytest_cov 12 | import white as w 13 | from urllib3 import contrib 14 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_pdm/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_poetry/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_poetry/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "foo" 3 | version = "0.0.1" 4 | description = "" 5 | authors = [] 6 | 7 | [tool.poetry.dependencies] 8 | python = ">=3.9" 9 | arrow = "1.3.0" 10 | pkginfo = "1.12.1.2" 11 | urllib3 = "2.4.0" 12 | 13 | click = { version = "8.1.8", optional = true } 14 | isort = { version = "6.0.1", optional = true } 15 | requests = { version = "2.32.3", optional = true } 16 | 17 | [tool.poetry.extras] 18 | foo = [ 19 | "click", 20 | "isort", 21 | ] 22 | bar = ["requests"] 23 | 24 | [tool.poetry.group.lint.dependencies] 25 | black = "25.1.0" 26 | mypy = "1.15.0" 27 | 28 | [tool.poetry.group.test.dependencies] 29 | pytest = "8.3.5" 30 | pytest-cov = "6.1.1" 31 | 32 | [tool.deptry.per_rule_ignores] 33 | DEP002 = ["pkginfo"] 34 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_poetry/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import click 6 | import mypy 7 | import pytest 8 | import pytest_cov 9 | import white as w 10 | from urllib3 import contrib 11 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_poetry/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_poetry_pep_621/poetry.toml: -------------------------------------------------------------------------------- 1 | [virtualenvs] 2 | in-project = true 3 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_poetry_pep_621/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | description = "" 5 | authors = [] 6 | requires-python = ">=3.9" 7 | dependencies = [ 8 | "arrow==1.3.0", 9 | "pkginfo==1.12.1.2", 10 | "requests", 11 | ] 12 | 13 | [tool.poetry.dependencies] 14 | requests = { git = "https://github.com/psf/requests", tag = "v2.32.3" } 15 | 16 | [project.optional-dependencies] 17 | foo = [ 18 | "click==8.1.8", 19 | "isort==6.0.1", 20 | ] 21 | bar = ["urllib3==2.4.0"] 22 | 23 | [tool.poetry.dev-dependencies] 24 | black = "25.1.0" 25 | 26 | [tool.poetry.group.lint.dependencies] 27 | mypy = "1.15.0" 28 | 29 | [tool.poetry.group.test.dependencies] 30 | pytest = "8.3.5" 31 | pytest-cov = "6.1.1" 32 | 33 | [tool.deptry.per_rule_ignores] 34 | DEP002 = ["pkginfo"] 35 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_poetry_pep_621/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import click 6 | import mypy 7 | import pytest 8 | import pytest_cov 9 | import white as w 10 | from urllib3 import contrib 11 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_poetry_pep_621/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_pyproject_different_directory/a_sub_directory/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | requires-python = ">=3.9" 5 | dependencies = [ 6 | "arrow==1.3.0", 7 | "click==8.1.8", 8 | "isort==6.0.1", 9 | "pkginfo==1.12.1.2", 10 | "requests==2.32.3", 11 | "urllib3==2.4.0", 12 | ] 13 | 14 | [project.optional-dependencies] 15 | dev = [ 16 | "black==25.1.0", 17 | "mypy==1.15.0", 18 | ] 19 | test = ["pytest==8.3.5"] 20 | 21 | [build-system] 22 | requires = ["setuptools>=61.0.0"] 23 | build-backend = "setuptools.build_meta" 24 | 25 | [tool.deptry.per_rule_ignores] 26 | DEP002 = ["pkginfo"] 27 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_pyproject_different_directory/src/project_with_src_directory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/fixtures/project_with_pyproject_different_directory/src/project_with_src_directory/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/project_with_pyproject_different_directory/src/project_with_src_directory/bar.py: -------------------------------------------------------------------------------- 1 | from project_with_src_directory.foo import a_local_method 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_pyproject_different_directory/src/project_with_src_directory/foo.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import click 6 | import white as w 7 | from urllib3 import contrib 8 | 9 | 10 | def a_local_method(): ... 11 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_pyproject_different_directory/src/project_with_src_directory/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_in/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | 4 | [tool.deptry.per_rule_ignores] 5 | DEP001 = ["toml"] 6 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_in/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==25.1.0 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_in/requirements.in: -------------------------------------------------------------------------------- 1 | click==8.1.7 2 | isort==5.13.2 3 | urllib3==2.2.3 4 | uvicorn==0.32.0 5 | itchiodl==2.3.0 6 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_in/requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | # This file is autogenerated by pip-compile with Python 3.13 3 | # by the following command: 4 | # 5 | # pip-compile 6 | # 7 | args==0.1.0 8 | # via clint 9 | beautifulsoup4==4.13.4 10 | # via itchiodl 11 | certifi==2025.1.31 12 | # via requests 13 | charset-normalizer==3.4.1 14 | # via requests 15 | click==8.1.8 16 | # via 17 | # -r requirements.in 18 | # uvicorn 19 | clint==0.5.1 20 | # via itchiodl 21 | h11==0.14.0 22 | # via uvicorn 23 | idna==3.10 24 | # via requests 25 | isort==6.0.1 26 | # via -r requirements.in 27 | itchiodl==2.3.0 28 | # via -r requirements.in 29 | requests==2.32.3 30 | # via itchiodl 31 | soupsieve==2.7 32 | # via beautifulsoup4 33 | urllib3==2.4.0 34 | # via 35 | # -r requirements.in 36 | # requests 37 | uvicorn==0.34.2 38 | # via -r requirements.in 39 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_in/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import click 6 | import h11 7 | import white as w 8 | from urllib3 import contrib 9 | import bs4 10 | import itchiodl 11 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_in/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_txt/pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | 4 | [tool.deptry.per_rule_ignores] 5 | DEP002 = ["pkginfo"] 6 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_txt/requirements-2.txt: -------------------------------------------------------------------------------- 1 | urllib3==2.4.0 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_txt/requirements-dev.txt: -------------------------------------------------------------------------------- 1 | black==25.1.0 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_txt/requirements-from-other.txt: -------------------------------------------------------------------------------- 1 | isort==5.13.2 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_txt/requirements-typing.txt: -------------------------------------------------------------------------------- 1 | types-jsonschema==4.23.0.20241208 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_txt/requirements.txt: -------------------------------------------------------------------------------- 1 | -r requirements-from-other.txt 2 | arrow==1.3.0 3 | click==8.1.8 4 | pkginfo==1.12.1.2 5 | requests==2.32.3 6 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_txt/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import click 6 | import white as w 7 | from urllib3 import contrib 8 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_requirements_txt/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_setuptools_dynamic_dependencies/cli-requirements.txt: -------------------------------------------------------------------------------- 1 | click==8.1.8 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_setuptools_dynamic_dependencies/dev-requirements.txt: -------------------------------------------------------------------------------- 1 | isort==6.0.1 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_setuptools_dynamic_dependencies/pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools", "setuptools-scm"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "foo" 7 | version = "0.0.1" 8 | requires-python = ">=3.9" 9 | dynamic = ["dependencies", "optional-dependencies"] 10 | 11 | [tool.setuptools.dynamic] 12 | dependencies = { file = ["requirements.txt", "requirements-2.txt"] } 13 | 14 | [tool.setuptools.dynamic.optional-dependencies] 15 | # Both strings and list of strings are accepted. 16 | cli = { file = "cli-requirements.txt" } 17 | dev = { file = ["dev-requirements.txt"] } 18 | 19 | [tool.deptry] 20 | pep621_dev_dependency_groups = ["dev"] 21 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_setuptools_dynamic_dependencies/requirements-2.txt: -------------------------------------------------------------------------------- 1 | packaging==25.0 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_setuptools_dynamic_dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | arrow==1.3.0 2 | pkginfo==1.12.1.2 3 | requests==2.32.3 4 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_setuptools_dynamic_dependencies/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import click 5 | import isort 6 | import white as w 7 | from urllib3 import contrib 8 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_setuptools_dynamic_dependencies/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_src_directory/.ignore: -------------------------------------------------------------------------------- 1 | src/this_file_is_ignored.py 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_src_directory/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | requires-python = ">=3.9" 5 | dependencies = [ 6 | "arrow==1.3.0", 7 | "click==8.1.8", 8 | "isort==6.0.1", 9 | "pkginfo==1.12.1.2", 10 | "requests==2.32.3", 11 | "urllib3==2.4.0", 12 | ] 13 | 14 | [project.optional-dependencies] 15 | dev = [ 16 | "black==25.1.0", 17 | "mypy==1.15.0", 18 | ] 19 | test = ["pytest==8.3.5"] 20 | 21 | [build-system] 22 | requires = ["setuptools>=61.0.0"] 23 | build-backend = "setuptools.build_meta" 24 | 25 | [tool.deptry.per_rule_ignores] 26 | DEP002 = ["pkginfo"] 27 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_src_directory/src/foobar.py: -------------------------------------------------------------------------------- 1 | import httpx 2 | 3 | 4 | def another_local_method(): ... 5 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_src_directory/src/project_with_src_directory/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/fixtures/project_with_src_directory/src/project_with_src_directory/__init__.py -------------------------------------------------------------------------------- /tests/fixtures/project_with_src_directory/src/project_with_src_directory/bar.py: -------------------------------------------------------------------------------- 1 | from project_with_src_directory.foo import a_local_method 2 | from foobar import another_local_method 3 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_src_directory/src/project_with_src_directory/foo.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import click 6 | import white as w 7 | from urllib3 import contrib 8 | 9 | 10 | def a_local_method(): ... 11 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_src_directory/src/project_with_src_directory/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_src_directory/src/this_file_is_ignored.py: -------------------------------------------------------------------------------- 1 | import a_non_existing_module 2 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_uv/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "foo" 3 | version = "0.0.1" 4 | requires-python = ">=3.9" 5 | dependencies = [ 6 | "arrow==1.3.0", 7 | "pkginfo==1.12.1.2", 8 | "urllib3==2.4.0", 9 | ] 10 | 11 | [project.optional-dependencies] 12 | foo = [ 13 | "click==8.1.8", 14 | "isort==6.0.1", 15 | ] 16 | bar = ["requests==2.32.3"] 17 | 18 | [dependency-groups] 19 | foo = [ 20 | "certifi==2025.1.31", 21 | "idna==3.10", 22 | ] 23 | all = [{include-group = "foo"}, "packaging==25.0"] 24 | 25 | [tool.uv] 26 | dev-dependencies = [ 27 | "black==25.1.0", 28 | "mypy==1.15.0", 29 | "pytest==8.3.5", 30 | "pytest-cov==6.1.1", 31 | ] 32 | 33 | [tool.deptry] 34 | pep621_dev_dependency_groups = ["bar"] 35 | 36 | [tool.deptry.per_rule_ignores] 37 | DEP002 = ["pkginfo"] 38 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_uv/src/main.py: -------------------------------------------------------------------------------- 1 | from os import chdir, walk 2 | from pathlib import Path 3 | 4 | import black 5 | import certifi 6 | import click 7 | import idna 8 | import mypy 9 | import packaging 10 | import pytest 11 | import pytest_cov 12 | import white as w 13 | from urllib3 import contrib 14 | -------------------------------------------------------------------------------- /tests/fixtures/project_with_uv/src/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "import click\n", 11 | "from urllib3 import contrib\n", 12 | "import arrow" 13 | ] 14 | } 15 | ], 16 | "metadata": { 17 | "kernelspec": { 18 | "display_name": "Python 3 (ipykernel)", 19 | "language": "python", 20 | "name": "python3" 21 | }, 22 | "language_info": { 23 | "codemirror_mode": { 24 | "name": "ipython", 25 | "version": 3 26 | }, 27 | "file_extension": ".py", 28 | "mimetype": "text/x-python", 29 | "name": "python", 30 | "nbconvert_exporter": "python", 31 | "pygments_lexer": "ipython3", 32 | "version": "3.9.11" 33 | } 34 | }, 35 | "nbformat": 4, 36 | "nbformat_minor": 5 37 | } 38 | -------------------------------------------------------------------------------- /tests/fixtures/some_imports.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": 2, 6 | "id": "9f4924ec-2200-4801-9d49-d4833651cbc4", 7 | "metadata": {}, 8 | "outputs": [], 9 | "source": [ 10 | "!ls\n", 11 | "%timeit\n", 12 | "%%timeit\n", 13 | "import click\n", 14 | "from urllib3 import contrib\n", 15 | "import toml\n", 16 | "1 +\\\n", 17 | " 2" 18 | ] 19 | } 20 | ], 21 | "metadata": { 22 | "kernelspec": { 23 | "display_name": "Python 3 (ipykernel)", 24 | "language": "python", 25 | "name": "python3" 26 | }, 27 | "language_info": { 28 | "codemirror_mode": { 29 | "name": "ipython", 30 | "version": 3 31 | }, 32 | "file_extension": ".py", 33 | "mimetype": "text/x-python", 34 | "name": "python", 35 | "nbconvert_exporter": "python", 36 | "pygments_lexer": "ipython3", 37 | "version": "3.9.11" 38 | } 39 | }, 40 | "nbformat": 4, 41 | "nbformat_minor": 5 42 | } 43 | -------------------------------------------------------------------------------- /tests/fixtures/some_imports.py: -------------------------------------------------------------------------------- 1 | import typing 2 | from os import chdir, walk 3 | from pathlib import Path 4 | from typing import List, TYPE_CHECKING 5 | from importlib import import_module 6 | from importlib import import_module as im 7 | import importlib 8 | import importlib as il 9 | 10 | import numpy as np 11 | import pandas 12 | from numpy.random import sample 13 | 14 | from . import foo 15 | from .foo import bar 16 | 17 | x = 1 18 | if x > 0: 19 | import httpx 20 | elif x < 0: 21 | from baz import Bar 22 | else: 23 | import foobar 24 | 25 | import barfoo as bf 26 | from randomizer import random 27 | 28 | if TYPE_CHECKING: 29 | import mypy_boto3_s3 30 | 31 | if typing.TYPE_CHECKING: 32 | import mypy_boto3_sagemaker 33 | 34 | try: 35 | import click 36 | except: 37 | import not_click 38 | 39 | 40 | def func(): 41 | import module_in_func 42 | 43 | 44 | class MyClass: 45 | def __init__(self): 46 | import module_in_class 47 | 48 | import_module("patito") 49 | importlib.import_module("polars") 50 | im("uvicorn") 51 | import_module("http.server") 52 | il.import_module("xml.etree.ElementTree") 53 | -------------------------------------------------------------------------------- /tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/functional/__init__.py -------------------------------------------------------------------------------- /tests/functional/cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/functional/cli/__init__.py -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_multiple_source_directories.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PipVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.MULTIPLE_SOURCE_DIRECTORIES) 17 | def test_cli_with_multiple_source_directories(pip_venv_factory: PipVenvFactory) -> None: 18 | with pip_venv_factory(Project.MULTIPLE_SOURCE_DIRECTORIES) as virtual_env: 19 | issue_report = f"{uuid.uuid4()}.json" 20 | result = virtual_env.run(f"deptry src worker -o {issue_report}") 21 | 22 | assert result.returncode == 1 23 | assert get_issues_report(Path(issue_report)) == [ 24 | { 25 | "error": {"code": "DEP002", "message": "'arrow' defined as a dependency but not used in the codebase"}, 26 | "module": "arrow", 27 | "location": {"file": str(Path("pyproject.toml")), "line": None, "column": None}, 28 | }, 29 | { 30 | "error": {"code": "DEP001", "message": "'httpx' imported but missing from the dependency definitions"}, 31 | "module": "httpx", 32 | "location": {"file": str(Path("src/foo.py")), "line": 1, "column": 8}, 33 | }, 34 | { 35 | "error": {"code": "DEP001", "message": "'celery' imported but missing from the dependency definitions"}, 36 | "module": "celery", 37 | "location": {"file": str(Path("worker/foo.py")), "line": 1, "column": 8}, 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_namespace.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PipVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.NAMESPACE) 17 | def test_cli_with_namespace(pip_venv_factory: PipVenvFactory) -> None: 18 | with pip_venv_factory(Project.NAMESPACE) as virtual_env: 19 | issue_report = f"{uuid.uuid4()}.json" 20 | result = virtual_env.run(f"deptry . --experimental-namespace-package -o {issue_report}") 21 | 22 | assert result.returncode == 1 23 | assert get_issues_report(Path(issue_report)) == [ 24 | { 25 | "error": {"code": "DEP004", "message": "'flake8' imported but declared as a dev dependency"}, 26 | "module": "flake8", 27 | "location": {"file": str(Path("foo/database/bar.py")), "line": 4, "column": 8}, 28 | }, 29 | { 30 | "error": {"code": "DEP001", "message": "'white' imported but missing from the dependency definitions"}, 31 | "module": "white", 32 | "location": {"file": str(Path("foo/database/bar.py")), "line": 5, "column": 8}, 33 | }, 34 | { 35 | "error": {"code": "DEP002", "message": "'arrow' defined as a dependency but not used in the codebase"}, 36 | "module": "arrow", 37 | "location": {"file": str(Path("pyproject.toml")), "line": None, "column": None}, 38 | }, 39 | ] 40 | 41 | 42 | @pytest.mark.xdist_group(name=Project.NAMESPACE) 43 | def test_cli_with_namespace_without_experimental_flag(pip_venv_factory: PipVenvFactory) -> None: 44 | with pip_venv_factory(Project.NAMESPACE) as virtual_env: 45 | issue_report = f"{uuid.uuid4()}.json" 46 | result = virtual_env.run(f"deptry . -o {issue_report}") 47 | 48 | assert result.returncode == 1 49 | assert get_issues_report(Path(issue_report)) == [ 50 | { 51 | "error": {"code": "DEP004", "message": "'flake8' imported but declared as a dev dependency"}, 52 | "module": "flake8", 53 | "location": {"file": str(Path("foo/database/bar.py")), "line": 4, "column": 8}, 54 | }, 55 | { 56 | "error": {"code": "DEP001", "message": "'white' imported but missing from the dependency definitions"}, 57 | "module": "white", 58 | "location": {"file": str(Path("foo/database/bar.py")), "line": 5, "column": 8}, 59 | }, 60 | { 61 | "error": {"code": "DEP003", "message": "'foo' imported but it is a transitive dependency"}, 62 | "module": "foo", 63 | "location": {"file": str(Path("foo/database/bar.py")), "line": 7, "column": 1}, 64 | }, 65 | { 66 | "error": {"code": "DEP002", "message": "'arrow' defined as a dependency but not used in the codebase"}, 67 | "module": "arrow", 68 | "location": {"file": str(Path("pyproject.toml")), "line": None, "column": None}, 69 | }, 70 | ] 71 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_pdm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PDMVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.PDM) 17 | def test_cli_with_pdm(pdm_venv_factory: PDMVenvFactory) -> None: 18 | with pdm_venv_factory(Project.PDM) as virtual_env: 19 | issue_report = f"{uuid.uuid4()}.json" 20 | result = virtual_env.run(f"deptry . -o {issue_report}") 21 | 22 | assert result.returncode == 1 23 | assert get_issues_report(Path(issue_report)) == [ 24 | { 25 | "error": { 26 | "code": "DEP002", 27 | "message": "'isort' defined as a dependency but not used in the codebase", 28 | }, 29 | "module": "isort", 30 | "location": { 31 | "file": str(Path("pyproject.toml")), 32 | "line": None, 33 | "column": None, 34 | }, 35 | }, 36 | { 37 | "error": { 38 | "code": "DEP004", 39 | "message": "'black' imported but declared as a dev dependency", 40 | }, 41 | "module": "black", 42 | "location": { 43 | "file": str(Path("src/main.py")), 44 | "line": 4, 45 | "column": 8, 46 | }, 47 | }, 48 | { 49 | "error": { 50 | "code": "DEP004", 51 | "message": "'certifi' imported but declared as a dev dependency", 52 | }, 53 | "module": "certifi", 54 | "location": { 55 | "file": str(Path("src/main.py")), 56 | "line": 5, 57 | "column": 8, 58 | }, 59 | }, 60 | { 61 | "error": { 62 | "code": "DEP004", 63 | "message": "'idna' imported but declared as a dev dependency", 64 | }, 65 | "module": "idna", 66 | "location": { 67 | "file": str(Path("src/main.py")), 68 | "line": 7, 69 | "column": 8, 70 | }, 71 | }, 72 | { 73 | "error": { 74 | "code": "DEP004", 75 | "message": "'mypy' imported but declared as a dev dependency", 76 | }, 77 | "module": "mypy", 78 | "location": { 79 | "file": str(Path("src/main.py")), 80 | "line": 8, 81 | "column": 8, 82 | }, 83 | }, 84 | { 85 | "error": { 86 | "code": "DEP004", 87 | "message": "'packaging' imported but declared as a dev dependency", 88 | }, 89 | "module": "packaging", 90 | "location": { 91 | "file": str(Path("src/main.py")), 92 | "line": 9, 93 | "column": 8, 94 | }, 95 | }, 96 | { 97 | "error": { 98 | "code": "DEP004", 99 | "message": "'pytest' imported but declared as a dev dependency", 100 | }, 101 | "module": "pytest", 102 | "location": { 103 | "file": str(Path("src/main.py")), 104 | "line": 10, 105 | "column": 8, 106 | }, 107 | }, 108 | { 109 | "error": { 110 | "code": "DEP004", 111 | "message": "'pytest_cov' imported but declared as a dev dependency", 112 | }, 113 | "module": "pytest_cov", 114 | "location": { 115 | "file": str(Path("src/main.py")), 116 | "line": 11, 117 | "column": 8, 118 | }, 119 | }, 120 | { 121 | "error": { 122 | "code": "DEP001", 123 | "message": "'white' imported but missing from the dependency definitions", 124 | }, 125 | "module": "white", 126 | "location": { 127 | "file": str(Path("src/main.py")), 128 | "line": 12, 129 | "column": 8, 130 | }, 131 | }, 132 | ] 133 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_pep_621.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PipVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.PEP_621) 17 | def test_cli_with_pep_621(pip_venv_factory: PipVenvFactory) -> None: 18 | with pip_venv_factory(Project.PEP_621) as virtual_env: 19 | issue_report = f"{uuid.uuid4()}.json" 20 | result = virtual_env.run(f"deptry . -o {issue_report}") 21 | 22 | assert result.returncode == 1 23 | assert get_issues_report(Path(issue_report)) == [ 24 | { 25 | "error": {"code": "DEP002", "message": "'isort' defined as a dependency but not used in the codebase"}, 26 | "module": "isort", 27 | "location": {"file": str(Path("pyproject.toml")), "line": None, "column": None}, 28 | }, 29 | { 30 | "error": { 31 | "code": "DEP002", 32 | "message": "'requests' defined as a dependency but not used in the codebase", 33 | }, 34 | "module": "requests", 35 | "location": {"file": str(Path("pyproject.toml")), "line": None, "column": None}, 36 | }, 37 | { 38 | "error": {"code": "DEP002", "message": "'pytest' defined as a dependency but not used in the codebase"}, 39 | "module": "pytest", 40 | "location": {"file": str(Path("pyproject.toml")), "line": None, "column": None}, 41 | }, 42 | { 43 | "error": { 44 | "code": "DEP002", 45 | "message": "'matplotlib' defined as a dependency but not used in the codebase", 46 | }, 47 | "module": "matplotlib", 48 | "location": {"file": str(Path("pyproject.toml")), "line": None, "column": None}, 49 | }, 50 | { 51 | "error": { 52 | "code": "DEP005", 53 | "message": "'asyncio' is defined as a dependency but it is included in the Python standard library.", 54 | }, 55 | "module": "asyncio", 56 | "location": {"file": "pyproject.toml", "line": None, "column": None}, 57 | }, 58 | { 59 | "error": {"code": "DEP004", "message": "'black' imported but declared as a dev dependency"}, 60 | "module": "black", 61 | "location": {"file": str(Path("src/main.py")), "line": 5, "column": 8}, 62 | }, 63 | { 64 | "error": {"code": "DEP004", "message": "'certifi' imported but declared as a dev dependency"}, 65 | "module": "certifi", 66 | "location": {"file": str(Path("src/main.py")), "line": 6, "column": 8}, 67 | }, 68 | { 69 | "error": {"code": "DEP004", "message": "'idna' imported but declared as a dev dependency"}, 70 | "module": "idna", 71 | "location": {"file": str(Path("src/main.py")), "line": 8, "column": 8}, 72 | }, 73 | { 74 | "error": {"code": "DEP004", "message": "'packaging' imported but declared as a dev dependency"}, 75 | "module": "packaging", 76 | "location": {"file": str(Path("src/main.py")), "line": 9, "column": 8}, 77 | }, 78 | { 79 | "error": {"code": "DEP001", "message": "'white' imported but missing from the dependency definitions"}, 80 | "module": "white", 81 | "location": {"file": str(Path("src/main.py")), "line": 10, "column": 8}, 82 | }, 83 | ] 84 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_poetry.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PoetryVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.POETRY) 17 | def test_cli_with_poetry(poetry_venv_factory: PoetryVenvFactory) -> None: 18 | with poetry_venv_factory(Project.POETRY) as virtual_env: 19 | issue_report = f"{uuid.uuid4()}.json" 20 | result = virtual_env.run(f"deptry . -o {issue_report}") 21 | 22 | assert result.returncode == 1 23 | assert get_issues_report(Path(issue_report)) == [ 24 | { 25 | "error": { 26 | "code": "DEP002", 27 | "message": "'isort' defined as a dependency but not used in the codebase", 28 | }, 29 | "module": "isort", 30 | "location": { 31 | "file": str(Path("pyproject.toml")), 32 | "line": None, 33 | "column": None, 34 | }, 35 | }, 36 | { 37 | "error": { 38 | "code": "DEP002", 39 | "message": "'requests' defined as a dependency but not used in the codebase", 40 | }, 41 | "module": "requests", 42 | "location": { 43 | "file": str(Path("pyproject.toml")), 44 | "line": None, 45 | "column": None, 46 | }, 47 | }, 48 | { 49 | "error": { 50 | "code": "DEP004", 51 | "message": "'black' imported but declared as a dev dependency", 52 | }, 53 | "module": "black", 54 | "location": { 55 | "file": str(Path("src/main.py")), 56 | "line": 4, 57 | "column": 8, 58 | }, 59 | }, 60 | { 61 | "error": { 62 | "code": "DEP004", 63 | "message": "'mypy' imported but declared as a dev dependency", 64 | }, 65 | "module": "mypy", 66 | "location": { 67 | "file": str(Path("src/main.py")), 68 | "line": 6, 69 | "column": 8, 70 | }, 71 | }, 72 | { 73 | "error": { 74 | "code": "DEP004", 75 | "message": "'pytest' imported but declared as a dev dependency", 76 | }, 77 | "module": "pytest", 78 | "location": { 79 | "file": str(Path("src/main.py")), 80 | "line": 7, 81 | "column": 8, 82 | }, 83 | }, 84 | { 85 | "error": { 86 | "code": "DEP004", 87 | "message": "'pytest_cov' imported but declared as a dev dependency", 88 | }, 89 | "module": "pytest_cov", 90 | "location": { 91 | "file": str(Path("src/main.py")), 92 | "line": 8, 93 | "column": 8, 94 | }, 95 | }, 96 | { 97 | "error": { 98 | "code": "DEP001", 99 | "message": "'white' imported but missing from the dependency definitions", 100 | }, 101 | "module": "white", 102 | "location": { 103 | "file": str(Path("src/main.py")), 104 | "line": 9, 105 | "column": 8, 106 | }, 107 | }, 108 | ] 109 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_poetry_pep_621.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PoetryVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.POETRY_PEP_621) 17 | def test_cli_with_poetry_pep_621(poetry_venv_factory: PoetryVenvFactory) -> None: 18 | with poetry_venv_factory(Project.POETRY_PEP_621) as virtual_env: 19 | issue_report = f"{uuid.uuid4()}.json" 20 | result = virtual_env.run(f"deptry . -o {issue_report}") 21 | 22 | assert result.returncode == 1 23 | assert get_issues_report(Path(issue_report)) == [ 24 | { 25 | "error": { 26 | "code": "DEP002", 27 | "message": "'requests' defined as a dependency but not used in the codebase", 28 | }, 29 | "module": "requests", 30 | "location": { 31 | "file": str(Path("pyproject.toml")), 32 | "line": None, 33 | "column": None, 34 | }, 35 | }, 36 | { 37 | "error": { 38 | "code": "DEP002", 39 | "message": "'isort' defined as a dependency but not used in the codebase", 40 | }, 41 | "module": "isort", 42 | "location": { 43 | "file": str(Path("pyproject.toml")), 44 | "line": None, 45 | "column": None, 46 | }, 47 | }, 48 | { 49 | "error": { 50 | "code": "DEP004", 51 | "message": "'black' imported but declared as a dev dependency", 52 | }, 53 | "module": "black", 54 | "location": { 55 | "file": str(Path("src/main.py")), 56 | "line": 4, 57 | "column": 8, 58 | }, 59 | }, 60 | { 61 | "error": { 62 | "code": "DEP004", 63 | "message": "'mypy' imported but declared as a dev dependency", 64 | }, 65 | "module": "mypy", 66 | "location": { 67 | "file": str(Path("src/main.py")), 68 | "line": 6, 69 | "column": 8, 70 | }, 71 | }, 72 | { 73 | "error": { 74 | "code": "DEP004", 75 | "message": "'pytest' imported but declared as a dev dependency", 76 | }, 77 | "module": "pytest", 78 | "location": { 79 | "file": str(Path("src/main.py")), 80 | "line": 7, 81 | "column": 8, 82 | }, 83 | }, 84 | { 85 | "error": { 86 | "code": "DEP004", 87 | "message": "'pytest_cov' imported but declared as a dev dependency", 88 | }, 89 | "module": "pytest_cov", 90 | "location": { 91 | "file": str(Path("src/main.py")), 92 | "line": 8, 93 | "column": 8, 94 | }, 95 | }, 96 | { 97 | "error": { 98 | "code": "DEP001", 99 | "message": "'white' imported but missing from the dependency definitions", 100 | }, 101 | "module": "white", 102 | "location": { 103 | "file": str(Path("src/main.py")), 104 | "line": 9, 105 | "column": 8, 106 | }, 107 | }, 108 | ] 109 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_pyproject_different_directory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PipVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.PYPROJECT_DIFFERENT_DIRECTORY) 17 | def test_cli_with_pyproject_different_directory(pip_venv_factory: PipVenvFactory) -> None: 18 | with pip_venv_factory( 19 | Project.PYPROJECT_DIFFERENT_DIRECTORY, install_command="pip install ./a_sub_directory" 20 | ) as virtual_env: 21 | issue_report = f"{uuid.uuid4()}.json" 22 | result = virtual_env.run(f"deptry src --config a_sub_directory/pyproject.toml -o {issue_report}") 23 | 24 | assert result.returncode == 1 25 | assert get_issues_report(Path(issue_report)) == [ 26 | { 27 | "error": { 28 | "code": "DEP002", 29 | "message": "'isort' defined as a dependency but not used in the codebase", 30 | }, 31 | "module": "isort", 32 | "location": { 33 | "file": str(Path("a_sub_directory/pyproject.toml")), 34 | "line": None, 35 | "column": None, 36 | }, 37 | }, 38 | { 39 | "error": { 40 | "code": "DEP002", 41 | "message": "'requests' defined as a dependency but not used in the codebase", 42 | }, 43 | "module": "requests", 44 | "location": { 45 | "file": str(Path("a_sub_directory/pyproject.toml")), 46 | "line": None, 47 | "column": None, 48 | }, 49 | }, 50 | { 51 | "error": { 52 | "code": "DEP002", 53 | "message": "'mypy' defined as a dependency but not used in the codebase", 54 | }, 55 | "module": "mypy", 56 | "location": { 57 | "file": str(Path("a_sub_directory/pyproject.toml")), 58 | "line": None, 59 | "column": None, 60 | }, 61 | }, 62 | { 63 | "error": { 64 | "code": "DEP002", 65 | "message": "'pytest' defined as a dependency but not used in the codebase", 66 | }, 67 | "module": "pytest", 68 | "location": { 69 | "file": str(Path("a_sub_directory/pyproject.toml")), 70 | "line": None, 71 | "column": None, 72 | }, 73 | }, 74 | { 75 | "error": { 76 | "code": "DEP001", 77 | "message": "'white' imported but missing from the dependency definitions", 78 | }, 79 | "module": "white", 80 | "location": { 81 | "file": str(Path("src/project_with_src_directory/foo.py")), 82 | "line": 6, 83 | "column": 8, 84 | }, 85 | }, 86 | ] 87 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_setuptools_dynamic_dependencies.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PipVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.SETUPTOOLS_DYNAMIC_DEPENDENCIES) 17 | def test_cli_setuptools_dynamic_dependencies(pip_venv_factory: PipVenvFactory) -> None: 18 | with pip_venv_factory( 19 | Project.SETUPTOOLS_DYNAMIC_DEPENDENCIES, 20 | install_command="pip install -r requirements.txt -r requirements-2.txt -r cli-requirements.txt -r dev-requirements.txt", 21 | ) as virtual_env: 22 | issue_report = f"{uuid.uuid4()}.json" 23 | result = virtual_env.run(f"deptry . -o {issue_report}") 24 | 25 | assert result.returncode == 1 26 | assert get_issues_report(Path(issue_report)) == [ 27 | { 28 | "error": { 29 | "code": "DEP002", 30 | "message": "'packaging' defined as a dependency but not used in the codebase", 31 | }, 32 | "module": "packaging", 33 | "location": { 34 | "file": "requirements-2.txt", 35 | "line": None, 36 | "column": None, 37 | }, 38 | }, 39 | { 40 | "error": { 41 | "code": "DEP002", 42 | "message": "'pkginfo' defined as a dependency but not used in the codebase", 43 | }, 44 | "module": "pkginfo", 45 | "location": { 46 | "file": str(Path("requirements.txt")), 47 | "line": None, 48 | "column": None, 49 | }, 50 | }, 51 | { 52 | "error": { 53 | "code": "DEP002", 54 | "message": "'requests' defined as a dependency but not used in the codebase", 55 | }, 56 | "module": "requests", 57 | "location": { 58 | "file": str(Path("requirements.txt")), 59 | "line": None, 60 | "column": None, 61 | }, 62 | }, 63 | { 64 | "error": { 65 | "code": "DEP004", 66 | "message": "'isort' imported but declared as a dev dependency", 67 | }, 68 | "module": "isort", 69 | "location": { 70 | "file": str(Path("src/main.py")), 71 | "line": 5, 72 | "column": 8, 73 | }, 74 | }, 75 | { 76 | "error": { 77 | "code": "DEP001", 78 | "message": "'white' imported but missing from the dependency definitions", 79 | }, 80 | "module": "white", 81 | "location": { 82 | "file": str(Path("src/main.py")), 83 | "line": 6, 84 | "column": 8, 85 | }, 86 | }, 87 | { 88 | "error": { 89 | "code": "DEP003", 90 | "message": "'urllib3' imported but it is a transitive dependency", 91 | }, 92 | "module": "urllib3", 93 | "location": { 94 | "file": str(Path("src/main.py")), 95 | "line": 7, 96 | "column": 1, 97 | }, 98 | }, 99 | { 100 | "error": { 101 | "code": "DEP003", 102 | "message": "'urllib3' imported but it is a transitive dependency", 103 | }, 104 | "module": "urllib3", 105 | "location": { 106 | "file": str(Path("src/notebook.ipynb")), 107 | "line": 2, 108 | "column": 1, 109 | }, 110 | }, 111 | ] 112 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_src_directory.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import PipVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.SRC_DIRECTORY) 17 | def test_cli_with_src_directory(pip_venv_factory: PipVenvFactory) -> None: 18 | with pip_venv_factory(Project.SRC_DIRECTORY) as virtual_env: 19 | issue_report = f"{uuid.uuid4()}.json" 20 | result = virtual_env.run(f"deptry src -o {issue_report}") 21 | 22 | assert result.returncode == 1 23 | assert get_issues_report(Path(issue_report)) == [ 24 | { 25 | "error": { 26 | "code": "DEP002", 27 | "message": "'isort' defined as a dependency but not used in the codebase", 28 | }, 29 | "module": "isort", 30 | "location": { 31 | "file": str(Path("pyproject.toml")), 32 | "line": None, 33 | "column": None, 34 | }, 35 | }, 36 | { 37 | "error": { 38 | "code": "DEP002", 39 | "message": "'requests' defined as a dependency but not used in the codebase", 40 | }, 41 | "module": "requests", 42 | "location": { 43 | "file": str(Path("pyproject.toml")), 44 | "line": None, 45 | "column": None, 46 | }, 47 | }, 48 | { 49 | "error": { 50 | "code": "DEP002", 51 | "message": "'mypy' defined as a dependency but not used in the codebase", 52 | }, 53 | "module": "mypy", 54 | "location": { 55 | "file": str(Path("pyproject.toml")), 56 | "line": None, 57 | "column": None, 58 | }, 59 | }, 60 | { 61 | "error": { 62 | "code": "DEP002", 63 | "message": "'pytest' defined as a dependency but not used in the codebase", 64 | }, 65 | "module": "pytest", 66 | "location": { 67 | "file": str(Path("pyproject.toml")), 68 | "line": None, 69 | "column": None, 70 | }, 71 | }, 72 | { 73 | "error": { 74 | "code": "DEP001", 75 | "message": "'httpx' imported but missing from the dependency definitions", 76 | }, 77 | "module": "httpx", 78 | "location": { 79 | "file": str(Path("src/foobar.py")), 80 | "line": 1, 81 | "column": 8, 82 | }, 83 | }, 84 | { 85 | "error": { 86 | "code": "DEP001", 87 | "message": "'white' imported but missing from the dependency definitions", 88 | }, 89 | "module": "white", 90 | "location": { 91 | "file": str(Path("src/project_with_src_directory/foo.py")), 92 | "line": 6, 93 | "column": 8, 94 | }, 95 | }, 96 | ] 97 | -------------------------------------------------------------------------------- /tests/functional/cli/test_cli_uv.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import uuid 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import pytest 8 | 9 | from tests.functional.utils import Project 10 | from tests.utils import get_issues_report 11 | 12 | if TYPE_CHECKING: 13 | from tests.utils import UvVenvFactory 14 | 15 | 16 | @pytest.mark.xdist_group(name=Project.UV) 17 | def test_cli_with_uv(uv_venv_factory: UvVenvFactory) -> None: 18 | with uv_venv_factory(Project.UV) as virtual_env: 19 | issue_report = f"{uuid.uuid4()}.json" 20 | result = virtual_env.run(f"deptry . -o {issue_report}") 21 | 22 | assert result.returncode == 1 23 | assert get_issues_report(Path(issue_report)) == [ 24 | { 25 | "error": { 26 | "code": "DEP002", 27 | "message": "'isort' defined as a dependency but not used in the codebase", 28 | }, 29 | "module": "isort", 30 | "location": { 31 | "file": str(Path("pyproject.toml")), 32 | "line": None, 33 | "column": None, 34 | }, 35 | }, 36 | { 37 | "error": { 38 | "code": "DEP004", 39 | "message": "'black' imported but declared as a dev dependency", 40 | }, 41 | "module": "black", 42 | "location": { 43 | "file": str(Path("src/main.py")), 44 | "line": 4, 45 | "column": 8, 46 | }, 47 | }, 48 | { 49 | "error": { 50 | "code": "DEP004", 51 | "message": "'certifi' imported but declared as a dev dependency", 52 | }, 53 | "module": "certifi", 54 | "location": { 55 | "file": str(Path("src/main.py")), 56 | "line": 5, 57 | "column": 8, 58 | }, 59 | }, 60 | { 61 | "error": { 62 | "code": "DEP004", 63 | "message": "'idna' imported but declared as a dev dependency", 64 | }, 65 | "module": "idna", 66 | "location": { 67 | "file": str(Path("src/main.py")), 68 | "line": 7, 69 | "column": 8, 70 | }, 71 | }, 72 | { 73 | "error": { 74 | "code": "DEP004", 75 | "message": "'mypy' imported but declared as a dev dependency", 76 | }, 77 | "module": "mypy", 78 | "location": { 79 | "file": str(Path("src/main.py")), 80 | "line": 8, 81 | "column": 8, 82 | }, 83 | }, 84 | { 85 | "error": { 86 | "code": "DEP004", 87 | "message": "'packaging' imported but declared as a dev dependency", 88 | }, 89 | "module": "packaging", 90 | "location": { 91 | "file": str(Path("src/main.py")), 92 | "line": 9, 93 | "column": 8, 94 | }, 95 | }, 96 | { 97 | "error": { 98 | "code": "DEP004", 99 | "message": "'pytest' imported but declared as a dev dependency", 100 | }, 101 | "module": "pytest", 102 | "location": { 103 | "file": str(Path("src/main.py")), 104 | "line": 10, 105 | "column": 8, 106 | }, 107 | }, 108 | { 109 | "error": { 110 | "code": "DEP004", 111 | "message": "'pytest_cov' imported but declared as a dev dependency", 112 | }, 113 | "module": "pytest_cov", 114 | "location": { 115 | "file": str(Path("src/main.py")), 116 | "line": 11, 117 | "column": 8, 118 | }, 119 | }, 120 | { 121 | "error": { 122 | "code": "DEP001", 123 | "message": "'white' imported but missing from the dependency definitions", 124 | }, 125 | "module": "white", 126 | "location": { 127 | "file": str(Path("src/main.py")), 128 | "line": 12, 129 | "column": 8, 130 | }, 131 | }, 132 | ] 133 | -------------------------------------------------------------------------------- /tests/functional/conftest.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import shlex 4 | import subprocess 5 | import sys 6 | from pathlib import Path 7 | 8 | import pytest 9 | import xdist 10 | 11 | from tests.functional.utils import DEPTRY_WHEEL_DIRECTORY 12 | from tests.utils import PDMVenvFactory, PipVenvFactory, PoetryVenvFactory, UvVenvFactory 13 | 14 | 15 | def pytest_sessionstart(session: pytest.Session) -> None: 16 | # When running the tests on multiple workers with pytest-xdist, the hook will be run several times: 17 | # - once from "master" node, when test suite starts 18 | # - X times (where X is the number of workers), when tests start in each worker 19 | # We only want to run the hook once, so we explicitly skip the hook if running on a pytest-xdist worker. 20 | if xdist.is_xdist_worker(session): 21 | return None 22 | 23 | deptry_wheel_path = Path(DEPTRY_WHEEL_DIRECTORY) 24 | 25 | print(f"Building `deptry` wheel in {deptry_wheel_path} to use it on functional tests...") # noqa: T201 26 | 27 | try: 28 | result = subprocess.run( 29 | shlex.split(f"uv build --verbose --wheel --out-dir {deptry_wheel_path}", posix=sys.platform != "win32"), 30 | capture_output=True, 31 | text=True, 32 | check=True, 33 | ) 34 | print(f"uv build output: {result.stdout}") # noqa: T201 35 | print(f"uv build errors: {result.stderr}") # noqa: T201 36 | except subprocess.CalledProcessError as e: 37 | print(f"Output: {e.output}") # noqa: T201 38 | print(f"Errors: {e.stderr}") # noqa: T201 39 | raise 40 | 41 | 42 | @pytest.fixture(scope="session") 43 | def pdm_venv_factory(tmp_path_factory: pytest.TempPathFactory) -> PDMVenvFactory: 44 | return PDMVenvFactory(tmp_path_factory.getbasetemp() / "venvs") 45 | 46 | 47 | @pytest.fixture(scope="session") 48 | def uv_venv_factory(tmp_path_factory: pytest.TempPathFactory) -> UvVenvFactory: 49 | return UvVenvFactory(tmp_path_factory.getbasetemp() / "venvs") 50 | 51 | 52 | @pytest.fixture(scope="session") 53 | def poetry_venv_factory(tmp_path_factory: pytest.TempPathFactory) -> PoetryVenvFactory: 54 | return PoetryVenvFactory(tmp_path_factory.getbasetemp() / "venvs") 55 | 56 | 57 | @pytest.fixture(scope="session") 58 | def pip_venv_factory(tmp_path_factory: pytest.TempPathFactory) -> PipVenvFactory: 59 | return PipVenvFactory(tmp_path_factory.getbasetemp() / "venvs") 60 | -------------------------------------------------------------------------------- /tests/functional/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import TYPE_CHECKING, Protocol 4 | 5 | if TYPE_CHECKING: 6 | from pathlib import Path 7 | 8 | 9 | class ProjectBuilder(Protocol): 10 | def __call__(self, root_directory: Path, project: str, setup_commands: list[str], cwd: str | None) -> Path: ... 11 | 12 | 13 | class ToolSpecificProjectBuilder(Protocol): 14 | def __call__(self, root_directory: Path, project: str, cwd: str | None = None) -> Path: ... 15 | -------------------------------------------------------------------------------- /tests/functional/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from enum import Enum 4 | 5 | DEPTRY_WHEEL_DIRECTORY = "build/functional_tests/deptry" 6 | 7 | 8 | class Project(str, Enum): 9 | EXAMPLE = "example_project" 10 | PEP_621 = "pep_621_project" 11 | GITIGNORE = "project_with_gitignore" 12 | MULTIPLE_SOURCE_DIRECTORIES = "project_with_multiple_source_directories" 13 | NAMESPACE = "project_using_namespace" 14 | PDM = "project_with_pdm" 15 | POETRY = "project_with_poetry" 16 | POETRY_PEP_621 = "project_with_poetry_pep_621" 17 | PYPROJECT_DIFFERENT_DIRECTORY = "project_with_pyproject_different_directory" 18 | REQUIREMENTS_TXT = "project_with_requirements_txt" 19 | REQUIREMENTS_IN = "project_with_requirements_in" 20 | SETUPTOOLS_DYNAMIC_DEPENDENCIES = "project_with_setuptools_dynamic_dependencies" 21 | SRC_DIRECTORY = "project_with_src_directory" 22 | UV = "project_with_uv" 23 | 24 | def __str__(self) -> str: 25 | return self.value 26 | -------------------------------------------------------------------------------- /tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/__init__.py -------------------------------------------------------------------------------- /tests/unit/dependency_getter/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/dependency_getter/__init__.py -------------------------------------------------------------------------------- /tests/unit/dependency_getter/test_pdm.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from deptry.dependency_getter.pep621.pdm import PDMDependencyGetter 6 | from tests.utils import run_within_dir 7 | 8 | 9 | def test_dependency_getter(tmp_path: Path) -> None: 10 | fake_pyproject_toml = """\ 11 | [project] 12 | # PEP 621 project metadata 13 | # See https://www.python.org/dev/peps/pep-0621/ 14 | dependencies = [ 15 | "qux", 16 | "bar>=20.9", 17 | "optional-foo[option]>=0.12.11", 18 | "conditional-bar>=1.1.0; python_version < '3.11'", 19 | "fox-python", # top level module is called "fox" 20 | ] 21 | 22 | [dependency-groups] 23 | dev-group = ["foo", "baz"] 24 | all = [{include-group = "dev-group"}, "foobaz"] 25 | 26 | [tool.pdm.dev-dependencies] 27 | test = [ 28 | "qux", 29 | "bar; python_version < '3.11'" 30 | ] 31 | tox = ["foo-bar"] 32 | """ 33 | 34 | with run_within_dir(tmp_path): 35 | with Path("pyproject.toml").open("w") as f: 36 | f.write(fake_pyproject_toml) 37 | 38 | dependencies_extract = PDMDependencyGetter( 39 | config=Path("pyproject.toml"), 40 | package_module_name_map={"fox-python": ("fox",)}, 41 | ).get() 42 | 43 | dependencies = dependencies_extract.dependencies 44 | dev_dependencies = dependencies_extract.dev_dependencies 45 | 46 | assert len(dependencies) == 5 47 | assert len(dev_dependencies) == 6 48 | 49 | assert dependencies[0].name == "qux" 50 | assert "qux" in dependencies[0].top_levels 51 | 52 | assert dependencies[1].name == "bar" 53 | assert "bar" in dependencies[1].top_levels 54 | 55 | assert dependencies[2].name == "optional-foo" 56 | assert "optional_foo" in dependencies[2].top_levels 57 | 58 | assert dependencies[3].name == "conditional-bar" 59 | assert "conditional_bar" in dependencies[3].top_levels 60 | 61 | assert dependencies[4].name == "fox-python" 62 | assert "fox" in dependencies[4].top_levels 63 | 64 | assert dev_dependencies[0].name == "foo" 65 | assert "foo" in dev_dependencies[0].top_levels 66 | 67 | assert dev_dependencies[1].name == "baz" 68 | assert "baz" in dev_dependencies[1].top_levels 69 | 70 | assert dev_dependencies[2].name == "foobaz" 71 | assert "foobaz" in dev_dependencies[2].top_levels 72 | 73 | assert dev_dependencies[3].name == "qux" 74 | assert "qux" in dev_dependencies[3].top_levels 75 | 76 | assert dev_dependencies[4].name == "bar" 77 | assert "bar" in dev_dependencies[4].top_levels 78 | 79 | assert dev_dependencies[5].name == "foo-bar" 80 | assert "foo_bar" in dev_dependencies[5].top_levels 81 | -------------------------------------------------------------------------------- /tests/unit/dependency_getter/test_uv.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from deptry.dependency_getter.pep621.uv import UvDependencyGetter 6 | from tests.utils import run_within_dir 7 | 8 | 9 | def test_dependency_getter(tmp_path: Path) -> None: 10 | fake_pyproject_toml = """\ 11 | [project] 12 | # PEP 621 project metadata 13 | # See https://www.python.org/dev/peps/pep-0621/ 14 | dependencies = [ 15 | "qux", 16 | "bar>=20.9", 17 | "optional-foo[option]>=0.12.11", 18 | "conditional-bar>=1.1.0; python_version < '3.11'", 19 | "fox-python", # top level module is called "fox" 20 | ] 21 | 22 | [dependency-groups] 23 | dev-group = ["foo", "baz"] 24 | all = [{include-group = "dev-group"}, "foobaz"] 25 | 26 | [tool.uv] 27 | dev-dependencies = [ 28 | "qux", 29 | "bar; python_version < '3.11'", 30 | "foo-bar", 31 | ] 32 | """ 33 | 34 | with run_within_dir(tmp_path): 35 | with Path("pyproject.toml").open("w") as f: 36 | f.write(fake_pyproject_toml) 37 | 38 | dependencies_extract = UvDependencyGetter( 39 | config=Path("pyproject.toml"), 40 | package_module_name_map={"fox-python": ("fox",)}, 41 | ).get() 42 | 43 | dependencies = dependencies_extract.dependencies 44 | dev_dependencies = dependencies_extract.dev_dependencies 45 | 46 | assert len(dependencies) == 5 47 | assert len(dev_dependencies) == 6 48 | 49 | assert dependencies[0].name == "qux" 50 | assert "qux" in dependencies[0].top_levels 51 | 52 | assert dependencies[1].name == "bar" 53 | assert "bar" in dependencies[1].top_levels 54 | 55 | assert dependencies[2].name == "optional-foo" 56 | assert "optional_foo" in dependencies[2].top_levels 57 | 58 | assert dependencies[3].name == "conditional-bar" 59 | assert "conditional_bar" in dependencies[3].top_levels 60 | 61 | assert dependencies[4].name == "fox-python" 62 | assert "fox" in dependencies[4].top_levels 63 | 64 | assert dev_dependencies[0].name == "foo" 65 | assert "foo" in dev_dependencies[0].top_levels 66 | 67 | assert dev_dependencies[1].name == "baz" 68 | assert "baz" in dev_dependencies[1].top_levels 69 | 70 | assert dev_dependencies[2].name == "foobaz" 71 | assert "foobaz" in dev_dependencies[2].top_levels 72 | 73 | assert dev_dependencies[3].name == "qux" 74 | assert "qux" in dev_dependencies[3].top_levels 75 | 76 | assert dev_dependencies[4].name == "bar" 77 | assert "bar" in dev_dependencies[4].top_levels 78 | 79 | assert dev_dependencies[5].name == "foo-bar" 80 | assert "foo_bar" in dev_dependencies[5].top_levels 81 | -------------------------------------------------------------------------------- /tests/unit/imports/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/imports/__init__.py -------------------------------------------------------------------------------- /tests/unit/reporters/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/reporters/__init__.py -------------------------------------------------------------------------------- /tests/unit/reporters/test_json.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import json 4 | from pathlib import Path 5 | 6 | from deptry.dependency import Dependency 7 | from deptry.imports.location import Location 8 | from deptry.module import Module 9 | from deptry.reporters import JSONReporter 10 | from deptry.violations import ( 11 | DEP001MissingDependencyViolation, 12 | DEP002UnusedDependencyViolation, 13 | DEP003TransitiveDependencyViolation, 14 | DEP004MisplacedDevDependencyViolation, 15 | ) 16 | from tests.utils import run_within_dir 17 | 18 | 19 | def test_simple(tmp_path: Path) -> None: 20 | with run_within_dir(tmp_path): 21 | JSONReporter( 22 | [ 23 | DEP001MissingDependencyViolation(Module("foo", package="foo-package"), Location(Path("foo.py"), 1, 2)), 24 | DEP002UnusedDependencyViolation( 25 | Dependency("foo", Path("pyproject.toml")), Location(Path("pyproject.toml")) 26 | ), 27 | DEP003TransitiveDependencyViolation( 28 | Module("foo", package="foo-package"), Location(Path("foo/bar.py"), 1, 2) 29 | ), 30 | DEP004MisplacedDevDependencyViolation( 31 | Module("foo", package="foo-package"), Location(Path("foo.py"), 1, 2) 32 | ), 33 | ], 34 | "output.json", 35 | ).report() 36 | 37 | with Path("output.json").open() as f: 38 | data = json.load(f) 39 | 40 | assert data == [ 41 | { 42 | "error": {"code": "DEP001", "message": "'foo' imported but missing from the dependency definitions"}, 43 | "module": "foo", 44 | "location": { 45 | "file": str(Path("foo.py")), 46 | "line": 1, 47 | "column": 2, 48 | }, 49 | }, 50 | { 51 | "error": {"code": "DEP002", "message": "'foo' defined as a dependency but not used in the codebase"}, 52 | "module": "foo", 53 | "location": { 54 | "file": str(Path("pyproject.toml")), 55 | "line": None, 56 | "column": None, 57 | }, 58 | }, 59 | { 60 | "error": {"code": "DEP003", "message": "'foo' imported but it is a transitive dependency"}, 61 | "module": "foo", 62 | "location": { 63 | "file": str(Path("foo/bar.py")), 64 | "line": 1, 65 | "column": 2, 66 | }, 67 | }, 68 | { 69 | "error": {"code": "DEP004", "message": "'foo' imported but declared as a dev dependency"}, 70 | "module": "foo", 71 | "location": { 72 | "file": str(Path("foo.py")), 73 | "line": 1, 74 | "column": 2, 75 | }, 76 | }, 77 | ] 78 | -------------------------------------------------------------------------------- /tests/unit/test_config.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | from pathlib import Path 5 | from typing import TYPE_CHECKING 6 | 7 | import click 8 | import pytest 9 | from click import Argument 10 | 11 | from deptry.config import read_configuration_from_pyproject_toml 12 | from deptry.exceptions import InvalidPyprojectTOMLOptionsError 13 | from tests.utils import run_within_dir 14 | 15 | if TYPE_CHECKING: 16 | from _pytest.logging import LogCaptureFixture 17 | 18 | 19 | click_command = click.Command( 20 | "", 21 | params=[ 22 | Argument(param_decls=["exclude"]), 23 | Argument(param_decls=["extend_exclude"]), 24 | Argument(param_decls=["per_rule_ignores"]), 25 | Argument(param_decls=["ignore"]), 26 | Argument(param_decls=["ignore_notebooks"]), 27 | Argument(param_decls=["requirements_files"]), 28 | Argument(param_decls=["requirements_files_dev"]), 29 | ], 30 | ) 31 | 32 | 33 | def test_read_configuration_from_pyproject_toml_exists(tmp_path: Path) -> None: 34 | click_context = click.Context( 35 | click_command, 36 | default_map={ 37 | "exclude": ["bar"], 38 | "extend_exclude": ["foo"], 39 | "per_rule_ignores": { 40 | "DEP002": ["baz", "bar"], 41 | }, 42 | "ignore": [], 43 | "requirements_files": "requirements.txt", 44 | "requirements_files_dev": ["requirements-dev.txt"], 45 | }, 46 | ) 47 | 48 | pyproject_toml_content = """ 49 | [tool.deptry] 50 | exclude = ["foo", "bar"] 51 | extend_exclude = ["bar", "foo"] 52 | ignore_notebooks = true 53 | ignore = ["DEP001", "DEP002", "DEP003", "DEP004"] 54 | requirements_files = "foo.txt" 55 | requirements_files_dev = ["dev.txt", "tests.txt"] 56 | 57 | [tool.deptry.per_rule_ignores] 58 | DEP001 = ["baz", "foobar"] 59 | DEP002 = ["foo"] 60 | DEP003 = ["foobaz"] 61 | DEP004 = ["barfoo"] 62 | """ 63 | 64 | with run_within_dir(tmp_path): 65 | pyproject_toml_path = Path("pyproject.toml") 66 | 67 | with pyproject_toml_path.open("w") as f: 68 | f.write(pyproject_toml_content) 69 | 70 | assert ( 71 | read_configuration_from_pyproject_toml(click_context, click.UNPROCESSED(None), pyproject_toml_path) 72 | == pyproject_toml_path 73 | ) 74 | 75 | assert click_context.default_map == { 76 | "exclude": ["foo", "bar"], 77 | "extend_exclude": ["bar", "foo"], 78 | "ignore_notebooks": True, 79 | "per_rule_ignores": { 80 | "DEP001": ["baz", "foobar"], 81 | "DEP002": ["foo"], 82 | "DEP003": ["foobaz"], 83 | "DEP004": ["barfoo"], 84 | }, 85 | "ignore": ["DEP001", "DEP002", "DEP003", "DEP004"], 86 | "requirements_files": "foo.txt", 87 | "requirements_files_dev": ["dev.txt", "tests.txt"], 88 | } 89 | 90 | 91 | def test_read_configuration_from_pyproject_toml_file_not_found(caplog: LogCaptureFixture) -> None: 92 | pyproject_toml_path = Path("a_non_existent_pyproject.toml") 93 | 94 | with caplog.at_level(logging.DEBUG): 95 | assert ( 96 | read_configuration_from_pyproject_toml( 97 | click.Context(click_command), click.UNPROCESSED(None), pyproject_toml_path 98 | ) 99 | == pyproject_toml_path 100 | ) 101 | 102 | assert "No pyproject.toml file to read configuration from." in caplog.text 103 | 104 | 105 | def test_read_configuration_from_pyproject_toml_file_without_deptry_section( 106 | caplog: LogCaptureFixture, tmp_path: Path 107 | ) -> None: 108 | pyproject_toml_content = """ 109 | [tool.something] 110 | exclude = ["foo", "bar"] 111 | """ 112 | 113 | with run_within_dir(tmp_path): 114 | pyproject_toml_path = Path("pyproject.toml") 115 | 116 | with pyproject_toml_path.open("w") as f: 117 | f.write(pyproject_toml_content) 118 | 119 | with caplog.at_level(logging.DEBUG): 120 | assert read_configuration_from_pyproject_toml( 121 | click.Context(click_command), click.UNPROCESSED(None), pyproject_toml_path 122 | ) == Path("pyproject.toml") 123 | 124 | assert "No configuration for deptry was found in pyproject.toml." in caplog.text 125 | 126 | 127 | def test_read_configuration_from_pyproject_toml_file_with_invalid_options( 128 | caplog: LogCaptureFixture, tmp_path: Path 129 | ) -> None: 130 | pyproject_toml_content = """ 131 | [tool.deptry] 132 | exclude = ["foo", "bar"] 133 | invalid_option = "nope" 134 | another_invalid_option = "still nope" 135 | extend_exclude = ["bar", "foo"] 136 | """ 137 | 138 | with run_within_dir(tmp_path): 139 | pyproject_toml_path = Path("pyproject.toml") 140 | 141 | with pyproject_toml_path.open("w") as f: 142 | f.write(pyproject_toml_content) 143 | 144 | with pytest.raises(InvalidPyprojectTOMLOptionsError): 145 | assert read_configuration_from_pyproject_toml( 146 | click.Context(click_command), click.UNPROCESSED(None), pyproject_toml_path 147 | ) == Path("pyproject.toml") 148 | -------------------------------------------------------------------------------- /tests/unit/test_module.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from importlib.metadata import PackageNotFoundError 4 | from pathlib import Path 5 | from unittest.mock import patch 6 | 7 | import pytest 8 | 9 | from deptry.dependency import Dependency 10 | from deptry.module import ModuleBuilder 11 | 12 | 13 | def test_simple_import() -> None: 14 | module = ModuleBuilder("click", {"foo", "bar"}, frozenset()).build() 15 | assert module.package == "click" 16 | assert module.standard_library is False 17 | assert module.local_module is False 18 | 19 | 20 | def test_top_level() -> None: 21 | # Test if no error is raised, argument is accepted. 22 | dependency = Dependency("beautifulsoup4", Path("pyproject.toml")) 23 | dependency.top_levels = {"bs4"} 24 | module = ModuleBuilder("bs4", {"foo", "bar"}, frozenset(), [dependency]).build() 25 | assert module.package is None 26 | assert module.standard_library is False 27 | assert module.local_module is False 28 | 29 | 30 | def test_stdlib() -> None: 31 | module = ModuleBuilder("sys", {"foo", "bar"}, frozenset({"sys"})).build() 32 | assert module.package is None 33 | assert module.standard_library is True 34 | assert module.local_module is False 35 | 36 | 37 | def test_local_module() -> None: 38 | module = ModuleBuilder("click", {"foo", "click"}, frozenset()).build() 39 | assert module.package is None 40 | assert module.standard_library is False 41 | assert module.local_module is True 42 | 43 | 44 | def test_transitive_module() -> None: 45 | with ( 46 | patch("deptry.module.metadata", side_effect=PackageNotFoundError), 47 | patch("deptry.module.find_spec", return_value="bar"), 48 | ): 49 | module = ModuleBuilder("foo", set(), frozenset()).build() 50 | 51 | assert module.package == "foo" 52 | assert module.standard_library is False 53 | assert module.local_module is False 54 | 55 | 56 | def test_transitive_module_no_spec() -> None: 57 | with ( 58 | patch("deptry.module.metadata", side_effect=PackageNotFoundError), 59 | patch("deptry.module.find_spec", return_value=None), 60 | ): 61 | module = ModuleBuilder("foo", set(), frozenset()).build() 62 | 63 | assert module.package is None 64 | assert module.standard_library is False 65 | assert module.local_module is False 66 | 67 | 68 | @pytest.mark.parametrize("exception", [ModuleNotFoundError, ValueError]) 69 | def test_transitive_module_spec_error(exception: Exception) -> None: 70 | with ( 71 | patch("deptry.module.metadata", side_effect=PackageNotFoundError), 72 | patch("deptry.module.find_spec", side_effect=exception), 73 | ): 74 | module = ModuleBuilder("foo", set(), frozenset()).build() 75 | 76 | assert module.package is None 77 | assert module.standard_library is False 78 | assert module.local_module is False 79 | -------------------------------------------------------------------------------- /tests/unit/test_utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | import pytest 6 | 7 | from deptry.exceptions import PyprojectFileNotFoundError 8 | from deptry.utils import load_pyproject_toml 9 | from tests.utils import run_within_dir 10 | 11 | 12 | def test_load_pyproject_toml(tmp_path: Path) -> None: 13 | pyproject_toml = """\ 14 | [project] 15 | name = "foo" 16 | dependencies = ["bar", "baz>=20.9",] 17 | 18 | [dependency-groups] 19 | dev = ["foobar", "foobaz"] 20 | """ 21 | with run_within_dir(tmp_path): 22 | with Path("pyproject.toml").open("w") as f: 23 | f.write(pyproject_toml) 24 | 25 | assert load_pyproject_toml(Path("pyproject.toml")) == { 26 | "project": { 27 | "name": "foo", 28 | "dependencies": ["bar", "baz>=20.9"], 29 | }, 30 | "dependency-groups": { 31 | "dev": ["foobar", "foobaz"], 32 | }, 33 | } 34 | 35 | 36 | def test_load_pyproject_toml_not_found(tmp_path: Path) -> None: 37 | with run_within_dir(tmp_path), pytest.raises(PyprojectFileNotFoundError): 38 | load_pyproject_toml(Path("non_existing_pyproject.toml")) 39 | -------------------------------------------------------------------------------- /tests/unit/violations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/violations/__init__.py -------------------------------------------------------------------------------- /tests/unit/violations/dep001_missing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/violations/dep001_missing/__init__.py -------------------------------------------------------------------------------- /tests/unit/violations/dep001_missing/test_finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from deptry.dependency import Dependency 6 | from deptry.imports.location import Location 7 | from deptry.module import ModuleBuilder, ModuleLocations 8 | from deptry.violations import DEP001MissingDependenciesFinder, DEP001MissingDependencyViolation 9 | 10 | 11 | def test_simple() -> None: 12 | dependencies: list[Dependency] = [] 13 | 14 | module_foobar_locations = [Location(Path("foo.py"), 1, 2), Location(Path("bar.py"), 3, 4)] 15 | module_foobar = ModuleBuilder("foobar", {"foo"}, frozenset(), dependencies).build() 16 | 17 | modules_locations = [ModuleLocations(module_foobar, module_foobar_locations)] 18 | 19 | assert DEP001MissingDependenciesFinder(modules_locations, dependencies, frozenset()).find() == [ 20 | DEP001MissingDependencyViolation(module_foobar, location) for location in module_foobar_locations 21 | ] 22 | 23 | 24 | def test_local_module() -> None: 25 | dependencies: list[Dependency] = [] 26 | modules_locations = [ 27 | ModuleLocations( 28 | ModuleBuilder("foobar", {"foo", "foobar"}, frozenset(), dependencies).build(), 29 | [Location(Path("foo.py"), 1, 2)], 30 | ) 31 | ] 32 | 33 | assert DEP001MissingDependenciesFinder(modules_locations, dependencies, frozenset()).find() == [] 34 | 35 | 36 | def test_simple_with_ignore() -> None: 37 | dependencies: list[Dependency] = [] 38 | modules_locations = [ 39 | ModuleLocations( 40 | ModuleBuilder("foobar", {"foo", "bar"}, frozenset(), dependencies).build(), [Location(Path("foo.py"), 1, 2)] 41 | ) 42 | ] 43 | 44 | assert ( 45 | DEP001MissingDependenciesFinder( 46 | modules_locations, dependencies, frozenset(), ignored_modules=("foobar",) 47 | ).find() 48 | == [] 49 | ) 50 | 51 | 52 | def test_simple_with_standard_library() -> None: 53 | dependencies: list[Dependency] = [] 54 | standard_library_modules = frozenset(["foobar"]) 55 | modules_locations = [ 56 | ModuleLocations( 57 | ModuleBuilder("foobar", set(), standard_library_modules, dependencies).build(), 58 | [Location(Path("foo.py"), 1, 2)], 59 | ) 60 | ] 61 | 62 | assert DEP001MissingDependenciesFinder(modules_locations, dependencies, frozenset()).find() == [] 63 | 64 | 65 | def test_no_error() -> None: 66 | """ 67 | This should run without an error, even though `foo` is not installed. 68 | """ 69 | 70 | dependencies = [Dependency("foo", Path("pyproject.toml"))] 71 | modules_locations = [ 72 | ModuleLocations( 73 | ModuleBuilder("foo", {"bar"}, frozenset(), dependencies).build(), 74 | [Location(Path("foo.py"), 1, 2)], 75 | ) 76 | ] 77 | 78 | assert DEP001MissingDependenciesFinder(modules_locations, dependencies, frozenset()).find() == [] 79 | -------------------------------------------------------------------------------- /tests/unit/violations/dep002_unused/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/violations/dep002_unused/__init__.py -------------------------------------------------------------------------------- /tests/unit/violations/dep002_unused/test_finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from deptry.dependency import Dependency 6 | from deptry.imports.location import Location 7 | from deptry.module import ModuleBuilder, ModuleLocations 8 | from deptry.violations import DEP002UnusedDependenciesFinder, DEP002UnusedDependencyViolation 9 | 10 | 11 | def test_simple() -> None: 12 | dependency_toml = Dependency("toml", Path("pyproject.toml")) 13 | dependencies = [Dependency("click", Path("pyproject.toml")), dependency_toml] 14 | modules_locations = [ 15 | ModuleLocations( 16 | ModuleBuilder("click", {"foo"}, frozenset(), dependencies).build(), [Location(Path("foo.py"), 1, 2)] 17 | ) 18 | ] 19 | 20 | assert DEP002UnusedDependenciesFinder(modules_locations, dependencies, frozenset()).find() == [ 21 | DEP002UnusedDependencyViolation(dependency_toml, Location(Path("pyproject.toml"))) 22 | ] 23 | 24 | 25 | def test_simple_with_ignore() -> None: 26 | dependencies = [Dependency("click", Path("pyproject.toml")), Dependency("toml", Path("pyproject.toml"))] 27 | modules_locations = [ 28 | ModuleLocations( 29 | ModuleBuilder("toml", {"foo"}, frozenset(), dependencies).build(), [Location(Path("foo.py"), 1, 2)] 30 | ) 31 | ] 32 | 33 | assert ( 34 | DEP002UnusedDependenciesFinder(modules_locations, dependencies, frozenset(), ignored_modules=("click",)).find() 35 | == [] 36 | ) 37 | 38 | 39 | def test_top_level() -> None: 40 | """ 41 | Test if top-level information is read, and correctly used to not mark a dependency as unused. 42 | blackd is in the top-level of black, so black should not be marked as an unused dependency. 43 | """ 44 | dependencies = [Dependency("black", Path("pyproject.toml"), module_names=("black", "blackd"))] 45 | modules_locations = [ 46 | ModuleLocations( 47 | ModuleBuilder("blackd", {"foo"}, frozenset(), dependencies).build(), [Location(Path("foo.py"), 1, 2)] 48 | ) 49 | ] 50 | 51 | deps = DEP002UnusedDependenciesFinder(modules_locations, dependencies, frozenset()).find() 52 | 53 | assert deps == [] 54 | 55 | 56 | def test_without_top_level() -> None: 57 | """ 58 | Test if packages without top-level information are correctly maked as unused 59 | """ 60 | dependencies = [Dependency("isort", Path("pyproject.toml"))] 61 | modules_locations = [ 62 | ModuleLocations( 63 | ModuleBuilder("isort", {"foo"}, frozenset(), dependencies).build(), [Location(Path("foo.py"), 1, 2)] 64 | ) 65 | ] 66 | 67 | assert DEP002UnusedDependenciesFinder(modules_locations, dependencies, frozenset()).find() == [] 68 | -------------------------------------------------------------------------------- /tests/unit/violations/dep003_transitive/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/violations/dep003_transitive/__init__.py -------------------------------------------------------------------------------- /tests/unit/violations/dep003_transitive/test_finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | from unittest.mock import patch 5 | 6 | from deptry.imports.location import Location 7 | from deptry.module import ModuleBuilder, ModuleLocations 8 | from deptry.violations import DEP003TransitiveDependenciesFinder 9 | from deptry.violations.dep003_transitive.violation import DEP003TransitiveDependencyViolation 10 | 11 | 12 | def test_simple() -> None: 13 | module = ModuleBuilder("foo", set(), frozenset()).build() 14 | 15 | with patch.object(module, "package", return_value="foo"): 16 | issues = DEP003TransitiveDependenciesFinder( 17 | [ModuleLocations(module, [Location(Path("foo.py"), 1, 2)])], 18 | [], 19 | frozenset(), 20 | ).find() 21 | 22 | assert issues == [ 23 | DEP003TransitiveDependencyViolation( 24 | issue=module, 25 | location=Location( 26 | file=Path("foo.py"), 27 | line=1, 28 | column=2, 29 | ), 30 | ), 31 | ] 32 | 33 | 34 | def test_simple_with_ignore() -> None: 35 | module = ModuleBuilder("foo", set(), frozenset()).build() 36 | 37 | with patch.object(module, "package", return_value="foo"): 38 | issues = DEP003TransitiveDependenciesFinder( 39 | [ModuleLocations(module, [Location(Path("foo.py"), 1, 2)])], 40 | [], 41 | frozenset(), 42 | ignored_modules=("foo",), 43 | ).find() 44 | 45 | assert issues == [] 46 | 47 | 48 | def test_simple_with_standard_library() -> None: 49 | module = ModuleBuilder("foo", set(), standard_library_modules=frozenset(["foo"])).build() 50 | 51 | with patch.object(module, "package", return_value="foo"): 52 | issues = DEP003TransitiveDependenciesFinder( 53 | [ModuleLocations(module, [Location(Path("foo.py"), 1, 2)])], [], frozenset() 54 | ).find() 55 | 56 | assert issues == [] 57 | -------------------------------------------------------------------------------- /tests/unit/violations/dep004_misplaced_dev/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/violations/dep004_misplaced_dev/__init__.py -------------------------------------------------------------------------------- /tests/unit/violations/dep004_misplaced_dev/test_finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from deptry.dependency import Dependency 6 | from deptry.imports.location import Location 7 | from deptry.module import Module, ModuleLocations 8 | from deptry.violations import DEP004MisplacedDevDependencyViolation 9 | from deptry.violations.dep004_misplaced_dev.finder import DEP004MisplacedDevDependenciesFinder 10 | 11 | 12 | def test_simple() -> None: 13 | dependencies = [Dependency("bar", Path("pyproject.toml"))] 14 | 15 | module_foo_locations = [Location(Path("foo.py"), 1, 2), Location(Path("bar.py"), 3, 4)] 16 | module_foo = Module("foo", dev_top_levels=["foo"], is_provided_by_dev_dependency=True) 17 | 18 | modules_locations = [ModuleLocations(module_foo, module_foo_locations)] 19 | 20 | assert DEP004MisplacedDevDependenciesFinder(modules_locations, dependencies, frozenset()).find() == [ 21 | DEP004MisplacedDevDependencyViolation(module_foo, location) for location in module_foo_locations 22 | ] 23 | 24 | 25 | def test_regular_and_dev_dependency() -> None: 26 | """ 27 | If a dependency is both a regular and a development dependency, no 'misplaced dev dependency' violation 28 | should be detected if it is used in the code base. 29 | """ 30 | 31 | dependencies = [Dependency("foo", Path("pyproject.toml"))] 32 | 33 | module_foo_locations = [Location(Path("foo.py"), 1, 2)] 34 | module_foo = Module( 35 | "foo", dev_top_levels=["foo"], is_provided_by_dev_dependency=True, is_provided_by_dependency=True 36 | ) 37 | 38 | modules_locations = [ModuleLocations(module_foo, module_foo_locations)] 39 | 40 | assert not DEP004MisplacedDevDependenciesFinder(modules_locations, dependencies, frozenset()).find() 41 | -------------------------------------------------------------------------------- /tests/unit/violations/dep005_standard_library/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/fpgmaas/deptry/27a6fd869c9cbba498f9afec09ac276230fe8aaf/tests/unit/violations/dep005_standard_library/__init__.py -------------------------------------------------------------------------------- /tests/unit/violations/dep005_standard_library/test_finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from deptry.dependency import Dependency 6 | from deptry.imports.location import Location 7 | from deptry.module import ModuleBuilder, ModuleLocations 8 | from deptry.violations import DEP005StandardLibraryDependenciesFinder, DEP005StandardLibraryDependencyViolation 9 | 10 | 11 | def test_simple() -> None: 12 | dependency_asyncio = Dependency("asyncio", Path("pyproject.toml")) 13 | dependencies = [dependency_asyncio] 14 | modules_locations = [ 15 | ModuleLocations( 16 | ModuleBuilder("asyncio", {"foo"}, frozenset(), dependencies).build(), [Location(Path("foo.py"), 1, 2)] 17 | ) 18 | ] 19 | 20 | assert DEP005StandardLibraryDependenciesFinder( 21 | imported_modules_with_locations=modules_locations, 22 | dependencies=dependencies, 23 | standard_library_modules=frozenset(["asyncio"]), 24 | ).find() == [DEP005StandardLibraryDependencyViolation(dependency_asyncio, Location(Path("pyproject.toml")))] 25 | 26 | 27 | def test_simple_with_ignore() -> None: 28 | dependency_asyncio = Dependency("asyncio", Path("pyproject.toml")) 29 | dependencies = [dependency_asyncio] 30 | modules_locations = [ 31 | ModuleLocations( 32 | ModuleBuilder("asyncio", {"foo"}, frozenset(), dependencies).build(), [Location(Path("foo.py"), 1, 2)] 33 | ) 34 | ] 35 | 36 | assert ( 37 | DEP005StandardLibraryDependenciesFinder( 38 | imported_modules_with_locations=modules_locations, 39 | dependencies=dependencies, 40 | standard_library_modules=frozenset(["asyncio"]), 41 | ignored_modules=("asyncio",), 42 | ).find() 43 | == [] 44 | ) 45 | -------------------------------------------------------------------------------- /tests/unit/violations/test_finder.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from pathlib import Path 4 | 5 | from deptry.imports.location import Location 6 | from deptry.module import Module 7 | from deptry.violations import DEP001MissingDependencyViolation, DEP004MisplacedDevDependencyViolation 8 | from deptry.violations.finder import _get_sorted_violations 9 | 10 | 11 | def test__get_sorted_violations() -> None: 12 | violations = [ 13 | DEP004MisplacedDevDependencyViolation(Module("foo"), Location(Path("foo.py"), 1, 0)), 14 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("foo.py"), 2, 0)), 15 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("foo.py"), 1, 0)), 16 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("bar.py"), 3, 1)), 17 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("bar.py"), 2, 1)), 18 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("bar.py"), 3, 0)), 19 | ] 20 | 21 | assert _get_sorted_violations(violations) == [ 22 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("bar.py"), 2, 1)), 23 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("bar.py"), 3, 0)), 24 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("bar.py"), 3, 1)), 25 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("foo.py"), 1, 0)), 26 | DEP004MisplacedDevDependencyViolation(Module("foo"), Location(Path("foo.py"), 1, 0)), 27 | DEP001MissingDependencyViolation(Module("foo"), Location(Path("foo.py"), 2, 0)), 28 | ] 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | skipsdist = true 3 | envlist = py39, py310, py311, py312, py313 4 | 5 | [testenv] 6 | allowlist_externals = uv 7 | skip_install = true 8 | commands_pre = uv sync --frozen 9 | commands = 10 | uv run pytest 11 | vu run mypy 12 | uv run deptry python 13 | --------------------------------------------------------------------------------