././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1704568248.9799533
vulture-2.11/ 0000775 0001750 0001750 00000000000 14546322671 013055 5 ustar 00jendrik jendrik ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704568215.0
vulture-2.11/CHANGELOG.md 0000664 0001750 0001750 00000024361 14546322627 014675 0 ustar 00jendrik jendrik # 2.11 (2024-01-06)
* Switch to tomllib/tomli to support heterogeneous arrays (Sebastian Csar, #340).
* Bump flake8, flake8-comprehensions and flake8-bugbear (Sebastian Csar, #341).
* Provide whitelist parity for `MagicMock` and `Mock` (maxrake, #342).
# 2.10 (2023-10-06)
* Drop support for Python 3.7 (Jendrik Seipp, #323).
* Add support for Python 3.12 (Jendrik Seipp, #332).
* Use `end_lineno` AST attribute to obtain more accurate line counts (Jendrik Seipp).
# 2.9.1 (2023-08-21)
* Use exit code 0 for `--help` and `--version` again (Jendrik Seipp, #321).
# 2.9 (2023-08-20)
* Use exit code 3 when dead code is found (whosayn, #319).
* Treat non-supported decorator names as "@" instead of crashing (Llandy3d and Jendrik Seipp, #284).
* Drop support for Python 3.6 (Jendrik Seipp).
# 2.8 (2023-08-10)
* Add `UnicodeEncodeError` exception handling to `core.py` (milanbalazs, #299).
* Add whitelist for `Enum` attributes `_name_` and `_value_` (Eugene Toder, #305).
* Run tests and add PyPI trove for Python 3.11 (Jendrik Seipp).
# 2.7 (2023-01-08)
* Ignore `setup_module()`, `teardown_module()`, etc. in pytest `test_*.py` files (Jendrik Seipp).
* Add whitelist for `socketserver.TCPServer.allow_reuse_address` (Ben Elliston).
* Clarify that `--exclude` patterns are matched against absolute paths (Jendrik Seipp, #260).
* Fix example in README file (Jendrik Seipp, #272).
# 2.6 (2022-09-19)
* Add basic `match` statement support (kreathon, #276, #291).
# 2.5 (2022-07-03)
* Mark imports in `__all__` as used (kreathon, #172, #282).
* Add whitelist for `pint.UnitRegistry.default_formatter` (Ben Elliston, #258).
# 2.4 (2022-05-19)
* Print absolute filepaths as relative again (as in version 2.1 and before)
if they are below the current directory (The-Compiler, #246).
* Run tests and add PyPI trove for Python 3.10 (chayim, #266).
* Allow using the `del` keyword to mark unused variables (sshishov, #279).
# 2.3 (2021-01-16)
* Add [pre-commit](https://pre-commit.com) hook (Clément Robert, #244).
# 2.2 (2021-01-15)
* Only parse format strings when being used with `locals()` (jingw, #225).
* Don't override paths in pyproject.toml with empty CLI paths (bcbnz, #228).
* Run continuous integration tests for Python 3.9 (ju-sh, #232).
* Use pathlib internally (ju-sh, #226).
# 2.1 (2020-08-19)
* Treat `getattr/hasattr(obj, "constant_string", ...)` as a reference to
`obj.constant_string` (jingw, #219).
* Fix false positives when assigning to `x.some_name` but reading via
`some_name`, at the cost of potential false negatives (jingw, #221).
* Allow reading options from `pyproject.toml` (Michel Albert, #164, #215).
# 2.0 (2020-08-11)
* Parse `# type: ...` comments if on Python 3.8+ (jingw, #220).
* Bump minimum Python version to 3.6 (Jendrik Seipp, #218). The last
Vulture release that supports Python 2.7 and Python 3.5 is version 1.6.
* Consider all files under `test` or `tests` directories test files
(Jendrik Seipp).
* Ignore `logging.Logger.propagate` attribute (Jendrik Seipp).
# 1.6 (2020-07-28)
* Differentiate between functions and methods (Jendrik Seipp, #112, #209).
* Move from Travis to GitHub actions (RJ722, #211).
# 1.5 (2020-05-24)
* Support flake8 "noqa" error codes F401 (unused import) and F841 (unused
local variable) (RJ722, #195).
* Detect unreachable code in conditional expressions
(Agathiyan Bragadeesh, #178).
# 1.4 (2020-03-30)
* Ignore unused import statements in `__init__.py` (RJ722, #192).
* Report first decorator's line number for unused decorated objects on
Python 3.8+ (RJ722, #200).
* Check code with black and pyupgrade.
# 1.3 (2020-02-03)
* Detect redundant 'if' conditions without 'else' blocks.
* Add whitelist for `string.Formatter` (Joseph Bylund, #183).
# 1.2 (2019-11-22)
* Fix tests for Python 3.8 (#166).
* Use new `Constant` AST node under Python 3.8+ (#175).
* Add test for f-strings (#177).
* Add whitelist for `logging` module.
# 1.1 (2019-09-23)
* Add `sys.excepthook` to `sys` whitelist.
* Add whitelist for `ctypes` module.
* Check that type annotations are parsed and type comments are ignored
(thanks @kx-chen).
* Support checking files with BOM under Python 2.7 (#170).
# 1.0 (2018-10-23)
* Add `--ignore-decorators` flag (thanks @RJ722).
* Add whitelist for `threading` module (thanks @andrewhalle).
# 0.29 (2018-07-31)
* Add `--ignore-names` flag for ignoring names matching the given glob
patterns (thanks @RJ722).
# 0.28 (2018-07-05)
* Add `--make-whitelist` flag for reporting output in whitelist format
(thanks @RJ722).
* Ignore case of `--exclude` arguments on Windows.
* Add `*-test.py` to recognized test file patterns.
* Add `failureException`, `longMessage` and `maxDiff` to `unittest`
whitelist.
* Refer to actual objects rather than their mocks in default
whitelists (thanks @RJ722).
* Don't import any Vulture modules in setup.py (thanks @RJ722).
# 0.27 (2018-06-05)
* Report `while (True): ... else: ...` as unreachable (thanks @RJ722).
* Use `argparse` instead of `optparse`.
* Whitelist Mock.return\_value and Mock.side\_effect in unittest.mock
module.
* Drop support for Python 2.6 and 3.3.
* Improve documentation and test coverage (thanks @RJ722).
# 0.26 (2017-08-28)
* Detect `async` function definitions (thanks @RJ722).
* Add `Item.get_report()` method (thanks @RJ722).
* Move method for finding Python modules out of Vulture class.
# 0.25 (2017-08-15)
* Detect unsatisfiable statements containing `and`, `or` and `not`.
* Use filenames and line numbers as tie-breakers when sorting by size.
* Store first and last line numbers in Item objects.
* Pass relevant options directly to `scavenge()` and `report()`.
# 0.24 (2017-08-14)
* Detect unsatisfiable `while`-conditions (thanks @RJ722).
* Detect unsatisfiable `if`- and `else`-conditions (thanks @RJ722).
* Handle null bytes in source code.
# 0.23 (2017-08-10)
* Add `--min-confidence` flag (thanks @RJ722).
# 0.22 (2017-08-04)
* Detect unreachable code after `return`, `break`, `continue` and
`raise` (thanks @RJ722).
* Parse all variable and attribute names in new format strings.
* Extend ast whitelist.
# 0.21 (2017-07-26)
* If an unused item is defined multiple times, report it multiple
times.
* Make size estimates for function calls more accurate.
* Create wheel files for Vulture (thanks @RJ722).
# 0.20 (2017-07-26)
* Report unused tuple assignments as dead code.
* Report attribute names that have the same names as variables as dead
code.
* Let Item class inherit from `object` (thanks @RJ722).
* Handle names imported as aliases like all other used variable names.
* Rename Vulture.used\_vars to Vulture.used\_names.
* Use function for determining which imports to ignore.
* Only try to import each whitelist file once.
* Store used names and used attributes in sets instead of lists.
* Fix estimating the size of code containing ellipses (...).
* Refactor and simplify code.
# 0.19 (2017-07-20)
* Don't ignore \_\_foo variable names.
* Use separate methods for determining whether to ignore classes and
functions.
* Only try to find a whitelist for each defined import once (thanks
@roivanov).
* Fix finding the last child for many types of AST nodes.
# 0.18 (2017-07-17)
* Make --sort-by-size faster and more
accurate (thanks @RJ722).
# 0.17 (2017-07-17)
* Add get\_unused\_code() method.
* Return with exit code 1 when syntax errors are found or files can't
be read.
# 0.16 (2017-07-12)
* Differentiate between unused classes and functions (thanks @RJ722).
* Add --sort-by-size option (thanks @jackric and @RJ722).
* Count imports as used if they are accessed as module attributes.
# 0.15 (2017-07-04)
* Automatically include whitelists based on imported modules (thanks
@RJ722).
* Add --version parameter (thanks @RJ722).
* Add appveyor tests for testing on Windows (thanks @RJ722).
# 0.14 (2017-04-06)
* Add stub whitelist file for Python standard library (thanks @RJ722)
* Ignore class names starting with "Test" in "test\_" files (thanks
@thisch).
* Ignore "test\_" functions only in "test\_" files.
# 0.13 (2017-03-06)
* Ignore star-imported names since we cannot detect whether they are
used.
* Move repository to GitHub.
# 0.12 (2017-01-05)
* Detect unused imports.
* Use tokenize.open() on Python \>= 3.2 for reading input files,
assume UTF-8 encoding on older Python versions.
# 0.11 (2016-11-27)
* Use the system's default encoding when reading files.
* Report syntax errors instead of aborting.
# 0.10 (2016-07-14)
* Detect unused function and method arguments (issue #15).
* Detect unused \*args and \*\*kwargs parameters.
* Change license from GPL to MIT.
# 0.9 (2016-06-29)
* Don't flag attributes as unused if they are used as global variables
in another module (thanks Florian Bruhin).
* Don't consider "True" and "False" variable names.
* Abort with error message when invoked on .pyc files.
# 0.8.1 (2015-09-28)
* Fix code for Python 3.
# 0.8 (2015-09-28)
* Do not flag names imported with "import as" as dead code (thanks Tom
Terrace).
# 0.7 (2015-09-26)
* Exit with exitcode 1 if path on commandline can't be found.
* Test vulture with vulture using a whitelist module for false
positives.
* Add tests that run vulture as a script.
* Add "python setup.py test" command for running tests.
* Add support for tox.
* Raise test coverage to 100%.
* Remove ez\_setup.py.
# 0.6 (2014-09-06)
* Ignore function names starting with "test\_".
* Parse variable names in new format strings (e.g. "This is
{x}".format(x="nice")).
* Only parse alphanumeric variable names in format strings and ignore
types.
* Abort with exit code 1 on syntax errors.
* Support installation under Windows by using setuptools (thanks
Reuben Fletcher-Costin).
# 0.5 (2014-05-09)
* If dead code is found, exit with 1.
# 0.4.1 (2013-09-17)
* Only warn if a path given on the command line cannot be found.
# 0.4 (2013-06-23)
* Ignore unused variables starting with an underscore.
* Show warning for syntax errors instead of aborting directly.
* Print warning if a file cannot be found.
# 0.3 (2012-03-19)
* Add support for python3
* Report unused attributes
* Find tuple assignments in comprehensions
* Scan files given on the command line even if they don't end with .py
# 0.2 (2012-03-18)
* Only format nodes in verbose mode (gives 4x speedup).
# 0.1 (2012-03-17)
* First release.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1567253105.0
vulture-2.11/CODE_OF_CONDUCT.md 0000644 0001750 0001750 00000006432 13532461161 015647 0 ustar 00jendrik jendrik # Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at jendrikseipp@gmail.com. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/CONTRIBUTING.md 0000664 0001750 0001750 00000006045 14470671470 015313 0 ustar 00jendrik jendrik # Contributing to Vulture
## Creating and cloning a fork
Fork the Vulture repository on GitHub by clicking the "fork" button on the
top right. Then clone your fork to your local machine:
$ git clone https://github.com/USERNAME/vulture.git # Use your GitHub username.
$ cd vulture
## Installation
We recommend using a Python virtual environment to isolate the
installation of vulture.
### Setting up the virtual environment
You can read more about `virtualenv` in the [virtualenv
documentation](http://virtualenv.readthedocs.org).
To install the `virtualenv` package using `pip`, run:
$ python3 -m pip install virtualenv
Now you can create your own environment (named `vulture_dev`):
$ virtualenv vulture_dev
Now, whenever you work on the project, activate the corresponding
environment.
- On **Unix-based** systems, this can be done with:
$ source vulture_dev/bin/activate
- And on **Windows** this is done with:
$ vulture_dev\scripts\activate
To leave the virtual environment use:
(vulture_dev)$ deactivate
### Installing vulture
Navigate to your cloned `vulture` directory, and run the following to
install in development mode:
$ pip install --editable .
### Installing test tools
Vulture uses tox for testing. You can read more about it in the [tox
documentation](https://tox.readthedocs.io).
To install `tox`, run:
$ pip install tox
It's also recommended that you use `pre-commit` to catch style errors
early:
$ pip install pre-commit
$ pre-commit install
## Coding standards
### Creating a new branch
To start working on a pull request, create a new branch to work on. You
should never develop on your main branch because your main branch
should always be synchronized with the main repo’s main branch, which
is challenging if it has new commits. Create a branch using:
$ git checkout -b your-new-branch
#### Naming branches
Branch names should describe the feature/issue that you want to work on,
but at the same time be short.
### Commits
Each commit should be atomic and its message should adequately describe
the change in a clear manner. Use imperative, e.g., "Fix issue12." instead
of "Fixed issue12.". Please make sure that you only fix the issue at hand
or implement the desired new feature instead of making "drive-by" changes
like adding type hints.
## Testing
Run `tox` using:
$ tox
## Pull requests
### How to send a pull request?
Push your changes to your fork with:
$ git push --set-upstream origin BRANCHNAME
Then visit your fork on GitHub, change the branch to the one you committed
to, and click the `New Pull Request` button.
### Follow-up
In case your PR needs to be updated (tests fail or reviewer requests some
changes), update it by committing on top of your branch. It is not
necessary to amend your previous commit, since we will usually squash all
commits when merging anyway.
### Feedback
Take reviewer feedback positively. It's unlikely for a PR to be merged on
the first attempt, but don’t worry that’s just how it works. It helps to
keep the code clean.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1596492040.0
vulture-2.11/LICENSE.txt 0000644 0001750 0001750 00000002126 13712104410 014656 0 ustar 00jendrik jendrik The MIT License (MIT)
Copyright (c) 2012-2020 Jendrik Seipp (jendrikseipp@gmail.com)
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1580740334.0
vulture-2.11/MANIFEST.in 0000644 0001750 0001750 00000000136 13616027356 014610 0 ustar 00jendrik jendrik include *.md
include *.txt
include tests/*.py
include tox.ini
include vulture/whitelists/*.py
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1704568248.9799533
vulture-2.11/PKG-INFO 0000664 0001750 0001750 00000055652 14546322671 014167 0 ustar 00jendrik jendrik Metadata-Version: 2.1
Name: vulture
Version: 2.11
Summary: Find dead code
Home-page: https://github.com/jendrikseipp/vulture
Author: Jendrik Seipp
Author-email: jendrikseipp@gmail.com
License: MIT
Keywords: dead-code-removal
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.txt
# Vulture - Find dead code

[](https://codecov.io/gh/jendrikseipp/vulture?branch=main)
Vulture finds unused code in Python programs. This is useful for
cleaning up and finding errors in large code bases. If you run Vulture
on both your library and test suite you can find untested code.
Due to Python's dynamic nature, static code analyzers like Vulture are
likely to miss some dead code. Also, code that is only called implicitly
may be reported as unused. Nonetheless, Vulture can be a very helpful
tool for higher code quality.
## Features
* fast: uses static code analysis
* tested: tests itself and has complete test coverage
* complements pyflakes and has the same output syntax
* sorts unused classes and functions by size with `--sort-by-size`
## Installation
$ pip install vulture
## Usage
$ vulture myscript.py # or
$ python3 -m vulture myscript.py
$ vulture myscript.py mypackage/
$ vulture myscript.py --min-confidence 100 # Only report 100% dead code.
The provided arguments may be Python files or directories. For each
directory Vulture analyzes all contained
\*.py files.
After you have found and deleted dead code, run Vulture again, because
it may discover more dead code.
## Types of unused code
In addition to finding unused functions, classes, etc., Vulture can detect
unreachable code. Each chunk of dead code is assigned a *confidence value*
between 60% and 100%, where a value of 100% signals that it is certain that the
code won't be executed. Values below 100% are *very rough* estimates (based on
the type of code chunk) for how likely it is that the code is unused.
| Code type | Confidence value |
| ------------------- | -- |
| function/method/class argument, unreachable code | 100% |
| import | 90% |
| attribute, class, function, method, property, variable | 60% |
You can use the `--min-confidence` flag to set the minimum confidence
for code to be reported as unused. Use `--min-confidence 100` to only
report code that is guaranteed to be unused within the analyzed files.
## Handling false positives
When Vulture incorrectly reports chunks of code as unused, you have
several options for suppressing the false positives. If fixing your false
positives could benefit other users as well, please file an issue report.
#### Whitelists
The recommended option is to add used code that is reported as unused to a
Python module and add it to the list of scanned paths. To obtain such a
whitelist automatically, pass `--make-whitelist` to Vulture:
$ vulture mydir --make-whitelist > whitelist.py
$ vulture mydir whitelist.py
Note that the resulting `whitelist.py` file will contain valid Python
syntax, but for Python to be able to *run* it, you will usually have to
make some modifications.
We collect whitelists for common Python modules and packages in
`vulture/whitelists/` (pull requests are welcome).
#### Ignoring files
If you want to ignore a whole file or directory, use the `--exclude` parameter
(e.g., `--exclude "*settings.py,*/docs/*.py,*/test_*.py,*/.venv/*.py"`). The
exclude patterns are matched against absolute paths.
#### Flake8 noqa comments
For compatibility with [flake8](https://flake8.pycqa.org/), Vulture
supports the [F401 and
F841](https://flake8.pycqa.org/en/latest/user/error-codes.html) error
codes for ignoring unused imports (`# noqa: F401`) and unused local
variables (`# noqa: F841`). However, we recommend using whitelists instead
of `noqa` comments, since `noqa` comments add visual noise to the code and
make it harder to read.
#### Ignoring names
You can use `--ignore-names foo*,ba[rz]` to let Vulture ignore all names
starting with `foo` and the names `bar` and `baz`. Additionally, the
`--ignore-decorators` option can be used to ignore the names of functions
decorated with the given decorator (but not their arguments or function body).
This is helpful for example in Flask
projects, where you can use `--ignore-decorators "@app.route"` to ignore all
function names with the `@app.route` decorator. Note that Vulture simplifies
decorators it cannot parse: `@foo.bar(x, y)` becomes "@foo.bar" and
`@foo.bar(x, y).baz` becomes "@" internally.
We recommend using whitelists instead of `--ignore-names` or
`--ignore-decorators` whenever possible, since whitelists are
automatically checked for syntactic correctness when passed to Vulture
and often you can even pass them to your Python interpreter and let it
check that all whitelisted code actually still exists in your project.
#### Marking unused variables
There are situations where you can't just remove unused variables, e.g.,
in function signatures. The recommended solution is to use the `del`
keyword as described in the
[PyLint manual](http://pylint-messages.wikidot.com/messages:w0613) and on
[StackOverflow](https://stackoverflow.com/a/14836005):
```python
def foo(x, y):
del y
return x + 3
```
Vulture will also ignore all variables that start with an underscore, so
you can use `_x, y = get_pos()` to mark unused tuple assignments or
function arguments, e.g., `def foo(x, _y)`.
#### Minimum confidence
Raise the minimum [confidence value](#types-of-unused-code) with the `--min-confidence` flag.
#### Unreachable code
If Vulture complains about code like `if False:`, you can use a Boolean
flag `debug = False` and write `if debug:` instead. This makes the code
more readable and silences Vulture.
#### Forward references for type annotations
See [#216](https://github.com/jendrikseipp/vulture/issues/216). For
example, instead of `def foo(arg: "Sequence"): ...`, we recommend using
``` python
from __future__ import annotations
def foo(arg: Sequence):
...
```
## Configuration
You can also store command line arguments in `pyproject.toml` under the
`tool.vulture` section. Simply remove leading dashes and replace all
remaining dashes with underscores.
Options given on the command line have precedence over options in
`pyproject.toml`.
Example Config:
``` toml
[tool.vulture]
exclude = ["*file*.py", "dir/"]
ignore_decorators = ["@app.route", "@require_*"]
ignore_names = ["visit_*", "do_*"]
make_whitelist = true
min_confidence = 80
paths = ["myscript.py", "mydir", "whitelist.py"]
sort_by_size = true
verbose = true
```
## Version control integration
You can use a [pre-commit](https://pre-commit.com/#install) hook to run
Vulture before each commit. For this, install pre-commit and add the
following to the `.pre-commit-config.yaml` file in your repository:
```yaml
repos:
- repo: https://github.com/jendrikseipp/vulture
rev: 'v2.3' # or any later Vulture version
hooks:
- id: vulture
```
Then run `pre-commit install`. Finally, create a `pyproject.toml` file
in your repository and specify all files that Vulture should check under
`[tool.vulture] --> paths` (see above).
## How does it work?
Vulture uses the `ast` module to build abstract syntax trees for all
given files. While traversing all syntax trees it records the names of
defined and used objects. Afterwards, it reports the objects which have
been defined, but not used. This analysis ignores scopes and only takes
object names into account.
Vulture also detects unreachable code by looking for code after
`return`, `break`, `continue` and `raise` statements, and by searching
for unsatisfiable `if`- and `while`-conditions.
## Sort by size
When using the `--sort-by-size` option, Vulture sorts unused code by its
number of lines. This helps developers prioritize where to look for dead
code first.
## Examples
Consider the following Python script (`dead_code.py`):
``` python
import os
class Greeter:
def greet(self):
print("Hi")
def hello_world():
message = "Hello, world!"
greeter = Greeter()
func_name = "greet"
greet_func = getattr(greeter, func_name)
greet_func()
if __name__ == "__main__":
hello_world()
```
Calling :
$ vulture dead_code.py
results in the following output:
dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:4: unused function 'greet' (60% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
Vulture correctly reports `os` and `message` as unused but it fails to
detect that `greet` is actually used. The recommended method to deal
with false positives like this is to create a whitelist Python file.
**Preparing whitelists**
In a whitelist we simulate the usage of variables, attributes, etc. For
the program above, a whitelist could look as follows:
``` python
# whitelist_dead_code.py
from dead_code import Greeter
Greeter.greet
```
Alternatively, you can pass `--make-whitelist` to Vulture and obtain an
automatically generated whitelist.
Passing both the original program and the whitelist to Vulture
$ vulture dead_code.py whitelist_dead_code.py
makes Vulture ignore the `greet` method:
dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
## Exit codes
| Exit code | Description |
| --------- | ------------------------------------------------------------- |
| 0 | No dead code found |
| 1 | Invalid input (file missing, syntax error, wrong encoding) |
| 2 | Invalid command line arguments |
| 3 | Dead code found |
## Similar programs
- [pyflakes](https://pypi.org/project/pyflakes/) finds unused imports
and unused local variables (in addition to many other programmatic
errors).
- [coverage](https://pypi.org/project/coverage/) finds unused code
more reliably than Vulture, but requires all branches of the code to
actually be run.
- [uncalled](https://pypi.org/project/uncalled/) finds dead code by
using the abstract syntax tree (like Vulture), regular expressions,
or both.
- [dead](https://pypi.org/project/dead/) finds dead code by using the
abstract syntax tree (like Vulture).
## Participate
Please visit to report any
issues or to make pull requests.
- Contributing guide:
[CONTRIBUTING.md](https://github.com/jendrikseipp/vulture/blob/main/CONTRIBUTING.md)
- Release notes:
[CHANGELOG.md](https://github.com/jendrikseipp/vulture/blob/main/CHANGELOG.md)
- Roadmap:
[TODO.md](https://github.com/jendrikseipp/vulture/blob/main/TODO.md)
# 2.11 (2024-01-06)
* Switch to tomllib/tomli to support heterogeneous arrays (Sebastian Csar, #340).
* Bump flake8, flake8-comprehensions and flake8-bugbear (Sebastian Csar, #341).
* Provide whitelist parity for `MagicMock` and `Mock` (maxrake, #342).
# 2.10 (2023-10-06)
* Drop support for Python 3.7 (Jendrik Seipp, #323).
* Add support for Python 3.12 (Jendrik Seipp, #332).
* Use `end_lineno` AST attribute to obtain more accurate line counts (Jendrik Seipp).
# 2.9.1 (2023-08-21)
* Use exit code 0 for `--help` and `--version` again (Jendrik Seipp, #321).
# 2.9 (2023-08-20)
* Use exit code 3 when dead code is found (whosayn, #319).
* Treat non-supported decorator names as "@" instead of crashing (Llandy3d and Jendrik Seipp, #284).
* Drop support for Python 3.6 (Jendrik Seipp).
# 2.8 (2023-08-10)
* Add `UnicodeEncodeError` exception handling to `core.py` (milanbalazs, #299).
* Add whitelist for `Enum` attributes `_name_` and `_value_` (Eugene Toder, #305).
* Run tests and add PyPI trove for Python 3.11 (Jendrik Seipp).
# 2.7 (2023-01-08)
* Ignore `setup_module()`, `teardown_module()`, etc. in pytest `test_*.py` files (Jendrik Seipp).
* Add whitelist for `socketserver.TCPServer.allow_reuse_address` (Ben Elliston).
* Clarify that `--exclude` patterns are matched against absolute paths (Jendrik Seipp, #260).
* Fix example in README file (Jendrik Seipp, #272).
# 2.6 (2022-09-19)
* Add basic `match` statement support (kreathon, #276, #291).
# 2.5 (2022-07-03)
* Mark imports in `__all__` as used (kreathon, #172, #282).
* Add whitelist for `pint.UnitRegistry.default_formatter` (Ben Elliston, #258).
# 2.4 (2022-05-19)
* Print absolute filepaths as relative again (as in version 2.1 and before)
if they are below the current directory (The-Compiler, #246).
* Run tests and add PyPI trove for Python 3.10 (chayim, #266).
* Allow using the `del` keyword to mark unused variables (sshishov, #279).
# 2.3 (2021-01-16)
* Add [pre-commit](https://pre-commit.com) hook (Clément Robert, #244).
# 2.2 (2021-01-15)
* Only parse format strings when being used with `locals()` (jingw, #225).
* Don't override paths in pyproject.toml with empty CLI paths (bcbnz, #228).
* Run continuous integration tests for Python 3.9 (ju-sh, #232).
* Use pathlib internally (ju-sh, #226).
# 2.1 (2020-08-19)
* Treat `getattr/hasattr(obj, "constant_string", ...)` as a reference to
`obj.constant_string` (jingw, #219).
* Fix false positives when assigning to `x.some_name` but reading via
`some_name`, at the cost of potential false negatives (jingw, #221).
* Allow reading options from `pyproject.toml` (Michel Albert, #164, #215).
# 2.0 (2020-08-11)
* Parse `# type: ...` comments if on Python 3.8+ (jingw, #220).
* Bump minimum Python version to 3.6 (Jendrik Seipp, #218). The last
Vulture release that supports Python 2.7 and Python 3.5 is version 1.6.
* Consider all files under `test` or `tests` directories test files
(Jendrik Seipp).
* Ignore `logging.Logger.propagate` attribute (Jendrik Seipp).
# 1.6 (2020-07-28)
* Differentiate between functions and methods (Jendrik Seipp, #112, #209).
* Move from Travis to GitHub actions (RJ722, #211).
# 1.5 (2020-05-24)
* Support flake8 "noqa" error codes F401 (unused import) and F841 (unused
local variable) (RJ722, #195).
* Detect unreachable code in conditional expressions
(Agathiyan Bragadeesh, #178).
# 1.4 (2020-03-30)
* Ignore unused import statements in `__init__.py` (RJ722, #192).
* Report first decorator's line number for unused decorated objects on
Python 3.8+ (RJ722, #200).
* Check code with black and pyupgrade.
# 1.3 (2020-02-03)
* Detect redundant 'if' conditions without 'else' blocks.
* Add whitelist for `string.Formatter` (Joseph Bylund, #183).
# 1.2 (2019-11-22)
* Fix tests for Python 3.8 (#166).
* Use new `Constant` AST node under Python 3.8+ (#175).
* Add test for f-strings (#177).
* Add whitelist for `logging` module.
# 1.1 (2019-09-23)
* Add `sys.excepthook` to `sys` whitelist.
* Add whitelist for `ctypes` module.
* Check that type annotations are parsed and type comments are ignored
(thanks @kx-chen).
* Support checking files with BOM under Python 2.7 (#170).
# 1.0 (2018-10-23)
* Add `--ignore-decorators` flag (thanks @RJ722).
* Add whitelist for `threading` module (thanks @andrewhalle).
# 0.29 (2018-07-31)
* Add `--ignore-names` flag for ignoring names matching the given glob
patterns (thanks @RJ722).
# 0.28 (2018-07-05)
* Add `--make-whitelist` flag for reporting output in whitelist format
(thanks @RJ722).
* Ignore case of `--exclude` arguments on Windows.
* Add `*-test.py` to recognized test file patterns.
* Add `failureException`, `longMessage` and `maxDiff` to `unittest`
whitelist.
* Refer to actual objects rather than their mocks in default
whitelists (thanks @RJ722).
* Don't import any Vulture modules in setup.py (thanks @RJ722).
# 0.27 (2018-06-05)
* Report `while (True): ... else: ...` as unreachable (thanks @RJ722).
* Use `argparse` instead of `optparse`.
* Whitelist Mock.return\_value and Mock.side\_effect in unittest.mock
module.
* Drop support for Python 2.6 and 3.3.
* Improve documentation and test coverage (thanks @RJ722).
# 0.26 (2017-08-28)
* Detect `async` function definitions (thanks @RJ722).
* Add `Item.get_report()` method (thanks @RJ722).
* Move method for finding Python modules out of Vulture class.
# 0.25 (2017-08-15)
* Detect unsatisfiable statements containing `and`, `or` and `not`.
* Use filenames and line numbers as tie-breakers when sorting by size.
* Store first and last line numbers in Item objects.
* Pass relevant options directly to `scavenge()` and `report()`.
# 0.24 (2017-08-14)
* Detect unsatisfiable `while`-conditions (thanks @RJ722).
* Detect unsatisfiable `if`- and `else`-conditions (thanks @RJ722).
* Handle null bytes in source code.
# 0.23 (2017-08-10)
* Add `--min-confidence` flag (thanks @RJ722).
# 0.22 (2017-08-04)
* Detect unreachable code after `return`, `break`, `continue` and
`raise` (thanks @RJ722).
* Parse all variable and attribute names in new format strings.
* Extend ast whitelist.
# 0.21 (2017-07-26)
* If an unused item is defined multiple times, report it multiple
times.
* Make size estimates for function calls more accurate.
* Create wheel files for Vulture (thanks @RJ722).
# 0.20 (2017-07-26)
* Report unused tuple assignments as dead code.
* Report attribute names that have the same names as variables as dead
code.
* Let Item class inherit from `object` (thanks @RJ722).
* Handle names imported as aliases like all other used variable names.
* Rename Vulture.used\_vars to Vulture.used\_names.
* Use function for determining which imports to ignore.
* Only try to import each whitelist file once.
* Store used names and used attributes in sets instead of lists.
* Fix estimating the size of code containing ellipses (...).
* Refactor and simplify code.
# 0.19 (2017-07-20)
* Don't ignore \_\_foo variable names.
* Use separate methods for determining whether to ignore classes and
functions.
* Only try to find a whitelist for each defined import once (thanks
@roivanov).
* Fix finding the last child for many types of AST nodes.
# 0.18 (2017-07-17)
* Make --sort-by-size faster and more
accurate (thanks @RJ722).
# 0.17 (2017-07-17)
* Add get\_unused\_code() method.
* Return with exit code 1 when syntax errors are found or files can't
be read.
# 0.16 (2017-07-12)
* Differentiate between unused classes and functions (thanks @RJ722).
* Add --sort-by-size option (thanks @jackric and @RJ722).
* Count imports as used if they are accessed as module attributes.
# 0.15 (2017-07-04)
* Automatically include whitelists based on imported modules (thanks
@RJ722).
* Add --version parameter (thanks @RJ722).
* Add appveyor tests for testing on Windows (thanks @RJ722).
# 0.14 (2017-04-06)
* Add stub whitelist file for Python standard library (thanks @RJ722)
* Ignore class names starting with "Test" in "test\_" files (thanks
@thisch).
* Ignore "test\_" functions only in "test\_" files.
# 0.13 (2017-03-06)
* Ignore star-imported names since we cannot detect whether they are
used.
* Move repository to GitHub.
# 0.12 (2017-01-05)
* Detect unused imports.
* Use tokenize.open() on Python \>= 3.2 for reading input files,
assume UTF-8 encoding on older Python versions.
# 0.11 (2016-11-27)
* Use the system's default encoding when reading files.
* Report syntax errors instead of aborting.
# 0.10 (2016-07-14)
* Detect unused function and method arguments (issue #15).
* Detect unused \*args and \*\*kwargs parameters.
* Change license from GPL to MIT.
# 0.9 (2016-06-29)
* Don't flag attributes as unused if they are used as global variables
in another module (thanks Florian Bruhin).
* Don't consider "True" and "False" variable names.
* Abort with error message when invoked on .pyc files.
# 0.8.1 (2015-09-28)
* Fix code for Python 3.
# 0.8 (2015-09-28)
* Do not flag names imported with "import as" as dead code (thanks Tom
Terrace).
# 0.7 (2015-09-26)
* Exit with exitcode 1 if path on commandline can't be found.
* Test vulture with vulture using a whitelist module for false
positives.
* Add tests that run vulture as a script.
* Add "python setup.py test" command for running tests.
* Add support for tox.
* Raise test coverage to 100%.
* Remove ez\_setup.py.
# 0.6 (2014-09-06)
* Ignore function names starting with "test\_".
* Parse variable names in new format strings (e.g. "This is
{x}".format(x="nice")).
* Only parse alphanumeric variable names in format strings and ignore
types.
* Abort with exit code 1 on syntax errors.
* Support installation under Windows by using setuptools (thanks
Reuben Fletcher-Costin).
# 0.5 (2014-05-09)
* If dead code is found, exit with 1.
# 0.4.1 (2013-09-17)
* Only warn if a path given on the command line cannot be found.
# 0.4 (2013-06-23)
* Ignore unused variables starting with an underscore.
* Show warning for syntax errors instead of aborting directly.
* Print warning if a file cannot be found.
# 0.3 (2012-03-19)
* Add support for python3
* Report unused attributes
* Find tuple assignments in comprehensions
* Scan files given on the command line even if they don't end with .py
# 0.2 (2012-03-18)
* Only format nodes in verbose mode (gives 4x speedup).
# 0.1 (2012-03-17)
* First release.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704566857.0
vulture-2.11/README.md 0000664 0001750 0001750 00000027236 14546320111 014331 0 ustar 00jendrik jendrik # Vulture - Find dead code

[](https://codecov.io/gh/jendrikseipp/vulture?branch=main)
Vulture finds unused code in Python programs. This is useful for
cleaning up and finding errors in large code bases. If you run Vulture
on both your library and test suite you can find untested code.
Due to Python's dynamic nature, static code analyzers like Vulture are
likely to miss some dead code. Also, code that is only called implicitly
may be reported as unused. Nonetheless, Vulture can be a very helpful
tool for higher code quality.
## Features
* fast: uses static code analysis
* tested: tests itself and has complete test coverage
* complements pyflakes and has the same output syntax
* sorts unused classes and functions by size with `--sort-by-size`
## Installation
$ pip install vulture
## Usage
$ vulture myscript.py # or
$ python3 -m vulture myscript.py
$ vulture myscript.py mypackage/
$ vulture myscript.py --min-confidence 100 # Only report 100% dead code.
The provided arguments may be Python files or directories. For each
directory Vulture analyzes all contained
\*.py files.
After you have found and deleted dead code, run Vulture again, because
it may discover more dead code.
## Types of unused code
In addition to finding unused functions, classes, etc., Vulture can detect
unreachable code. Each chunk of dead code is assigned a *confidence value*
between 60% and 100%, where a value of 100% signals that it is certain that the
code won't be executed. Values below 100% are *very rough* estimates (based on
the type of code chunk) for how likely it is that the code is unused.
| Code type | Confidence value |
| ------------------- | -- |
| function/method/class argument, unreachable code | 100% |
| import | 90% |
| attribute, class, function, method, property, variable | 60% |
You can use the `--min-confidence` flag to set the minimum confidence
for code to be reported as unused. Use `--min-confidence 100` to only
report code that is guaranteed to be unused within the analyzed files.
## Handling false positives
When Vulture incorrectly reports chunks of code as unused, you have
several options for suppressing the false positives. If fixing your false
positives could benefit other users as well, please file an issue report.
#### Whitelists
The recommended option is to add used code that is reported as unused to a
Python module and add it to the list of scanned paths. To obtain such a
whitelist automatically, pass `--make-whitelist` to Vulture:
$ vulture mydir --make-whitelist > whitelist.py
$ vulture mydir whitelist.py
Note that the resulting `whitelist.py` file will contain valid Python
syntax, but for Python to be able to *run* it, you will usually have to
make some modifications.
We collect whitelists for common Python modules and packages in
`vulture/whitelists/` (pull requests are welcome).
#### Ignoring files
If you want to ignore a whole file or directory, use the `--exclude` parameter
(e.g., `--exclude "*settings.py,*/docs/*.py,*/test_*.py,*/.venv/*.py"`). The
exclude patterns are matched against absolute paths.
#### Flake8 noqa comments
For compatibility with [flake8](https://flake8.pycqa.org/), Vulture
supports the [F401 and
F841](https://flake8.pycqa.org/en/latest/user/error-codes.html) error
codes for ignoring unused imports (`# noqa: F401`) and unused local
variables (`# noqa: F841`). However, we recommend using whitelists instead
of `noqa` comments, since `noqa` comments add visual noise to the code and
make it harder to read.
#### Ignoring names
You can use `--ignore-names foo*,ba[rz]` to let Vulture ignore all names
starting with `foo` and the names `bar` and `baz`. Additionally, the
`--ignore-decorators` option can be used to ignore the names of functions
decorated with the given decorator (but not their arguments or function body).
This is helpful for example in Flask
projects, where you can use `--ignore-decorators "@app.route"` to ignore all
function names with the `@app.route` decorator. Note that Vulture simplifies
decorators it cannot parse: `@foo.bar(x, y)` becomes "@foo.bar" and
`@foo.bar(x, y).baz` becomes "@" internally.
We recommend using whitelists instead of `--ignore-names` or
`--ignore-decorators` whenever possible, since whitelists are
automatically checked for syntactic correctness when passed to Vulture
and often you can even pass them to your Python interpreter and let it
check that all whitelisted code actually still exists in your project.
#### Marking unused variables
There are situations where you can't just remove unused variables, e.g.,
in function signatures. The recommended solution is to use the `del`
keyword as described in the
[PyLint manual](http://pylint-messages.wikidot.com/messages:w0613) and on
[StackOverflow](https://stackoverflow.com/a/14836005):
```python
def foo(x, y):
del y
return x + 3
```
Vulture will also ignore all variables that start with an underscore, so
you can use `_x, y = get_pos()` to mark unused tuple assignments or
function arguments, e.g., `def foo(x, _y)`.
#### Minimum confidence
Raise the minimum [confidence value](#types-of-unused-code) with the `--min-confidence` flag.
#### Unreachable code
If Vulture complains about code like `if False:`, you can use a Boolean
flag `debug = False` and write `if debug:` instead. This makes the code
more readable and silences Vulture.
#### Forward references for type annotations
See [#216](https://github.com/jendrikseipp/vulture/issues/216). For
example, instead of `def foo(arg: "Sequence"): ...`, we recommend using
``` python
from __future__ import annotations
def foo(arg: Sequence):
...
```
## Configuration
You can also store command line arguments in `pyproject.toml` under the
`tool.vulture` section. Simply remove leading dashes and replace all
remaining dashes with underscores.
Options given on the command line have precedence over options in
`pyproject.toml`.
Example Config:
``` toml
[tool.vulture]
exclude = ["*file*.py", "dir/"]
ignore_decorators = ["@app.route", "@require_*"]
ignore_names = ["visit_*", "do_*"]
make_whitelist = true
min_confidence = 80
paths = ["myscript.py", "mydir", "whitelist.py"]
sort_by_size = true
verbose = true
```
## Version control integration
You can use a [pre-commit](https://pre-commit.com/#install) hook to run
Vulture before each commit. For this, install pre-commit and add the
following to the `.pre-commit-config.yaml` file in your repository:
```yaml
repos:
- repo: https://github.com/jendrikseipp/vulture
rev: 'v2.3' # or any later Vulture version
hooks:
- id: vulture
```
Then run `pre-commit install`. Finally, create a `pyproject.toml` file
in your repository and specify all files that Vulture should check under
`[tool.vulture] --> paths` (see above).
## How does it work?
Vulture uses the `ast` module to build abstract syntax trees for all
given files. While traversing all syntax trees it records the names of
defined and used objects. Afterwards, it reports the objects which have
been defined, but not used. This analysis ignores scopes and only takes
object names into account.
Vulture also detects unreachable code by looking for code after
`return`, `break`, `continue` and `raise` statements, and by searching
for unsatisfiable `if`- and `while`-conditions.
## Sort by size
When using the `--sort-by-size` option, Vulture sorts unused code by its
number of lines. This helps developers prioritize where to look for dead
code first.
## Examples
Consider the following Python script (`dead_code.py`):
``` python
import os
class Greeter:
def greet(self):
print("Hi")
def hello_world():
message = "Hello, world!"
greeter = Greeter()
func_name = "greet"
greet_func = getattr(greeter, func_name)
greet_func()
if __name__ == "__main__":
hello_world()
```
Calling :
$ vulture dead_code.py
results in the following output:
dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:4: unused function 'greet' (60% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
Vulture correctly reports `os` and `message` as unused but it fails to
detect that `greet` is actually used. The recommended method to deal
with false positives like this is to create a whitelist Python file.
**Preparing whitelists**
In a whitelist we simulate the usage of variables, attributes, etc. For
the program above, a whitelist could look as follows:
``` python
# whitelist_dead_code.py
from dead_code import Greeter
Greeter.greet
```
Alternatively, you can pass `--make-whitelist` to Vulture and obtain an
automatically generated whitelist.
Passing both the original program and the whitelist to Vulture
$ vulture dead_code.py whitelist_dead_code.py
makes Vulture ignore the `greet` method:
dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
## Exit codes
| Exit code | Description |
| --------- | ------------------------------------------------------------- |
| 0 | No dead code found |
| 1 | Invalid input (file missing, syntax error, wrong encoding) |
| 2 | Invalid command line arguments |
| 3 | Dead code found |
## Similar programs
- [pyflakes](https://pypi.org/project/pyflakes/) finds unused imports
and unused local variables (in addition to many other programmatic
errors).
- [coverage](https://pypi.org/project/coverage/) finds unused code
more reliably than Vulture, but requires all branches of the code to
actually be run.
- [uncalled](https://pypi.org/project/uncalled/) finds dead code by
using the abstract syntax tree (like Vulture), regular expressions,
or both.
- [dead](https://pypi.org/project/dead/) finds dead code by using the
abstract syntax tree (like Vulture).
## Participate
Please visit to report any
issues or to make pull requests.
- Contributing guide:
[CONTRIBUTING.md](https://github.com/jendrikseipp/vulture/blob/main/CONTRIBUTING.md)
- Release notes:
[CHANGELOG.md](https://github.com/jendrikseipp/vulture/blob/main/CHANGELOG.md)
- Roadmap:
[TODO.md](https://github.com/jendrikseipp/vulture/blob/main/TODO.md)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1693469034.0
vulture-2.11/TODO.md 0000664 0001750 0001750 00000003271 14474044552 014146 0 ustar 00jendrik jendrik # TODOs
* Add --ignore-attributes-for-classes option. When visiting such a class,
mark all its attributes as used. Fixes (partly):
* https://github.com/jendrikseipp/vulture/issues/309
* https://github.com/jendrikseipp/vulture/issues/264
* https://github.com/jendrikseipp/vulture/issues/249
* https://github.com/jendrikseipp/vulture/issues/315
Use these as test cases.
* Honor (speaking) pylint error codes (e.g., # pylint:
disable=unused-import): unused-import, unused-variable, unused-argument,
possibly-unused-variable and unreachable-code. See
https://github.com/janjur/readable-pylint-messages#unused-import.
# Non-TODOs
* Ignore hidden files and directories (might be unexpected, use
--exclude instead).
* Use Assign instead of Name AST nodes for estimating the size of
assignments (KISS).
* Only count lines for unused code by storing a function `get_size` in
Item for computing the size on demand. This is 1.5 times as slow as
computing no sizes.
* Compute sizes on demand. Storing nodes increases memory usage from
~120 MiB to ~580 MiB for tensorflow's Python code.
* Detect unreachable code for `ast.Assert` (`assert False` is common
idiom for aborting rogue code).
* Detect superfluous expressions like `a <= b`, `42`, `foo and bar`
occurring outside of a statement (hard to detect if code is
unneeded).
* Detect that body of `if foo:` is unreachable if foo is only assigned
"false" values (complicated: e.g., foo = \[\]; foo.append(1); if
foo: ...).
* Use coverage.py to detect false-positives (\#109). Workflow too
complicated.
* Ignore some decorators by default: @app.route, @cli.command.
* Ignore functions in conftest.py files that start with "pytest_".
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/pyproject.toml 0000664 0001750 0001750 00000000616 14470671470 015774 0 ustar 00jendrik jendrik # NOTE: you have to use single-quoted strings in TOML for regular expressions.
# It's the equivalent of r-strings in Python. Multiline strings are treated as
# verbose regular expressions by Black. Use [ ] to denote a significant space
# character.
[tool.black]
line-length = 79
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.tox
| \.venv
| _build
| build
| dist
)/
'''
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704567215.0
vulture-2.11/requirements.txt 0000664 0001750 0001750 00000000050 14546320657 016336 0 ustar 00jendrik jendrik tomli >= 1.1.0; python_version < '3.11'
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1704568248.9799533
vulture-2.11/setup.cfg 0000664 0001750 0001750 00000000346 14546322671 014701 0 ustar 00jendrik jendrik [coverage:run]
omit =
setup.py
.tox/*
parallel = true
[tool:pytest]
addopts = --cov vulture --cov-report=html --cov-report=term --cov-report=xml --cov-append
[bdist_wheel]
universal = 1
[egg_info]
tag_build =
tag_date = 0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704567045.0
vulture-2.11/setup.py 0000664 0001750 0001750 00000003707 14546320405 014567 0 ustar 00jendrik jendrik #! /usr/bin/env python
import pathlib
import re
import setuptools
def find_version(*parts):
here = pathlib.Path(__file__).parent
version_file = here.joinpath(*parts).read_text()
version_match = re.search(
r"^__version__ = ['\"]([^'\"]*)['\"]$", version_file, re.M
)
if version_match:
return version_match.group(1)
raise RuntimeError("Unable to find version string.")
with open("README.md") as f1, open("CHANGELOG.md") as f2:
long_description = f1.read() + "\n\n" + f2.read()
with open("requirements.txt") as f:
install_requires = f.read().splitlines()
setuptools.setup(
name="vulture",
version=find_version("vulture", "version.py"),
description="Find dead code",
long_description=long_description,
long_description_content_type="text/markdown",
keywords="dead-code-removal",
author="Jendrik Seipp",
author_email="jendrikseipp@gmail.com",
url="https://github.com/jendrikseipp/vulture",
license="MIT",
classifiers=[
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Topic :: Software Development :: Quality Assurance",
],
install_requires=install_requires,
entry_points={"console_scripts": ["vulture = vulture.core:main"]},
python_requires=">=3.8",
packages=setuptools.find_packages(exclude=["tests"]),
package_data={"vulture": ["whitelists/*.py"]},
)
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1704568248.975953
vulture-2.11/tests/ 0000775 0001750 0001750 00000000000 14546322671 014217 5 ustar 00jendrik jendrik ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1602094901.0
vulture-2.11/tests/__init__.py 0000664 0001750 0001750 00000001707 13737403465 016337 0 ustar 00jendrik jendrik import pathlib
import subprocess
import sys
import pytest
from vulture import core
REPO = pathlib.Path(__file__).resolve().parents[1]
WHITELISTS = [
str(path) for path in (REPO / "vulture" / "whitelists").glob("*.py")
]
def call_vulture(args, **kwargs):
return subprocess.call(
[sys.executable, "-m", "vulture"] + args, cwd=REPO, **kwargs
)
def check(items_or_names, expected_names):
"""items_or_names must be a collection of Items or a set of strings."""
try:
assert sorted(item.name for item in items_or_names) == sorted(
expected_names
)
except AttributeError:
assert items_or_names == set(expected_names)
def check_unreachable(v, lineno, size, name):
assert len(v.unreachable_code) == 1
item = v.unreachable_code[0]
assert item.first_lineno == lineno
assert item.size == size
assert item.name == name
@pytest.fixture
def v():
return core.Vulture(verbose=True)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/tests/test_conditions.py 0000664 0001750 0001750 00000010045 14470671470 020001 0 ustar 00jendrik jendrik import ast
from vulture import utils
from . import check_unreachable
from . import v
assert v # Silence pyflakes
def check_condition(code, result):
condition = ast.parse(code, mode="eval").body
if result:
assert utils.condition_is_always_true(condition)
else:
assert utils.condition_is_always_false(condition)
def test_false():
check_condition("False", False)
check_condition("None", False)
check_condition("0", False)
def test_empty():
check_condition("''", False)
check_condition("[]", False)
check_condition("{}", False)
def test_true():
check_condition("True", True)
check_condition("2", True)
check_condition("'s'", True)
check_condition("['foo', 'bar']", True)
check_condition("{'a': 1, 'b': 2}", True)
def test_complex_conditions():
conditions = [
("foo and False", True, False),
("foo or False", False, False),
("foo and True", False, False),
("foo or True", False, True),
("False and foo", True, False),
("False and 1", True, False),
("not False", False, True),
("not True", True, False),
("not foo", False, False),
("foo and (False or [])", True, False),
('(foo and bar) or {"a": 1}', False, True),
]
for condition, always_false, always_true in conditions:
condition = ast.parse(condition, mode="eval").body
assert not (always_false and always_true)
assert utils.condition_is_always_false(condition) == always_false
assert utils.condition_is_always_true(condition) == always_true
def test_errors():
conditions = [
"foo",
'__name__ == "__main__"',
"chr(-1)",
'getattr(True, "foo")',
'hasattr(str, "foo")',
"isinstance(True, True)",
"globals()",
"locals()",
"().__class__",
]
for condition in conditions:
condition = ast.parse(condition, mode="eval").body
assert not utils.condition_is_always_false(condition)
assert not utils.condition_is_always_true(condition)
def test_while(v):
v.scan(
"""\
while False:
pass
"""
)
check_unreachable(v, 1, 2, "while")
def test_while_nested(v):
v.scan(
"""\
while True:
while False:
pass
"""
)
check_unreachable(v, 2, 2, "while")
def test_if_false(v):
v.scan(
"""\
if False:
pass
"""
)
check_unreachable(v, 1, 2, "if")
def test_elif_false(v):
v.scan(
"""\
if bar():
pass
elif False:
print("Unreachable")
"""
)
check_unreachable(v, 3, 2, "if")
def test_nested_if_statements_false(v):
v.scan(
"""\
if foo():
if bar():
pass
elif False:
print("Unreachable")
pass
elif something():
print("Reachable")
else:
pass
else:
pass
"""
)
check_unreachable(v, 4, 3, "if")
def test_if_false_same_line(v):
v.scan(
"""\
if False: a = 1
else: c = 3
"""
)
check_unreachable(v, 1, 1, "if")
def test_if_true(v):
v.scan(
"""\
if True:
a = 1
b = 2
else:
c = 3
d = 3
"""
)
# For simplicity, we don't report the "else" line as dead code.
check_unreachable(v, 5, 2, "else")
def test_if_true_same_line(v):
v.scan(
"""\
if True:
a = 1
b = 2
else: c = 3
d = 3
"""
)
check_unreachable(v, 4, 1, "else")
def test_nested_if_statements_true(v):
v.scan(
"""\
if foo():
if bar():
pass
elif True:
if something():
pass
else:
pass
elif something_else():
print("foo")
else:
print("bar")
else:
pass
"""
)
check_unreachable(v, 9, 4, "else")
def test_redundant_if(v):
v.scan(
"""\
if [5]:
pass
"""
)
print(v.unreachable_code[0].size)
check_unreachable(v, 1, 2, "if")
def test_if_exp_true(v):
v.scan("foo if True else bar")
check_unreachable(v, 1, 1, "ternary")
def test_if_exp_false(v):
v.scan("foo if False else bar")
check_unreachable(v, 1, 1, "ternary")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1596841917.0
vulture-2.11/tests/test_confidence.py 0000644 0001750 0001750 00000003162 13713357675 017734 0 ustar 00jendrik jendrik from vulture import core
dc = core.DEFAULT_CONFIDENCE
def check_min_confidence(code, min_confidence, expected):
v = core.Vulture(verbose=True)
v.scan(code)
detected = {
item.name: item.confidence
for item in v.get_unused_code(min_confidence=min_confidence)
}
assert detected == expected
def test_confidence_import():
code = """\
import foo
"""
check_min_confidence(code, 50, {"foo": 90})
check_min_confidence(code, 100, {})
def test_confidence_unreachable():
code = """\
def foo():
return
bar()
foo()
"""
check_min_confidence(code, 50, {"return": 100})
check_min_confidence(code, 100, {"return": 100})
def test_function_arg():
code = """\
def foo(a):
b = 3
foo(5)
"""
check_min_confidence(code, 50, {"a": 100, "b": dc})
check_min_confidence(code, dc, {"a": 100, "b": dc})
check_min_confidence(code, 100, {"a": 100})
def test_confidence_class():
code = """\
class Foo:
pass
"""
check_min_confidence(code, 50, {"Foo": dc})
check_min_confidence(code, 100, {})
def test_confidence_attr():
code = "A.b = 'something'"
check_min_confidence(code, 50, {"b": dc})
check_min_confidence(code, 100, {})
def test_confidence_props():
code = """\
class Foo:
@property
def some_prop():
pass
Foo()
"""
check_min_confidence(code, 50, {"some_prop": dc})
check_min_confidence(code, 100, {})
def test_confidence_async_def():
code = """\
async def foo():
if bar():
pass
else:
print("Else")
"""
check_min_confidence(code, 50, {"foo": dc})
check_min_confidence(code, 75, {})
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704114798.0
vulture-2.11/tests/test_config.py 0000664 0001750 0001750 00000013734 14544535156 017107 0 ustar 00jendrik jendrik """
Unit tests for config file and CLI argument parsing.
"""
from io import BytesIO
from textwrap import dedent
import pytest
from vulture.config import (
DEFAULTS,
_check_input_config,
_parse_args,
_parse_toml,
make_config,
InputError,
)
def get_toml_bytes(toml_str: str) -> BytesIO:
"""
Wrap a string in BytesIO to play the role of the incoming config stream.
"""
return BytesIO(bytes(toml_str, "utf-8"))
def test_cli_args():
"""
Ensure that CLI arguments are converted to a config object.
"""
expected = dict(
paths=["path1", "path2"],
exclude=["file*.py", "dir/"],
ignore_decorators=["deco1", "deco2"],
ignore_names=["name1", "name2"],
make_whitelist=True,
min_confidence=10,
sort_by_size=True,
verbose=True,
)
result = _parse_args(
[
"--exclude=file*.py,dir/",
"--ignore-decorators=deco1,deco2",
"--ignore-names=name1,name2",
"--make-whitelist",
"--min-confidence=10",
"--sort-by-size",
"--verbose",
"path1",
"path2",
]
)
assert isinstance(result, dict)
assert result == expected
def test_toml_config():
"""
Ensure parsing of TOML files results in a valid config object.
"""
expected = dict(
paths=["path1", "path2"],
exclude=["file*.py", "dir/"],
ignore_decorators=["deco1", "deco2"],
ignore_names=["name1", "name2"],
make_whitelist=True,
min_confidence=10,
sort_by_size=True,
verbose=True,
)
data = get_toml_bytes(
dedent(
"""\
[tool.vulture]
exclude = ["file*.py", "dir/"]
ignore_decorators = ["deco1", "deco2"]
ignore_names = ["name1", "name2"]
make_whitelist = true
min_confidence = 10
sort_by_size = true
verbose = true
paths = ["path1", "path2"]
"""
)
)
result = _parse_toml(data)
assert isinstance(result, dict)
assert result == expected
def test_toml_config_with_heterogenous_array():
"""
Ensure parsing of TOML files results in a valid config object, even if some
other part of the file contains an array of mixed types.
"""
expected = dict(
paths=["path1", "path2"],
exclude=["file*.py", "dir/"],
ignore_decorators=["deco1", "deco2"],
ignore_names=["name1", "name2"],
make_whitelist=True,
min_confidence=10,
sort_by_size=True,
verbose=True,
)
data = get_toml_bytes(
dedent(
"""\
[tool.foo]
# comment for good measure
problem_array = [{a = 1}, [2,3,4], "foo"]
[tool.vulture]
exclude = ["file*.py", "dir/"]
ignore_decorators = ["deco1", "deco2"]
ignore_names = ["name1", "name2"]
make_whitelist = true
min_confidence = 10
sort_by_size = true
verbose = true
paths = ["path1", "path2"]
"""
)
)
result = _parse_toml(data)
assert isinstance(result, dict)
assert result == expected
def test_config_merging():
"""
If we have both CLI args and a ``pyproject.toml`` file, the CLI args should
have precedence.
"""
toml = get_toml_bytes(
dedent(
"""\
[tool.vulture]
exclude = ["toml_exclude"]
ignore_decorators = ["toml_deco"]
ignore_names = ["toml_name"]
make_whitelist = false
min_confidence = 10
sort_by_size = false
verbose = false
paths = ["toml_path"]
"""
)
)
cliargs = [
"--exclude=cli_exclude",
"--ignore-decorators=cli_deco",
"--ignore-names=cli_name",
"--make-whitelist",
"--min-confidence=20",
"--sort-by-size",
"--verbose",
"cli_path",
]
result = make_config(cliargs, toml)
expected = dict(
paths=["cli_path"],
exclude=["cli_exclude"],
ignore_decorators=["cli_deco"],
ignore_names=["cli_name"],
make_whitelist=True,
min_confidence=20,
sort_by_size=True,
verbose=True,
)
assert result == expected
def test_config_merging_missing():
"""
If we have set a boolean value in the TOML file, but not on the CLI, we
want the TOML value to be taken.
"""
toml = get_toml_bytes(
dedent(
"""\
[tool.vulture]
verbose = true
ignore_names = ["name1"]
"""
)
)
cliargs = [
"cli_path",
]
result = make_config(cliargs, toml)
assert result["verbose"] is True
assert result["ignore_names"] == ["name1"]
def test_config_merging_toml_paths_only():
"""
If we have paths in the TOML but not on the CLI, the TOML paths should be
used.
"""
toml = get_toml_bytes(
dedent(
"""\
[tool.vulture]
paths = ["path1", "path2"]
"""
)
)
cliargs = [
"--exclude=test_*.py",
]
result = make_config(cliargs, toml)
assert result["paths"] == ["path1", "path2"]
assert result["exclude"] == ["test_*.py"]
def test_invalid_config_options_output():
"""
If the config file contains unknown options we want to abort.
"""
with pytest.raises(InputError):
_check_input_config({"unknown_key_1": 1})
@pytest.mark.parametrize("key, value", list(DEFAULTS.items()))
def test_incompatible_option_type(key, value):
"""
If a config value has a different type from the default value we abort.
"""
wrong_types = {int, str, list, bool} - {type(value)}
for wrong_type in wrong_types:
test_value = wrong_type()
with pytest.raises(InputError):
_check_input_config({key: test_value})
def test_missing_paths():
"""
If the script is run without any paths, we want to abort.
"""
with pytest.raises(InputError):
make_config([])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/tests/test_encoding.py 0000664 0001750 0001750 00000001741 14470671470 017421 0 ustar 00jendrik jendrik import codecs
from . import v
from vulture.utils import ExitCode
assert v # Silence pyflakes.
def test_encoding1(v):
v.scan(
"""\
# -*- coding: utf-8 -*-
pass
"""
)
assert v.exit_code == ExitCode.NoDeadCode
def test_encoding2(v):
v.scan(
"""\
#! /usr/bin/env python
# -*- coding: utf-8 -*-
pass
"""
)
assert v.exit_code == ExitCode.NoDeadCode
def test_non_utf8_encoding(v, tmp_path):
code = ""
name = "non_utf8"
non_utf_8_file = tmp_path / (name + ".py")
with open(non_utf_8_file, mode="wb") as f:
f.write(codecs.BOM_UTF16_LE)
f.write(code.encode("utf_16_le"))
v.scavenge([non_utf_8_file])
assert v.exit_code == ExitCode.InvalidInput
def test_utf8_with_bom(v, tmp_path):
name = "utf8_bom"
filepath = tmp_path / (name + ".py")
# utf8_sig prepends the BOM to the file.
filepath.write_text("", encoding="utf-8-sig")
v.scavenge([filepath])
assert v.exit_code == ExitCode.NoDeadCode
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/tests/test_errors.py 0000664 0001750 0001750 00000001171 14470671470 017144 0 ustar 00jendrik jendrik import pytest
from . import v, call_vulture
from vulture.utils import ExitCode
assert v # Silence pyflakes.
def test_syntax_error(v):
v.scan("foo bar")
assert int(v.report()) == ExitCode.InvalidInput
def test_null_byte(v):
v.scan("\x00")
assert int(v.report()) == ExitCode.InvalidInput
def test_confidence_range(v):
v.scan(
"""\
def foo():
pass
"""
)
with pytest.raises(ValueError):
v.get_unused_code(min_confidence=150)
def test_invalid_cmdline_args():
assert (
call_vulture(["vulture/", "--invalid-argument"])
== ExitCode.InvalidCmdlineArguments
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1599907056.0
vulture-2.11/tests/test_format_strings.py 0000644 0001750 0001750 00000003026 13727122360 020661 0 ustar 00jendrik jendrik from . import check, v
assert v # Silence pyflakes.
def test_old_format_string(v):
v.scan("'%(a)s, %(b)d' % locals()")
check(v.used_names, ["a", "b", "locals"])
def test_new_format_string(v):
v.scan("'{a}, {b:0d} {c:<30} {d:.2%}'.format(**locals())")
check(v.used_names, ["a", "b", "c", "d", "format", "locals"])
def test_f_string(v):
v.scan(
"""\
f'{a}, {b:0d} {c:<30} {d:.2%} {e()} {f:{width}.{precision}}'
f'{ {x:y for (x, y) in ((1, 2), (3, 4))} }'
"""
)
check(
v.used_names,
["a", "b", "c", "d", "e", "f", "precision", "width", "x", "y"],
)
def test_new_format_string_access(v):
v.scan("'{a.b}, {c.d.e} {f[g]} {h[i][j].k}'.format(**locals())")
check(
v.used_names,
["a", "b", "c", "d", "e", "f", "h", "k", "format", "locals"],
)
def test_new_format_string_numbers(v):
v.scan("'{0.b}, {0.d.e} {0[1]} {0[1][1].k}'.format(**locals())")
check(v.used_names, ["b", "d", "e", "k", "format", "locals"])
def test_incorrect_format_string(v):
v.scan('"{"')
v.scan('"{!-a:}"')
check(v.used_names, [])
def test_format_string_not_using_locals(v):
"""Strings that are not formatted with locals() should not be parsed."""
v.scan(
"""\
"{variable}"
def foobar():
'''
Return data of the form
{this_looks_like_a_format_string: 1}
'''
pass
"%(thing)s" % {"thing": 1}
"%(apple)s" * locals()
"{} {a} {b}".format(1, a=used_var, b=locals())
"""
)
check(v.used_names, ["used_var", "locals", "format"])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1597479718.0
vulture-2.11/tests/test_ignore.py 0000644 0001750 0001750 00000005617 13715715446 017125 0 ustar 00jendrik jendrik from vulture import core
from . import check
def check_ignore(code, ignore_names, ignore_decorators, expected):
v = core.Vulture(
verbose=True,
ignore_names=ignore_names,
ignore_decorators=ignore_decorators,
)
v.scan(code)
check(v.get_unused_code(), expected)
def test_var():
code = """\
fio = 1
fao = 2
bar = 2
ftobar = 3
baz = 10000
funny = True
"""
check_ignore(code, ["f?o*", "ba[rz]"], [], ["funny"])
def test_function():
code = """\
def foo_one():
pass
def foo_two():
pass
def foo():
pass
def bar():
pass
"""
check_ignore(code, ["foo*"], [], ["bar"])
def test_async_function():
code = """\
async def foobar():
pass
async def bar():
pass
"""
check_ignore(code, ["foo*"], [], ["bar"])
def test_class():
code = """\
class Foo:
def __init__(self):
pass
"""
check_ignore(code, ["Foo"], [], [])
def test_class_ignore():
code = """\
@bar
class Foo:
pass
class Bar:
pass
"""
check_ignore(code, [], [], ["Foo", "Bar"])
def test_property():
code = """\
class Foo:
@property
def some_property(self, a):
return a
@property
@bar
def foo_bar(self):
return 'bar'
"""
check_ignore(code, ["Foo"], ["@property"], [])
check_ignore(code, ["Foo"], [], ["some_property", "foo_bar"])
def test_attribute():
code = """\
class Foo:
def __init__(self, attr_foo, attr_bar):
self._attr_foo = attr_foo
self._attr_bar = attr_bar
"""
check_ignore(code, ["foo", "*_foo"], [], ["Foo", "_attr_bar"])
def test_decorated_functions():
code = """\
def decor():
return help
class FooBar:
def foobar(self):
return help
@property
def prop_one(self):
pass
f = FooBar()
@decor()
def bar():
pass
@f.foobar
def foo():
pass
@bar
@foo
@f.foobar()
def barfoo():
pass
"""
check_ignore(code, [], ["@decor", "*@f.foobar"], ["prop_one"])
check_ignore(code, [], ["*decor", "@*f.foobar"], ["prop_one"])
def test_decorated_async_functions():
code = """\
@app.route('something')
@foobar
async def async_function():
pass
@a.b.c
async def foo():
pass
"""
check_ignore(code, [], ["@app.route", "@a.b"], ["foo"])
def test_decorated_property():
code = """\
@bar
@property
def foo():
pass
"""
check_ignore(code, [], ["@bar"], [])
check_ignore(code, [], ["@baz"], ["foo"])
check_ignore(code, [], ["@property"], [])
def test_decorated_property_reversed():
code = """\
@property
@bar
def foo():
pass
"""
check_ignore(code, [], ["@bar"], [])
check_ignore(code, [], ["@property"], [])
check_ignore(code, [], ["@b*r"], [])
check_ignore(code, [], ["@barfoo"], ["foo"])
def test_decorated_class():
code = """\
@barfoo
@foo.bar('foo')
class Bar:
def __init__(self):
pass
"""
check_ignore(code, [], [], ["Bar"])
check_ignore(code, [], ["@bar*"], [])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/tests/test_imports.py 0000664 0001750 0001750 00000014522 14470671470 017331 0 ustar 00jendrik jendrik from . import check, v
assert v # Silence pyflakes.
def test_import_star(v):
v.scan(
"""\
from a import *
from a.b import *
"""
)
check(v.defined_imports, [])
check(v.unused_imports, [])
def test_import_from_future(v):
v.scan("""from __future__ import division""")
check(v.defined_imports, [])
check(v.unused_imports, [])
def test_double_import(v):
v.scan(
"""\
import foo as bar
import foo
"""
)
check(v.defined_imports, ["bar", "foo"])
# Once the bar import is removed, the foo import will be detected.
check(v.unused_imports, ["bar"])
def test_attribute_access(v):
v.scan(
"""\
# foo.py
class Foo:
pass
# bar.py
from foo import Foo
# main.py
import bar
bar.Foo
"""
)
check(v.defined_imports, ["Foo", "bar"])
check(v.unused_imports, [])
def test_nested_import(v):
v.scan(
"""\
import os.path
os.path.expanduser("~")
"""
)
check(v.defined_imports, ["os"])
check(v.used_names, ["os", "path", "expanduser"])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_vars, [])
definitions = """\
class A(object):
pass
class B(object):
pass
def C():
pass
D = 42
"""
imports = """\
from any_module import A
import B, C
import D
"""
aliased_imports = """\
from any_module import A as AliasA
import B as AliasB, C as AliasC
import D as AliasD
"""
uses = """\
A()
B()
C()
D()
"""
aliased_uses = """\
AliasA()
AliasB()
AliasC()
AliasD()
"""
def test_definitions(v):
v.scan(definitions)
check(v.defined_classes, ["A", "B"])
check(v.defined_funcs, ["C"])
check(v.defined_imports, [])
check(v.defined_vars, ["D"])
check(v.used_names, [])
check(v.unused_classes, ["A", "B"])
check(v.unused_funcs, ["C"])
check(v.unused_imports, [])
check(v.unused_vars, ["D"])
def test_use_original(v):
v.scan(definitions + uses)
check(v.defined_classes, ["A", "B"])
check(v.defined_funcs, ["C"])
check(v.defined_imports, [])
check(v.defined_vars, ["D"])
check(v.used_names, ["A", "B", "C", "D"])
check(v.unused_funcs, [])
check(v.unused_classes, [])
check(v.unused_imports, [])
check(v.unused_vars, [])
def test_import_original(v):
v.scan(definitions + imports)
check(v.defined_classes, ["A", "B"])
check(v.defined_funcs, ["C"])
check(v.defined_imports, ["A", "B", "C", "D"])
check(v.defined_vars, ["D"])
check(v.used_names, [])
check(v.unused_classes, ["A", "B"])
check(v.unused_funcs, ["C"])
check(v.unused_imports, ["A", "B", "C", "D"])
check(v.unused_vars, ["D"])
def test_import_original_use_original(v):
v.scan(definitions + imports + uses)
check(v.defined_classes, ["A", "B"])
check(v.defined_funcs, ["C"])
check(v.defined_imports, ["A", "B", "C", "D"])
check(v.defined_vars, ["D"])
check(v.used_names, ["A", "B", "C", "D"])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_vars, [])
def test_import_original_use_alias(v):
v.scan(definitions + imports + aliased_uses)
check(v.defined_classes, ["A", "B"])
check(v.defined_funcs, ["C"])
check(v.defined_imports, ["A", "B", "C", "D"])
check(v.defined_vars, ["D"])
check(v.used_names, ["AliasA", "AliasB", "AliasC", "AliasD"])
check(v.unused_classes, ["A", "B"])
check(v.unused_funcs, ["C"])
check(v.unused_imports, ["A", "B", "C", "D"])
check(v.unused_vars, ["D"])
def test_import_alias(v):
v.scan(definitions + aliased_imports)
check(v.defined_classes, ["A", "B"])
check(v.defined_funcs, ["C"])
check(v.defined_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
check(v.defined_vars, ["D"])
check(v.used_names, ["A", "B", "C", "D"])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
check(v.unused_vars, [])
def test_import_alias_use_original(v):
v.scan(definitions + aliased_imports + uses)
check(v.defined_classes, ["A", "B"])
check(v.defined_funcs, ["C"])
check(v.defined_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
check(v.defined_vars, ["D"])
check(v.used_names, ["A", "B", "C", "D"])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
check(v.unused_vars, [])
def test_import_alias_use_alias(v):
v.scan(definitions + aliased_imports + aliased_uses)
check(v.defined_classes, ["A", "B"])
check(v.defined_funcs, ["C"])
check(v.defined_imports, ["AliasA", "AliasB", "AliasC", "AliasD"])
check(v.defined_vars, ["D"])
check(
v.used_names,
["A", "B", "C", "D", "AliasA", "AliasB", "AliasC", "AliasD"],
)
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_vars, [])
def test_import_with__all__(v):
v.scan(
"""\
# define.py
class Foo:
pass
class Bar:
pass
# main.py
from define import Foo, Bar
__all__ = ["Foo"]
"""
)
check(v.defined_imports, ["Foo", "Bar"])
check(v.unused_imports, ["Bar"])
def test_import_with__all__normal_reference(v):
v.scan(
"""\
# define.py
class Foo:
pass
class Bar:
pass
# main.py
from define import Foo, Bar
__all__ = [Foo]
"""
)
check(v.defined_imports, ["Foo", "Bar"])
check(v.unused_imports, ["Bar"])
def test_import_with__all__string(v):
v.scan(
"""\
# define.py
class Foo:
pass
class Bar:
pass
# main.py
from define import Foo, Bar
__all__ = "Foo"
"""
)
check(v.defined_imports, ["Foo", "Bar"])
# __all__ is not a list or tuple, so Foo is unused.
check(v.unused_imports, ["Foo", "Bar"])
def test_import_with__all__assign_other_module(v):
v.scan(
"""\
# define.py
class Foo:
pass
class Bar:
pass
# main.py
import define
from define import Foo, Bar
define.__all__ = ["Foo"]
"""
)
check(v.defined_imports, ["define", "Foo", "Bar"])
# Only assignments to __all__ of the current module are covered.
check(v.unused_imports, ["Foo", "Bar"])
def test_ignore_init_py_files(v):
v.scan(
"""\
import bar
from foo import *
from zoo import zebra
unused_var = 'monty'
""",
filename="nested/project/__init__.py",
)
check(v.unused_imports, [])
check(v.unused_vars, ["unused_var"])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/tests/test_item.py 0000664 0001750 0001750 00000003332 14470671470 016567 0 ustar 00jendrik jendrik from . import v
assert v # Silence pyflakes
def test_item_repr(v):
v.scan(
"""\
import os
message = "foobar"
class Foo:
def bar():
pass
"""
)
for item in v.get_unused_code():
assert repr(item) == f"{item.name!r}"
def test_item_attr(v):
v.scan("foo.bar = 'bar'")
assert len(v.unused_attrs) == 1
a = v.unused_attrs[0]
assert a.name == "bar"
assert a.first_lineno == 1
assert a.last_lineno == 1
def test_item_class(v):
v.scan(
"""\
class Foo:
pass
"""
)
assert len(v.unused_classes) == 1
c = v.unused_classes[0]
assert c.name == "Foo"
assert c.first_lineno == 1
assert c.last_lineno == 2
def test_item_function(v):
v.scan(
"""\
def add(a, b):
return a + b
"""
)
assert len(v.unused_funcs) == 1
f = v.unused_funcs[0]
assert f.name == "add"
assert f.first_lineno == 1
assert f.last_lineno == 2
def test_item_import(v):
v.scan(
"""\
import bar
from foo import *
"""
)
assert len(v.unused_imports) == 1
i = v.unused_imports[0]
assert i.name == "bar"
assert i.first_lineno == 1
assert i.last_lineno == 1
def test_item_property(v):
v.scan(
"""\
@awesomify
class Foo:
@property
@wifi(
username='dog',
password='cat',
)
def bar(self):
pass
Foo()
"""
)
assert len(v.unused_props) == 1
p = v.unused_props[0]
assert p.name == "bar"
assert p.first_lineno == 3
assert p.last_lineno == 9
def test_item_variable(v):
v.scan("v = 'Vulture'")
assert len(v.unused_vars) == 1
var = v.unused_vars[0]
assert var.name == "v"
assert var.first_lineno == 1
assert var.last_lineno == 1
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1580740985.0
vulture-2.11/tests/test_make_whitelist.py 0000644 0001750 0001750 00000003017 13616030571 020630 0 ustar 00jendrik jendrik import pytest
from . import check, v
assert v # silence pyflakes
@pytest.fixture
def check_whitelist(v):
def examine(code, results_before, results_after):
v.scan(code)
check(v.get_unused_code(), results_before)
for item in v.get_unused_code():
v.scan(item.get_whitelist_string())
check(v.get_unused_code(), results_after)
return examine
def test_unused_function(check_whitelist):
code = """\
def func():
pass
"""
check_whitelist(code, ["func"], [])
def test_unused_class(check_whitelist):
code = """\
class Foo:
def __init__(self):
pass
"""
check_whitelist(code, ["Foo"], [])
def test_unused_variables(check_whitelist):
code = """\
foo = 'unused'
bar = 'variable'
"""
check_whitelist(code, ["foo", "bar"], [])
def test_unused_import(check_whitelist):
code = """\
import xyz
import foo as bar
from abc import iou
from lorem import ipsum as dolor
"""
check_whitelist(code, ["xyz", "bar", "iou", "dolor"], [])
def test_unused_attribute(check_whitelist):
code = """\
class Foo:
def bar(self):
self.foobar = 'unused attr'
"""
check_whitelist(code, ["Foo", "bar", "foobar"], [])
def test_unused_property(check_whitelist):
code = """\
class Foo:
@property
def bar(self):
pass
"""
check_whitelist(code, ["Foo", "bar"], [])
def test_unreachable_code(check_whitelist):
code = """\
def foo():
return "Foo Bar"
print("Hello")
"""
check_whitelist(code, ["foo", "return"], ["return"])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1591096450.0
vulture-2.11/tests/test_noqa.py 0000644 0001750 0001750 00000014330 13665432202 016556 0 ustar 00jendrik jendrik import pytest
from vulture.core import ERROR_CODES
from vulture.noqa import NOQA_CODE_MAP, NOQA_REGEXP, _parse_error_codes
from . import check, v
assert v # Silence pyflakes.
@pytest.mark.parametrize(
"line, codes",
[
("# noqa", ["all"]),
("## noqa", ["all"]),
("# noqa Hi, go on.", ["all"]),
("# noqa: V101", ["V101"]),
("# noqa: V101, V106", ["V101", "V106"]),
("# NoQA: V101, V103, \t V104", ["V101", "V103", "V104"]),
],
)
def test_noqa_regex_present(line, codes):
match = NOQA_REGEXP.search(line)
parsed = _parse_error_codes(match)
assert parsed == codes
@pytest.mark.parametrize(
"line",
[
("# noqa: 123V"),
("# noqa explanation: V012"),
("# noqa: ,V101"),
("# noqa: #noqa: V102"),
("# noqa: # noqa: V102"),
],
)
def test_noqa_regex_no_groups(line):
assert NOQA_REGEXP.search(line).groupdict()["codes"] is None
@pytest.mark.parametrize(
"line",
[("#noqa"), ("##noqa"), ("# n o q a"), ("#NOQA"), ("# Hello, noqa")],
)
def test_noqa_regex_not_present(line):
assert not NOQA_REGEXP.search(line)
def test_noqa_without_codes(v):
v.scan(
"""\
import this # noqa
@underground # noqa
class Cellar:
@property # noqa
def wine(self):
grapes = True # noqa
@without_ice # noqa
def serve(self, quantity=50):
self.quantity_served = quantity # noqa
return
self.pour() # noqa
"""
)
check(v.unused_attrs, [])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_props, [])
check(v.unreachable_code, [])
check(v.unused_vars, [])
def test_noqa_specific_issue_codes(v):
v.scan(
"""\
import this # noqa: V104
@underground # noqa: V102
class Cellar:
@property # noqa: V106
def wine(self):
grapes = True # noqa: V107
@without_ice # noqa: V103
def serve(self, quantity=50):
self.quantity_served = quantity # noqa: V101
return
self.pour() # noqa: V201
"""
)
check(v.unused_attrs, [])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_imports, [])
check(v.unused_methods, ["serve"])
check(v.unused_props, [])
check(v.unreachable_code, [])
check(v.unused_vars, [])
def test_noqa_attributes(v):
v.scan(
"""\
something.x = 'x' # noqa: V101
something.z = 'z' # noqa: V107 (code for unused variable)
something.u = 'u' # noqa
"""
)
check(v.unused_attrs, ["z"])
def test_noqa_classes(v):
v.scan(
"""\
class QtWidget: # noqa: V102
pass
class ABC(QtWidget):
pass # noqa: V102 (should not ignore)
class DEF: # noqa
pass
"""
)
check(v.unused_classes, ["ABC"])
def test_noqa_functions(v):
v.scan(
"""\
def play(tune, instrument='bongs', _hz='50'): # noqa: V103
pass
# noqa
def problems(): # noqa: V104
pass # noqa: V103
def hello(name): # noqa
print("Hello")
"""
)
check(v.unused_funcs, ["problems"])
check(v.unused_vars, ["instrument", "tune"])
def test_noqa_imports(v):
v.scan(
"""\
import foo
import this # noqa: V104
import zoo
from koo import boo # noqa
from me import *
import dis # noqa: V101 (code for unused attr)
"""
)
check(v.unused_imports, ["foo", "zoo", "dis"])
def test_noqa_properties(v):
v.scan(
"""\
class Zoo:
@property
def no_of_koalas(self): # noqa
pass
@property
def area(self, width, depth): # noqa: V105
pass
@property # noqa
def entry_gates(self):
pass
@property # noqa: V103 (code for unused function)
def tickets(self):
pass
"""
)
check(v.unused_props, ["no_of_koalas", "area", "tickets"])
check(v.unused_classes, ["Zoo"])
check(v.unused_vars, ["width", "depth"])
def test_noqa_multiple_decorators(v):
v.scan(
"""\
@bar # noqa: V102
class Foo:
@property # noqa: V106
@make_it_cool
@log
def something(self):
pass
@coolify
@property
def something_else(self): # noqa: V106
pass
@a
@property
@b # noqa
def abcd(self):
pass
"""
)
check(v.unused_props, ["something_else", "abcd"])
check(v.unused_classes, [])
def test_noqa_unreacahble_code(v):
v.scan(
"""\
def shave_sheep(sheep):
for a_sheep in sheep:
if a_sheep.is_bald:
continue
a_sheep.grow_hair() # noqa: V201
a_sheep.shave()
return
for a_sheep in sheep: # noqa: V201
if a_sheep.still_has_hair:
a_sheep.shave_again()
"""
)
check(v.unreachable_code, [])
check(v.unused_funcs, ["shave_sheep"])
def test_noqa_variables(v):
v.scan(
"""\
mitsi = "Mother" # noqa: V107
harry = "Father" # noqa
shero = "doggy" # noqa: V101, V104 (code for unused import, attr)
shinchan.friend = ['masao'] # noqa: V107 (code for unused variable)
"""
)
check(v.unused_vars, ["shero"])
check(v.unused_attrs, ["friend"])
def test_noqa_with_multiple_issue_codes(v):
v.scan(
"""\
def world(axis): # noqa: V103, V201
pass
for _ in range(3):
continue
xyz = hello(something, else): # noqa: V201, V107
"""
)
check(v.get_unused_code(), [])
def test_noqa_on_empty_line(v):
v.scan(
"""\
# noqa
import this
# noqa
"""
)
check(v.unused_imports, ["this"])
def test_noqa_with_invalid_codes(v):
v.scan(
"""\
import this # V098, A123, F876
"""
)
check(v.unused_imports, ["this"])
@pytest.mark.parametrize(
"first_file, second_file",
[
("foo = None", "bar = None # noqa"),
("bar = None # noqa", "foo = None"),
],
)
def test_noqa_multiple_files(first_file, second_file, v):
v.scan(first_file, filename="first_file.py")
v.scan(second_file, filename="second_file.py")
check(v.unused_vars, ["foo"])
def test_flake8_noqa_codes(v):
assert NOQA_CODE_MAP["F401"] == ERROR_CODES["import"]
assert NOQA_CODE_MAP["F841"] == ERROR_CODES["variable"]
v.scan(
"""\
import this # noqa: F401
def foo():
bar = 2 # noqa: F841
"""
)
check(v.unused_funcs, ["foo"])
check(v.unused_imports, [])
check(v.unused_vars, [])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/tests/test_report.py 0000664 0001750 0001750 00000004012 14470671470 017140 0 ustar 00jendrik jendrik import pytest
from . import v
assert v # Silence pyflakes
mock_code = """\
import foo
class Foo:
def __init__(self):
print("Initialized foo")
def bar(self):
self.foobar = "unused attribute"
foobar = "unused variable"
日本人 = "unused variable"
return
print("unreachable")
@property
def myprop(self):
pass
def myfunc():
pass
"""
@pytest.fixture
def check_report(v, capsys):
def test_report(code, expected, make_whitelist=False):
filename = "foo.py"
v.scan(code, filename=filename)
capsys.readouterr()
ret = v.report(make_whitelist=make_whitelist)
assert ret
assert capsys.readouterr().out == expected.format(filename=filename)
return test_report
def test_logging(v, capsys):
expected = "\u65e5\u672c\u4eba\xc0\n"
v._log("日本人À")
assert capsys.readouterr().out == expected
def test_item_report(check_report):
expected = """\
{filename}:1: unused import 'foo' (90% confidence)
{filename}:3: unused class 'Foo' (60% confidence)
{filename}:7: unused method 'bar' (60% confidence)
{filename}:8: unused attribute 'foobar' (60% confidence)
{filename}:9: unused variable 'foobar' (60% confidence)
{filename}:10: unused variable '\u65e5\u672c\u4eba' (60% confidence)
{filename}:12: unreachable code after 'return' (100% confidence)
{filename}:14: unused property 'myprop' (60% confidence)
{filename}:18: unused function 'myfunc' (60% confidence)
"""
check_report(mock_code, expected)
def test_make_whitelist(check_report):
expected = """\
foo # unused import ({filename}:1)
Foo # unused class ({filename}:3)
_.bar # unused method ({filename}:7)
_.foobar # unused attribute ({filename}:8)
foobar # unused variable ({filename}:9)
\u65e5\u672c\u4eba # unused variable ({filename}:10)
# unreachable code after 'return' ({filename}:12)
_.myprop # unused property ({filename}:14)
myfunc # unused function ({filename}:18)
"""
check_report(mock_code, expected, make_whitelist=True)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1693468535.0
vulture-2.11/tests/test_scavenging.py 0000664 0001750 0001750 00000036230 14474043567 017764 0 ustar 00jendrik jendrik import sys
import pytest
from . import check, v
from vulture.utils import ExitCode
assert v # Silence pyflakes.
def test_function_object1(v):
v.scan(
"""\
def func():
pass
a = func
"""
)
check(v.defined_funcs, ["func"])
check(v.unused_funcs, [])
def test_function_object2(v):
v.scan(
"""\
def func():
pass
func
"""
)
check(v.defined_funcs, ["func"])
check(v.unused_funcs, [])
def test_function1(v):
v.scan(
"""\
def func1(a):
pass
def func2(b):
func1(b)
"""
)
check(v.defined_funcs, ["func1", "func2"])
check(v.unused_funcs, ["func2"])
def test_function2(v):
v.scan(
"""\
def func(a):
pass
func(5)
"""
)
check(v.unused_funcs, [])
check(v.defined_funcs, ["func"])
def test_function3(v):
v.scan(
"""\
def foo(a):
pass
b = foo(5)
"""
)
check(v.unused_funcs, [])
check(v.defined_funcs, ["foo"])
def test_async_function(v):
v.scan(
"""\
async def foo():
pass
"""
)
check(v.defined_funcs, ["foo"])
check(v.unused_funcs, ["foo"])
def test_async_method(v):
v.scan(
"""\
class Foo:
async def bar(self):
pass
"""
)
check(v.defined_classes, ["Foo"])
check(v.defined_funcs, [])
check(v.defined_methods, ["bar"])
check(v.unused_classes, ["Foo"])
check(v.unused_methods, ["bar"])
def test_function_and_method1(v):
v.scan(
"""\
class Bar(object):
def func(self):
pass
def func():
pass
func()
"""
)
check(v.defined_classes, ["Bar"])
check(v.defined_funcs, ["func"])
check(v.defined_methods, ["func"])
check(v.unused_classes, ["Bar"])
check(v.unused_funcs, [])
# Bar.func is unused, but it's hard to detect this without producing a
# false positive in test_function_and_method2.
check(v.unused_methods, [])
def test_function_and_method2(v):
v.scan(
"""\
class Bar(object):
def func(self):
pass
other_name_for_func = func
Bar().other_name_for_func()
"""
)
check(v.defined_classes, ["Bar"])
check(v.defined_funcs, [])
check(v.defined_methods, ["func"])
check(v.defined_vars, ["other_name_for_func"])
check(v.unused_classes, [])
check(v.unused_funcs, [])
check(v.unused_methods, [])
check(v.unused_vars, [])
def test_attribute1(v):
v.scan(
"""\
foo.bar = 1
foo.bar = 2
"""
)
check(v.unused_funcs, [])
check(v.defined_funcs, [])
check(v.defined_attrs, ["bar", "bar"])
check(v.used_names, ["foo"])
check(v.unused_attrs, ["bar", "bar"])
def test_ignored_attributes(v):
v.scan(
"""\
A._ = 0
A._a = 1
A.__b = 2
A.__c__ = 3
A._d_ = 4
"""
)
check(v.defined_attrs, ["_", "_a", "__b", "__c__", "_d_"])
check(v.used_names, ["A"])
check(v.unused_attrs, ["_", "__b", "__c__", "_a", "_d_"])
check(v.unused_vars, [])
def test_getattr(v):
v.scan(
"""\
class Thing:
used_attr1 = 1
used_attr2 = 2
used_attr3 = 3
unused_attr = 4
getattr(Thing, "used_attr1")
getattr(Thing, "used_attr2", None)
hasattr(Thing, "used_attr3")
# Weird calls ignored
hasattr(Thing, "unused_attr", None)
getattr(Thing)
getattr("unused_attr")
getattr(Thing, "unused_attr", 1, 2)
"""
)
check(v.unused_vars, ["unused_attr"])
check(
v.used_names,
[
"Thing",
"getattr",
"hasattr",
"used_attr1",
"used_attr2",
"used_attr3",
],
)
def test_callback1(v):
v.scan(
"""\
class Bar(object):
def foo(self):
pass
b = Bar()
b.foo
"""
)
check(v.used_names, ["Bar", "b", "foo"])
check(v.defined_classes, ["Bar"])
check(v.defined_funcs, [])
check(v.defined_methods, ["foo"])
check(v.unused_classes, [])
check(v.unused_funcs, [])
def test_class1(v):
v.scan(
"""\
class Bar(object):
pass
"""
)
check(v.used_names, [])
check(v.defined_classes, ["Bar"])
check(v.unused_classes, ["Bar"])
def test_class2(v):
v.scan(
"""\
class Bar():
pass
class Foo(Bar):
pass
Foo()
"""
)
check(v.used_names, ["Bar", "Foo"])
check(v.defined_classes, ["Bar", "Foo"])
check(v.unused_classes, [])
def test_class3(v):
v.scan(
"""\
class Bar():
pass
[Bar]
"""
)
check(v.used_names, ["Bar"])
check(v.defined_classes, ["Bar"])
check(v.unused_classes, [])
def test_class4(v):
v.scan(
"""\
class Bar():
pass
Bar()
"""
)
check(v.used_names, ["Bar"])
check(v.defined_classes, ["Bar"])
check(v.unused_classes, [])
def test_class5(v):
v.scan(
"""\
class Bar():
pass
b = Bar()
"""
)
check(v.used_names, ["Bar"])
check(v.defined_classes, ["Bar"])
check(v.unused_classes, [])
check(v.unused_vars, ["b"])
def test_class6(v):
v.scan(
"""\
class Bar():
pass
a = []
a.insert(0, Bar())
"""
)
check(v.defined_classes, ["Bar"])
check(v.unused_classes, [])
def test_class7(v):
v.scan(
"""\
class Bar(object):
pass
class Foo(object):
def __init__(self):
self.b = xyz.Bar(self)
"""
)
check(v.defined_classes, ["Bar", "Foo"])
check(v.unused_classes, ["Foo"])
def test_method1(v):
v.scan(
"""\
def __init__(self):
self.a.foo()
class Bar(object):
def foo(self):
pass
@classmethod
def bar(cls):
pass
@staticmethod
def foobar():
pass
"""
)
check(v.defined_classes, ["Bar"])
check(v.defined_funcs, [])
check(v.defined_methods, ["foo", "bar", "foobar"])
check(v.unused_classes, ["Bar"])
check(v.unused_funcs, [])
check(v.unused_methods, ["bar", "foobar"])
def test_token_types(v):
v.scan(
"""\
a
b = 2
c()
x.d
"""
)
check(v.defined_funcs, [])
check(v.defined_vars, ["b"])
check(v.used_names, ["a", "c", "d", "x"])
check(v.unused_attrs, [])
check(v.unused_funcs, [])
check(v.unused_props, [])
check(v.unused_vars, ["b"])
def test_variable1(v):
v.scan("a = 1\nb = a")
check(v.defined_funcs, [])
check(v.used_names, ["a"])
check(v.defined_vars, ["a", "b"])
check(v.unused_vars, ["b"])
def test_variable2(v):
v.scan("a = 1\nc = b.a")
check(v.defined_funcs, [])
check(v.defined_vars, ["a", "c"])
check(v.used_names, ["a", "b"])
check(v.unused_vars, ["c"])
def test_variable3(v):
v.scan("(a, b), c = (d, e, f)")
check(v.defined_funcs, [])
check(v.defined_vars, ["a", "b", "c"])
check(v.used_names, ["d", "e", "f"])
check(v.unused_vars, ["a", "b", "c"])
def test_variable4(v):
v.scan("for a, b in func(): a")
check(v.defined_funcs, [])
check(v.defined_vars, ["a", "b"])
check(v.used_names, ["a", "func"])
check(v.unused_vars, ["b"])
def test_variable5(v):
v.scan("[a for a, b in func()]")
check(v.defined_vars, ["a", "b"])
check(v.used_names, ["a", "func"])
check(v.unused_vars, ["b"])
def test_ignored_variables(v):
v.scan(
"""\
_ = 0
_a = 1
__b = 2
__c__ = 3
_d_ = 4
"""
)
check(v.defined_vars, ["__b"])
check(sorted(v.used_names), [])
check(v.unused_vars, ["__b"])
def test_prop1(v):
v.scan(
"""\
class Bar(object):
@property
def prop(self):
pass
c = Bar()
c.prop
"""
)
check(v.defined_classes, ["Bar"])
check(v.defined_props, ["prop"])
check(v.unused_classes, [])
check(v.unused_props, [])
def test_prop2(v):
v.scan(
"""\
class Bar(object):
@property
def prop(self):
pass
prop = 1
"""
)
check(v.defined_classes, ["Bar"])
check(v.defined_props, ["prop"])
check(v.defined_vars, ["prop"])
check(v.unused_classes, ["Bar"])
check(v.unused_props, ["prop"])
def test_object_attribute(v):
v.scan(
"""\
class Bar(object):
def __init__(self):
self.a = []
"""
)
check(v.defined_attrs, ["a"])
check(v.defined_classes, ["Bar"])
check(v.defined_vars, [])
check(v.used_names, [])
check(v.unused_attrs, ["a"])
check(v.unused_classes, ["Bar"])
def test_function_names_in_test_file(v):
v.scan(
"""\
def setup_module(module):
module
def teardown_module(module):
module
def setup_function(function):
function
def teardown_function(function):
function
def test_func():
pass
def other_func():
pass
class TestClass:
@classmethod
def setup_class(cls):
cls
@classmethod
def teardown_class(cls):
pass
def setup_method(self, method):
method
def teardown_method(self, method):
pass
class BasicTestCase:
pass
class OtherClass:
pass
""",
filename="dir/test_function_names.py",
)
check(v.defined_attrs, [])
check(v.defined_classes, ["OtherClass"])
check(v.defined_funcs, ["other_func"])
check(v.defined_methods, [])
check(
v.defined_vars,
[
"cls",
"cls",
"function",
"function",
"method",
"method",
"module",
"module",
],
)
check(v.used_names, ["classmethod", "cls", "function", "method", "module"])
check(v.unused_attrs, [])
check(v.unused_classes, ["OtherClass"])
check(v.unused_funcs, ["other_func"])
check(v.unused_methods, [])
check(v.unused_vars, [])
def test_async_function_name_in_test_file(v):
v.scan(
"""\
async def test_func():
pass
async def other_func():
pass
""",
filename="dir/test_function_names.py",
)
check(v.defined_funcs, ["other_func"])
check(v.unused_funcs, ["other_func"])
def test_async_function_name_in_normal_file(v):
v.scan(
"""\
async def test_func():
pass
async def other_func():
pass
""",
filename="dir/function_names.py",
)
check(v.defined_funcs, ["test_func", "other_func"])
check(v.unused_funcs, ["other_func", "test_func"])
def test_function_names_in_normal_file(v):
v.scan(
"""\
def test_func():
pass
def other_func():
pass
class TestClass:
pass
class BasicTestCase:
pass
class OtherClass:
pass
"""
)
check(v.defined_attrs, [])
check(v.defined_classes, ["BasicTestCase", "OtherClass", "TestClass"])
check(v.defined_funcs, ["test_func", "other_func"])
check(v.defined_vars, [])
check(v.used_names, [])
check(v.unused_attrs, [])
check(v.unused_classes, ["BasicTestCase", "OtherClass", "TestClass"])
check(v.unused_funcs, ["other_func", "test_func"])
def test_global_attribute(v):
v.scan(
"""\
# Module foo:
a = 1
if a == 1:
pass
# Module bar:
import foo
foo.a = 2
"""
)
check(v.defined_attrs, ["a"])
check(v.defined_vars, ["a"])
check(v.used_names, ["a", "foo"])
check(v.unused_attrs, [])
def test_boolean(v):
v.scan(
"""\
a = True
a
"""
)
check(v.defined_vars, ["a"])
check(v.used_names, ["a"])
check(v.unused_vars, [])
def test_builtin_types(v):
v.scan(
"""\
a = b
a = 1
a = "s"
a = object
a = False
"""
)
check(v.defined_vars, ["a"] * 5)
check(v.used_names, ["b"])
check(v.unused_vars, ["a"] * 5)
def test_unused_args(v):
v.scan(
"""\
def foo(x, y):
return x + 1
"""
)
check(v.defined_vars, ["x", "y"])
check(v.used_names, ["x"])
check(v.unused_vars, ["y"])
def test_unused_kwargs(v):
v.scan(
"""\
def foo(x, y=3, **kwargs):
return x + 1
"""
)
check(v.defined_vars, ["kwargs", "x", "y"])
check(v.used_names, ["x"])
check(v.unused_vars, ["kwargs", "y"])
def test_unused_kwargs_with_odd_name(v):
v.scan(
"""\
def foo(**bar):
pass
"""
)
check(v.defined_vars, ["bar"])
check(v.used_names, [])
check(v.unused_vars, ["bar"])
def test_unused_vararg(v):
v.scan(
"""\
def foo(*bar):
pass
"""
)
check(v.defined_vars, ["bar"])
check(v.used_names, [])
check(v.unused_vars, ["bar"])
def test_multiple_definition(v):
v.scan(
"""\
a = 1
a = 2
"""
)
check(v.defined_vars, ["a", "a"])
check(v.used_names, [])
check(v.unused_vars, ["a", "a"])
def test_arg_type_annotation(v):
v.scan(
"""\
from typing import Iterable
def f(n: int) -> Iterable[int]:
yield n
"""
)
check(v.unused_vars, [])
check(v.unused_funcs, ["f"])
check(v.unused_imports, [])
def test_var_type_annotation(v):
v.scan(
"""\
from typing import List
x: List[int] = [1]
"""
)
check(v.unused_vars, ["x"])
check(v.unused_funcs, [])
check(v.unused_imports, [])
def test_type_hint_comments(v):
v.scan(
"""\
from typing import Any, Dict, List, Text, Tuple
def plain_function(arg):
# type: (Text) -> None
pass
async def async_function(arg):
# type: (List[int]) -> None
pass
some_var = {} # type: Dict[str, str]
class Thing:
def __init__(self):
self.some_attr = (1, 2) # type: Tuple[int, int]
for x in []: # type: Any
print(x)
"""
)
check(v.unused_imports, [])
assert v.exit_code == ExitCode.NoDeadCode
def test_invalid_type_comment(v):
v.scan(
"""\
def bad():
# type: bogus
pass
bad()
"""
)
assert v.exit_code == ExitCode.InvalidInput
def test_unused_args_with_del(v):
v.scan(
"""\
def foo(a, b, c, d=3):
del c, d
return a + b
foo(1, 2)
"""
)
check(v.defined_funcs, ["foo"])
check(v.defined_vars, ["a", "b", "c", "d"])
check(v.used_names, ["foo", "a", "b", "c", "d"])
check(v.unused_vars, [])
check(v.unused_funcs, [])
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="requires python3.10 or higher"
)
def test_match_class_simple(v):
v.scan(
"""\
from dataclasses import dataclass
@dataclass
class X:
a: int
b: int
c: int
u: int
x = input()
match x:
case X(a=0):
print("a")
case X(b=0, c=0):
print("b c")
"""
)
check(v.defined_classes, ["X"])
check(v.defined_vars, ["a", "b", "c", "u", "x"])
check(v.unused_classes, [])
check(v.unused_vars, ["u"])
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="requires python3.10 or higher"
)
def test_match_class_embedded(v):
v.scan(
"""\
from dataclasses import dataclass
@dataclass
class X:
a: int
b: int
c: int
d: int
e: int
u: int
x = input()
match x:
case X(a=1) | X(b=0):
print("Or")
case [X(c=1), X(d=0)]:
print("Sequence")
case {"k": X(e=1)}:
print("Mapping")
"""
)
check(v.defined_classes, ["X"])
check(v.defined_vars, ["a", "b", "c", "d", "e", "u", "x"])
check(v.unused_classes, [])
check(v.unused_vars, ["u"])
@pytest.mark.skipif(
sys.version_info < (3, 10), reason="requires python3.10 or higher"
)
def test_match_enum(v):
v.scan(
"""\
from enum import Enum
class Color(Enum):
RED = 0
YELLOW = 1
GREEN = 2
BLUE = 3
color = input()
match color:
case Color.RED:
print("Real danger!")
case Color.YELLOW | Color.GREEN:
print("No danger!")
"""
)
check(v.defined_classes, ["Color"])
check(v.defined_vars, ["RED", "YELLOW", "GREEN", "BLUE", "color"])
check(v.unused_classes, [])
check(v.unused_vars, ["BLUE"])
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627993.0
vulture-2.11/tests/test_script.py 0000664 0001750 0001750 00000004227 14470672031 017133 0 ustar 00jendrik jendrik import glob
import os.path
import subprocess
import sys
from . import call_vulture, REPO, WHITELISTS
from vulture.utils import ExitCode
def test_module_with_explicit_whitelists():
assert call_vulture(["vulture/"] + WHITELISTS) == ExitCode.NoDeadCode
def test_module_with_implicit_whitelists():
assert call_vulture(["vulture/"]) == ExitCode.NoDeadCode
def test_module_without_whitelists():
assert (
call_vulture(["vulture/", "--exclude", "whitelists"])
== ExitCode.DeadCode
)
def test_missing_file():
assert call_vulture(["missing.py"]) == ExitCode.InvalidInput
def test_tests():
assert call_vulture(["tests/"]) == ExitCode.NoDeadCode
def test_whitelists_with_python():
for whitelist in WHITELISTS:
assert (
subprocess.call([sys.executable, whitelist], cwd=REPO)
== ExitCode.NoDeadCode
)
def test_pyc():
assert call_vulture(["missing.pyc"]) == 1
def test_sort_by_size():
assert (
call_vulture(["vulture/utils.py", "--sort-by-size"])
== ExitCode.DeadCode
)
def test_min_confidence():
assert (
call_vulture(
[
"vulture/core.py",
"--exclude",
"whitelists",
"--min-confidence",
"100",
]
)
== ExitCode.NoDeadCode
)
def test_exclude():
def get_csv(paths):
return ",".join(os.path.join("vulture", path) for path in paths)
def call_vulture_with_excludes(excludes):
return call_vulture(["vulture/", "--exclude", get_csv(excludes)])
assert (
call_vulture_with_excludes(["core.py", "utils.py"])
== ExitCode.DeadCode
)
assert (
call_vulture_with_excludes(glob.glob("vulture/*.py"))
== ExitCode.NoDeadCode
)
def test_make_whitelist():
assert (
call_vulture(
["vulture/", "--make-whitelist", "--exclude", "whitelists"]
)
== ExitCode.DeadCode
)
assert (
call_vulture(["vulture/", "--make-whitelist"]) == ExitCode.NoDeadCode
)
def test_version():
assert call_vulture(["--version"]) == ExitCode.NoDeadCode
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1693469034.0
vulture-2.11/tests/test_size.py 0000664 0001750 0001750 00000012474 14474044552 016611 0 ustar 00jendrik jendrik import ast
from vulture import lines
def count_lines(node):
"""Estimate the number of lines of the given AST node."""
last_lineno = lines.get_last_line_number(node)
return last_lineno - lines.get_first_line_number(node) + 1
def check_size(example, size):
tree = ast.parse(example)
for node in tree.body:
if isinstance(node, ast.ClassDef) and node.name == "Foo":
assert count_lines(node) == size
break
else:
raise AssertionError('Failed to find top-level class "Foo" in code')
def test_size_basic():
example = """
class Foo:
foo = 1
bar = 2
"""
check_size(example, 3)
def test_size_class():
example = """
class Foo(object):
def bar():
pass
@staticmethod
def func():
if "foo" == "bar":
return "xyz"
import sys
return len(sys.argv)
"""
check_size(example, 10)
def test_size_if_else():
example = """
@identity
class Foo(object):
@identity
@identity
def bar(self):
if "a" == "b":
pass
elif "b" == "c":
pass
else:
pass
"""
size = 11
check_size(example, size)
def test_size_decorated_class():
example = """
@foo
@property
@xoo
class Foo:
def zoo(self):
pass
"""
check_size(example, 6)
def test_size_while():
example = """
class Foo:
while 1:
print(1)
"""
check_size(example, 3)
def test_size_while_else():
example = """
class Foo:
while "b" > "a":
pass
else:
pass
"""
check_size(example, 5)
def test_size_with():
example = """
class Foo:
with open("/dev/null") as f:
f.write("")
"""
check_size(example, 3)
def test_size_try_except_else():
example = """
class Foo:
try:
x = sys.argv[99]
except IndexError:
pass
except Exception:
pass
else:
pass
"""
check_size(example, 9)
def test_size_try_finally():
example = """
class Foo:
try:
1/0
finally:
return 99
"""
check_size(example, 5)
def test_size_try_except():
example = """
class Foo:
try:
foo()
except:
bar()
"""
check_size(example, 5)
def test_size_try_excepts():
example = """
class Foo:
try:
foo()
except IOError:
bar()
except AttributeError:
pass
"""
check_size(example, 7)
def test_size_for():
example = """
class Foo:
for i in range(10):
print(i)
"""
check_size(example, 3)
def test_size_for_else():
example = """
class Foo:
for arg in sys.argv:
print("loop")
else:
print("else")
"""
check_size(example, 5)
def test_size_class_nested():
example = """
class Foo:
class Bar:
pass
"""
check_size(example, 3)
# We currently cannot handle code ending with multiline strings.
def test_size_multi_line_return():
example = """
class Foo:
def foo():
return (
'very'
'long'
'string')
"""
check_size(example, 6)
# We currently cannot handle code ending with comment lines.
def test_size_comment_after_last_line():
example = """
class Foo:
def bar():
# A comment.
pass
# This comment won't be detected.
"""
check_size(example, 4)
def test_size_generator():
example = """
class Foo:
def bar():
yield something
"""
check_size(example, 3)
def test_size_exec():
example = """
class Foo:
exec('a')
"""
check_size(example, 2)
def test_size_print1():
example = """
class Foo:
print(
'foo')
"""
check_size(example, 3)
def test_size_print2():
example = """
class Foo:
print(
'foo',)
"""
check_size(example, 3)
def test_size_return():
example = """
class Foo:
return (True and
False)
"""
check_size(example, 3)
def test_size_import_from():
example = """
class Foo:
from a import b
"""
check_size(example, 2)
def test_size_delete():
example = """
class Foo:
del a[:
foo()]
"""
check_size(example, 3)
def test_size_list_comprehension():
example = """
class Foo:
[a
for a in
b]
"""
check_size(example, 4)
# We currently cannot handle closing brackets on a separate line.
def test_size_list():
example = """
class Foo:
[a, b
]
"""
check_size(example, 3)
def test_size_ellipsis():
example = """
class Foo:
bar[1:2,
...]
"""
check_size(example, 3)
def test_size_starargs():
example = """
class Foo:
def foo():
bar(*a,
b=c)
"""
check_size(example, 4)
# If we add a line break between a and b, the code is too greedy and moves
# down to the slice which has no line numbers. If we took b or c into
# account, the line count would be correct.
def test_size_assign():
example = """
class Foo:
bar = foo(a, b)[c,:]
"""
check_size(example, 2)
def test_size_async_function_def():
example = """
class Foo:
async def foo(some_attr):
pass
"""
check_size(example, 3)
def test_size_async_with():
example = """
class Foo:
async def bar():
async with x:
pass
"""
check_size(example, 4)
def test_size_async_for():
example = """
class Foo:
async def foo():
async for a in b:
pass
"""
check_size(example, 4)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1580740985.0
vulture-2.11/tests/test_sorting.py 0000644 0001750 0001750 00000000650 13616030571 017304 0 ustar 00jendrik jendrik from . import v
assert v # Silence pyflakes
def test_sorting(v):
v.scan(
"""\
def foo():
print("Hello, I am a long function.")
return "World"
def bar():
pass
"""
)
assert [item.name for item in v.get_unused_code(sort_by_size=True)] == [
"bar",
"foo",
]
assert [item.name for item in v.get_unused_code(sort_by_size=False)] == [
"foo",
"bar",
]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1580740985.0
vulture-2.11/tests/test_unreachable.py 0000644 0001750 0001750 00000011762 13616030571 020076 0 ustar 00jendrik jendrik from . import check_unreachable
from . import v
assert v # Silence pyflakes
def test_return_assignment(v):
v.scan(
"""\
def foo():
print("Hello World")
return
a = 1
"""
)
check_unreachable(v, 4, 1, "return")
def test_return_multiline_return_statements(v):
v.scan(
"""\
def foo():
print("Something")
return (something,
that,
spans,
over,
multiple,
lines)
print("Hello World")
"""
)
check_unreachable(v, 9, 1, "return")
def test_return_multiple_return_statements(v):
v.scan(
"""\
def foo():
return something
return None
return (some, statement)
"""
)
check_unreachable(v, 3, 2, "return")
def test_return_pass(v):
v.scan(
"""\
def foo():
return
pass
return something
"""
)
check_unreachable(v, 3, 2, "return")
def test_return_multiline_return(v):
v.scan(
"""
def foo():
return \
"Hello"
print("Unreachable code")
"""
)
check_unreachable(v, 4, 1, "return")
def test_return_recursive_functions(v):
v.scan(
"""\
def foo(a):
if a == 1:
return 1
else:
return foo(a - 1)
print("This line is never executed")
"""
)
check_unreachable(v, 6, 1, "return")
def test_return_semicolon(v):
v.scan(
"""\
def foo():
return; a = 1
"""
)
check_unreachable(v, 2, 1, "return")
def test_return_list(v):
v.scan(
"""\
def foo(a):
return
a[1:2]
"""
)
check_unreachable(v, 3, 1, "return")
def test_return_continue(v):
v.scan(
"""\
def foo():
if foo():
return True
continue
else:
return False
"""
)
check_unreachable(v, 4, 1, "return")
def test_raise_assignment(v):
v.scan(
"""\
def foo():
raise ValueError
li = []
"""
)
check_unreachable(v, 3, 1, "raise")
def test_multiple_raise_statements(v):
v.scan(
"""\
def foo():
a = 1
raise
raise KeyError
# a comment
b = 2
raise CustomDefinedError
"""
)
check_unreachable(v, 4, 4, "raise")
def test_return_with_raise(v):
v.scan(
"""\
def foo():
a = 1
return
raise ValueError
return
"""
)
check_unreachable(v, 4, 2, "return")
def test_return_comment_and_code(v):
v.scan(
"""\
def foo():
return
# This is a comment
print("Hello World")
"""
)
check_unreachable(v, 4, 1, "return")
def test_raise_with_return(v):
v.scan(
"""\
def foo():
a = 1
raise
return a
"""
)
check_unreachable(v, 4, 1, "raise")
def test_raise_error_message(v):
v.scan(
"""\
def foo():
raise SomeError("There is a problem")
print("I am unreachable")
"""
)
check_unreachable(v, 3, 1, "raise")
def test_raise_try_except(v):
v.scan(
"""\
def foo():
try:
a = 1
raise
except IOError as e:
print("We have some problem.")
raise
print(":-(")
"""
)
check_unreachable(v, 8, 1, "raise")
def test_raise_with_comment_and_code(v):
v.scan(
"""\
def foo():
raise
# This is a comment
print("Something")
return None
"""
)
check_unreachable(v, 4, 2, "raise")
def test_continue_basic(v):
v.scan(
"""\
def foo():
if bar():
a = 1
else:
continue
a = 2
"""
)
check_unreachable(v, 6, 1, "continue")
def test_continue_one_liner(v):
v.scan(
"""\
def foo():
for i in range(1, 10):
if i == 5: continue
print(1 / i)
"""
)
assert v.unreachable_code == []
def test_continue_nested_loops(v):
v.scan(
"""\
def foo():
a = 0
if something():
foo()
if bar():
a = 2
continue
# This is unreachable
a = 1
elif a == 1:
pass
else:
a = 3
continue
else:
continue
"""
)
check_unreachable(v, 9, 1, "continue")
def test_continue_with_comment_and_code(v):
v.scan(
"""\
def foo():
if bar1():
bar2()
else:
a = 1
continue
# Just a comment
raise ValueError
"""
)
check_unreachable(v, 8, 1, "continue")
def test_break_basic(v):
v.scan(
"""\
def foo():
for i in range(123):
break
# A comment
return
dead = 1
"""
)
check_unreachable(v, 5, 2, "break")
def test_break_one_liner(v):
v.scan(
"""\
def foo():
for i in range(10):
if i == 3: break
print(i)
"""
)
assert v.unreachable_code == []
def test_break_with_comment_and_code(v):
v.scan(
"""\
while True:
break
# some comment
print("Hello")
"""
)
check_unreachable(v, 4, 1, "break")
def test_while_true_else(v):
v.scan(
"""\
while True:
print("I won't stop")
else:
print("I won't run")
"""
)
check_unreachable(v, 4, 1, "else")
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/tests/test_utils.py 0000664 0001750 0001750 00000006725 14470671470 017002 0 ustar 00jendrik jendrik import ast
import os
import pathlib
import sys
import pytest
from vulture import utils
class TestFormatPath:
@pytest.fixture
def tmp_cwd(self, tmp_path, monkeypatch):
cwd = tmp_path / "workingdir"
cwd.mkdir()
monkeypatch.chdir(cwd)
return cwd
def test_relative_inside(self):
filepath = pathlib.Path("testfile.py")
formatted = utils.format_path(filepath)
assert formatted == filepath
assert not formatted.is_absolute()
def test_relative_outside(self, tmp_cwd):
filepath = pathlib.Path(os.pardir) / "testfile.py"
formatted = utils.format_path(filepath)
assert formatted == filepath
assert not formatted.is_absolute()
def test_absolute_inside(self, tmp_cwd):
filepath = tmp_cwd / "testfile.py"
formatted = utils.format_path(filepath)
assert formatted == pathlib.Path("testfile.py")
assert not formatted.is_absolute()
def test_absolute_outside(self, tmp_cwd):
filepath = (tmp_cwd / os.pardir / "testfile.py").resolve()
formatted = utils.format_path(filepath)
assert formatted == filepath
assert formatted.is_absolute()
def check_decorator_names(code, expected_names):
decorator_names = []
def visit_FunctionDef(node):
for decorator in node.decorator_list:
decorator_names.append(utils.get_decorator_name(decorator))
node_visitor = ast.NodeVisitor()
node_visitor.visit_AsyncFunctionDef = visit_FunctionDef
node_visitor.visit_ClassDef = visit_FunctionDef
node_visitor.visit_FunctionDef = visit_FunctionDef
node_visitor.visit(ast.parse(code))
assert expected_names == decorator_names
def test_get_decorator_name_simple():
code = """\
@foobar
def hoo():
pass
"""
check_decorator_names(code, ["@foobar"])
def test_get_decorator_name_call():
code = """\
@xyz()
def bar():
pass
"""
check_decorator_names(code, ["@xyz"])
def test_get_decorator_name_async():
code = """\
@foo.bar.route('/foobar')
async def async_function(request):
print(request)
"""
check_decorator_names(code, ["@foo.bar.route"])
def test_get_decorator_name_multiple_attrs():
code = """\
@x.y.z
def doo():
pass
"""
check_decorator_names(code, ["@x.y.z"])
def test_get_decorator_name_multiple_attrs_called():
code = """\
@a.b.c.d.foo("Foo and Bar")
def hoofoo():
pass
"""
check_decorator_names(code, ["@a.b.c.d.foo"])
def test_get_decorator_name_multiple_decorators():
code = """\
@foo
@bar()
@x.y.z.a('foobar')
def func():
pass
"""
check_decorator_names(code, ["@foo", "@bar", "@x.y.z.a"])
def test_get_decorator_name_class():
code = """\
@foo
@bar.yz
class Foo:
pass
"""
check_decorator_names(code, ["@foo", "@bar.yz"])
def test_get_decorator_name_end_function_call():
code = """\
@foo.bar(x, y, z)
def bar():
pass
"""
check_decorator_names(code, ["@foo.bar"])
@pytest.mark.skipif(
sys.version_info < (3, 9), reason="requires Python 3.9 or higher"
)
@pytest.mark.parametrize(
"decorated",
[
("def foo():"),
("async def foo():"),
("class Foo:"),
],
)
def test_get_decorator_name_multiple_callables(decorated):
decorated = f"{decorated}\n pass"
code = f"""\
@foo
@bar.prop
@z.func("hi").bar().k.foo
@k("hello").doo("world").x
@k.hello("world")
@foo[2]
{decorated}
"""
check_decorator_names(
code,
["@foo", "@bar.prop", "@", "@", "@k.hello", "@"],
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704114798.0
vulture-2.11/tox.ini 0000664 0001750 0001750 00000002450 14544535156 014373 0 ustar 00jendrik jendrik [tox]
envlist = cleanup, py{38,310,311,312}, style # Skip py39 since it chokes on distutils.
skip_missing_interpreters = true
# Erase old coverage results, then accumulate them during this tox run.
[testenv:cleanup]
deps =
coverage==7.0.5
commands =
coverage erase
[testenv]
deps =
coverage==7.0.5
pint # Use latest version to catch API changes.
pytest==7.4.2
pytest-cov==4.0.0
commands =
pytest {posargs}
# Install package as wheel in all envs (https://hynek.me/articles/turbo-charge-tox/).
package = wheel
wheel_build_env = .pkg
[pytest]
filterwarnings =
error::DeprecationWarning
error::PendingDeprecationWarning
[testenv:style]
basepython = python3
deps =
black==22.3.0
flake8==6.1.0
flake8-2020==1.7.0
flake8-bugbear==23.9.16
flake8-comprehensions==3.14.0
pyupgrade==2.28.0
allowlist_externals =
bash
commands =
black --check --diff .
# B028: use !r conversion flag
# C408: unnecessary dict call
flake8 --extend-ignore=B028,C408 setup.py tests/ vulture/
bash -c "pyupgrade --py38-plus `find dev/ tests/ vulture/ -name '*.py'` setup.py"
[testenv:fix-style]
basepython = python3
deps =
black==22.3.0
pyupgrade==2.28.0
allowlist_externals =
bash
commands =
black .
bash -c "pyupgrade --py38-plus --exit-zero `find dev/ tests/ vulture/ -name '*.py'` setup.py"
././@PaxHeader 0000000 0000000 0000000 00000000033 00000000000 010211 x ustar 00 27 mtime=1704568248.975953
vulture-2.11/vulture/ 0000775 0001750 0001750 00000000000 14546322671 014563 5 ustar 00jendrik jendrik ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1597828171.0
vulture-2.11/vulture/__init__.py 0000644 0001750 0001750 00000000154 13717166113 016666 0 ustar 00jendrik jendrik from vulture.core import Vulture
from vulture.version import __version__
assert __version__
assert Vulture
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1530355315.0
vulture-2.11/vulture/__main__.py 0000644 0001750 0001750 00000000046 13315657163 016653 0 ustar 00jendrik jendrik from vulture.core import main
main()
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704114798.0
vulture-2.11/vulture/config.py 0000664 0001750 0001750 00000015201 14544535156 016403 0 ustar 00jendrik jendrik """
This module handles retrieval of configuration values from either the
command-line arguments or the pyproject.toml file.
"""
import argparse
import pathlib
try:
import tomllib
except ModuleNotFoundError:
import tomli as tomllib
from .version import __version__
#: Possible configuration options and their respective defaults
DEFAULTS = {
"min_confidence": 0,
"paths": [],
"exclude": [],
"ignore_decorators": [],
"ignore_names": [],
"make_whitelist": False,
"sort_by_size": False,
"verbose": False,
}
class InputError(Exception):
def __init__(self, message):
self.message = message
def _check_input_config(data):
"""
Checks the types of the values in *data* against the expected types of
config-values. If a value has the wrong type, raise an InputError.
"""
for key, value in data.items():
if key not in DEFAULTS:
raise InputError(f"Unknown configuration key: {key}")
# The linter suggests to use "isinstance" here but this fails to
# detect the difference between `int` and `bool`.
if type(value) is not type(DEFAULTS[key]): # noqa: E721
expected_type = type(DEFAULTS[key]).__name__
raise InputError(f"Data type for {key} must be {expected_type!r}")
def _check_output_config(config):
"""
Run sanity checks on the generated config after all parsing and
preprocessing is done.
Raise InputError if an error is encountered.
"""
if not config["paths"]:
raise InputError("Please pass at least one file or directory")
def _parse_toml(infile):
"""
Parse a TOML file for config values.
It will search for a section named ``[tool.vulture]`` which contains the
same keys as the CLI arguments seen with ``--help``. All leading dashes are
removed and other dashes are replaced by underscores (so ``--sort-by-size``
becomes ``sort_by_size``).
Arguments containing multiple values are standard TOML lists.
Example::
[tool.vulture]
exclude = ["file*.py", "dir/"]
ignore_decorators = ["deco1", "deco2"]
ignore_names = ["name1", "name2"]
make_whitelist = true
min_confidence = 10
sort_by_size = true
verbose = true
paths = ["path1", "path2"]
"""
data = tomllib.load(infile)
settings = data.get("tool", {}).get("vulture", {})
_check_input_config(settings)
return settings
def _parse_args(args=None):
"""
Parse CLI arguments.
:param args: A list of strings representing the CLI arguments. If left to
the default, this will default to ``sys.argv``.
"""
# Sentinel value to distinguish between "False" and "no default given".
missing = object()
def csv(exclude):
return exclude.split(",")
usage = "%(prog)s [options] [PATH ...]"
version = f"vulture {__version__}"
glob_help = "Patterns may contain glob wildcards (*, ?, [abc], [!abc])."
parser = argparse.ArgumentParser(prog="vulture", usage=usage)
parser.add_argument(
"paths",
nargs="*",
metavar="PATH",
default=missing,
help="Paths may be Python files or directories. For each directory"
" Vulture analyzes all contained *.py files.",
)
parser.add_argument(
"--exclude",
metavar="PATTERNS",
type=csv,
default=missing,
help=f"Comma-separated list of path patterns to ignore (e.g.,"
f' "*settings.py,docs,*/test_*.py,venv"). {glob_help} A PATTERN'
f" without glob wildcards is treated as *PATTERN*. Patterns are"
f" matched against absolute paths.",
)
parser.add_argument(
"--ignore-decorators",
metavar="PATTERNS",
type=csv,
default=missing,
help=f"Comma-separated list of decorators. Functions and classes using"
f' these decorators are ignored (e.g., "@app.route,@require_*").'
f" {glob_help}",
)
parser.add_argument(
"--ignore-names",
metavar="PATTERNS",
type=csv,
default=missing,
help=f'Comma-separated list of names to ignore (e.g., "visit_*,do_*").'
f" {glob_help}",
)
parser.add_argument(
"--make-whitelist",
action="store_true",
default=missing,
help="Report unused code in a format that can be added to a"
" whitelist module.",
)
parser.add_argument(
"--min-confidence",
type=int,
default=missing,
help="Minimum confidence (between 0 and 100) for code to be"
" reported as unused.",
)
parser.add_argument(
"--sort-by-size",
action="store_true",
default=missing,
help="Sort unused functions and classes by their lines of code.",
)
parser.add_argument(
"-v", "--verbose", action="store_true", default=missing
)
parser.add_argument("--version", action="version", version=version)
namespace = parser.parse_args(args)
cli_args = {
key: value
for key, value in vars(namespace).items()
if value is not missing
}
_check_input_config(cli_args)
return cli_args
def make_config(argv=None, tomlfile=None):
"""
Returns a config object for vulture, merging both ``pyproject.toml`` and
CLI arguments (CLI arguments have precedence).
:param argv: The CLI arguments to be parsed. This value is transparently
passed through to :py:meth:`argparse.ArgumentParser.parse_args`.
:param tomlfile: An IO instance containing TOML data. By default this will
auto-detect an existing ``pyproject.toml`` file and exists solely for
unit-testing.
"""
# Parse CLI first to skip sanity checks when --version or --help is given.
cli_config = _parse_args(argv)
# If we loaded data from a TOML file, we want to print this out on stdout
# in verbose mode so we need to keep the value around.
detected_toml_path = ""
if tomlfile:
config = _parse_toml(tomlfile)
detected_toml_path = str(tomlfile)
else:
toml_path = pathlib.Path("pyproject.toml").resolve()
if toml_path.is_file():
with open(toml_path, "rb") as fconfig:
config = _parse_toml(fconfig)
detected_toml_path = str(toml_path)
else:
config = {}
# Overwrite TOML options with CLI options, if given.
config.update(cli_config)
# Set defaults for missing options.
for key, value in DEFAULTS.items():
config.setdefault(key, value)
if detected_toml_path and config["verbose"]:
print(f"Reading configuration from {detected_toml_path}")
_check_output_config(config)
return config
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704566926.0
vulture-2.11/vulture/core.py 0000664 0001750 0001750 00000055774 14546320216 016100 0 ustar 00jendrik jendrik import ast
from fnmatch import fnmatch, fnmatchcase
from pathlib import Path
import pkgutil
import re
import string
import sys
from vulture import lines
from vulture import noqa
from vulture import utils
from vulture.config import InputError, make_config
from vulture.utils import ExitCode
DEFAULT_CONFIDENCE = 60
IGNORED_VARIABLE_NAMES = {"object", "self"}
PYTEST_FUNCTION_NAMES = {
"setup_module",
"teardown_module",
"setup_function",
"teardown_function",
}
PYTEST_METHOD_NAMES = {
"setup_class",
"teardown_class",
"setup_method",
"teardown_method",
}
ERROR_CODES = {
"attribute": "V101",
"class": "V102",
"function": "V103",
"import": "V104",
"method": "V105",
"property": "V106",
"variable": "V107",
"unreachable_code": "V201",
}
def _get_unused_items(defined_items, used_names):
unused_items = [
item for item in set(defined_items) if item.name not in used_names
]
unused_items.sort(key=lambda item: item.name.lower())
return unused_items
def _is_special_name(name):
return name.startswith("__") and name.endswith("__")
def _match(name, patterns, case=True):
func = fnmatchcase if case else fnmatch
return any(func(name, pattern) for pattern in patterns)
def _is_test_file(filename):
return _match(
filename.resolve(),
["*/test/*", "*/tests/*", "*/test*.py", "*[-_]test.py"],
case=False,
)
def _assigns_special_variable__all__(node):
assert isinstance(node, ast.Assign)
return isinstance(node.value, (ast.List, ast.Tuple)) and any(
target.id == "__all__"
for target in node.targets
if isinstance(target, ast.Name)
)
def _ignore_class(filename, class_name):
return _is_test_file(filename) and "Test" in class_name
def _ignore_import(filename, import_name):
"""
Ignore star-imported names since we can't detect whether they are used.
Ignore imports from __init__.py files since they're commonly used to
collect objects from a package.
"""
return filename.name == "__init__.py" or import_name == "*"
def _ignore_function(filename, function_name):
return (
function_name in PYTEST_FUNCTION_NAMES
or function_name.startswith("test_")
) and _is_test_file(filename)
def _ignore_method(filename, method_name):
return _is_special_name(method_name) or (
(method_name in PYTEST_METHOD_NAMES or method_name.startswith("test_"))
and _is_test_file(filename)
)
def _ignore_variable(filename, varname):
"""
Ignore _ (Python idiom), _x (pylint convention) and
__x__ (special variable or method), but not __x.
"""
return (
varname in IGNORED_VARIABLE_NAMES
or (varname.startswith("_") and not varname.startswith("__"))
or _is_special_name(varname)
)
class Item:
"""
Hold the name, type and location of defined code.
"""
__slots__ = (
"name",
"typ",
"filename",
"first_lineno",
"last_lineno",
"message",
"confidence",
)
def __init__(
self,
name,
typ,
filename,
first_lineno,
last_lineno,
message="",
confidence=DEFAULT_CONFIDENCE,
):
self.name = name
self.typ = typ
self.filename = filename
self.first_lineno = first_lineno
self.last_lineno = last_lineno
self.message = message or f"unused {typ} '{name}'"
self.confidence = confidence
@property
def size(self):
assert self.last_lineno >= self.first_lineno
return self.last_lineno - self.first_lineno + 1
def get_report(self, add_size=False):
if add_size:
line_format = "line" if self.size == 1 else "lines"
size_report = f", {self.size:d} {line_format}"
else:
size_report = ""
return "{}:{:d}: {} ({}% confidence{})".format(
utils.format_path(self.filename),
self.first_lineno,
self.message,
self.confidence,
size_report,
)
def get_whitelist_string(self):
filename = utils.format_path(self.filename)
if self.typ == "unreachable_code":
return f"# {self.message} ({filename}:{self.first_lineno})"
else:
prefix = ""
if self.typ in ["attribute", "method", "property"]:
prefix = "_."
return "{}{} # unused {} ({}:{:d})".format(
prefix, self.name, self.typ, filename, self.first_lineno
)
def _tuple(self):
return (self.filename, self.first_lineno, self.name)
def __repr__(self):
return repr(self.name)
def __eq__(self, other):
return self._tuple() == other._tuple()
def __hash__(self):
return hash(self._tuple())
class Vulture(ast.NodeVisitor):
"""Find dead code."""
def __init__(
self, verbose=False, ignore_names=None, ignore_decorators=None
):
self.verbose = verbose
def get_list(typ):
return utils.LoggingList(typ, self.verbose)
self.defined_attrs = get_list("attribute")
self.defined_classes = get_list("class")
self.defined_funcs = get_list("function")
self.defined_imports = get_list("import")
self.defined_methods = get_list("method")
self.defined_props = get_list("property")
self.defined_vars = get_list("variable")
self.unreachable_code = get_list("unreachable_code")
self.used_names = utils.LoggingSet("name", self.verbose)
self.ignore_names = ignore_names or []
self.ignore_decorators = ignore_decorators or []
self.filename = Path()
self.code = []
self.exit_code = ExitCode.NoDeadCode
def scan(self, code, filename=""):
filename = Path(filename)
self.code = code.splitlines()
self.noqa_lines = noqa.parse_noqa(self.code)
self.filename = filename
def handle_syntax_error(e):
text = f' at "{e.text.strip()}"' if e.text else ""
self._log(
f"{utils.format_path(filename)}:{e.lineno}: {e.msg}{text}",
file=sys.stderr,
force=True,
)
self.exit_code = ExitCode.InvalidInput
try:
node = ast.parse(
code, filename=str(self.filename), type_comments=True
)
except SyntaxError as err:
handle_syntax_error(err)
except ValueError as err:
# ValueError is raised if source contains null bytes.
self._log(
f'{utils.format_path(filename)}: invalid source code "{err}"',
file=sys.stderr,
force=True,
)
self.exit_code = ExitCode.InvalidInput
else:
# When parsing type comments, visiting can throw SyntaxError.
try:
self.visit(node)
except SyntaxError as err:
handle_syntax_error(err)
def scavenge(self, paths, exclude=None):
def prepare_pattern(pattern):
if not any(char in pattern for char in "*?["):
pattern = f"*{pattern}*"
return pattern
exclude = [prepare_pattern(pattern) for pattern in (exclude or [])]
def exclude_path(path):
return _match(path, exclude, case=False)
paths = [Path(path) for path in paths]
for module in utils.get_modules(paths):
if exclude_path(module):
self._log("Excluded:", module)
continue
self._log("Scanning:", module)
try:
module_string = utils.read_file(module)
except utils.VultureInputException as err: # noqa: F841
self._log(
f"Error: Could not read file {module} - {err}\n"
f"Try to change the encoding to UTF-8.",
file=sys.stderr,
force=True,
)
self.exit_code = ExitCode.InvalidInput
else:
self.scan(module_string, filename=module)
unique_imports = {item.name for item in self.defined_imports}
for import_name in unique_imports:
path = Path("whitelists") / (import_name + "_whitelist.py")
if exclude_path(path):
self._log("Excluded whitelist:", path)
else:
try:
module_data = pkgutil.get_data("vulture", str(path))
self._log("Included whitelist:", path)
except OSError:
# Most imported modules don't have a whitelist.
continue
module_string = module_data.decode("utf-8")
self.scan(module_string, filename=path)
def get_unused_code(self, min_confidence=0, sort_by_size=False):
"""
Return ordered list of unused Item objects.
"""
if not 0 <= min_confidence <= 100:
raise ValueError("min_confidence must be between 0 and 100.")
def by_name(item):
return (str(item.filename).lower(), item.first_lineno)
def by_size(item):
return (item.size,) + by_name(item)
unused_code = (
self.unused_attrs
+ self.unused_classes
+ self.unused_funcs
+ self.unused_imports
+ self.unused_methods
+ self.unused_props
+ self.unused_vars
+ self.unreachable_code
)
confidently_unused = [
obj for obj in unused_code if obj.confidence >= min_confidence
]
return sorted(
confidently_unused, key=by_size if sort_by_size else by_name
)
def report(
self, min_confidence=0, sort_by_size=False, make_whitelist=False
):
"""
Print ordered list of Item objects to stdout.
"""
for item in self.get_unused_code(
min_confidence=min_confidence, sort_by_size=sort_by_size
):
self._log(
item.get_whitelist_string()
if make_whitelist
else item.get_report(add_size=sort_by_size),
force=True,
)
self.exit_code = ExitCode.DeadCode
return self.exit_code
@property
def unused_classes(self):
return _get_unused_items(self.defined_classes, self.used_names)
@property
def unused_funcs(self):
return _get_unused_items(self.defined_funcs, self.used_names)
@property
def unused_imports(self):
return _get_unused_items(self.defined_imports, self.used_names)
@property
def unused_methods(self):
return _get_unused_items(self.defined_methods, self.used_names)
@property
def unused_props(self):
return _get_unused_items(self.defined_props, self.used_names)
@property
def unused_vars(self):
return _get_unused_items(self.defined_vars, self.used_names)
@property
def unused_attrs(self):
return _get_unused_items(self.defined_attrs, self.used_names)
def _log(self, *args, file=None, force=False):
if self.verbose or force:
file = file or sys.stdout
try:
print(*args, file=file)
except UnicodeEncodeError:
# Some terminals can't print Unicode symbols.
x = " ".join(map(str, args))
print(x.encode(), file=file)
def _add_aliases(self, node):
"""
We delegate to this method instead of using visit_alias() to have
access to line numbers and to filter imports from __future__.
"""
assert isinstance(node, (ast.Import, ast.ImportFrom))
for name_and_alias in node.names:
# Store only top-level module name ("os.path" -> "os").
# We can't easily detect when "os.path" is used.
name = name_and_alias.name.partition(".")[0]
alias = name_and_alias.asname
self._define(
self.defined_imports,
alias or name,
node,
confidence=90,
ignore=_ignore_import,
)
if alias is not None:
self.used_names.add(name_and_alias.name)
def _handle_conditional_node(self, node, name):
if utils.condition_is_always_false(node.test):
self._define(
self.unreachable_code,
name,
node,
last_node=node.body
if isinstance(node, ast.IfExp)
else node.body[-1],
message=f"unsatisfiable '{name}' condition",
confidence=100,
)
elif utils.condition_is_always_true(node.test):
else_body = node.orelse
if name == "ternary":
self._define(
self.unreachable_code,
name,
else_body,
message="unreachable 'else' expression",
confidence=100,
)
elif else_body:
self._define(
self.unreachable_code,
"else",
else_body[0],
last_node=else_body[-1],
message="unreachable 'else' block",
confidence=100,
)
elif name == "if":
# Redundant if-condition without else block.
self._define(
self.unreachable_code,
name,
node,
message="redundant if-condition",
confidence=100,
)
def _define(
self,
collection,
name,
first_node,
last_node=None,
message="",
confidence=DEFAULT_CONFIDENCE,
ignore=None,
):
def ignored(lineno):
return (
(ignore and ignore(self.filename, name))
or _match(name, self.ignore_names)
or noqa.ignore_line(self.noqa_lines, lineno, ERROR_CODES[typ])
)
last_node = last_node or first_node
typ = collection.typ
first_lineno = lines.get_first_line_number(first_node)
if ignored(first_lineno):
self._log(f'Ignoring {typ} "{name}"')
else:
collection.append(
Item(
name,
typ,
self.filename,
first_lineno,
lines.get_last_line_number(last_node),
message=message,
confidence=confidence,
)
)
def _define_variable(self, name, node, confidence=DEFAULT_CONFIDENCE):
self._define(
self.defined_vars,
name,
node,
confidence=confidence,
ignore=_ignore_variable,
)
def visit_arg(self, node):
"""Function argument"""
self._define_variable(node.arg, node, confidence=100)
def visit_AsyncFunctionDef(self, node):
return self.visit_FunctionDef(node)
def visit_Attribute(self, node):
if isinstance(node.ctx, ast.Store):
self._define(self.defined_attrs, node.attr, node)
elif isinstance(node.ctx, ast.Load):
self.used_names.add(node.attr)
def visit_BinOp(self, node):
"""
Parse variable names in old format strings:
"%(my_var)s" % locals()
"""
if (
utils.is_ast_string(node.left)
and isinstance(node.op, ast.Mod)
and self._is_locals_call(node.right)
):
self.used_names |= set(re.findall(r"%\((\w+)\)", node.left.value))
def visit_Call(self, node):
# Count getattr/hasattr(x, "some_attr", ...) as usage of some_attr.
if isinstance(node.func, ast.Name) and (
(node.func.id == "getattr" and 2 <= len(node.args) <= 3)
or (node.func.id == "hasattr" and len(node.args) == 2)
):
attr_name_arg = node.args[1]
if utils.is_ast_string(attr_name_arg):
self.used_names.add(attr_name_arg.value)
# Parse variable names in new format strings:
# "{my_var}".format(**locals())
if (
isinstance(node.func, ast.Attribute)
and utils.is_ast_string(node.func.value)
and node.func.attr == "format"
and any(
kw.arg is None and self._is_locals_call(kw.value)
for kw in node.keywords
)
):
self._handle_new_format_string(node.func.value.value)
def _handle_new_format_string(self, s):
def is_identifier(name):
return bool(re.match(r"[a-zA-Z_][a-zA-Z0-9_]*", name))
parser = string.Formatter()
try:
names = [name for _, name, _, _ in parser.parse(s) if name]
except ValueError:
# Invalid format string.
names = []
for field_name in names:
# Remove brackets and their contents: "a[0][b].c[d].e" -> "a.c.e",
# then split the resulting string: "a.b.c" -> ["a", "b", "c"]
vars = re.sub(r"\[\w*\]", "", field_name).split(".")
for var in vars:
if is_identifier(var):
self.used_names.add(var)
@staticmethod
def _is_locals_call(node):
"""Return True if the node is `locals()`."""
return (
isinstance(node, ast.Call)
and isinstance(node.func, ast.Name)
and node.func.id == "locals"
and not node.args
and not node.keywords
)
def visit_ClassDef(self, node):
for decorator in node.decorator_list:
if _match(
utils.get_decorator_name(decorator), self.ignore_decorators
):
self._log(
f'Ignoring class "{node.name}" (decorator whitelisted)'
)
break
else:
self._define(
self.defined_classes, node.name, node, ignore=_ignore_class
)
def visit_FunctionDef(self, node):
decorator_names = [
utils.get_decorator_name(decorator)
for decorator in node.decorator_list
]
first_arg = node.args.args[0].arg if node.args.args else None
if "@property" in decorator_names:
typ = "property"
elif (
"@staticmethod" in decorator_names
or "@classmethod" in decorator_names
or first_arg == "self"
):
typ = "method"
else:
typ = "function"
if any(
_match(name, self.ignore_decorators) for name in decorator_names
):
self._log(f'Ignoring {typ} "{node.name}" (decorator whitelisted)')
elif typ == "property":
self._define(self.defined_props, node.name, node)
elif typ == "method":
self._define(
self.defined_methods, node.name, node, ignore=_ignore_method
)
else:
self._define(
self.defined_funcs, node.name, node, ignore=_ignore_function
)
def visit_If(self, node):
self._handle_conditional_node(node, "if")
def visit_IfExp(self, node):
self._handle_conditional_node(node, "ternary")
def visit_Import(self, node):
self._add_aliases(node)
def visit_ImportFrom(self, node):
if node.module != "__future__":
self._add_aliases(node)
def visit_Name(self, node):
if (
isinstance(node.ctx, (ast.Load, ast.Del))
and node.id not in IGNORED_VARIABLE_NAMES
):
self.used_names.add(node.id)
elif isinstance(node.ctx, (ast.Param, ast.Store)):
self._define_variable(node.id, node)
def visit_Assign(self, node):
if _assigns_special_variable__all__(node):
assert isinstance(node.value, (ast.List, ast.Tuple))
for elt in node.value.elts:
if utils.is_ast_string(elt):
self.used_names.add(elt.value)
def visit_While(self, node):
self._handle_conditional_node(node, "while")
def visit_MatchClass(self, node):
for kwd_attr in node.kwd_attrs:
self.used_names.add(kwd_attr)
def visit(self, node):
method = "visit_" + node.__class__.__name__
visitor = getattr(self, method, None)
if self.verbose:
lineno = getattr(node, "lineno", 1)
line = self.code[lineno - 1] if self.code else ""
self._log(lineno, ast.dump(node), line)
if visitor:
visitor(node)
# There isn't a clean subset of node types that might have type
# comments, so just check all of them.
type_comment = getattr(node, "type_comment", None)
if type_comment is not None:
mode = (
"func_type"
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef))
else "eval"
)
self.visit(
ast.parse(type_comment, filename="", mode=mode)
)
return self.generic_visit(node)
def _handle_ast_list(self, ast_list):
"""
Find unreachable nodes in the given sequence of ast nodes.
"""
for index, node in enumerate(ast_list):
if isinstance(
node, (ast.Break, ast.Continue, ast.Raise, ast.Return)
):
try:
first_unreachable_node = ast_list[index + 1]
except IndexError:
continue
class_name = node.__class__.__name__.lower()
self._define(
self.unreachable_code,
class_name,
first_unreachable_node,
last_node=ast_list[-1],
message=f"unreachable code after '{class_name}'",
confidence=100,
)
return
def generic_visit(self, node):
"""Called if no explicit visitor function exists for a node."""
for _, value in ast.iter_fields(node):
if isinstance(value, list):
self._handle_ast_list(value)
for item in value:
if isinstance(item, ast.AST):
self.visit(item)
elif isinstance(value, ast.AST):
self.visit(value)
def main():
try:
config = make_config()
except InputError as e:
print(e, file=sys.stderr)
sys.exit(ExitCode.InvalidCmdlineArguments)
vulture = Vulture(
verbose=config["verbose"],
ignore_names=config["ignore_names"],
ignore_decorators=config["ignore_decorators"],
)
vulture.scavenge(config["paths"], exclude=config["exclude"])
sys.exit(
vulture.report(
min_confidence=config["min_confidence"],
sort_by_size=config["sort_by_size"],
make_whitelist=config["make_whitelist"],
)
)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1693469034.0
vulture-2.11/vulture/lines.py 0000664 0001750 0001750 00000001333 14474044552 016246 0 ustar 00jendrik jendrik def get_last_line_number(node):
return node.end_lineno
def get_first_line_number(node):
"""
From Python 3.8 onwards, lineno for decorated objects is the line at which
the object definition starts, which is different from what Python < 3.8
reported -- the lineno of the first decorator. To preserve this behaviour
of Vulture for newer Python versions, which is also more accurate for
counting the size of the unused code chunk (if the property is unused, we
also don't need it's decorators), we return the lineno of the first
decorator, if there are any.
"""
decorators = getattr(node, "decorator_list", [])
if decorators:
return decorators[0].lineno
return node.lineno
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/vulture/noqa.py 0000664 0001750 0001750 00000002507 14470671470 016077 0 ustar 00jendrik jendrik from collections import defaultdict
import re
NOQA_REGEXP = re.compile(
# Use the same regex as flake8 does.
# https://github.com/pycqa/flake8/blob/main/src/flake8/defaults.py
# We're looking for items that look like this:
# `# noqa`
# `# noqa: E123`
# `# noqa: E123,W451,F921`
# `# NoQA: E123,W451,F921`
r"# noqa(?::[\s]?(?P([A-Z]+[0-9]+(?:[,\s]+)?)+))?",
re.IGNORECASE,
)
NOQA_CODE_MAP = {
# flake8 F401: module imported but unused.
"F401": "V104",
# flake8 F841: local variable is assigned to but never used.
"F841": "V107",
}
def _parse_error_codes(match):
# If no error code is specified, add the line to the "all" category.
return [
c.strip() for c in (match.groupdict()["codes"] or "all").split(",")
]
def parse_noqa(code):
noqa_lines = defaultdict(set)
for lineno, line in enumerate(code, start=1):
match = NOQA_REGEXP.search(line)
if match:
for error_code in _parse_error_codes(match):
error_code = NOQA_CODE_MAP.get(error_code, error_code)
noqa_lines[error_code].add(lineno)
return noqa_lines
def ignore_line(noqa_lines, lineno, error_code):
"""Check if the reported line is annotated with "# noqa"."""
return lineno in noqa_lines[error_code] or lineno in noqa_lines["all"]
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1696607387.0
vulture-2.11/vulture/utils.py 0000664 0001750 0001750 00000007066 14510026233 016270 0 ustar 00jendrik jendrik import ast
from enum import IntEnum
import pathlib
import sys
import tokenize
class VultureInputException(Exception):
pass
class ExitCode(IntEnum):
NoDeadCode = 0
InvalidInput = 1
InvalidCmdlineArguments = 2
DeadCode = 3
def _safe_eval(node, default):
"""
Safely evaluate the Boolean expression under the given AST node.
Substitute `default` for all sub-expressions that cannot be
evaluated (because variables or functions are undefined).
We could use eval() to evaluate more sub-expressions. However, this
function is not safe for arbitrary Python code. Even after
overwriting the "__builtins__" dictionary, the original dictionary
can be restored
(https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html).
"""
if isinstance(node, ast.BoolOp):
results = [_safe_eval(value, default) for value in node.values]
if isinstance(node.op, ast.And):
return all(results)
else:
return any(results)
elif isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
return not _safe_eval(node.operand, not default)
else:
try:
return ast.literal_eval(node)
except ValueError:
return default
def condition_is_always_false(condition):
return not _safe_eval(condition, True)
def condition_is_always_true(condition):
return _safe_eval(condition, False)
def is_ast_string(node):
return isinstance(node, ast.Constant) and isinstance(node.value, str)
def format_path(path):
try:
return path.relative_to(pathlib.Path.cwd())
except ValueError:
# Path is not below the current directory.
return path
def get_decorator_name(decorator):
if isinstance(decorator, ast.Call):
decorator = decorator.func
try:
parts = []
while isinstance(decorator, ast.Attribute):
parts.append(decorator.attr)
decorator = decorator.value
parts.append(decorator.id)
except AttributeError:
parts = []
return "@" + ".".join(reversed(parts))
def get_modules(paths):
"""Retrieve Python files to check.
Loop over all given paths, abort if any ends with .pyc, add the other given
files (even those not ending with .py) and collect all .py files under the
given directories.
"""
modules = []
for path in paths:
path = path.resolve()
if path.is_file():
if path.suffix == ".pyc":
sys.exit(f"Error: *.pyc files are not supported: {path}")
else:
modules.append(path)
elif path.is_dir():
modules.extend(path.rglob("*.py"))
else:
sys.exit(f"Error: {path} could not be found.")
return modules
def read_file(filename):
try:
# Use encoding detected by tokenize.detect_encoding().
with tokenize.open(filename) as f:
return f.read()
except (SyntaxError, UnicodeDecodeError) as err:
raise VultureInputException(err)
class LoggingList(list):
def __init__(self, typ, verbose):
self.typ = typ
self._verbose = verbose
return super().__init__()
def append(self, item):
if self._verbose:
print(f'define {self.typ} "{item.name}"')
super().append(item)
class LoggingSet(set):
def __init__(self, typ, verbose):
self.typ = typ
self._verbose = verbose
return super().__init__()
def add(self, name):
if self._verbose:
print(f'use {self.typ} "{name}"')
super().add(name)
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704568248.0
vulture-2.11/vulture/version.py 0000664 0001750 0001750 00000000025 14546322670 016616 0 ustar 00jendrik jendrik __version__ = "2.11"
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1704568248.9799533
vulture-2.11/vulture/whitelists/ 0000775 0001750 0001750 00000000000 14546322671 016762 5 ustar 00jendrik jendrik ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1580740350.0
vulture-2.11/vulture/whitelists/argparse_whitelist.py 0000664 0001750 0001750 00000000255 13616027376 023237 0 ustar 00jendrik jendrik import argparse
argparse.ArgumentParser().epilog
argparse.ArgumentDefaultsHelpFormatter("prog")._fill_text
argparse.ArgumentDefaultsHelpFormatter("prog")._get_help_string
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/vulture/whitelists/ast_whitelist.py 0000664 0001750 0001750 00000005031 14470671470 022216 0 ustar 00jendrik jendrik from whitelist_utils import Whitelist
# NodeVisitor methods are called implicitly.
whitelist_node_visitor = Whitelist()
whitelist_node_visitor.visit_Assert
whitelist_node_visitor.visit_Assign
whitelist_node_visitor.visit_AsyncFor
whitelist_node_visitor.visit_AsyncFunctionDef
whitelist_node_visitor.visit_AsyncWith
whitelist_node_visitor.visit_Attribute
whitelist_node_visitor.visit_AugAssign
whitelist_node_visitor.visit_Await
whitelist_node_visitor.visit_BinOp
whitelist_node_visitor.visit_BoolOp
whitelist_node_visitor.visit_Bytes
whitelist_node_visitor.visit_Call
whitelist_node_visitor.visit_ClassDef
whitelist_node_visitor.visit_Compare
whitelist_node_visitor.visit_Constant
whitelist_node_visitor.visit_Delete
whitelist_node_visitor.visit_Dict
whitelist_node_visitor.visit_DictComp
whitelist_node_visitor.visit_ExceptHandler
whitelist_node_visitor.visit_Exec
whitelist_node_visitor.visit_Expr
whitelist_node_visitor.visit_Expression
whitelist_node_visitor.visit_ExtSlice
whitelist_node_visitor.visit_For
whitelist_node_visitor.visit_FunctionDef
whitelist_node_visitor.visit_GeneratorExp
whitelist_node_visitor.visit_Global
whitelist_node_visitor.visit_If
whitelist_node_visitor.visit_IfExp
whitelist_node_visitor.visit_Import
whitelist_node_visitor.visit_ImportFrom
whitelist_node_visitor.visit_Index
whitelist_node_visitor.visit_Interactive
whitelist_node_visitor.visit_Lambda
whitelist_node_visitor.visit_List
whitelist_node_visitor.visit_ListComp
whitelist_node_visitor.visit_MatchClass
whitelist_node_visitor.visit_Module
whitelist_node_visitor.visit_Name
whitelist_node_visitor.visit_NameConstant
whitelist_node_visitor.visit_Nonlocal
whitelist_node_visitor.visit_Num
whitelist_node_visitor.visit_Print
whitelist_node_visitor.visit_Raise
whitelist_node_visitor.visit_Repr
whitelist_node_visitor.visit_Return
whitelist_node_visitor.visit_Set
whitelist_node_visitor.visit_SetComp
whitelist_node_visitor.visit_Slice
whitelist_node_visitor.visit_Starred
whitelist_node_visitor.visit_Str
whitelist_node_visitor.visit_Subscript
whitelist_node_visitor.visit_Suite
whitelist_node_visitor.visit_Try
whitelist_node_visitor.visit_TryExcept
whitelist_node_visitor.visit_TryFinally
whitelist_node_visitor.visit_Tuple
whitelist_node_visitor.visit_UnaryOp
whitelist_node_visitor.visit_While
whitelist_node_visitor.visit_With
whitelist_node_visitor.visit_Yield
whitelist_node_visitor.visit_YieldFrom
whitelist_node_visitor.visit_alias
whitelist_node_visitor.visit_arg
whitelist_node_visitor.visit_arguments
whitelist_node_visitor.visit_comprehension
whitelist_node_visitor.visit_keyword
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1573498174.0
vulture-2.11/vulture/whitelists/collections_whitelist.py 0000664 0001750 0001750 00000000204 13562326476 023746 0 ustar 00jendrik jendrik import collections
# To free memory, the "default_factory" attribute can be set to None.
collections.defaultdict().default_factory
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1572731492.0
vulture-2.11/vulture/whitelists/ctypes_whitelist.py 0000644 0001750 0001750 00000000205 13557375144 022736 0 ustar 00jendrik jendrik from ctypes import _CFuncPtr
from ctypes import _Pointer
_CFuncPtr.argtypes
_CFuncPtr.errcheck
_CFuncPtr.restype
_Pointer.contents
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/vulture/whitelists/enum_whitelist.py 0000664 0001750 0001750 00000000270 14470671470 022373 0 ustar 00jendrik jendrik import enum
class EnumWhitelist(enum.Enum):
EnumWhitelist = 1
# Special attributes used by enum classes.
EnumWhitelist.EnumWhitelist._name_
EnumWhitelist.EnumWhitelist._value_
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1597011167.0
vulture-2.11/vulture/whitelists/logging_whitelist.py 0000664 0001750 0001750 00000000137 13714072337 023054 0 ustar 00jendrik jendrik import logging
logging.Filter.filter
logging.getLogger().propagate
logging.StreamHandler.emit
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/vulture/whitelists/pint_whitelist.py 0000664 0001750 0001750 00000000074 14470671470 022403 0 ustar 00jendrik jendrik import pint
ureg = pint.UnitRegistry()
ureg.default_format
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1692627768.0
vulture-2.11/vulture/whitelists/socketserver_whitelist.py 0000664 0001750 0001750 00000000100 14470671470 024136 0 ustar 00jendrik jendrik import socketserver
socketserver.TCPServer.allow_reuse_address
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1579719978.0
vulture-2.11/vulture/whitelists/string_whitelist.py 0000644 0001750 0001750 00000000355 13612116452 022726 0 ustar 00jendrik jendrik import string
string.Formatter.check_unused_args
string.Formatter.convert_field
string.Formatter.format
string.Formatter.format_field
string.Formatter.get_field
string.Formatter.get_value
string.Formatter.parse
string.Formatter.vformat
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1540815940.0
vulture-2.11/vulture/whitelists/sys_whitelist.py 0000644 0001750 0001750 00000000151 13365576104 022241 0 ustar 00jendrik jendrik import sys
sys.excepthook
# Never report redirected streams as unused.
sys.stderr
sys.stdin
sys.stdout
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1572731531.0
vulture-2.11/vulture/whitelists/threading_whitelist.py 0000644 0001750 0001750 00000000125 13557375213 023372 0 ustar 00jendrik jendrik import threading
threading.Thread.daemon
threading.Thread.name
threading.Thread.run
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704114798.0
vulture-2.11/vulture/whitelists/unittest_whitelist.py 0000664 0001750 0001750 00000000526 14544535156 023314 0 ustar 00jendrik jendrik from unittest import mock, TestCase
TestCase.setUp
TestCase.tearDown
TestCase.setUpClass
TestCase.tearDownClass
TestCase.run
TestCase.skipTest
TestCase.debug
TestCase.failureException
TestCase.longMessage
TestCase.maxDiff
TestCase.subTest
mock.Mock.return_value
mock.Mock.side_effect
mock.MagicMock.return_value
mock.MagicMock.side_effect
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1580740350.0
vulture-2.11/vulture/whitelists/whitelist_utils.py 0000644 0001750 0001750 00000001552 13616027376 022572 0 ustar 00jendrik jendrik """
Vulture sometimes reports used code as unused. To avoid these
false-positives, you can write a Python file that explicitly uses the
code and pass it to vulture:
vulture myscript.py mydir mywhitelist.py
When creating a whitelist file, you have to make sure not to write code
that hides unused code in other files. E.g., this is why we don't import
and access the "sys" module below. If we did import it, vulture would
not be able to detect whether other files import "sys" without using it.
This file explicitly uses code from the Python standard library that is
often incorrectly detected as unused.
"""
class Whitelist:
"""
Helper class that allows mocking Python objects.
Use it to create whitelist files that are not only syntactically
correct, but can also be executed.
"""
def __getattr__(self, _):
pass
assert Whitelist
././@PaxHeader 0000000 0000000 0000000 00000000034 00000000000 010212 x ustar 00 28 mtime=1704568248.9799533
vulture-2.11/vulture.egg-info/ 0000775 0001750 0001750 00000000000 14546322671 016255 5 ustar 00jendrik jendrik ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704568248.0
vulture-2.11/vulture.egg-info/PKG-INFO 0000644 0001750 0001750 00000055652 14546322670 017364 0 ustar 00jendrik jendrik Metadata-Version: 2.1
Name: vulture
Version: 2.11
Summary: Find dead code
Home-page: https://github.com/jendrikseipp/vulture
Author: Jendrik Seipp
Author-email: jendrikseipp@gmail.com
License: MIT
Keywords: dead-code-removal
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE.txt
# Vulture - Find dead code

[](https://codecov.io/gh/jendrikseipp/vulture?branch=main)
Vulture finds unused code in Python programs. This is useful for
cleaning up and finding errors in large code bases. If you run Vulture
on both your library and test suite you can find untested code.
Due to Python's dynamic nature, static code analyzers like Vulture are
likely to miss some dead code. Also, code that is only called implicitly
may be reported as unused. Nonetheless, Vulture can be a very helpful
tool for higher code quality.
## Features
* fast: uses static code analysis
* tested: tests itself and has complete test coverage
* complements pyflakes and has the same output syntax
* sorts unused classes and functions by size with `--sort-by-size`
## Installation
$ pip install vulture
## Usage
$ vulture myscript.py # or
$ python3 -m vulture myscript.py
$ vulture myscript.py mypackage/
$ vulture myscript.py --min-confidence 100 # Only report 100% dead code.
The provided arguments may be Python files or directories. For each
directory Vulture analyzes all contained
\*.py files.
After you have found and deleted dead code, run Vulture again, because
it may discover more dead code.
## Types of unused code
In addition to finding unused functions, classes, etc., Vulture can detect
unreachable code. Each chunk of dead code is assigned a *confidence value*
between 60% and 100%, where a value of 100% signals that it is certain that the
code won't be executed. Values below 100% are *very rough* estimates (based on
the type of code chunk) for how likely it is that the code is unused.
| Code type | Confidence value |
| ------------------- | -- |
| function/method/class argument, unreachable code | 100% |
| import | 90% |
| attribute, class, function, method, property, variable | 60% |
You can use the `--min-confidence` flag to set the minimum confidence
for code to be reported as unused. Use `--min-confidence 100` to only
report code that is guaranteed to be unused within the analyzed files.
## Handling false positives
When Vulture incorrectly reports chunks of code as unused, you have
several options for suppressing the false positives. If fixing your false
positives could benefit other users as well, please file an issue report.
#### Whitelists
The recommended option is to add used code that is reported as unused to a
Python module and add it to the list of scanned paths. To obtain such a
whitelist automatically, pass `--make-whitelist` to Vulture:
$ vulture mydir --make-whitelist > whitelist.py
$ vulture mydir whitelist.py
Note that the resulting `whitelist.py` file will contain valid Python
syntax, but for Python to be able to *run* it, you will usually have to
make some modifications.
We collect whitelists for common Python modules and packages in
`vulture/whitelists/` (pull requests are welcome).
#### Ignoring files
If you want to ignore a whole file or directory, use the `--exclude` parameter
(e.g., `--exclude "*settings.py,*/docs/*.py,*/test_*.py,*/.venv/*.py"`). The
exclude patterns are matched against absolute paths.
#### Flake8 noqa comments
For compatibility with [flake8](https://flake8.pycqa.org/), Vulture
supports the [F401 and
F841](https://flake8.pycqa.org/en/latest/user/error-codes.html) error
codes for ignoring unused imports (`# noqa: F401`) and unused local
variables (`# noqa: F841`). However, we recommend using whitelists instead
of `noqa` comments, since `noqa` comments add visual noise to the code and
make it harder to read.
#### Ignoring names
You can use `--ignore-names foo*,ba[rz]` to let Vulture ignore all names
starting with `foo` and the names `bar` and `baz`. Additionally, the
`--ignore-decorators` option can be used to ignore the names of functions
decorated with the given decorator (but not their arguments or function body).
This is helpful for example in Flask
projects, where you can use `--ignore-decorators "@app.route"` to ignore all
function names with the `@app.route` decorator. Note that Vulture simplifies
decorators it cannot parse: `@foo.bar(x, y)` becomes "@foo.bar" and
`@foo.bar(x, y).baz` becomes "@" internally.
We recommend using whitelists instead of `--ignore-names` or
`--ignore-decorators` whenever possible, since whitelists are
automatically checked for syntactic correctness when passed to Vulture
and often you can even pass them to your Python interpreter and let it
check that all whitelisted code actually still exists in your project.
#### Marking unused variables
There are situations where you can't just remove unused variables, e.g.,
in function signatures. The recommended solution is to use the `del`
keyword as described in the
[PyLint manual](http://pylint-messages.wikidot.com/messages:w0613) and on
[StackOverflow](https://stackoverflow.com/a/14836005):
```python
def foo(x, y):
del y
return x + 3
```
Vulture will also ignore all variables that start with an underscore, so
you can use `_x, y = get_pos()` to mark unused tuple assignments or
function arguments, e.g., `def foo(x, _y)`.
#### Minimum confidence
Raise the minimum [confidence value](#types-of-unused-code) with the `--min-confidence` flag.
#### Unreachable code
If Vulture complains about code like `if False:`, you can use a Boolean
flag `debug = False` and write `if debug:` instead. This makes the code
more readable and silences Vulture.
#### Forward references for type annotations
See [#216](https://github.com/jendrikseipp/vulture/issues/216). For
example, instead of `def foo(arg: "Sequence"): ...`, we recommend using
``` python
from __future__ import annotations
def foo(arg: Sequence):
...
```
## Configuration
You can also store command line arguments in `pyproject.toml` under the
`tool.vulture` section. Simply remove leading dashes and replace all
remaining dashes with underscores.
Options given on the command line have precedence over options in
`pyproject.toml`.
Example Config:
``` toml
[tool.vulture]
exclude = ["*file*.py", "dir/"]
ignore_decorators = ["@app.route", "@require_*"]
ignore_names = ["visit_*", "do_*"]
make_whitelist = true
min_confidence = 80
paths = ["myscript.py", "mydir", "whitelist.py"]
sort_by_size = true
verbose = true
```
## Version control integration
You can use a [pre-commit](https://pre-commit.com/#install) hook to run
Vulture before each commit. For this, install pre-commit and add the
following to the `.pre-commit-config.yaml` file in your repository:
```yaml
repos:
- repo: https://github.com/jendrikseipp/vulture
rev: 'v2.3' # or any later Vulture version
hooks:
- id: vulture
```
Then run `pre-commit install`. Finally, create a `pyproject.toml` file
in your repository and specify all files that Vulture should check under
`[tool.vulture] --> paths` (see above).
## How does it work?
Vulture uses the `ast` module to build abstract syntax trees for all
given files. While traversing all syntax trees it records the names of
defined and used objects. Afterwards, it reports the objects which have
been defined, but not used. This analysis ignores scopes and only takes
object names into account.
Vulture also detects unreachable code by looking for code after
`return`, `break`, `continue` and `raise` statements, and by searching
for unsatisfiable `if`- and `while`-conditions.
## Sort by size
When using the `--sort-by-size` option, Vulture sorts unused code by its
number of lines. This helps developers prioritize where to look for dead
code first.
## Examples
Consider the following Python script (`dead_code.py`):
``` python
import os
class Greeter:
def greet(self):
print("Hi")
def hello_world():
message = "Hello, world!"
greeter = Greeter()
func_name = "greet"
greet_func = getattr(greeter, func_name)
greet_func()
if __name__ == "__main__":
hello_world()
```
Calling :
$ vulture dead_code.py
results in the following output:
dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:4: unused function 'greet' (60% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
Vulture correctly reports `os` and `message` as unused but it fails to
detect that `greet` is actually used. The recommended method to deal
with false positives like this is to create a whitelist Python file.
**Preparing whitelists**
In a whitelist we simulate the usage of variables, attributes, etc. For
the program above, a whitelist could look as follows:
``` python
# whitelist_dead_code.py
from dead_code import Greeter
Greeter.greet
```
Alternatively, you can pass `--make-whitelist` to Vulture and obtain an
automatically generated whitelist.
Passing both the original program and the whitelist to Vulture
$ vulture dead_code.py whitelist_dead_code.py
makes Vulture ignore the `greet` method:
dead_code.py:1: unused import 'os' (90% confidence)
dead_code.py:8: unused variable 'message' (60% confidence)
## Exit codes
| Exit code | Description |
| --------- | ------------------------------------------------------------- |
| 0 | No dead code found |
| 1 | Invalid input (file missing, syntax error, wrong encoding) |
| 2 | Invalid command line arguments |
| 3 | Dead code found |
## Similar programs
- [pyflakes](https://pypi.org/project/pyflakes/) finds unused imports
and unused local variables (in addition to many other programmatic
errors).
- [coverage](https://pypi.org/project/coverage/) finds unused code
more reliably than Vulture, but requires all branches of the code to
actually be run.
- [uncalled](https://pypi.org/project/uncalled/) finds dead code by
using the abstract syntax tree (like Vulture), regular expressions,
or both.
- [dead](https://pypi.org/project/dead/) finds dead code by using the
abstract syntax tree (like Vulture).
## Participate
Please visit to report any
issues or to make pull requests.
- Contributing guide:
[CONTRIBUTING.md](https://github.com/jendrikseipp/vulture/blob/main/CONTRIBUTING.md)
- Release notes:
[CHANGELOG.md](https://github.com/jendrikseipp/vulture/blob/main/CHANGELOG.md)
- Roadmap:
[TODO.md](https://github.com/jendrikseipp/vulture/blob/main/TODO.md)
# 2.11 (2024-01-06)
* Switch to tomllib/tomli to support heterogeneous arrays (Sebastian Csar, #340).
* Bump flake8, flake8-comprehensions and flake8-bugbear (Sebastian Csar, #341).
* Provide whitelist parity for `MagicMock` and `Mock` (maxrake, #342).
# 2.10 (2023-10-06)
* Drop support for Python 3.7 (Jendrik Seipp, #323).
* Add support for Python 3.12 (Jendrik Seipp, #332).
* Use `end_lineno` AST attribute to obtain more accurate line counts (Jendrik Seipp).
# 2.9.1 (2023-08-21)
* Use exit code 0 for `--help` and `--version` again (Jendrik Seipp, #321).
# 2.9 (2023-08-20)
* Use exit code 3 when dead code is found (whosayn, #319).
* Treat non-supported decorator names as "@" instead of crashing (Llandy3d and Jendrik Seipp, #284).
* Drop support for Python 3.6 (Jendrik Seipp).
# 2.8 (2023-08-10)
* Add `UnicodeEncodeError` exception handling to `core.py` (milanbalazs, #299).
* Add whitelist for `Enum` attributes `_name_` and `_value_` (Eugene Toder, #305).
* Run tests and add PyPI trove for Python 3.11 (Jendrik Seipp).
# 2.7 (2023-01-08)
* Ignore `setup_module()`, `teardown_module()`, etc. in pytest `test_*.py` files (Jendrik Seipp).
* Add whitelist for `socketserver.TCPServer.allow_reuse_address` (Ben Elliston).
* Clarify that `--exclude` patterns are matched against absolute paths (Jendrik Seipp, #260).
* Fix example in README file (Jendrik Seipp, #272).
# 2.6 (2022-09-19)
* Add basic `match` statement support (kreathon, #276, #291).
# 2.5 (2022-07-03)
* Mark imports in `__all__` as used (kreathon, #172, #282).
* Add whitelist for `pint.UnitRegistry.default_formatter` (Ben Elliston, #258).
# 2.4 (2022-05-19)
* Print absolute filepaths as relative again (as in version 2.1 and before)
if they are below the current directory (The-Compiler, #246).
* Run tests and add PyPI trove for Python 3.10 (chayim, #266).
* Allow using the `del` keyword to mark unused variables (sshishov, #279).
# 2.3 (2021-01-16)
* Add [pre-commit](https://pre-commit.com) hook (Clément Robert, #244).
# 2.2 (2021-01-15)
* Only parse format strings when being used with `locals()` (jingw, #225).
* Don't override paths in pyproject.toml with empty CLI paths (bcbnz, #228).
* Run continuous integration tests for Python 3.9 (ju-sh, #232).
* Use pathlib internally (ju-sh, #226).
# 2.1 (2020-08-19)
* Treat `getattr/hasattr(obj, "constant_string", ...)` as a reference to
`obj.constant_string` (jingw, #219).
* Fix false positives when assigning to `x.some_name` but reading via
`some_name`, at the cost of potential false negatives (jingw, #221).
* Allow reading options from `pyproject.toml` (Michel Albert, #164, #215).
# 2.0 (2020-08-11)
* Parse `# type: ...` comments if on Python 3.8+ (jingw, #220).
* Bump minimum Python version to 3.6 (Jendrik Seipp, #218). The last
Vulture release that supports Python 2.7 and Python 3.5 is version 1.6.
* Consider all files under `test` or `tests` directories test files
(Jendrik Seipp).
* Ignore `logging.Logger.propagate` attribute (Jendrik Seipp).
# 1.6 (2020-07-28)
* Differentiate between functions and methods (Jendrik Seipp, #112, #209).
* Move from Travis to GitHub actions (RJ722, #211).
# 1.5 (2020-05-24)
* Support flake8 "noqa" error codes F401 (unused import) and F841 (unused
local variable) (RJ722, #195).
* Detect unreachable code in conditional expressions
(Agathiyan Bragadeesh, #178).
# 1.4 (2020-03-30)
* Ignore unused import statements in `__init__.py` (RJ722, #192).
* Report first decorator's line number for unused decorated objects on
Python 3.8+ (RJ722, #200).
* Check code with black and pyupgrade.
# 1.3 (2020-02-03)
* Detect redundant 'if' conditions without 'else' blocks.
* Add whitelist for `string.Formatter` (Joseph Bylund, #183).
# 1.2 (2019-11-22)
* Fix tests for Python 3.8 (#166).
* Use new `Constant` AST node under Python 3.8+ (#175).
* Add test for f-strings (#177).
* Add whitelist for `logging` module.
# 1.1 (2019-09-23)
* Add `sys.excepthook` to `sys` whitelist.
* Add whitelist for `ctypes` module.
* Check that type annotations are parsed and type comments are ignored
(thanks @kx-chen).
* Support checking files with BOM under Python 2.7 (#170).
# 1.0 (2018-10-23)
* Add `--ignore-decorators` flag (thanks @RJ722).
* Add whitelist for `threading` module (thanks @andrewhalle).
# 0.29 (2018-07-31)
* Add `--ignore-names` flag for ignoring names matching the given glob
patterns (thanks @RJ722).
# 0.28 (2018-07-05)
* Add `--make-whitelist` flag for reporting output in whitelist format
(thanks @RJ722).
* Ignore case of `--exclude` arguments on Windows.
* Add `*-test.py` to recognized test file patterns.
* Add `failureException`, `longMessage` and `maxDiff` to `unittest`
whitelist.
* Refer to actual objects rather than their mocks in default
whitelists (thanks @RJ722).
* Don't import any Vulture modules in setup.py (thanks @RJ722).
# 0.27 (2018-06-05)
* Report `while (True): ... else: ...` as unreachable (thanks @RJ722).
* Use `argparse` instead of `optparse`.
* Whitelist Mock.return\_value and Mock.side\_effect in unittest.mock
module.
* Drop support for Python 2.6 and 3.3.
* Improve documentation and test coverage (thanks @RJ722).
# 0.26 (2017-08-28)
* Detect `async` function definitions (thanks @RJ722).
* Add `Item.get_report()` method (thanks @RJ722).
* Move method for finding Python modules out of Vulture class.
# 0.25 (2017-08-15)
* Detect unsatisfiable statements containing `and`, `or` and `not`.
* Use filenames and line numbers as tie-breakers when sorting by size.
* Store first and last line numbers in Item objects.
* Pass relevant options directly to `scavenge()` and `report()`.
# 0.24 (2017-08-14)
* Detect unsatisfiable `while`-conditions (thanks @RJ722).
* Detect unsatisfiable `if`- and `else`-conditions (thanks @RJ722).
* Handle null bytes in source code.
# 0.23 (2017-08-10)
* Add `--min-confidence` flag (thanks @RJ722).
# 0.22 (2017-08-04)
* Detect unreachable code after `return`, `break`, `continue` and
`raise` (thanks @RJ722).
* Parse all variable and attribute names in new format strings.
* Extend ast whitelist.
# 0.21 (2017-07-26)
* If an unused item is defined multiple times, report it multiple
times.
* Make size estimates for function calls more accurate.
* Create wheel files for Vulture (thanks @RJ722).
# 0.20 (2017-07-26)
* Report unused tuple assignments as dead code.
* Report attribute names that have the same names as variables as dead
code.
* Let Item class inherit from `object` (thanks @RJ722).
* Handle names imported as aliases like all other used variable names.
* Rename Vulture.used\_vars to Vulture.used\_names.
* Use function for determining which imports to ignore.
* Only try to import each whitelist file once.
* Store used names and used attributes in sets instead of lists.
* Fix estimating the size of code containing ellipses (...).
* Refactor and simplify code.
# 0.19 (2017-07-20)
* Don't ignore \_\_foo variable names.
* Use separate methods for determining whether to ignore classes and
functions.
* Only try to find a whitelist for each defined import once (thanks
@roivanov).
* Fix finding the last child for many types of AST nodes.
# 0.18 (2017-07-17)
* Make --sort-by-size faster and more
accurate (thanks @RJ722).
# 0.17 (2017-07-17)
* Add get\_unused\_code() method.
* Return with exit code 1 when syntax errors are found or files can't
be read.
# 0.16 (2017-07-12)
* Differentiate between unused classes and functions (thanks @RJ722).
* Add --sort-by-size option (thanks @jackric and @RJ722).
* Count imports as used if they are accessed as module attributes.
# 0.15 (2017-07-04)
* Automatically include whitelists based on imported modules (thanks
@RJ722).
* Add --version parameter (thanks @RJ722).
* Add appveyor tests for testing on Windows (thanks @RJ722).
# 0.14 (2017-04-06)
* Add stub whitelist file for Python standard library (thanks @RJ722)
* Ignore class names starting with "Test" in "test\_" files (thanks
@thisch).
* Ignore "test\_" functions only in "test\_" files.
# 0.13 (2017-03-06)
* Ignore star-imported names since we cannot detect whether they are
used.
* Move repository to GitHub.
# 0.12 (2017-01-05)
* Detect unused imports.
* Use tokenize.open() on Python \>= 3.2 for reading input files,
assume UTF-8 encoding on older Python versions.
# 0.11 (2016-11-27)
* Use the system's default encoding when reading files.
* Report syntax errors instead of aborting.
# 0.10 (2016-07-14)
* Detect unused function and method arguments (issue #15).
* Detect unused \*args and \*\*kwargs parameters.
* Change license from GPL to MIT.
# 0.9 (2016-06-29)
* Don't flag attributes as unused if they are used as global variables
in another module (thanks Florian Bruhin).
* Don't consider "True" and "False" variable names.
* Abort with error message when invoked on .pyc files.
# 0.8.1 (2015-09-28)
* Fix code for Python 3.
# 0.8 (2015-09-28)
* Do not flag names imported with "import as" as dead code (thanks Tom
Terrace).
# 0.7 (2015-09-26)
* Exit with exitcode 1 if path on commandline can't be found.
* Test vulture with vulture using a whitelist module for false
positives.
* Add tests that run vulture as a script.
* Add "python setup.py test" command for running tests.
* Add support for tox.
* Raise test coverage to 100%.
* Remove ez\_setup.py.
# 0.6 (2014-09-06)
* Ignore function names starting with "test\_".
* Parse variable names in new format strings (e.g. "This is
{x}".format(x="nice")).
* Only parse alphanumeric variable names in format strings and ignore
types.
* Abort with exit code 1 on syntax errors.
* Support installation under Windows by using setuptools (thanks
Reuben Fletcher-Costin).
# 0.5 (2014-05-09)
* If dead code is found, exit with 1.
# 0.4.1 (2013-09-17)
* Only warn if a path given on the command line cannot be found.
# 0.4 (2013-06-23)
* Ignore unused variables starting with an underscore.
* Show warning for syntax errors instead of aborting directly.
* Print warning if a file cannot be found.
# 0.3 (2012-03-19)
* Add support for python3
* Report unused attributes
* Find tuple assignments in comprehensions
* Scan files given on the command line even if they don't end with .py
# 0.2 (2012-03-18)
* Only format nodes in verbose mode (gives 4x speedup).
# 0.1 (2012-03-17)
* First release.
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704568248.0
vulture-2.11/vulture.egg-info/SOURCES.txt 0000664 0001750 0001750 00000002614 14546322670 020143 0 ustar 00jendrik jendrik CHANGELOG.md
CODE_OF_CONDUCT.md
CONTRIBUTING.md
LICENSE.txt
MANIFEST.in
README.md
TODO.md
pyproject.toml
requirements.txt
setup.cfg
setup.py
tox.ini
tests/__init__.py
tests/test_conditions.py
tests/test_confidence.py
tests/test_config.py
tests/test_encoding.py
tests/test_errors.py
tests/test_format_strings.py
tests/test_ignore.py
tests/test_imports.py
tests/test_item.py
tests/test_make_whitelist.py
tests/test_noqa.py
tests/test_report.py
tests/test_scavenging.py
tests/test_script.py
tests/test_size.py
tests/test_sorting.py
tests/test_unreachable.py
tests/test_utils.py
vulture/__init__.py
vulture/__main__.py
vulture/config.py
vulture/core.py
vulture/lines.py
vulture/noqa.py
vulture/utils.py
vulture/version.py
vulture.egg-info/PKG-INFO
vulture.egg-info/SOURCES.txt
vulture.egg-info/dependency_links.txt
vulture.egg-info/entry_points.txt
vulture.egg-info/requires.txt
vulture.egg-info/top_level.txt
vulture/whitelists/argparse_whitelist.py
vulture/whitelists/ast_whitelist.py
vulture/whitelists/collections_whitelist.py
vulture/whitelists/ctypes_whitelist.py
vulture/whitelists/enum_whitelist.py
vulture/whitelists/logging_whitelist.py
vulture/whitelists/pint_whitelist.py
vulture/whitelists/socketserver_whitelist.py
vulture/whitelists/string_whitelist.py
vulture/whitelists/sys_whitelist.py
vulture/whitelists/threading_whitelist.py
vulture/whitelists/unittest_whitelist.py
vulture/whitelists/whitelist_utils.py ././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704568248.0
vulture-2.11/vulture.egg-info/dependency_links.txt 0000664 0001750 0001750 00000000001 14546322670 022322 0 ustar 00jendrik jendrik
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704568248.0
vulture-2.11/vulture.egg-info/entry_points.txt 0000664 0001750 0001750 00000000056 14546322670 021553 0 ustar 00jendrik jendrik [console_scripts]
vulture = vulture.core:main
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704568248.0
vulture-2.11/vulture.egg-info/requires.txt 0000664 0001750 0001750 00000000051 14546322670 020650 0 ustar 00jendrik jendrik
[:python_version < "3.11"]
tomli>=1.1.0
././@PaxHeader 0000000 0000000 0000000 00000000026 00000000000 010213 x ustar 00 22 mtime=1704568248.0
vulture-2.11/vulture.egg-info/top_level.txt 0000664 0001750 0001750 00000000010 14546322670 020775 0 ustar 00jendrik jendrik vulture