././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1733679579.6835294 vulture-2.14/0000775000175000017500000000000014725354734013064 5ustar00jendrikjendrik././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733679519.0 vulture-2.14/CHANGELOG.md0000664000175000017500000002556514725354637014714 0ustar00jendrikjendrik# 2.14 (2024-12-08) * Improve reachability analysis (kreathon, #270, #302). * Add type hints for `get_unused_code` and the fields of the `Item` class (John Doknjas, #361). # 2.13 (2024-10-02) * Add support for Python 3.13 (Jendrik Seipp, #369). * Add PyPI and conda-forge badges to README file (Trevor James Smith, #356). * Include `tests/**/*.toml` in sdist (Colin Watson). # 2.12 (2024-09-17) * Use `ruff` for linting and formatting (Anh Trinh, #347, #349). * Replace `tox` by `pre-commit` for linting and formatting (Anh Trinh, #349). * Add `--config` flag to specify path to pyproject.toml configuration file (Glen Robertson, #352). # 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1567253105.0 vulture-2.14/CODE_OF_CONDUCT.md0000644000175000017500000000643213532461161015652 0ustar00jendrikjendrik# 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1705694072.0 vulture-2.14/CONTRIBUTING.md0000664000175000017500000000617314552551570015317 0ustar00jendrikjendrik# 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. ### Formating and linting Run `pre-commit` using: $ pre-commit run --all-files ## 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1596492040.0 vulture-2.14/LICENSE.txt0000644000175000017500000000212613712104410014661 0ustar00jendrikjendrikThe 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727805707.0 vulture-2.14/MANIFEST.in0000664000175000017500000000016614677034413014620 0ustar00jendrikjendrikinclude *.md include *.txt include tests/*.py include tests/**/*.toml include tox.ini include vulture/whitelists/*.py ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1733679579.6835294 vulture-2.14/PKG-INFO0000664000175000017500000006043414725354734014170 0ustar00jendrikjendrikMetadata-Version: 2.1 Name: vulture Version: 2.14 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 Platform: UNKNOWN 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 :: 3.13 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 [![PyPI Version](https://img.shields.io/pypi/v/vulture.svg)](https://pypi.python.org/pypi/vulture) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/vulture.svg)](https://anaconda.org/conda-forge/vulture) ![CI:Test](https://github.com/jendrikseipp/vulture/workflows/CI/badge.svg) [![Codecov Badge](https://codecov.io/gh/jendrikseipp/vulture/branch/main/graphs/badge.svg)](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 ``` Vulture will automatically look for a `pyproject.toml` in the current working directory. To use a `pyproject.toml` in another directory, you can use the `--config path/to/pyproject.toml` flag. ## Integrations 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). There's also a [GitHub Action for Vulture](https://github.com/gtkacz/vulture-action) and you can use Vulture programatically. For example: ``` python import vulture v = vulture.Vulture() v.scavenge(['.']) unused_code = v.get_unused_code() # returns a list of `Item` objects ``` ## 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.14 (2024-12-08) * Improve reachability analysis (kreathon, #270, #302). * Add type hints for `get_unused_code` and the fields of the `Item` class (John Doknjas, #361). # 2.13 (2024-10-02) * Add support for Python 3.13 (Jendrik Seipp, #369). * Add PyPI and conda-forge badges to README file (Trevor James Smith, #356). * Include `tests/**/*.toml` in sdist (Colin Watson). # 2.12 (2024-09-17) * Use `ruff` for linting and formatting (Anh Trinh, #347, #349). * Replace `tox` by `pre-commit` for linting and formatting (Anh Trinh, #349). * Add `--config` flag to specify path to pyproject.toml configuration file (Glen Robertson, #352). # 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728464160.0 vulture-2.14/README.md0000664000175000017500000003050514701442440014330 0ustar00jendrikjendrik# Vulture - Find dead code [![PyPI Version](https://img.shields.io/pypi/v/vulture.svg)](https://pypi.python.org/pypi/vulture) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/vulture.svg)](https://anaconda.org/conda-forge/vulture) ![CI:Test](https://github.com/jendrikseipp/vulture/workflows/CI/badge.svg) [![Codecov Badge](https://codecov.io/gh/jendrikseipp/vulture/branch/main/graphs/badge.svg)](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 ``` Vulture will automatically look for a `pyproject.toml` in the current working directory. To use a `pyproject.toml` in another directory, you can use the `--config path/to/pyproject.toml` flag. ## Integrations 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). There's also a [GitHub Action for Vulture](https://github.com/gtkacz/vulture-action) and you can use Vulture programatically. For example: ``` python import vulture v = vulture.Vulture() v.scavenge(['.']) unused_code = v.get_unused_code() # returns a list of `Item` objects ``` ## 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1693469034.0 vulture-2.14/TODO.md0000664000175000017500000000327114474044552014151 0ustar00jendrikjendrik# 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_". ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477479.0 vulture-2.14/pyproject.toml0000664000175000017500000000327514720701047015773 0ustar00jendrikjendrik[tool.ruff] exclude = [ ".eggs", ".git", "_build", "build", "dist", "htmlcov", "vulture.egg-info", ".cache", ".coverage", ".pytest_cache", ".tox", ".venv", ".vscode", ] # Same as Black. line-length = 79 indent-width = 4 target-version = "py38" [tool.ruff.lint] # ruff enables Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. select = [ "B", # flake8-bugbear "C4", # comprehensions "E", # pycodestyle "F", # pyflakes "I001", # isort "SIM", # flake8-simplify "UP", # pyupgrade ] ignore = [ "C408", # unnecessary dict call "SIM115", # Use context handler for opening files ] # Allow fix for all enabled rules (when `--fix`) is provided. fixable = ["ALL"] unfixable = [] # Allow unused variables when underscore-prefixed. dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" [tool.ruff.lint.per-file-ignores] "vulture/whitelists/*.py" = ["B018"] [tool.ruff.format] # Like Black, use double quotes for strings. quote-style = "double" # Like Black, indent with spaces, rather than tabs. indent-style = "space" # Like Black, respect magic trailing commas. skip-magic-trailing-comma = false # Like Black, automatically detect the appropriate line ending. line-ending = "auto" # Enable auto-formatting of code examples in docstrings. Markdown, # reStructuredText code/literal blocks and doctests are all supported. # # This is currently disabled by default, but it is planned for this # to be opt-out in the future. docstring-code-format = false # Set the line length limit used when formatting code snippets in # docstrings. # # This only has an effect when the `docstring-code-format` setting is # enabled. docstring-code-line-length = "dynamic" ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1704567215.0 vulture-2.14/requirements.txt0000664000175000017500000000005014546320657016341 0ustar00jendrikjendriktomli >= 1.1.0; python_version < '3.11' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1733679579.6835294 vulture-2.14/setup.cfg0000664000175000017500000000034614725354734014710 0ustar00jendrikjendrik[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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1727808945.0 vulture-2.14/setup.py0000664000175000017500000000377114677042661014605 0ustar00jendrikjendrik#! /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 :: 3.13", "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"]}, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1733679579.6825292 vulture-2.14/tests/0000775000175000017500000000000014725354734014226 5ustar00jendrikjendrik././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477038.0 vulture-2.14/tests/__init__.py0000664000175000017500000000257714720700156016336 0ustar00jendrikjendrikimport 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") # Pint is incompatible with Python 3.13 (https://github.com/hgrecco/pint/issues/1969). if sys.version_info < (3, 13) or path.name != "pint_whitelist.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 def check_multiple_unreachable(v, checks): assert len(v.unreachable_code) == len(checks) for item, (lineno, size, name) in zip(v.unreachable_code, checks): assert item.first_lineno == lineno assert item.size == size assert item.name == name @pytest.fixture def v(): return core.Vulture(verbose=True) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477038.0 vulture-2.14/tests/test_conditions.py0000664000175000017500000000402314720700156017773 0ustar00jendrikjendrikimport ast from vulture import utils 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1596841917.0 vulture-2.14/tests/test_confidence.py0000644000175000017500000000316213713357675017737 0ustar00jendrikjendrikfrom 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, {}) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/tests/test_config.py0000664000175000017500000001504714720701132017072 0ustar00jendrikjendrik""" Unit tests for config file and CLI argument parsing. """ import pathlib from io import BytesIO from textwrap import dedent import pytest from vulture.config import ( DEFAULTS, InputError, _check_input_config, _parse_args, _parse_toml, make_config, ) 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"], config="pyproject.toml", 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"], config="pyproject.toml", make_whitelist=True, min_confidence=20, sort_by_size=True, verbose=True, ) assert result == expected def test_toml_config_custom_path(): """ Ensure that TOML pyproject.toml files can be read from a custom path, other than the current working directory. Test file is in tests/toml/mock_pyproject.toml """ here = pathlib.Path(__file__).parent tomlfile_path = here.joinpath("toml", "mock_pyproject.toml") cliargs = [ f"--config={tomlfile_path}", "cli_path", ] result = make_config(cliargs) assert result["ignore_names"] == ["name_from_toml_file"] 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([]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/tests/test_encoding.py0000664000175000017500000000174214720701132017410 0ustar00jendrikjendrikimport codecs from vulture.utils import ExitCode from . import v 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/tests/test_errors.py0000664000175000017500000000117214720701132017133 0ustar00jendrikjendrikimport pytest from vulture.utils import ExitCode from . import call_vulture, v 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 ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1599907056.0 vulture-2.14/tests/test_format_strings.py0000644000175000017500000000302613727122360020664 0ustar00jendrikjendrikfrom . 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"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1597479718.0 vulture-2.14/tests/test_ignore.py0000644000175000017500000000561713715715446017130 0ustar00jendrikjendrikfrom 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*"], []) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692627768.0 vulture-2.14/tests/test_imports.py0000664000175000017500000001452214470671470017334 0ustar00jendrikjendrikfrom . 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"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692627768.0 vulture-2.14/tests/test_item.py0000664000175000017500000000333214470671470016572 0ustar00jendrikjendrikfrom . 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1580740985.0 vulture-2.14/tests/test_make_whitelist.py0000644000175000017500000000301713616030571020633 0ustar00jendrikjendrikimport 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"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/tests/test_noqa.py0000644000175000017500000001433114720701132016554 0ustar00jendrikjendrikimport 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, []) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728463882.0 vulture-2.14/tests/test_pytype.py0000664000175000017500000000035214701442012017146 0ustar00jendrikjendrikimport subprocess import sys import pytest @pytest.mark.skipif( sys.version_info >= (3, 13), reason="needs Python < 3.13 for pytype" ) def test_pytype(): assert subprocess.run(["pytype", "vulture/core.py"]).returncode == 0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/tests/test_reachability.py0000664000175000017500000002553214720701132020265 0ustar00jendrikjendrikfrom . import check_multiple_unreachable, check_unreachable, 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_return_function_definition(v): v.scan( """\ def foo(): return True def bar(): return False """ ) check_unreachable(v, 3, 2, "return") def test_raise_global(v): v.scan( """\ raise ValueError a = 1 """ ) check_unreachable(v, 2, 1, "raise") 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_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") def test_if_true_return(v): v.scan( """\ def foo(a): if True: return 0 print(":-(") """ ) check_multiple_unreachable(v, [(2, 2, "if"), (4, 1, "if")]) def test_if_true_return_else(v): v.scan( """\ def foo(a): if True: return 0 else: return 1 print(":-(") """ ) check_multiple_unreachable(v, [(5, 1, "else"), (6, 1, "if")]) def test_if_some_branches_return(v): v.scan( """\ def foo(a): if a == 0: return 0 elif a == 1: pass else: return 2 print(":-(") """ ) assert v.unreachable_code == [] def test_if_all_branches_return(v): v.scan( """\ def foo(a): if a == 0: return 0 elif a == 1: return 1 else: return 2 print(":-(") """ ) check_unreachable(v, 8, 1, "if") def test_if_all_branches_return_nested(v): v.scan( """\ def foo(a, b): if a: if b: return 1 return 2 else: return 3 print(":-(") """ ) check_unreachable(v, 8, 1, "if") def test_if_all_branches_return_or_raise(v): v.scan( """\ def foo(a): if a == 0: return 0 else: raise Exception() print(":-(") """ ) check_unreachable(v, 6, 1, "if") def test_try_fall_through(v): v.scan( """\ def foo(): try: pass except IndexError as e: raise e print(":-(") """ ) assert v.unreachable_code == [] def test_try_some_branches_raise(v): v.scan( """\ def foo(e): try: raise e except IndexError as e: pass except Exception as e: raise e print(":-(") """ ) assert v.unreachable_code == [] def test_try_all_branches_return_or_raise(v): v.scan( """\ def foo(): try: return 2 except IndexError as e: raise e except Exception as e: raise e print(":-(") """ ) check_unreachable(v, 8, 1, "try") def test_try_nested_no_fall_through(v): v.scan( """\ def foo(a): try: raise a except: try: return except Exception as e: raise e print(":-(") """ ) check_unreachable(v, 9, 1, "try") def test_try_reachable_else(v): v.scan( """\ def foo(): try: print(":-)") except: return 1 else: print(":-(") """ ) assert v.unreachable_code == [] def test_try_unreachable_else(v): v.scan( """\ def foo(): try: raise Exception() except Exception as e: return 1 else: print(":-(") """ ) check_unreachable(v, 7, 1, "else") def test_with_fall_through(v): v.scan( """\ def foo(a): with a(): raise Exception() print(":-(") """ ) assert v.unreachable_code == [] def test_async_with_fall_through(v): v.scan( """\ async def foo(a): async with a(): raise Exception() print(":-(") """ ) assert v.unreachable_code == [] def test_for_fall_through(v): v.scan( """\ def foo(a): for i in a: raise Exception() print(":-(") """ ) assert v.unreachable_code == [] def test_async_for_fall_through(v): v.scan( """\ async def foo(a): async for i in a: raise Exception() print(":-(") """ ) assert v.unreachable_code == [] def test_while_false(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_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") def test_while_fall_through(v): v.scan( """\ def foo(a): while a > 0: return 1 print(":-(") """ ) assert v.unreachable_code == [] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692627768.0 vulture-2.14/tests/test_report.py0000664000175000017500000000401214470671470017143 0ustar00jendrikjendrikimport 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/tests/test_scavenging.py0000664000175000017500000003623114720701132017747 0ustar00jendrikjendrikimport sys import pytest from vulture.utils import ExitCode from . import check, v 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"]) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/tests/test_script.py0000664000175000017500000000423014720701132017121 0ustar00jendrikjendrikimport glob import os.path import subprocess import sys from vulture.utils import ExitCode from . import REPO, WHITELISTS, call_vulture 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1693469034.0 vulture-2.14/tests/test_size.py0000664000175000017500000001247414474044552016614 0ustar00jendrikjendrikimport 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1580740985.0 vulture-2.14/tests/test_sorting.py0000644000175000017500000000065013616030571017307 0ustar00jendrikjendrikfrom . 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", ] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692627768.0 vulture-2.14/tests/test_utils.py0000664000175000017500000000672514470671470017005 0ustar00jendrikjendrikimport 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", "@"], ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1733679579.6825292 vulture-2.14/tests/toml/0000775000175000017500000000000014725354734015201 5ustar00jendrikjendrik././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714924016.0 vulture-2.14/tests/toml/mock_pyproject.toml0000664000175000017500000000022614615724760021124 0ustar00jendrikjendrik# This file exists for the test case: test_config::test_toml_config_custom_path [tool.vulture] verbose = true ignore_names = ["name_from_toml_file"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1728464315.0 vulture-2.14/tox.ini0000664000175000017500000000120314701442673014365 0ustar00jendrikjendrik[tox] envlist = cleanup, py{38,310,311,312,313} # 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 commands = coverage erase [testenv] deps = coverage pint # Use latest version to catch API changes. pytest pytest-cov pytype ; python_version < '3.13' 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1733679579.6825292 vulture-2.14/vulture/0000775000175000017500000000000014725354734014572 5ustar00jendrikjendrik././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1597828171.0 vulture-2.14/vulture/__init__.py0000644000175000017500000000015413717166113016671 0ustar00jendrikjendrikfrom vulture.core import Vulture from vulture.version import __version__ assert __version__ assert Vulture ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1530355315.0 vulture-2.14/vulture/__main__.py0000644000175000017500000000004613315657163016656 0ustar00jendrikjendrikfrom vulture.core import main main() ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1714924016.0 vulture-2.14/vulture/config.py0000664000175000017500000001550014615724760016410 0ustar00jendrikjendrik""" 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 = { "config": "pyproject.toml", "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( "--config", type=str, default="pyproject.toml", help="Path to pyproject.toml config file.", ) 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(cli_config["config"]).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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477752.0 vulture-2.14/vulture/core.py0000664000175000017500000005204014720701470016061 0ustar00jendrikjendrikimport ast import pkgutil import re import string import sys from fnmatch import fnmatch, fnmatchcase from functools import partial from pathlib import Path from typing import List from vulture import lines, noqa, utils from vulture.config import InputError, make_config from vulture.reachability import Reachability 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: str = name self.typ: str = typ self.filename: Path = filename self.first_lineno: int = first_lineno self.last_lineno: int = last_lineno self.message: str = message or f"unused {typ} '{name}'" self.confidence: int = 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 ( f"{utils.format_path(self.filename)}:{self.first_lineno:d}: " f"{self.message} ({self.confidence}% 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 ( f"{prefix}{self.name} # unused {self.typ} " f"({filename}:{self.first_lineno:d})" ) 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 self.noqa_lines = {} report = partial( self._define, collection=self.unreachable_code, confidence=100, ) self.reachability = Reachability(report=report) 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) # Reset the reachability internals for every module to reduce memory # usage. self.reachability.reset() 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 assert module_data is not None 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 ) -> List[Item]: """ 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 _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_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_MatchClass(self, node): for kwd_attr in node.kwd_attrs: self.used_names.add(kwd_attr) def visit(self, node): # Visit children nodes first to allow recursive reachability analysis. self.generic_visit(node) self.reachability.visit(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) ) 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): 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"], ) ) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1693469034.0 vulture-2.14/vulture/lines.py0000664000175000017500000000133314474044552016251 0ustar00jendrikjendrikdef 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/vulture/noqa.py0000664000175000017500000000250714720701132016065 0ustar00jendrikjendrikimport re from collections import defaultdict 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"] ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477038.0 vulture-2.14/vulture/reachability.py0000664000175000017500000001474214720700156017600 0ustar00jendrikjendrikimport ast from vulture import utils class Reachability: def __init__(self, report): self._report = report self._no_fall_through_nodes = set() def visit(self, node): """When called, all children of this node have already been visited.""" if isinstance(node, (ast.Break, ast.Continue, ast.Return, ast.Raise)): self._mark_as_no_fall_through(node) elif isinstance( node, ( ast.Module, ast.FunctionDef, ast.AsyncFunctionDef, ast.For, ast.AsyncFor, ast.With, ast.AsyncWith, ), ): self._can_fall_through_statements_analysis(node.body) elif isinstance(node, ast.While): self._handle_reachability_while(node) elif isinstance(node, ast.If): self._handle_reachability_if(node) elif isinstance(node, ast.IfExp): self._handle_reachability_if_expr(node) elif isinstance(node, ast.Try): self._handle_reachability_try(node) def reset(self): self._no_fall_through_nodes = set() def _can_fall_through(self, node): return node not in self._no_fall_through_nodes def _mark_as_no_fall_through(self, node): self._no_fall_through_nodes.add(node) def _can_fall_through_statements_analysis(self, statements): """Report unreachable statements. Return True if we can execute the full list of statements. """ for idx, statement in enumerate(statements): if not self._can_fall_through(statement): try: next_sibling = statements[idx + 1] except IndexError: next_sibling = None if next_sibling is not None: class_name = statement.__class__.__name__.lower() self._report( name=class_name, first_node=next_sibling, last_node=statements[-1], message=f"unreachable code after '{class_name}'", ) return False return True def _handle_reachability_if(self, node): has_else = bool(node.orelse) if utils.condition_is_always_false(node.test): self._report( name="if", first_node=node, last_node=node.body if isinstance(node, ast.IfExp) else node.body[-1], message="unsatisfiable 'if' condition", ) if_can_fall_through = True else_can_fall_through = self._can_else_fall_through( node.orelse, condition_always_true=False ) elif utils.condition_is_always_true(node.test): if_can_fall_through = self._can_fall_through_statements_analysis( node.body ) else_can_fall_through = self._can_else_fall_through( node.orelse, condition_always_true=True ) if has_else: self._report( name="else", first_node=node.orelse[0], last_node=node.orelse[-1], message="unreachable 'else' block", ) else: # Redundant if-condition without else block. self._report( name="if", first_node=node, message="redundant if-condition", ) else: if_can_fall_through = self._can_fall_through_statements_analysis( node.body ) else_can_fall_through = self._can_else_fall_through( node.orelse, condition_always_true=False ) statement_can_fall_through = ( if_can_fall_through or else_can_fall_through ) if not statement_can_fall_through: self._mark_as_no_fall_through(node) def _can_else_fall_through(self, orelse, condition_always_true): if not orelse: return not condition_always_true return self._can_fall_through_statements_analysis(orelse) def _handle_reachability_if_expr(self, node): if utils.condition_is_always_false(node.test): self._report( name="ternary", first_node=node, last_node=node.body if isinstance(node, ast.IfExp) else node.body[-1], message="unsatisfiable 'ternary' condition", ) elif utils.condition_is_always_true(node.test): else_body = node.orelse self._report( name="ternary", first_node=else_body, message="unreachable 'else' expression", ) def _handle_reachability_while(self, node): if utils.condition_is_always_false(node.test): self._report( name="while", first_node=node, last_node=node.body if isinstance(node, ast.IfExp) else node.body[-1], message="unsatisfiable 'while' condition", ) elif utils.condition_is_always_true(node.test): else_body = node.orelse if else_body: self._report( name="else", first_node=else_body[0], last_node=else_body[-1], message="unreachable 'else' block", ) self._can_fall_through_statements_analysis(node.body) def _handle_reachability_try(self, node): try_can_fall_through = self._can_fall_through_statements_analysis( node.body ) has_else = bool(node.orelse) if not try_can_fall_through and has_else: else_body = node.orelse self._report( name="else", first_node=else_body[0], last_node=else_body[-1], message="unreachable 'else' block", ) any_except_can_fall_through = any( self._can_fall_through_statements_analysis(handler.body) for handler in node.handlers ) statement_can_fall_through = ( try_can_fall_through or any_except_can_fall_through ) if not statement_can_fall_through: self._mark_as_no_fall_through(node) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/vulture/utils.py0000664000175000017500000000707214720701132016271 0ustar00jendrikjendrikimport ast import pathlib import sys import tokenize from enum import IntEnum 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 from 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) ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733679579.0 vulture-2.14/vulture/version.py0000664000175000017500000000002514725354733016625 0ustar00jendrikjendrik__version__ = "2.14" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1733679579.6835294 vulture-2.14/vulture/whitelists/0000775000175000017500000000000014725354734016771 5ustar00jendrikjendrik././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1580740350.0 vulture-2.14/vulture/whitelists/argparse_whitelist.py0000664000175000017500000000025513616027376023242 0ustar00jendrikjendrikimport argparse argparse.ArgumentParser().epilog argparse.ArgumentDefaultsHelpFormatter("prog")._fill_text argparse.ArgumentDefaultsHelpFormatter("prog")._get_help_string ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732475726.0 vulture-2.14/vulture/whitelists/ast_whitelist.py0000664000175000017500000000503114720675516022224 0ustar00jendrikjendrikfrom 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1573498174.0 vulture-2.14/vulture/whitelists/collections_whitelist.py0000664000175000017500000000020413562326476023751 0ustar00jendrikjendrikimport collections # To free memory, the "default_factory" attribute can be set to None. collections.defaultdict().default_factory ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/vulture/whitelists/ctypes_whitelist.py0000644000175000017500000000016314720701132022723 0ustar00jendrikjendrikfrom ctypes import _CFuncPtr, _Pointer _CFuncPtr.argtypes _CFuncPtr.errcheck _CFuncPtr.restype _Pointer.contents ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692627768.0 vulture-2.14/vulture/whitelists/enum_whitelist.py0000664000175000017500000000027014470671470022376 0ustar00jendrikjendrikimport enum class EnumWhitelist(enum.Enum): EnumWhitelist = 1 # Special attributes used by enum classes. EnumWhitelist.EnumWhitelist._name_ EnumWhitelist.EnumWhitelist._value_ ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1597011167.0 vulture-2.14/vulture/whitelists/logging_whitelist.py0000664000175000017500000000013713714072337023057 0ustar00jendrikjendrikimport logging logging.Filter.filter logging.getLogger().propagate logging.StreamHandler.emit ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692627768.0 vulture-2.14/vulture/whitelists/pint_whitelist.py0000664000175000017500000000007414470671470022406 0ustar00jendrikjendrikimport pint ureg = pint.UnitRegistry() ureg.default_format ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1692627768.0 vulture-2.14/vulture/whitelists/socketserver_whitelist.py0000664000175000017500000000010014470671470024141 0ustar00jendrikjendrikimport socketserver socketserver.TCPServer.allow_reuse_address ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1579719978.0 vulture-2.14/vulture/whitelists/string_whitelist.py0000644000175000017500000000035513612116452022731 0ustar00jendrikjendrikimport 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1540815940.0 vulture-2.14/vulture/whitelists/sys_whitelist.py0000644000175000017500000000015113365576104022244 0ustar00jendrikjendrikimport sys sys.excepthook # Never report redirected streams as unused. sys.stderr sys.stdin sys.stdout ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1572731531.0 vulture-2.14/vulture/whitelists/threading_whitelist.py0000644000175000017500000000012513557375213023375 0ustar00jendrikjendrikimport threading threading.Thread.daemon threading.Thread.name threading.Thread.run ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1732477530.0 vulture-2.14/vulture/whitelists/unittest_whitelist.py0000664000175000017500000000052614720701132023300 0ustar00jendrikjendrikfrom unittest import TestCase, mock 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 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1580740350.0 vulture-2.14/vulture/whitelists/whitelist_utils.py0000644000175000017500000000155213616027376022575 0ustar00jendrikjendrik""" 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 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1733679579.6825292 vulture-2.14/vulture.egg-info/0000775000175000017500000000000014725354734016264 5ustar00jendrikjendrik././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733679579.0 vulture-2.14/vulture.egg-info/PKG-INFO0000644000175000017500000006043414725354733017365 0ustar00jendrikjendrikMetadata-Version: 2.1 Name: vulture Version: 2.14 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 Platform: UNKNOWN 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 :: 3.13 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 [![PyPI Version](https://img.shields.io/pypi/v/vulture.svg)](https://pypi.python.org/pypi/vulture) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/vulture.svg)](https://anaconda.org/conda-forge/vulture) ![CI:Test](https://github.com/jendrikseipp/vulture/workflows/CI/badge.svg) [![Codecov Badge](https://codecov.io/gh/jendrikseipp/vulture/branch/main/graphs/badge.svg)](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 ``` Vulture will automatically look for a `pyproject.toml` in the current working directory. To use a `pyproject.toml` in another directory, you can use the `--config path/to/pyproject.toml` flag. ## Integrations 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). There's also a [GitHub Action for Vulture](https://github.com/gtkacz/vulture-action) and you can use Vulture programatically. For example: ``` python import vulture v = vulture.Vulture() v.scavenge(['.']) unused_code = v.get_unused_code() # returns a list of `Item` objects ``` ## 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.14 (2024-12-08) * Improve reachability analysis (kreathon, #270, #302). * Add type hints for `get_unused_code` and the fields of the `Item` class (John Doknjas, #361). # 2.13 (2024-10-02) * Add support for Python 3.13 (Jendrik Seipp, #369). * Add PyPI and conda-forge badges to README file (Trevor James Smith, #356). * Include `tests/**/*.toml` in sdist (Colin Watson). # 2.12 (2024-09-17) * Use `ruff` for linting and formatting (Anh Trinh, #347, #349). * Replace `tox` by `pre-commit` for linting and formatting (Anh Trinh, #349). * Add `--config` flag to specify path to pyproject.toml configuration file (Glen Robertson, #352). # 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. ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733679579.0 vulture-2.14/vulture.egg-info/SOURCES.txt0000664000175000017500000000273114725354733020152 0ustar00jendrikjendrikCHANGELOG.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_pytype.py tests/test_reachability.py tests/test_report.py tests/test_scavenging.py tests/test_script.py tests/test_size.py tests/test_sorting.py tests/test_utils.py tests/toml/mock_pyproject.toml vulture/__init__.py vulture/__main__.py vulture/config.py vulture/core.py vulture/lines.py vulture/noqa.py vulture/reachability.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././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733679579.0 vulture-2.14/vulture.egg-info/dependency_links.txt0000664000175000017500000000000114725354733022331 0ustar00jendrikjendrik ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733679579.0 vulture-2.14/vulture.egg-info/entry_points.txt0000664000175000017500000000005714725354733021563 0ustar00jendrikjendrik[console_scripts] vulture = vulture.core:main ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733679579.0 vulture-2.14/vulture.egg-info/requires.txt0000664000175000017500000000005114725354733020657 0ustar00jendrikjendrik [:python_version < "3.11"] tomli>=1.1.0 ././@PaxHeader0000000000000000000000000000002600000000000010213 xustar0022 mtime=1733679579.0 vulture-2.14/vulture.egg-info/top_level.txt0000664000175000017500000000001014725354733021004 0ustar00jendrikjendrikvulture