├── .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 |
3 |
4 |
5 | [](https://pypi.org/project/deptry/)
6 | [](https://github.com/fpgmaas/deptry/actions/workflows/main.yml)
7 | [](https://pypi.org/project/deptry/)
8 | [](https://codecov.io/gh/fpgmaas/deptry)
9 | [](https://pypistats.org/packages/deptry)
10 | [](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 | { width="460" }
9 |
10 |
11 | ---
12 |
13 | [](https://pypi.org/project/deptry/)
14 | [](https://github.com/fpgmaas/deptry/actions/workflows/main.yml)
15 | [](https://pypi.org/project/deptry/)
16 | [](https://codecov.io/gh/fpgmaas/deptry)
17 | [](https://pypistats.org/packages/deptry)
18 | [](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 |
--------------------------------------------------------------------------------