pax_global_header00006660000000000000000000000064145467266020014526gustar00rootroot0000000000000052 comment=a295cee6d188f5797aefe5d7cf77a353ed48ea93 refurb-1.27.0/000077500000000000000000000000001454672660200131025ustar00rootroot00000000000000refurb-1.27.0/.github/000077500000000000000000000000001454672660200144425ustar00rootroot00000000000000refurb-1.27.0/.github/ISSUE_TEMPLATE/000077500000000000000000000000001454672660200166255ustar00rootroot00000000000000refurb-1.27.0/.github/ISSUE_TEMPLATE/bug-report.yml000066400000000000000000000044271454672660200214450ustar00rootroot00000000000000name: "🐞 Bug Report" description: File a bug report title: "[Bug]: " labels: ["bug"] assignees: - dosisod body: - type: markdown attributes: value: Thank you for submitting a bug report for Refurb! Please fill out the information below so we can fix your issue as quickly as possible! - type: checkboxes id: tested-master attributes: label: Has your issue already been fixed? description: It is possible that your issue has already been fixed on `master`, but not released to PyPi. There could also already be an open issue for problem. options: - label: Have you checked to see if your issue still exists on the `master` branch? See [the docs](https://github.com/dosisod/refurb#developing) for instructions on how to setup a local build of Refurb. - label: Have you looked at the open/closed issues to see if anyone has already reported your issue? - type: textarea id: describe attributes: label: The Bug description: Describe what the issue you are experiencing with a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example). The placeholder example below is just an example, and can be changed as needed. value: | The following code: ```python # Your code here ``` Emits the following error: ``` $ refurb file.py # Some error here ``` But it should not be emitting an error instance because... validations: required: true - type: textarea id: refurb-versions attributes: label: Version Info description: What is the output of `refurb --version`? render: shell validations: required: true - type: input id: python-version attributes: label: Python Version description: What is the output of `python --version`? validations: required: true - type: textarea id: config-file attributes: label: Config File description: What is in the `[tool.refurb]` section of your `pyproject.toml` file, if any? value: "# N/A" render: TOML - type: textarea id: extra-info attributes: label: Extra Info description: Is there any extra information you would like to add? value: None refurb-1.27.0/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000000331454672660200206110ustar00rootroot00000000000000blank_issues_enabled: true refurb-1.27.0/.github/ISSUE_TEMPLATE/enhancement.yml000066400000000000000000000013621454672660200216370ustar00rootroot00000000000000name: "🚀 Enhancement" description: Suggest an enhancement to Refurb title: "[Enhancement]: " labels: ["enhancement"] body: - type: markdown attributes: value: Thank you for showing an interest in wanting to improve Refurb! - type: textarea id: brief attributes: label: Overview description: Give a brief (couple sentence max) description of your proposed change to Refurb. validations: required: true - type: textarea id: proposal attributes: label: Proposal description: Describe the changes you would like to see in more detail. Include images, examples, mock ups, or any other applicable information which helps convey your proposed change! validations: required: true refurb-1.27.0/.github/dependabot.yml000066400000000000000000000003141454672660200172700ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: "pip" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" refurb-1.27.0/.github/workflows/000077500000000000000000000000001454672660200164775ustar00rootroot00000000000000refurb-1.27.0/.github/workflows/ci.yml000066400000000000000000000021221454672660200176120ustar00rootroot00000000000000name: Test on: push: branches: - "master" pull_request: jobs: tests: runs-on: ubuntu-latest strategy: matrix: python-version: ["3.10", "3.11", "3.12"] env: FORCE_COLOR: 1 TERM: xterm-color MYPY_FORCE_COLOR: 1 MYPY_FORCE_TERMINAL_WIDTH: 200 PYTEST_ADDOPTS: --color=yes steps: - uses: actions/checkout@v3 - name: Install locales run: | sudo locale-gen zh_CN.GBK sudo update-locale - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - name: Pip install run: make install - name: Run ruff run: make ruff - name: Run mypy run: make mypy - name: Run black run: make black - name: Run isort run: make isort - name: Run typos run: make typos - name: Run unit tests run: make test - name: Run e2e tests run: make test-e2e # TODO: fail if docs are out of date - name: Build docs run: make docs refurb-1.27.0/.github/workflows/deploy.yml000066400000000000000000000006341454672660200205210ustar00rootroot00000000000000name: Deploy on: workflow_dispatch: push: tags: - 'v*' jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: '3.10' - name: Install Poetry run: pipx install poetry - name: Deploy run: poetry publish -u __token__ -p "${{ secrets.PYPI_DEPLOY }}" --build refurb-1.27.0/.gitignore000066400000000000000000000001321454672660200150660ustar00rootroot00000000000000.venv __pycache__ .mypy_cache dist .coverage # Used for local plugin development: tmp.py refurb-1.27.0/.pre-commit-hooks.yaml000066400000000000000000000002771454672660200172470ustar00rootroot00000000000000- id: refurb name: refurb description: A tool for refurbishing and modernizing Python codebases. entry: refurb language: python types: [python] require_serial: true refurb-1.27.0/LICENSE000066400000000000000000001045151454672660200141150ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . refurb-1.27.0/Makefile000066400000000000000000000013671454672660200145510ustar00rootroot00000000000000.PHONY: install ruff mypy black isort typos test test-e2e refurb docs all: ruff mypy black isort typos test refurb docs install: pip install -e . pip install -r dev-requirements.txt ruff: ruff refurb test mypy: mypy refurb mypy test --exclude "test/data*" black: black refurb test --check --diff isort: isort . --diff --check typos: typos --format brief test: pytest test-e2e: install refurb test/e2e/dummy.py refurb: refurb refurb test/*.py test/%.txt: test/%.py refurb "$^" --enable-all --quiet --no-color > "$@" || true update-tests: $(patsubst %.py,%.txt,$(wildcard test/data*/*.py)) docs: python3 -m docs.gen_checks fmt: ruff refurb test --fix isort . black refurb test clean: rm -rf .mypy_cache .ruff_cache .pytest_cache refurb-1.27.0/README.md000066400000000000000000000356561454672660200144000ustar00rootroot00000000000000# Refurb A tool for refurbishing and modernizing Python codebases. ## Example ```python # main.py for filename in ["file1.txt", "file2.txt"]: with open(filename) as f: contents = f.read() lines = contents.splitlines() for line in lines: if not line or line.startswith("# ") or line.startswith("// "): continue for word in line.split(): print(f"[{word}]", end="") print("") ``` Running: ``` $ refurb main.py main.py:3:17 [FURB109]: Use `in (x, y, z)` instead of `in [x, y, z]` main.py:4:5 [FURB101]: Use `y = Path(x).read_text()` instead of `with open(x, ...) as f: y = f.read()` main.py:10:40 [FURB102]: Replace `x.startswith(y) or x.startswith(z)` with `x.startswith((y, z))` main.py:16:9 [FURB105]: Use `print() instead of `print("")` ``` ## Installing ``` $ pipx install refurb $ refurb file.py folder/ ``` > **Note** > Refurb must be run on Python 3.10+, though it can check Python 3.7+ code by setting the `--python-version` flag. ## Explanations For Checks You can use `refurb --explain FURB123`, where `FURB123` is the error code you are trying to look up. For example: ```` $ refurb --explain FURB123 Don't cast a variable or literal if it is already of that type. For example: Bad: ``` name = str("bob") num = int(123) ``` Good: ``` name = "bob" num = 123 ``` ```` An online list of all available checks can be viewed [here](./docs/checks.md). ## Ignoring Errors Use `--ignore 123` to ignore error 123. The error code can be in the form `FURB123` or `123`. This flag can be repeated. > The `FURB` prefix indicates that this is a built-in error. The `FURB` prefix is optional, > but for all other errors (ie, `ABC123`), the prefix is required. You can also use inline comments to disable errors: ```python x = int(0) # noqa: FURB123 y = list() # noqa ``` Here, `noqa: FURB123` specifically ignores the FURB123 error for that line, and `noqa` ignores all errors on that line. You can also specify multiple errors to ignore by separating them with a comma/space: ```python x = not not int(0) # noqa: FURB114, FURB123 x = not not int(0) # noqa: FURB114 FURB123 ``` ## Enabling/Disabling Checks Certain checks are disabled by default, and need to be enabled first. You can do this using the `--enable ERR` flag, where `ERR` is the error code of the check you want to enable. A disabled check differs from an ignored check in that a disabled check will never be loaded, whereas an ignored check will be loaded, an error will be emitted, and the error will be suppressed. Use the `--verbose`/`-v` flag to get a full list of enabled checks. The opposite of `--enable` is `--disable`, which will disable a check. When `--enable` and `--disable` are both specified via the command line, whichever one comes last will take precedence. When using `enable` and `disable` via the config file, `disable` will always take precedence. Use the `--disable-all` flag to disable all checks. This allows you to incrementally `--enable` checks as you see fit, as opposed to adding a bunch of `--ignore` flags. To use this in the config file, set `disable_all` to `true`. Use the `--enable-all` flag to enable all checks by default. This allows you to opt into all checks that Refurb (and Refurb plugins) have to offer. This is a good option for new codebases. To use this in a config file, set `enable_all` to `true`. In the config file, `disable_all`/`enable_all` is applied first, and then the `enable` and `disable` fields are applied afterwards. > Note that `disable_all` and `enable_all` are mutually exclusive, both on the command line and in > the config file. You will get an error if you try to specify both. You can also disable checks by category using the `#category` syntax. For example, `--disable "#readability"` will disable all checks with the `readability` category. The same applies for `enable` and `ignore`. Also, if you disable an entire category you can still explicitly re-enable a check in that category. > Note that `#readability` is wrapped in quotes because your shell will interpret the `#` as the > start of a comment. ## Setting Python Version Use the `--python-version` flag to tell Refurb which version of Python your codebase is using. This should allow for better detection of language features, and allow for better error messages. The argument for this flag must be in the form `x.y`, for example, `3.10`. The syntax for using this in the config file is `python_version = "3.10"`. When the Python version is unspecified, Refurb uses whatever version your local Python installation uses. For example, if your `python --version` is `3.11.5`, Refurb uses `3.11`, dropping the `5` patch version. ## Changing Output Formats By default everything is outputted as plain text: ``` file.py:1:5 [FURB123]: Replace `int(x)` with `x` ``` Here are all of the available formats: * `text`: The default * `github`: Print output for use with [GitHub Annotations](https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions) * More to come! To change the default format use `--format XYZ` on the command line, or `format = "XYZ"` in the config file. ## Changing Sort Order By default errors are sorted by filename, then by error code. To change this, use the `--sort XYZ` flag on the command line, or `sort_by = "XYZ"` in the config file, where `XYZ` is one of the following sort modes: * `filename`: Sort files in alphabetical order (the default) * `error`: Sort by error first, then by filename ## Overriding Mypy Flags This is typically used for development purposes, but can also be used to better fine-tune Mypy from within Refurb. Any command line arguments after `--` are passed to Mypy. For example: ``` $ refurb files -- --show-traceback ``` This tells Mypy to show a traceback if it crashes. You can also use this in the config file by assigning an array of values to the `mypy_args` field. Note that any Mypy arguments passed via the command line arguments will override the `mypy_args` field in the config file. ## Configuring Refurb In addition to the command line arguments, you can also add your settings in the `pyproject.toml` file. For example, the following command line arguments: ``` refurb file.py --ignore 100 --load some_module --quiet ``` Corresponds to the following in your `pyproject.toml` file: ```toml [tool.refurb] ignore = [100] load = ["some_module"] quiet = true ``` Now all you need to type is `refurb file.py`! Note that the values in the config file will be merged with the values specified via the command line. In the case of boolean arguments like `--quiet`, the command line arguments take precedence. All other arguments (such as `ignore` and `load`) will be combined. You can use the `--config-file` flag to tell Refurb to use a different config file from the default `pyproject.toml` file. Note that it still must be in the same form as the normal `pyproject.toml` file. Click [here](./docs/configs/README.md) to see some example config files. ### Ignore Checks Per File/Folder If you have a large codebase you might want to ignore errors for certain files or folders, which allows you to incrementally fix errors as you see fit. To do that, add the following to your `pyproject.toml` file: ```toml # these settings will be applied globally [tool.refurb] enable_all = true # these will only be applied to the "src" folder [[tool.refurb.amend]] path = "src" ignore = ["FURB123", "FURB120"] # these will only be applied to the "src/util.py" file [[tool.refurb.amend]] path = "src/util.py" ignore = ["FURB125", "FURB148"] ``` > Note that only the `ignore` field is available in the `amend` sections. This is because > a check can only be enabled/disabled for the entire codebase, and cannot be selectively > enabled/disabled on a per-file basis. Assuming a check is enabled though, you can simply > `ignore` the errors for the files of your choosing. ## Using Refurb With `pre-commit` You can use Refurb with [pre-commit](https://pre-commit.com/) by adding the following to your `.pre-commit-config.yaml` file: ```yaml - repo: https://github.com/dosisod/refurb rev: REVISION hooks: - id: refurb ``` Replacing `REVISION` with a version or SHA of your choosing (or leave it blank to let `pre-commit` find the most recent one for you). ## Plugins Installing plugins for Refurb is very easy: ``` $ pip install refurb-plugin-example ``` Where `refurb-plugin-example` is the name of the plugin. Refurb will automatically load any installed plugins. To make your own Refurb plugin, see the [`refurb-plugin-example` repository](https://github.com/dosisod/refurb-plugin-example) for more info. ## Writing Your Own Check If you want to extend Refurb but don't want to make a full-fledged plugin, you can easily create a one-off check file with the `refurb gen` command. > Note that this command uses the `fzf` fuzzy-finder for getting user input, > so you will need to [install fzf](https://github.com/junegunn/fzf#installation) before continuing. Here is the basic overview for creating a new check using the `refurb gen` command: 1. First select the node type you want to accept 2. Then type in where you want to save the auto generated file 3. Add your code to the new file To get an idea of what you need to add to your check, use the `--debug` flag to see the AST representation for a given file (ie, `refurb --debug file.py`). Take a look at the files in the `refurb/checks/` folder for some examples. Then, to load your new check, use `refurb file.py --load your.path.here` > Note that when using `--load`, you need to use dots in your argument, just like > importing a normal python module. If `your.path.here` is a directory, all checks > in that directory will be loaded. If it is a file, only that file will be loaded. ## Troubleshooting If Refurb is running slow, use the `--timing-stats` flag to diagnose why: ``` $ refurb file --timing-stats /tmp/stats.json ``` This will output a JSON file with the following information: * Total time Mypy took to parse the modules (a majority of the time usually). * Time Mypy spent parsing each module. Useful for finding very large/unused files. * Time Refurb spent checking each module. These numbers should be very small (less than 100ms). Larger files naturally take longer to check, but files that take way too long should be looked into, as an issue might only manifest themselves when a file reaches a certain size. ## Disable Color Color output is enabled by default in Refurb. To disable it, do one of the following: * Set the `NO_COLOR` env var. * Use the `--no-color` flag. * Set `color = false` in the config file. * Pipe/redirect Refurb output to another program or file. ## Developing / Contributing ### Setup To setup locally run: ``` $ git clone https://github.com/dosisod/refurb $ cd refurb $ make install ``` Tests can be ran all at once using `make`, or you can run each tool on its own using `make black`, `make flake8`, and so on. Unit tests can be ran with `pytest` or `make test`. > Since the end-to-end (e2e) tests are slow, they are not ran when running `make`. > You will need to run `make test-e2e` to run them. ### Updating Documentation We encourage people to update the documentation when they see typos and other issues! With that in mind though, don't directly modify the `docs/checks.md` file. It is auto-generated and will be overridden when new checks are added. The documentation for checks can be updated by changing the docstrings of in the checks themselves. For example, to update `FURB100`, change the docstring of the `ErrorInfo` class in the `refurb/checks/pathlib/with_suffix.py` file. You can find the file for a given check by grep-ing for `code = XYZ`, where `XYZ` is the check you are looking for but with the `FURB` prefix removed. Use the `--verbose` flag with `--explain` to find the filename for a given check. For example: ``` $ refurb --explain FURB123 --verbose Filename: refurb/checks/readability/no_unnecessary_cast.py FURB123: no-redundant-cast [readability] ... ``` ## Why Does This Exist? I love doing code reviews: I like taking something and making it better, faster, more elegant, and so on. Lots of static analysis tools already exist, but none of them seem to be focused on making code more elegant, more readable, or more modern. That is where Refurb comes in. Refurb is heavily inspired by [clippy](https://rust-lang.github.io/rust-clippy/master/index.html), the built-in linter for Rust. ## What Refurb Is Not Refurb is not a style/type checker. It is not meant as a first-line of defense for linting and finding bugs, it is meant for making good code even better. ## Comparison To Other Tools There are already lots of tools out there for linting and analyzing Python code, so you might be wondering why Refurb exists (skepticism is good!). As mentioned above, Refurb checks for code which can be made more elegant, something that no other linters (that I have found) specialize in. Here is a list of similar linters and analyzers, and how they differ from Refurb: [Black](https://github.com/psf/black): is more focused on the formatting and styling of the code (line length, trailing comas, indentation, and so on). It does a really good job of making other projects using Black look more or less the same. It doesn't do more complex things such as type checking or code smell/anti-pattern detection. [flake8](https://github.com/pycqa/flake8): flake8 is also a linter, is very extensible, and performs a lot of semantic analysis-related checks as well, such as "unused variable", "break outside of a loop", and so on. It also checks PEP8 conformance. Refurb won't try and replace flake8, because chances are you are already using flake8 anyways. [Pylint](https://github.com/PyCQA/pylint) has [a lot of checks](https://pylint.pycqa.org/en/latest/user_guide/messages/messages_overview.html) which cover a lot of ground, but in general, are focused on bad or buggy code, things which you probably didn't mean to do. Refurb assumes that you know what you are doing, and will try to cleanup what is already there the best it can. [Mypy](https://github.com/python/mypy), [Pyright](https://github.com/Microsoft/pyright), [Pyre](https://github.com/facebook/pyre-check), and [Pytype](https://github.com/google/pytype) are all type checkers, and basically just enforce types, ensures arguments match, functions are called in a type safe manner, and so on. They do much more then that, but that is the general idea. Refurb actually is built on top of Mypy, and uses its AST parser so that it gets good type information. [pyupgrade](https://github.com/asottile/pyupgrade): Pyupgrade has a lot of good checks for upgrading your older Python code to the newer syntax, which is really useful. Where Refurb differs is that Pyupgrade is more focused on upgrading your code to the newer version, whereas Refurb is more focused on cleaning up and simplifying what is already there. In conclusion, Refurb doesn't want you to throw out your old tools, since they cover different areas of your code, and all serve a different purpose. Refurb is meant to be used in conjunction with the above tools. refurb-1.27.0/dev-requirements.txt000066400000000000000000000004151454672660200171420ustar00rootroot00000000000000black==23.12.1 click==8.1.7 colorama==0.4.6 coverage==7.3.4 fastapi==0.100.0 iniconfig==2.0.0 isort==5.13.2 mypy-extensions==1.0.0 mypy==1.8.0 packaging==23.1 pathspec==0.12.1 platformdirs==4.1.0 pluggy==1.3.0 pytest-cov==4.1.0 pytest==7.4.3 ruff==0.1.9 typos==1.16.25 refurb-1.27.0/docs/000077500000000000000000000000001454672660200140325ustar00rootroot00000000000000refurb-1.27.0/docs/adding-new-checks.md000066400000000000000000000275411454672660200176400ustar00rootroot00000000000000# Adding New Checks This document is aimed at developers looking to contribute new checks to Refurb. After reading this you should be better equipped to work with Refurb, including Mypy internals, which are at the heart of Refurb. ## Setting Up See the "[Developing](/README.md#developing)" section of the README to see how to setup a dev environment for Refurb. ## Generating the Boilerplate First things first, you will want to generate the boilerplate code using the `refurb gen` command. See the "[Writing Your Own Check](/README.md#writing-your-own-check)" section of the README for more info. A few things to note: * Place your check in a folder that corresponds to what it is checking. For example, if it applies to string types, put it in the `string` folder. If it applies to the [pathlib](https://docs.python.org/3/library/pathlib.html) module, put it in the `pathlib` folder. * Base the filename of your check on what the check does to the code it checks. For example, FURB124 (`refurb/checks/logical/use_equal_chain.py`) is named `use_equal_chain` because it converts the expression `x == y or x == z` to `x == y == z`. The resulting code uses a chain of equal expressions, and it is named as such. * One exception is when your check is detecting something that you *shouldn't* be doing, in which case you should prefix it with `no_`. For example, the `no_del.py` check will check for usage of the `del` statement. * Choose a prefix which is between 3-4 characters, and is uppercase alpha (regex: `[A-Z]{3,4}`). * Make sure that the auto-generated error code id (the `code` field) is correct. It will try to detect the next id based off of the supplied prefix, but if it cannot find it, it will default to 100. Also, if you are making a check for Refurb itself, remove the `suffix` line, which defaults to `XYZ`. Deleting this will fallback to `FURB`, which is used for the built-in Refurb checks. ## Coming Up With An Idea For the rest of this article, we will be creating the following check: Basically, it will detect whenever you are comparing a floating point number using the `==` operator. For instance: ```python x = 1.0 y = 2.0 z = (x + y) == 0.3 ``` If you where to run the following code, `z` would be `False`, due to how floating point numbers work (see [0.30000000000000004.com](https://0.30000000000000004.com/) for more info). For this example, we will be writing our code in `refurb/checks/logical/no_float_cmp.py`, and our error code id will be `132`. Your name, number, and idea should be different, but the general idea is the same. Let's get started! ## Figuring Out What to Do The easiest way to see what you need to do is to create a small file with the code you want to check against. For example, lets create a file called `tmp.py`, and put our code from above into it: ```python x = 1.0 y = 2.0 z = (x + y) == 0.3 ``` Then, we will run `refurb tmp.py --debug --quiet`. You should see something like this: ``` MypyFile:1( tmp.py AssignmentStmt:1( NameExpr(x [tmp.x]) FloatExpr(1.0) builtins.float) AssignmentStmt:2( NameExpr(y [tmp.y]) FloatExpr(2.0) builtins.float) AssignmentStmt:4( NameExpr(z* [tmp.z]) ComparisonExpr:4( == OpExpr:4( + NameExpr(x [tmp.x]) NameExpr(y [tmp.y])) FloatExpr(0.3)))) ``` This is the [AST](https://en.wikipedia.org/wiki/Abstract_syntax_tree) representation of our code. Some things to note: * Files are represented with the `MypyFile` type. * Each `MypyFile` contains a `Block`, which is a list of statements. In this case, we have a bunch of `AssignmentStmt` statements. * Each `AssignmentStmt` is composed of 2 major parts: * The `NameExpr`, which is the name/variable being assigned to * The expression being assigned: `FloatExpr`, `ComparisonExpr`, etc. For our check, we only need to care about the `ComparisonExpr` part. Lets jump in! ## Writing the Check We will start by updating the `check` function in our `no_float_cmp.py` file to the following: ```python def check(node: ComparisonExpr, errors: list[Error]) -> None: match node: case ComparisonExpr(): errors.append(ErrorInfo(node.line, node.column)) ``` This code will basically emit an error whenever a `ComparisonExpr` is found, regardless of what we are comparing. Lets make sure it works first by running `refurb tmp.py` again. We should see an error like the following: ``` tmp.py:4:5 [FURB132]: Your message here ``` Now lets add some test code to the `tmp.py` file to test that we are actually checking the code we care about: ```python x = 1.0 y = 2.0 # these should match _ = x == 0.3 _ = (x + y) == 0.3 # these should not _ = x <= 0.3 _ = x == 1 ``` Notice how I switched the `z` variable to `_`. This is because `_` is a placeholder variable in Python, and will gobble up any value you put into it. Since we cannot just write `(x + y) == 0.3` on a line all by itself, we have to assign it to a variable instead [^1]. If we re-run, we get the following: ``` tmp.py:6:5 [FURB132]: Your message here tmp.py:7:5 [FURB132]: Your message here tmp.py:11:5 [FURB132]: Your message here tmp.py:12:5 [FURB132]: Your message here ``` Lets fix that! Basically, we only want to match on a `ComparisonExpr` that has an `==`, and a float on either the left or right hand side. Lets go to the definition of the `ComparisonExpr` class and see what we can find: ```python class ComparisonExpr(Expression): """Comparison expression (e.g. a < b > c < d).""" __slots__ = ("operators", "operands", "method_types") operators: list[str] operands: list[Expression] # Inferred type for the operator methods (when relevant; None for 'is'). method_types: list[mypy.types.Type | None] ... ``` Basically, a comparison expression can be a simple comparison, like `x == y`, or it can be a more complex comparison, such as `x < y < z`. This is why we have a list of operators, and a list of operands. To start with, lets match the following: ```python match node: case ComparisonExpr( operators=["=="], operands=[FloatExpr(), _] | [_, FloatExpr()], ): ``` This will match any `ComparisonExpr` that has a `FloatExpr` on the left or right hand side of an `==` expression. In this case, a `FloatExpr` is a floating point literal, such as `3.14`, and not a `float` variable. The `|` is an "Or Pattern", meaning an array with a `FloatExpr` on the left or right hand side will cause the pattern match to succeed. `_` is a placeholder meaning any value can be there. Now when we run, we get the following: ``` tmp.py:6:5 [FURB132]: Your message here tmp.py:7:5 [FURB132]: Your message here ``` Much better! One issue: The following code will not emit an error: ```python _ = x == y == 0.3 ``` This is because we only allow a `ComparisonExpr` if it has a single `==` operator. One way of fixing it is by changing our check to the following: ```python def check(node: ComparisonExpr, errors: list[Error]) -> None: match node: case ComparisonExpr(operators=operators, operands=operands): for oper, exprs in merge_comparison(operators, operands): if oper == "==": for expr in exprs: if isinstance(expr, FloatExpr): errors.append(ErrorInfo(expr.line, expr.column)) ``` Now in our `check` function, we: 1. Check if the operator is `==` 2. If it is, loop through the left and right hand expressions 3. If the expression is a `FloatExpr`, we emit an error Here is the definition of our `merge_comparison` function: ```python def merge_comparison( opers: list[str], exprs: list[Expression] ) -> Generator[tuple[str, tuple[Expression, Expression]], None, None]: exprs = exprs.copy() for oper in opers: yield (oper, (exprs[0], exprs[1])) exprs.pop(0) ``` The `merge_comparison` function will merge the operators and expressions into a list of tuples which we can very easily loop over. For example, `1 < 2 == 3` would be converted into: ``` [("<", (1, 2), ("==", (2, 3)))] ``` Except that `1`, `2`, and `3` would be an `Expression` instead of a plain number. That's it! ## Cleanup Our check works, but it could be simplified. For example, we know `node` is a `ComparisonExpr`, and all we are doing in the pattern match is pulling out the `operators` and `operands` fields, which we know exist on `node`. We could re-write it like so: ```python def check(node: ComparisonExpr, errors: list[Error]) -> None: for oper, exprs in merge_opers(node.operators, node.operands): if oper == "==": for expr in exprs: if isinstance(expr, FloatExpr): errors.append(ErrorInfo(expr.line, expr.column)) ``` The match statement is very good at checking very nested and complex structures, but in our case, we don't need to use it. Also, we should change the message in the `ErrorInfo` class to something like: ```python msg: str = "Don't compare float values with `==`" ``` ## The Final Code Here is the complete code for our check: ````python from dataclasses import dataclass from typing import Generator from mypy.nodes import ComparisonExpr, Expression, FloatExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ TODO: fill this in Bad: ``` # TODO: fill this in ``` Good: ``` # TODO: fill this in ``` """ code = 132 msg: str = "Don't compare float values with `==`" def merge_opers( opers: list[str], exprs: list[Expression] ) -> Generator[tuple[str, tuple[Expression, Expression]], None, None]: exprs = exprs.copy() for oper in opers: yield (oper, (exprs[0], exprs[1])) exprs.pop(0) def check(node: ComparisonExpr, errors: list[Error]) -> None: for oper, exprs in merge_opers(node.operators, node.operands): if oper == "==": for expr in exprs: if isinstance(expr, FloatExpr): errors.append(ErrorInfo(expr.line, expr.column)) ```` And the contents of our `tmp.py` testing file: ```python x = 1.0 y = 2.0 # these should match _ = x == 0.3 _ = (x + y) == 0.3 _ = x == y == 0.3 # these should not _ = x <= 0.3 _ = x == 1 ``` When we run, we get the following: ``` $ refurb tmp.py --quiet tmp.py:6:10 [FURB132]: Don't compare float values with `==` tmp.py:7:16 [FURB132]: Don't compare float values with `==` tmp.py:8:15 [FURB132]: Don't compare float values with `==` ``` ## Testing This should be pretty easy because we have been testing all along! All we need to do now is copy our code to the right place and we should be good to go: ``` $ cp tmp.py test/data/err_132.py $ refurb test/data/err_132.py --quiet > test/data/err_132.txt # or $ make update-tests ``` Now when we run `pytest`, all our tests should pass, and our coverage should be at 100%. The last step is running `make` which will run all of our linters, type-checkers, and so on. ## Common Gotchas ### Detecting Alternate Imports When you are writing your checks, one thing to keep in mind is the difference between `NameExpr`s and `MemberExpr`s: A `NameExpr` is a single identifier such as `x`, whereas a `MemberExpr` is when you use `.` to access a member of another expression, such as `x.y`. To help better explain the difference, take the following examples: ```python import sqlite3 from sqlite3 import connect db1 = sqlite3.connect("db1") db2 = connect("db2") ``` In the above example: * `MemberExpr(fullname="sqlite3.connect")` will match the value assigned to db1 * and `NameExpr(fullname="sqlite3.connect")` will match the value assigned to db2 If you want to match both expressions you need to use `RefExpr` instead. `RefExpr` is the base class for both of these expressions, which means you can catch both examples instead of one or the other. [^1]: We could write it on one line, but your linter might complain, and it is better to be more explicit that "we do not want to use this value, please ignore". refurb-1.27.0/docs/categories.md000066400000000000000000000113201454672660200164760ustar00rootroot00000000000000# Categories Here is a list of the built-in categories in Refurb, and their meanings. ## `abc` These check for code relating to [Abstract Base Classes](https://docs.python.org/3/library/abc.html). ## `builtin` Checks that have the `builtin` category cover a few different topics: * Built-in functions such as `print()`, `open()`, `str()`, and so on * Statements such as `del` * File system related operations such as `open()` and `readlines()` ## `control-flow` These checks deal with the control flow of a program, such as optimizing usage of `return` and `continue`, removing `if` statements under certain conditions, and so on. ## `contextlib` These checks are for the [contextlib](https://docs.python.org/3/library/contextlib.html) standard library module. ## `datetime` These checks are for the [datetime](https://docs.python.org/3/library/datetime.html) standard library module. ## `decimal` These checks are for the [decimal](https://docs.python.org/3/library/decimal.html) standard library module. ## `dict` These checks cover: * Usage of `dict` objects * In some cases, objects supporting the [Mapping](https://docs.python.org/3/library/collections.abc.html#collections.abc.Mapping) protocol ## `fastapi` These are checks relating to the third-party [FastAPI](https://github.com/tiangolo/fastapi) library. ## `fstring` These checks relate to Python's [f-strings](https://fstring.help/). ## `fractions` These checks are for the [fractions](https://docs.python.org/3/library/fractions.html) standard library module. ## `functools` These checks relate to the [functools](https://docs.python.org/3/library/functools.html) standard library module. ## `hashlib` These checks relate to the [hashlib](https://docs.python.org/3/library/hashlib.html) standard library module. ## `iterable` These checks cover: * Iterable types such as `list` and `tuple` * Standard library objects which are commonly iterated over such as `dict` keys ## `itertools` These checks relate to the [itertools](https://docs.python.org/3/library/itertools.html) standard library module. ## `math` These checks relate to the [math](https://docs.python.org/3/library/math.html) standard library module. ## `operator` These checks relate to the [operator](https://docs.python.org/3/library/operator.html) standard library module. ## `logical` These checks relate to logical cleanups and optimizations, primarily in `if` statements, but also in boolean expressions. ## `list` These checks cover usage of the built-in `list` object. ## `pattern-matching` Checks related to Python 3.10's [Structural Pattern Matching](https://peps.python.org/pep-0636/). ## `pathlib` These checks relate to the [pathlib](https://docs.python.org/3/library/pathlib.html) standard library module. ## `performance` These checks are supposted to find slow code that can be written faster. The threshold for "fast" and "slow" are somewhat arbitrary and depend on the check, but in general you should expect that a check in the `performance` category will make your code faster (and should never make it slower). ## `python39`, `python310`, `python311` These checks are only enabled for Python versions 3.9, 3.10, or 3.11 respectively, or in some way are improved in later versions of Python. For example, `isinstance(x, y) or isinstance(x, z)` can be written as `isinstance(x, (y, z))` in any Python version, but in Python 3.10+ it can be written as `isinstance(x, y | z)`. ## `pythonic` This is a general catch-all for things which are "unpythonic". It differs from the `readability` category because "unreadable" code can still be pythonic. ## `readability` These checks aim to make existing code more readable. This can be subjective, but in general, they reduce the horizontal or vertical length of your code, or make the underlying meaning of the code more apparent. ## `regex` These checks are for the [`re`](https://docs.python.org/3/library/contextlib.html) standard library module. ## `scoping` These checks have to do with Python's scoping rules. For more info on how Python's scoping rules work, read [this article](https://realpython.com/python-scope-legb-rule/). ## `secrets` These checks are for the [secrets](https://docs.python.org/3/library/secrets.html) standard library module. ## `set` These checks deal with usage of [`set`](https://docs.python.org/3/tutorial/datastructures.html#sets) objects in Python. ## `shlex` These checks are for the [shlex](https://docs.python.org/3/library/shlex.html) standard library module. ## `string` These checks deal with usage of [`str`](https://docs.python.org/3/library/stdtypes.html#string-methods) objects in Python. ## `truthy` These checks cover truthy and falsy operations in Python, primarily in the context of `assert` and `if` statements. refurb-1.27.0/docs/check-ideas.md000066400000000000000000000061441454672660200165210ustar00rootroot00000000000000# Check Ideas This is a running list of checks which I (and others) have suggested, and can be picked up by anyone looking to add new checks to Refurb! ## Pathlib ### Don't use `+` for path traversal, use `/` operator Bad: ```python file = "folder" + "/filename" ``` Good: ```python file = Path("folder") / "filename" ``` ## Dict ### Replace `{x[0]: x[1] for x in y}` with `dict(y)` We will need to make sure that `len(x) == 2` ## Typing These should be opt-in, since they can be quite noisy. * Convert `List` -> `list` (python 3.9+) - Include `dict`, `set`, `frozenset`, `defaultdict`, etc - See `TypeApplication` Mypy node * Convert `Optional[x]` -> `x | None` (python 3.10+) * Convert `Union[x, y]` -> `x | y` (python 3.10+) ## Dataclasses ### Replace boilerplate class with dataclasses This will be opt-in (dataclasses are simpler, but slightly slower). Bad: ```python class Person: name: str age: int def __init__(self, name: str, age: int) -> None: self.name = name self.age = age ``` Good: ```python @dataclass class Person: name: str age: int ``` ## String ### Use f-string instead of `+` Disable by default, will be noisy ### Don't use `" ".join(x.capitalize() for x in s.split())`, use `string.capwords(x)` Notes: * Check for `" "`, add as optional param to `capwords` * Allow for `title` as well as `capitalize` ### Simplify f-string Conversions: The following are only detected if in an f-string: * `x.center(y, z)` -> `f"{x:z^y}"` * `x.ljust(y, z)` -> `f"{x:z `f"{x:z>y}"` For example: ```python text = "centered" print(f"[{text.center(20)}]") # vs print(f"[{text:^20}]") ``` ## Equality/Logic ### Use Comparison Chain For example, convert `x > 0 and x < 10` into `0 < x < 10` ## Collections ### Counter Example TBD ### Default dict Replace instances similar to this: ```python d = {} if x in d: d[x].append(y) else: d[x] = [y] ``` With this: ```python d = defaultdict(list) d[x].append(y) ``` ## List ## Built-in methods See https://docs.python.org/3/library/stdtypes.html See https://docs.python.org/3/library/io.html * Use `frozenset` when `set` is never appended to * Don't roll your own max/min/sum functions, use `max`/`min`/`sum` instead * `print(f"{x} {y}")` -> `print(x, y)` * Don't use `_` in expressions * Use `x ** y` instead of `pow(x, y)` * Unless the `mod` param of `pow` is being used * Don't call `print()` repeatedly, call `print()` once with multi line string * Don't call `sys.stderr.write("asdf\n")`, use `print("asdf", file=sys.stderr)` ## Enum ### Use `auto()` if all members of enum are explicitly set, and only increment by one ## Itertools ### Use `itertools.product` Bad: ```python for x in range(10): for y in range(10): pass # no code here! ``` Good: ```python for x, y in product(range(10), range(10)): pass ``` ### Use `itertools.chain` Bad: ```python for item in (*list1, *list2, *list3): # do something ``` Good: ```python for item in itertools.chain(list1, list2, list3): # do something ``` ## Iteration ### Don't use `x = x[::-1]`, use `x.reverse()` refurb-1.27.0/docs/checks.md000066400000000000000000001141541454672660200156220ustar00rootroot00000000000000 # Available Checks ## FURB100: `use-pathlib-with-suffix` Categories: `pathlib` A common operation is changing the extension of a file. If you have an existing `Path` object, you don't need to convert it to a string, slice it, and append a new extension. Instead, use the `with_suffix()` method: Bad: ```python new_filepath = str(Path("file.txt"))[:4] + ".md" ``` Good: ```python new_filepath = Path("file.txt").with_suffix(".md") ``` ## FURB101: `use-pathlib-read-text-read-bytes` Categories: `pathlib` When you just want to save the contents of a file to a variable, using a `with` block is a bit overkill. A simpler alternative is to use pathlib's `read_text()` function: Bad: ```python with open(filename) as f: contents = f.read() ``` Good: ```python contents = Path(filename).read_text() ``` ## FURB102: `use-startswith-endswith-tuple` Categories: `string` `startswith()` and `endswith()` both take a tuple, so instead of calling `startswith()` multiple times on the same string, you can check them all at once: Bad: ```python name = "bob" if name.startswith("b") or name.startswith("B"): pass ``` Good: ```python name = "bob" if name.startswith(("b", "B")): pass ``` ## FURB103: `use-pathlib-write-text-write-bytes` Categories: `pathlib` When you just want to save some contents to a file, using a `with` block is a bit overkill. Instead you can use pathlib's `write_text()` method: Bad: ```python with open(filename, "w") as f: f.write("hello world") ``` Good: ```python Path(filename).write_text("hello world") ``` ## FURB104: `use-pathlib-cwd` Categories: `pathlib` A modern alternative to `os.getcwd()` is the `Path.cwd()` method: Bad: ```python cwd = os.getcwd() ``` Good: ```python cwd = Path.cwd() ``` ## FURB105: `simplify-print` Categories: `builtin` `readability` `print("")` can be simplified to just `print()`. ## FURB106: `use-expandtabs` Categories: `string` If you want to expand the tabs at the start of a string, don't use `.replace("\t", " " * 8)`, use `.expandtabs()` instead. Note that this only works if the tabs are at the start of the string, since `expandtabs()` will expand each tab to the nearest tab column. Bad: ```python spaces_8 = "\thello world".replace("\t", " " * 8) spaces_4 = "\thello world".replace("\t", " ") ``` Good: ```python spaces_8 = "\thello world".expandtabs() spaces_4 = "\thello world".expandtabs(4) ``` ## FURB107: `use-with-suppress` Categories: `contextlib` `readability` Often times you want to handle an exception and just ignore it. You can do this with a `try`/`except` block with a single `pass` in the `except` block, but there is a simpler and more concise way using the `suppress()` function from `contextlib`: Bad: ```python try: f() except FileNotFoundError: pass ``` Good: ```python with suppress(FileNotFoundError): f() ``` Note: `suppress()` is slower than using `try`/`except`, so for performance critical code you might consider ignoring this check. ## FURB108: `use-in-oper` Categories: `logical` `readability` When comparing a value to multiple possible options, don't `or` multiple comparison checks, use a single `in` expr: Bad: ```python if x == "abc" or x == "def": pass ``` Good: ```python if x in ("abc", "def"): pass ``` Note: This should not be used if the operands depend on boolean short circuiting, since the operands will be eagerly evaluated. This is primarily useful for comparing against a range of constant values. ## FURB109: `use-consistent-in-bracket` Categories: `iterable` `readability` Since tuple, list, and set literals can be used with the `in` operator, it is best to pick one and stick with it. Bad: ```python for x in (1, 2, 3): pass nums = [str(x) for x in [1, 2, 3]] ``` Good: ```python for x in (1, 2, 3): pass nums = [str(x) for x in (1, 2, 3)] ``` ## FURB110: `use-or-oper` Categories: `logical` `readability` Sometimes the ternary operator (aka, inline if statements) can be simplified to a single `or` expression. Bad: ```python z = x if x else y ``` Good: ```python z = x or y ``` Note: if `x` depends on side-effects, then this check should be ignored. ## FURB111: `use-func-name` Categories: `readability` Don't use a lambda if it is just forwarding its arguments to a function verbatim: Bad: ```python predicate = lambda x: bool(x) some_func(lambda x, y: print(x, y)) ``` Good: ```python predicate = bool some_func(print) ``` ## FURB112: `use-literal` Categories: `pythonic` `readability` Using `list` and `dict` without any arguments is slower, and not Pythonic. Use `[]` and `{}` instead: Bad: ```python nums = list() books = dict() ``` Good: ```python nums = [] books = {} ``` ## FURB113: `use-list-extend` Categories: `list` When appending multiple values to a list, you can use the `.extend()` method to add an iterable to the end of an existing list. This way, you don't have to call `.append()` on every element: Bad: ```python nums = [1, 2, 3] nums.append(4) nums.append(5) nums.append(6) ``` Good: ```python nums = [1, 2, 3] nums.extend((4, 5, 6)) ``` ## FURB114: `no-double-not` Categories: `builtin` `readability` `truthy` Double negatives are confusing, so use `bool(x)` instead of `not not x`. Bad: ```python if not not value: pass ``` Good: ```python if value: pass ``` ## FURB115: `no-len-compare` Categories: `iterable` `truthy` Don't check a container's length to determine if it is empty or not, use a truthiness check instead: Bad: ```python name = "bob" if len(name) == 0: pass nums = [1, 2, 3] if len(nums) >= 1: pass ``` Good: ```python name = "bob" if not name: pass nums = [1, 2, 3] if nums: pass ``` ## FURB116: `use-fstring-number-format` Categories: `builtin` `fstring` The `bin()`, `oct()`, and `hex()` functions return the string representation of a number but with a prefix attached. If you don't want the prefix, you might be tempted to just slice it off, but using an f-string will give you more flexibility and let you work with negative numbers: Bad: ```python print(bin(1337)[2:]) ``` Good: ```python print(f"{1337:b}") ``` ## FURB117: `use-pathlib-open` Categories: `pathlib` When you want to open a Path object, don't pass it to `open()`, just call `.open()` on the Path object itself: Bad: ```python path = Path("filename") with open(path) as f: pass ``` Good: ```python path = Path("filename") with path.open() as f: pass ``` ## FURB118: `use-operator` Categories: `operator` Don't write lambdas/functions to wrap builtin operators, use the `operator` module instead: Bad: ```python from functools import reduce nums = [1, 2, 3] print(reduce(lambda x, y: x + y, nums)) # 6 ``` Good: ```python from functools import reduce from operator import add nums = [1, 2, 3] print(reduce(add, nums)) # 6 ``` ## FURB119: `use-fstring-format` Categories: `builtin` `fstring` Certain expressions which are passed to f-strings are redundant because the f-string itself is capable of formatting it. For example: Bad: ```python print(f"{bin(1337)}") print(f"{ascii(input())}") print(f"{str(123)}") ``` Good: ```python print(f"{1337:#b}") print(f"{input()!a}") print(f"{123}") ``` ## FURB120: `use-implicit-default` Categories: Don't pass an argument if it is the same as the default value: Bad: ```python def greet(name: str = "bob") -> None: print(f"Hello {name}") greet("bob") {}.get("some key", None) ``` Good: ```python def greet(name: str = "bob") -> None: print(f"Hello {name}") greet() {}.get("some key") ``` ## FURB121: `use-isinstance-issubclass-tuple` Categories: `python310` `readability` `isinstance()` and `issubclass()` both take tuple arguments, so instead of calling them multiple times for the same object, you can check all of them at once: Bad: ```python if isinstance(num, float) or isinstance(num, int): pass ``` Good: ```python if isinstance(num, (float, int)): pass ``` Note: In Python 3.10+, you can also pass type unions as the second param to these functions: ```python if isinstance(num, float | int): pass ``` ## FURB122: `use-writelines` Categories: `builtin` `readability` When you want to write a list of lines to a file, don't call `.write()` for every line, use `.writelines()` instead: Bad: ```python lines = ["line 1\n", "line 2\n", "line 3\n"] with open("file") as f: for line in lines: f.write(line) ``` Good: ```python lines = ["line 1\n", "line 2\n", "line 3\n"] with open("file") as f: f.writelines(lines) ``` Note: If you have a more complex expression then just `lines`, you may need to use a list comprehension instead. For example: ```python f.writelines(f"{line}\n" for line in lines) ``` ## FURB123: `no-redundant-cast` Categories: `readability` Don't cast a variable or literal if it is already of that type. This usually is the result of not realizing a type is already the type you want, or artifacts of some debugging code. One example of where this might be intentional is when using container types like `dict` or `list`, which will create a shallow copy. If that is the case, it might be preferable to use `.copy()` instead, since it makes it more explicit that a copy is taking place. Examples: Bad: ```python name = str("bob") num = int(123) ages = {"bob": 123} copy = dict(ages) ``` Good: ```python name = "bob" num = 123 ages = {"bob": 123} copy = ages.copy() ``` ## FURB124: `use-comparison-chain` Categories: `logical` `readability` When checking that multiple objects are equal to each other, don't use an `and` expression. Use a comparison chain instead, for example: Bad: ```python if x == y and x == z: pass # and if x is None and y is None pass ``` Good: ```python if x == y == z: pass # and if x is y is None: pass ``` Note: if `x` depends on side-effects, then this check should be ignored. ## FURB125: `no-redundant-return` Categories: `control-flow` `readability` Don't explicitly return if you are already at the end of the control flow for the current function: Bad: ```python def func(): print("hello world!") return def func2(x): if x == 1: print("x is 1") else: print("x is not 1") return ``` Good: ```python def func(): print("hello world!") def func2(x): if x == 1: print("x is 1") else: print("x is not 1") ``` ## FURB126: `simplify-return` Categories: `control-flow` `readability` Sometimes a return statement can be written more succinctly: Bad: ```python def index_or_default(nums: list[Any], index: int, default: Any): if index >= len(nums): return default else: return nums[index] def is_on_axis(position: tuple[int, int]) -> bool: match position: case (0, _) | (_, 0): return True case _: return False ``` Good: ```python def index_or_default(nums: list[Any], index: int, default: Any): if index >= len(nums): return default return nums[index] def is_on_axis(position: tuple[int, int]) -> bool: match position: case (0, _) | (_, 0): return True return False ``` ## FURB127: `no-with-assign` Categories: `readability` `scoping` Due to Python's scoping rules, you can use a variable that has gone "out of scope" so long as all previous code paths can bind to it. Long story short, you don't need to declare a variable before you assign it in a `with` statement: Bad: ```python x = "" with open("file.txt") as f: x = f.read() ``` Good: ```python with open("file.txt") as f: x = f.read() ``` ## FURB128: `use-tuple-unpack-swap` Categories: `readability` You don't need to use a temporary variable to swap 2 variables, you can use tuple unpacking instead: Bad: ```python temp = x x = y y = temp ``` Good: ```python x, y = y, x ``` ## FURB129: `simplify-readlines` Categories: `builtin` `readability` When iterating over a file object line-by-line you don't need to add `.readlines()`, simply iterate over the object itself. This assumes you aren't passing an argument to readlines(). Bad: ```python with open("file.txt") as f: for line in f.readlines(): ... ``` Good: ```python with open("file.txt") as f: for line in f: ... ``` ## FURB130: `no-in-dict-keys` Categories: `dict` `readability` If you only want to check if a key exists in a dictionary, you don't need to call `.keys()` first, just use `in` on the dictionary itself: Bad: ```python d = {"key": "value"} if "key" in d.keys(): ... ``` Good: ```python d = {"key": "value"} if "key" in d: ... ``` ## FURB131: `no-del` Categories: `builtin` `readability` The `del` statement is commonly used for popping single elements from dicts and lists, though a slice can be used to remove a range of elements instead. When removing all elements via a slice, use the faster and more succinct `.clear()` method instead. Bad: ```python names = {"key": "value"} nums = [1, 2, 3] del names[:] del nums[:] ``` Good: ```python names = {"key": "value"} nums = [1, 2, 3] names.clear() nums.clear() ``` ## FURB132: `use-set-discard` Categories: `readability` `set` If you want to remove a value from a set regardless of whether it exists or not, use the `discard()` method instead of `remove()`: Bad: ```python nums = {123, 456} if 123 in nums: nums.remove(123) ``` Good: ```python nums = {123, 456} nums.discard(123) ``` ## FURB133: `no-redundant-continue` Categories: `control-flow` `readability` Don't explicitly continue if you are already at the end of the control flow for the current for/while loop: Bad: ```python def func(): for _ in range(10): print("hello world!") continue def func2(x): for x in range(10): if x == 1: print("x is 1") else: print("x is not 1") continue ``` Good: ```python def func(): for _ in range(10): print("hello world!") def func2(x): for x in range(10): if x == 1: print("x is 1") else: print("x is not 1") ``` ## FURB134: `use-cache` Categories: `functools` `python39` `readability` Python 3.9 introduces the `@cache` decorator which can be used as a short-hand for `@lru_cache(maxsize=None)`. Bad: ```python from functools import lru_cache @lru_cache(maxsize=None) def f(x: int) -> int: return x + 1 ``` Good: ```python from functools import cache @cache def f(x: int) -> int: return x + 1 ``` ## FURB135: `no-ignored-dict-items` Categories: `dict` Don't use `.items()` on a `dict` if you only care about the keys or the values, but not both: Bad: ```python books = {"Frank Herbert": "Dune"} for author, _ in books.items(): print(author) for _, book in books.items(): print(book) ``` Good: ```python books = {"Frank Herbert": "Dune"} for author in books: print(author) for book in books.values(): print(book) ``` ## FURB136: `use-min-max` Categories: `builtin` `logical` `readability` Certain ternary expressions can be written more succinctly using the builtin `min`/`max` functions: Bad: ```python score1 = 90 score2 = 99 highest_score = score1 if score1 > score2 else score2 ``` Good: ```python score1 = 90 score2 = 99 highest_score = max(score1, score2) ``` ## FURB137: `simplify-comprehension` Categories: `builtin` `iterable` `readability` Often times generator expressions and list/set/dict comprehensions can be written more succinctly. For example, passing a list comprehension to a function when a generator expression would suffice, or using the shorthand notation in the case of `list` and `set`. For example: Bad: ```python nums = [1, 1, 2, 3] nums_times_10 = list(num * 10 for num in nums) unique_squares = set(num ** 2 for num in nums) number_tuple = tuple([num ** 2 for num in nums]) ``` Good: ```python nums = [1, 1, 2, 3] nums_times_10 = [num * 10 for num in nums] unique_squares = {num ** 2 for num in nums} number_tuple = tuple(num ** 2 for num in nums) ``` ## FURB138: `use-list-comprehension` Categories: `performance` `readability` When constructing a new list it is usually more performant to use a list comprehension, and in some cases, it can be more readable. Bad: ```python nums = [1, 2, 3, 4] odds = [] for num in nums: if num % 2: odds.append(num) ``` Good: ```python nums = [1, 2, 3, 4] odds = [num for num in nums if num % 2] ``` ## FURB139: `no-multiline-strip` Categories: `readability` If you want to define a multi-line string but don't want a leading/trailing newline, use a continuation character ('\') instead of calling `lstrip()`, `rstrip()`, or `strip()`. Bad: ```python """ This is some docstring """.lstrip() """ This is another docstring """.strip() ``` Good: ```python """\ This is some docstring """ """\ This is another docstring\ """ ``` ## FURB140: `use-starmap` Categories: `itertools` `performance` If you only want to iterate and unpack values so that you can pass them to a function (in the same order and with no modifications), you should use the more performant `starmap` function: Bad: ```python scores = [85, 100, 60] passing_scores = [60, 80, 70] def passed_test(score: int, passing_score: int) -> bool: return score >= passing_score passed_all_tests = all( passed_test(score, passing_score) for score, passing_score in zip(scores, passing_scores) ) ``` Good: ```python from itertools import starmap scores = [85, 100, 60] passing_scores = [60, 80, 70] def passed_test(score: int, passing_score: int) -> bool: return score >= passing_score passed_all_tests = all(starmap(passed_test, zip(scores, passing_scores))) ``` ## FURB141: `use-pathlib-exists` Categories: `pathlib` When checking whether a file exists or not, try and use the more modern `pathlib` module instead of `os.path`. Bad: ```python import os if os.path.exists("filename"): pass ``` Good: ```python from pathlib import Path if Path("filename").exists(): pass ``` ## FURB142: `no-set-for-loop` Categories: `builtin` When you want to add/remove a bunch of items to/from a set, don't use a for loop, call the appropriate method on the set itself. Bad: ```python sentence = "hello world" vowels = "aeiou" letters = set(sentence) for vowel in vowels: letters.discard(vowel) ``` Good: ```python sentence = "hello world" vowels = "aeiou" letters = set(sentence) letters.difference_update(vowels) ``` ## FURB143: `no-default-or` Categories: `logical` `readability` Don't check an expression to see if it is falsey then assign the same falsey value to it. For example, if an expression used to be of type `int | None`, checking if the expression is falsey would make sense, since it could be `None` or `0`. But, if the expression is changed to be of type `int`, the falsey value is just `0`, so setting it to `0` if it is falsey (`0`) is redundant. Bad: ```python def is_markdown_header(line: str) -> bool: return (line or "").startswith("#") ``` Good: ```python def is_markdown_header(line: str) -> bool: return line.startswith("#") ``` ## FURB144: `use-pathlib-unlink` Categories: `pathlib` When removing a file, use the more modern `Path.unlink()` method instead of `os.remove()` or `os.unlink()`: The `pathlib` module allows for more flexibility when it comes to traversing folders, building file paths, and accessing/modifying files. Bad: ```python import os os.remove("filename") ``` Good: ```python from pathlib import Path Path("filename").unlink() ``` ## FURB145: `no-slice-copy` Categories: `readability` Don't use a slice expression (with no bounds) to make a copy of something, use the more readable `.copy()` method instead: Bad: ```python nums = [3.1415, 1234] copy = nums[:] ``` Good: ```python nums = [3.1415, 1234] copy = nums.copy() ``` ## FURB146: `use-pathlib-is-funcs` Categories: `pathlib` Don't use the `os.path.isfile` (or similar) functions, use the more modern `pathlib` module instead: Bad: ```python if os.path.isfile("file.txt"): pass ``` Good: ```python if Path("file.txt").is_file(): pass ``` ## FURB147: `no-path-join` Categories: `pathlib` When joining strings to make a filepath, use the more modern and flexible `Path()` object instead of `os.path.join`: Bad: ```python with open(os.path.join("folder", "file"), "w") as f: f.write("hello world!") ``` Good: ```python from pathlib import Path with open(Path("folder", "file"), "w") as f: f.write("hello world!") # even better ... with Path("folder", "file").open("w") as f: f.write("hello world!") # even better ... Path("folder", "file").write_text("hello world!") ``` Note that this check is disabled by default because `Path()` returns a Path object, not a string, meaning that the Path object will propagate through your code. This might be what you want, and might encourage you to use the pathlib module in more places, but since it is not a drop-in replacement it is disabled by default. ## FURB148: `no-ignored-enumerate-items` Categories: `builtin` Don't use `enumerate` if you are disregarding either the index or the value: Bad: ```python books = ["Ender's Game", "The Black Swan"] for index, _ in enumerate(books): print(index) for _, book in enumerate(books): print(book) ``` Good: ```python books = ["Ender's Game", "The Black Swan"] for index in range(len(books)): print(index) for book in books: print(book) ``` ## FURB149: `no-bool-literal-compare` Categories: `logical` `readability` `truthy` Don't use `is` or `==` to check if a boolean is True or False, simply use the name itself: Bad: ```python failed = True if failed is True: print("You failed") ``` Good: ```python failed = True if failed: print("You failed") ``` ## FURB150: `use-pathlib-mkdir` Categories: `pathlib` Use the `mkdir` method from the pathlib library instead of using the `mkdir` and `makedirs` functions from the `os` library: the pathlib library is more modern and provides better flexibility over the construction and manipulation of file paths. Bad: ```python import os os.mkdir("new_folder") ``` Good: ```python from pathlib import Path Path("new_folder").mkdir() ``` ## FURB151: `use-pathlib-touch` Categories: `pathlib` Don't use `open(x, "w").close()` if you just want to create an empty file, use the less confusing `Path.touch()` method instead. Bad: ```python open("file.txt", "w").close() ``` Good: ```python from pathlib import Path Path("file.txt").touch() ``` This check is disabled by default because `touch()` will throw a `FileExistsError` if the file already exists, and (at least on Linux) it sets different file permissions, meaning it is not a drop-in replacement. If you don't care about the file permissions or know that the file doesn't exist beforehand this check may be for you. ## FURB152: `use-math-constant` Categories: `math` `readability` Don't hardcode math constants like pi, tau, or e, use the `math.pi`, `math.tau`, or `math.e` constants respectively. Bad: ```python def area(r: float) -> float: return 3.1415 * r * r ``` Good: ```python import math def area(r: float) -> float: return math.pi * r * r ``` ## FURB153: `simplify-path-constructor` Categories: `pathlib` `readability` The Path() constructor defaults to the current directory, so don't pass the current directory explicitly. Bad: ```python file = Path(".") ``` Good: ```python file = Path() ``` Note: Lots of different values can trigger this check, including `"."`, `""`, `os.curdir`, and `os.path.curdir`. ## FURB154: `simplify-global-and-nonlocal` Categories: `builtin` `readability` The `global` and `nonlocal` keywords can take multiple comma-separated names, removing the need for multiple lines. Bad: ```python def some_func(): global x global y print(x, y) ``` Good: ```python def some_func(): global x, y print(x, y) ``` ## FURB155: `use-pathlib-stat` Categories: `pathlib` Don't use the `os.path.getsize` (or similar) functions, use the more modern `pathlib` module instead: Bad: ```python if os.path.getsize("file.txt"): pass ``` Good: ```python if Path("file.txt").stat().st_size: pass ``` ## FURB156: `use-string-charsets` Categories: `readability` `string` Python includes some pre-defined charsets such as digits (0-9), upper and lower case alpha characters, and so on. You don't have to define them yourself, and they are usually more readable. Bad: ```python digits = "0123456789" if c in digits: pass if c in "0123456789abcdefABCDEF": pass ``` Good: ```python if c in string.digits: pass if c in string.hexdigits: pass ``` Note that when using a literal string, the corresponding `string.xyz` value must be exact, but when used in an `in` comparison, the characters can be out of order since `in` will compare every character in the string. ## FURB157: `simplify-decimal-ctor` Categories: `decimal` Under certain circumstances the `Decimal()` constructor can be made more succinct. Bad: ```python if x == Decimal("0"): pass if y == Decimal(float("Infinity")): pass ``` Good: ```python if x == Decimal(0): pass if y == Decimal("Infinity"): pass ``` ## FURB158: `simplify-as-pattern-with-builtin` Categories: `pattern-matching` `readability` When pattern matching builtin classes such as `int()` and `str()`, don't use an `as` pattern to bind to the value, since the most common builtin classes can use positional patterns instead. Bad: ```python match x: case str() as name: print(f"Hello {name}") ``` Good: ```python match x: case str(name): print(f"Hello {name}") ``` ## FURB159: `simplify-strip` Categories: `readability` `string` In some situations the `.lstrip()`, `.rstrip()` and `.strip()` string methods can be written more succinctly: `strip()` is the same thing as calling both `lstrip()` and `rstrip()` together, and all the strip functions take an iterable argument of the characters to strip, meaning you don't need to call strip methods multiple times with different arguments, you can just concatenate them and call it once. Bad: ```python name = input().lstrip().rstrip() num = " -123".lstrip(" ").lstrip("-") ``` Good: ```python name = input().strip() num = " -123".lstrip(" -") ``` ## FURB160: `no-redundant-assignment` Categories: `readability` Sometimes when you are debugging (or copy-pasting code) you will end up with a variable that is assigning itself to itself. These lines can be removed. Bad: ```python name = input("What is your name? ") name = name ``` Good: ```python name = input("What is your name? ") ``` ## FURB161: `use-bit-count` Categories: `builtin` `performance` `python310` `readability` Python 3.10 adds a very helpful `bit_count()` function for integers which counts the number of set bits. This new function is more descriptive and faster compared to converting/counting characters in a string. Bad: ```python x = bin(0b1010).count("1") assert x == 2 ``` Good: ```python x = 0b1010.bit_count() assert x == 2 ``` ## FURB162: `simplify-fromisoformat` Categories: `datetime` `python311` `readability` Python 3.11 adds support for parsing UTC timestamps that end with `Z`, thus removing the need to strip and append the `+00:00` timezone. Bad: ```python date = "2023-02-21T02:23:15Z" start_date = datetime.fromisoformat(date.replace("Z", "+00:00")) ``` Good: ```python date = "2023-02-21T02:23:15Z" start_date = datetime.fromisoformat(date) ``` ## FURB163: `simplify-math-log` Categories: `math` `readability` Use the shorthand `log2` and `log10` functions instead of passing 2 or 10 as the second argument to the `log` function. If `math.e` is used as the second argument, just use `math.log(x)` instead, since `e` is the default. Bad: ```python power = math.log(x, 10) ``` Good: ```python power = math.log10(x) ``` ## FURB164: `no-from-float` Categories: `decimal` `fractions` `readability` When constructing a Fraction or Decimal using a float, don't use the `from_float()` or `from_decimal()` class methods: Just use the more concise `Fraction()` and `Decimal()` class constructors instead. Bad: ```python ratio = Fraction.from_float(1.2) score = Decimal.from_float(98.0) ``` Good: ```python ratio = Fraction(1.2) score = Decimal(98.0) ``` ## FURB165: `no-temp-class-object` Categories: `readability` You don't need to construct a class object to call a static method or a class method, just invoke the method on the class directly: Bad: ```python cwd = Path().cwd() ``` Good: ```python cwd = Path.cwd() ``` ## FURB166: `use-int-base-zero` Categories: `builtin` `readability` When converting a string starting with `0b`, `0o`, or `0x` to an int, you don't need to slice the string and set the base yourself: just call `int()` with a base of zero. Doing this will autodeduce the correct base to use based on the string prefix. Bad: ```python num = "0xABC" if num.startswith("0b"): i = int(num[2:], 2) elif num.startswith("0o"): i = int(num[2:], 8) elif num.startswith("0x"): i = int(num[2:], 16) print(i) ``` Good: ```python num = "0xABC" i = int(num, 0) print(i) ``` This check is disabled by default because there is no way for Refurb to detect whether the prefixes that are being stripped are valid Python int prefixes (like `0x`) or some other prefix which would fail if parsed using this method. ## FURB167: `use-long-regex-flag` Categories: `readability` `regex` Regex operations can be changed using flags such as `re.I`, which will make the regex case-insensitive. These single-character flag names can be harder to read/remember, and should be replaced with the longer aliases so that they are more descriptive. Bad: ```python if re.match("^hello", "hello world", re.I): pass ``` Good: ```python if re.match("^hello", "hello world", re.IGNORECASE): pass ``` ## FURB168: `no-isinstance-type-none` Categories: `pythonic` `readability` Checking if an object is `None` using `isinstance()` is un-pythonic: use an `is` comparison instead. Bad: ```python x = 123 if isinstance(x, type(None)): pass ``` Good: ```python x = 123 if x is None: pass ``` ## FURB169: `no-is-type-none` Categories: `pythonic` `readability` Don't use `type(None)` to check if the type of an object is `None`, use an `is` comparison instead. Bad: ```python x = 123 if type(x) is type(None): pass ``` Good: ```python x = 123 if x is None: pass ``` ## FURB170: `use-regex-pattern-methods` Categories: `readability` `regex` If you are passing a compiled regular expression to a regex function, consider calling the regex method on the pattern itself: It is faster, and can improve readability. Bad: ```python import re COMMENT = re.compile(".*(#.*)") found_comment = re.match(COMMENT, "this is a # comment") ``` Good: ```python import re COMMENT = re.compile(".*(#.*)") found_comment = COMMENT.match("this is a # comment") ``` ## FURB171: `no-single-item-in` Categories: `iterable` `readability` Don't use `in` to check against a single value, use `==` instead: Bad: ```python if name in ("bob",): pass ``` Good: ```python if name == "bob": pass ``` ## FURB172: `use-suffix` Categories: `pathlib` When checking the file extension for a Path object don't call `endswith()` on the `name` field, directly check against `suffix` instead. Bad: ```python from pathlib import Path def is_markdown_file(file: Path) -> bool: return file.name.endswith(".md") ``` Good: ```python from pathlib import Path def is_markdown_file(file: Path) -> bool: return file.suffix == ".md" ``` Note: The `suffix` field will only contain the last file extension, so don't use `suffix` if you are checking for an extension like `.tar.gz`. Refurb won't warn in those cases, but it is good to remember in case you plan to use this in other places. ## FURB173: `use-dict-union` Categories: `dict` `readability` Dicts can be created/combined in many ways, one of which is the `**` operator (inside the dict), and another is the `|` operator (used outside the dict). While they both have valid uses, the `|` operator allows for more flexibility, including using `|=` to update an existing dict. See PEP 584 for more info. Bad: ```python def add_defaults(settings: dict[str, str]) -> dict[str, str]: return {"color": "1", **settings} ``` Good: ```python def add_defaults(settings: dict[str, str]) -> dict[str, str]: return {"color": "1"} | settings ``` ## FURB174: `simplify-token-function` Categories: `readability` `secrets` Depending on how you are using the `secrets` module, there might be more expressive ways of writing what it is you're trying to write. Bad: ```python random_hex = token_bytes().hex() random_url = token_urlsafe()[:16] ``` Good: ```python random_hex = token_hex() random_url = token_urlsafe(16) ``` ## FURB175: `simplify-fastapi-query` Categories: `fastapi` `readability` FastAPI will automatically pass along query parameters to your function, so you only need to use `Query()` when you use params other than `default`. Bad: ```python @app.get("/") def index(name: str = Query()) -> str: return f"Your name is {name}" ``` Good: ```python @app.get("/") def index(name: str) -> str: return f"Your name is {name}" ``` ## FURB176: `unreliable-utc-usage` Categories: `datetime` Because naive `datetime` objects are treated by many `datetime` methods as local times, it is preferred to use aware datetimes to represent times in UTC. This check affects `datetime.utcnow` and `datetime.utcfromtimestamp`. Bad: ```python from datetime import datetime now = datetime.utcnow() past_date = datetime.utcfromtimestamp(some_timestamp) ``` Good: ```python from datetime import datetime, timezone datetime.now(timezone.utc) datetime.fromtimestamp(some_timestamp, tz=timezone.utc) ``` ## FURB177: `no-implicit-cwd` Categories: `pathlib` If you want to get the current working directory don't call `resolve()` on an empty `Path()` object, use `Path.cwd()` instead. Bad: ```python cwd = Path().resolve() ``` Good: ```python cwd = Path.cwd() ``` ## FURB178: `use-shlex-join` Categories: `readability` `shlex` When using `shlex` to escape and join a bunch of strings consider using the `shlex.join` method instead. Bad: ```python args = ["hello", "world!"] cmd = " ".join(shlex.quote(arg) for arg in args) ``` Good: ```python args = ["hello", "world!"] cmd = shlex.join(args) ``` ## FURB179: `use-chain-from-iterable` Categories: `itertools` `performance` `readability` When flattening a list of lists, use the `chain.from_iterable()` function from the `itertools` stdlib package. This function is faster than native list/generator comprehensions or using `sum()` with a list default. Bad: ```python from itertools import chain rows = [[1, 2], [3, 4]] # using list comprehension flat = [col for row in rows for col in row] # using sum() flat = sum(rows, []) # using chain(*x) flat = chain(*rows) ``` Good: ```python from itertools import chain rows = [[1, 2], [3, 4]] flat = chain.from_iterable(rows) ``` Note: `chain.from_iterable()` returns an iterator, which means you might need to wrap it in `list()` depending on your use case. Refurb cannot detect this (yet), so this is something you will need to keep in mind. Note: `chain(*x)` may be marginally faster/slower depending on the length of `x`. Since `*` might potentially expand to a lot of arguments, it is better to use `chain.from_iterable()` when you are unsure. ## FURB180: `use-abc-shorthand` Categories: `abc` `readability` Instead of setting `metaclass` directly, inherit from the `ABC` wrapper class. This is semantically the same thing, but more succinct. Bad: ```python class C(metaclass=ABCMeta): pass ``` Good: ```python class C(ABC): pass ``` ## FURB181: `use-hexdigest-hashlib` Categories: `hashlib` `readability` Use `.hexdigest()` to get a hex digest from a hash. Bad: ```python from hashlib import sha512 hashed = sha512(b"some data").digest().hex() ``` Good: ```python from hashlib import sha512 hashed = sha512(b"some data").hexdigest() ``` ## FURB182: `simplify-hashlib-ctor` Categories: `hashlib` `readability` You can pass data into `hashlib` constructors, so instead of creating a hash object and immediately updating it, pass the data directly. Bad: ```python from hashlib import sha512 h = sha512() h.update(b"data) ``` Good: ```python from hashlib import sha512 h = sha512(b"data") ``` ## FURB183: `use-str-func` Categories: `readability` If you want to stringify a single value without concatenating anything, use the `str()` function instead. Bad: ```python nums = [123, 456] num = f"{num[0]}") ``` Good: ```python nums = [123, 456] num = str(num[0]) ``` ## FURB184: `use-fluid-interface` Categories: `readability` When an API has a Fluent Interface (the ability to chain multiple calls together), you should chain those calls instead of repeatedly assigning and using the value. Sometimes a return statement can be written more succinctly: Bad: ```pythonpython def get_tensors(device: str) -> torch.Tensor: t1 = torch.ones(2, 1) t2 = t1.long() t3 = t2.to(device) return t3 def process(file_name: str): common_columns = ["col1_renamed", "col2_renamed", "custom_col"] df = spark.read.parquet(file_name) df = df \ .withColumnRenamed('col1', 'col1_renamed') \ .withColumnRenamed('col2', 'col2_renamed') df = df \ .select(common_columns) \ .withColumn('service_type', F.lit('green')) return df ``` Good: ```pythonpython def get_tensors(device: str) -> torch.Tensor: t3 = ( torch.ones(2, 1) .long() .to(device) ) return t3 def process(file_name: str): common_columns = ["col1_renamed", "col2_renamed", "custom_col"] df = ( spark.read.parquet(file_name) .withColumnRenamed('col1', 'col1_renamed') .withColumnRenamed('col2', 'col2_renamed') .select(common_columns) .withColumn('service_type', F.lit('green')) ) return df ``` ## FURB185: `no-copy-with-merge` Categories: `readability` You don't need to call `.copy()` on a dict/set when using it in a union since the original dict/set is not modified. Bad: ```python d = {"a": 1} merged = d.copy() | {"b": 2} ``` Good: ```python d = {"a": 1} merged = d | {"b": 2} ``` ## FURB186: `use-sort` Categories: `performance` `readability` Don't use `sorted()` to sort a list and reassign it to itself, use the faster in-place `.sort()` method instead. Bad: ```python names = ["Bob", "Alice", "Charlie"] names = sorted(names) ``` Good: ```python names = ["Bob", "Alice", "Charlie"] names.sort() ```refurb-1.27.0/docs/configs/000077500000000000000000000000001454672660200154625ustar00rootroot00000000000000refurb-1.27.0/docs/configs/README.md000066400000000000000000000003261454672660200167420ustar00rootroot00000000000000# Config File Examples This folder contains 2 example files: * `default.toml`: Config file representing the default settings used in Refurb * `reference.toml`: All of the available config file settings in Refurb refurb-1.27.0/docs/configs/default.toml000066400000000000000000000005071454672660200200050ustar00rootroot00000000000000# This is a config file that emulates the default settings used by Refurb. ignore = [] load = [] disable = ["FURB106", "FURB120", "FURB137", "FURB147", "FURB151", "FURB166", "FURB172"] quiet = false enable_all = false disable_all = false python_version = "" # uses current python version format = "text" sort_by = "filename" refurb-1.27.0/docs/configs/reference.toml000066400000000000000000000016551454672660200203240ustar00rootroot00000000000000# This is an example config file that shows all the available configuration # options in Refurb. You probably won't need most of them, but are here for # completion. Some of these configurations conflict with one another, so only # use this file as a reference! # The following error codes are identical ignore = ["FURB100", 100] # Enable FURB100 enable = ["FURB100"] # Disable FURB100 disable = ["FURB100"] # Enable all checks (good for new codebases) enable_all = true # Disable all checks (good for incrementally adopting Refurb) disable_all = true # Disable "use --explain" error message quiet = true # Specify a specific Python version to use python_version = "3.10" format = "github" # or "text" sort_by = "filename" # or "error" # Add custom path to look for potential Refurb plugins load = ["custom_module"] # Ignore certain checks for specific folders [[tool.refurb.amend]] path = "src" ignore = ["FURB123", "FURB120"] refurb-1.27.0/docs/gen_checks.py000066400000000000000000000017141454672660200165000ustar00rootroot00000000000000import re from pathlib import Path from textwrap import dedent from refurb.error import ErrorCode from refurb.loader import get_error_class, get_modules docs: dict[str, str] = {} for module in get_modules([]): if error := get_error_class(module): error_code = ErrorCode.from_error(error) header = f"## {error_code}: `{error.name}`" categories = " ".join(f"`{cat}`" for cat in error.categories) categories = "Categories: " + categories body = dedent(error.__doc__ or "").strip() body = re.sub(r"```([\s\S]*?)```", r"```python\1```", body) docs[str(error_code)] = "\n\n".join([header, categories, body]) HEADER = """\ # Available Checks""" with (Path(__file__).parent / "checks.md").open("w+") as f: f.write(HEADER) for _, v in sorted(docs.items()): f.write(f"\n\n{v}") refurb-1.27.0/pyproject.toml000066400000000000000000000051661454672660200160260ustar00rootroot00000000000000[tool.poetry] name = "refurb" version = "1.27.0" description = "A tool for refurbish and modernize Python codebases" authors = ["dosisod"] license = "GPL-3.0-only" readme = "README.md" repository = "https://github.com/dosisod/refurb" classifiers = [ "Development Status :: 5 - Production/Stable", "Environment :: Console", "Intended Audience :: Developers", "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", "Topic :: Software Development :: Testing", "Typing :: Typed" ] [tool.poetry.dependencies] python = ">=3.10" mypy = ">=0.981" tomli = {version = "^2.0.1", python = "<3.11"} [tool.poetry.dev-dependencies] black = "^22.6.0" isort = "^5.10.1" pytest = "^7.1.2" [tool.poetry.scripts] refurb = "refurb.__main__:main" [tool.isort] line_length = 99 multi_line_output = 3 include_trailing_comma = true color_output = true extend_skip = ["test/data"] [tool.mypy] allow_redefinition = true disallow_any_decorated = true disallow_any_explicit = true disallow_any_unimported = true namespace_packages = true pretty = true strict = true warn_unreachable = true [[tool.mypy.overrides]] module = "test.*" allow_untyped_defs = true [tool.coverage.run] omit = [ "refurb/__main__.py", "refurb/gen.py", "refurb/visitor/traverser.py" ] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", ] skip_covered = true skip_empty = true [tool.black] exclude = "test/data/*" line-length = 99 color = true [tool.pytest.ini_options] addopts = "--cov=refurb --cov-report=html --cov-report=term-missing --cov-fail-under=100" testpaths = ["test"] [tool.ruff] line-length = 99 preview = true select = ["ALL"] # TODO: fix RUF100 not playing well with refurb extend-ignore = [ "A001", "A002", "A003", "ANN101", "ANN102", "ANN401", "ARG001", "B905", "C901", "COM812", "D100", "D101", "D102", "D103", "D104", "D105", "D107", "D200", "D202", "D203", "D205", "D212", "D214", "D400", "D401", "D404", "D405", "D406", "D407", "D412", "D415", "D416", "EM101", "EM102", "F821", "FBT001", "FIX002", "FIX004", "I001", "INP001", "N813", "N818", "PGH003", "PLR0911", "PLR0912", "PLR0914", "PLR0915", "PLR2004", "PT012", "RUF100", "S101", "SIM102", "SIM108", "SLF001", "T201", "TD002", "TD003", "TRY003", "TRY004", # Consider this "CPY001", ] extend-exclude = ["test/data*"] target-version = "py310" [tool.ruff.per-file-ignores] "test/*" = ["ANN201", "ARG001", "E501", "TCH001", "TCH002"] "refurb/main.py" = ["E501"] "refurb/visitor/traverser.py" = ["ALL"] "test/e2e/gbk.py" = ["FURB105"] [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" refurb-1.27.0/refurb/000077500000000000000000000000001454672660200143675ustar00rootroot00000000000000refurb-1.27.0/refurb/__init__.py000066400000000000000000000000001454672660200164660ustar00rootroot00000000000000refurb-1.27.0/refurb/__main__.py000066400000000000000000000002221454672660200164550ustar00rootroot00000000000000import sys from refurb.main import main as _main def main() -> None: sys.exit(_main(sys.argv[1:])) if __name__ == "__main__": main() refurb-1.27.0/refurb/checks/000077500000000000000000000000001454672660200156275ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/__init__.py000066400000000000000000000000001454672660200177260ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/builtin/000077500000000000000000000000001454672660200172755ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/builtin/__init__.py000066400000000000000000000000001454672660200213740ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/builtin/list_extend.py000066400000000000000000000034721454672660200221770ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( Block, CallExpr, ExpressionStmt, MemberExpr, MypyFile, NameExpr, Statement, Var, ) from refurb.checks.common import check_block_like from refurb.error import Error @dataclass class ErrorInfo(Error): """ When appending multiple values to a list, you can use the `.extend()` method to add an iterable to the end of an existing list. This way, you don't have to call `.append()` on every element: Bad: ``` nums = [1, 2, 3] nums.append(4) nums.append(5) nums.append(6) ``` Good: ``` nums = [1, 2, 3] nums.extend((4, 5, 6)) ``` """ name = "use-list-extend" code = 113 msg: str = "Use `x.extend(...)` instead of repeatedly calling `x.append()`" categories = ("list",) @dataclass class Last: name: str = "" line: int = 0 column: int = 0 did_error: bool = False def check(node: Block | MypyFile, errors: list[Error]) -> None: check_block_like(check_stmts, node, errors) def check_stmts(stmts: list[Statement], errors: list[Error]) -> None: last = Last() for stmt in stmts: match stmt: case ExpressionStmt( expr=CallExpr( callee=MemberExpr( expr=NameExpr(name=name, node=Var(type=ty)), name="append", ), ) ) if str(ty).startswith("builtins.list["): if not last.did_error and name == last.name: errors.append(ErrorInfo(last.line, last.column)) last.did_error = True last.name = name last.line = stmt.line last.column = stmt.column case _: last = Last() refurb-1.27.0/refurb/checks/builtin/no_del.py000066400000000000000000000022671454672660200211160ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import DelStmt, IndexExpr, NameExpr, SliceExpr, Var from refurb.error import Error @dataclass class ErrorInfo(Error): """ The `del` statement is commonly used for popping single elements from dicts and lists, though a slice can be used to remove a range of elements instead. When removing all elements via a slice, use the faster and more succinct `.clear()` method instead. Bad: ``` names = {"key": "value"} nums = [1, 2, 3] del names[:] del nums[:] ``` Good: ``` names = {"key": "value"} nums = [1, 2, 3] names.clear() nums.clear() ``` """ name = "no-del" code = 131 categories = ("builtin", "readability") msg: str = "Replace `del x[:]` with `x.clear()`" def check(node: DelStmt, errors: list[Error]) -> None: match node: case DelStmt(expr=IndexExpr(base=NameExpr(node=Var(type=ty)), index=index)) if str( ty ).startswith(("builtins.dict[", "builtins.list[")): match index: case SliceExpr(begin_index=None, end_index=None): errors.append(ErrorInfo.from_node(node)) refurb-1.27.0/refurb/checks/builtin/no_ignored_dict_items.py000066400000000000000000000037551454672660200242100ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( CallExpr, DictionaryComprehension, ForStmt, GeneratorExpr, MemberExpr, NameExpr, Node, TupleExpr, Var, ) from refurb.checks.common import check_for_loop_like, is_name_unused_in_contexts from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use `.items()` on a `dict` if you only care about the keys or the values, but not both: Bad: ``` books = {"Frank Herbert": "Dune"} for author, _ in books.items(): print(author) for _, book in books.items(): print(book) ``` Good: ``` books = {"Frank Herbert": "Dune"} for author in books: print(author) for book in books.values(): print(book) ``` """ name = "no-ignored-dict-items" code = 135 categories = ("dict",) def check( node: ForStmt | GeneratorExpr | DictionaryComprehension, errors: list[Error], ) -> None: check_for_loop_like(check_dict_items_call, node, errors) def check_dict_items_call( index: Node, expr: Node, contexts: list[Node], errors: list[Error] ) -> None: match index, expr: case ( TupleExpr(items=[NameExpr() as key, NameExpr() as value]), CallExpr( callee=MemberExpr( expr=NameExpr(node=Var(type=ty)), name="items", ) ), ) if str(ty).startswith("builtins.dict["): check_unused_key_or_value(key, value, contexts, errors) def check_unused_key_or_value( key: NameExpr, value: NameExpr, contexts: list[Node], errors: list[Error] ) -> None: if is_name_unused_in_contexts(key, contexts): errors.append( ErrorInfo.from_node(key, "Key is unused, use `for value in d.values()` instead") ) if is_name_unused_in_contexts(value, contexts): errors.append(ErrorInfo.from_node(value, "Value is unused, use `for key in d` instead")) refurb-1.27.0/refurb/checks/builtin/no_ignored_enumerate.py000066400000000000000000000042161454672660200240420ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( CallExpr, DictionaryComprehension, ForStmt, GeneratorExpr, NameExpr, Node, TupleExpr, Var, ) from refurb.checks.common import check_for_loop_like, is_name_unused_in_contexts from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use `enumerate` if you are disregarding either the index or the value: Bad: ``` books = ["Ender's Game", "The Black Swan"] for index, _ in enumerate(books): print(index) for _, book in enumerate(books): print(book) ``` Good: ``` books = ["Ender's Game", "The Black Swan"] for index in range(len(books)): print(index) for book in books: print(book) ``` """ name = "no-ignored-enumerate-items" code = 148 categories = ("builtin",) def check( node: ForStmt | GeneratorExpr | DictionaryComprehension, errors: list[Error], ) -> None: check_for_loop_like(check_enumerate_call, node, errors) def check_enumerate_call( index: Node, expr: Node, contexts: list[Node], errors: list[Error] ) -> None: match index, expr: case ( TupleExpr(items=[NameExpr() as index, NameExpr() as value]), CallExpr( callee=NameExpr(fullname="builtins.enumerate"), args=[NameExpr(node=Var(type=ty))], ), ) if is_sequence_type(str(ty)): check_unused_index_or_value(index, value, contexts, errors) def check_unused_index_or_value( index: NameExpr, value: NameExpr, contexts: list[Node], errors: list[Error] ) -> None: if is_name_unused_in_contexts(index, contexts): errors.append(ErrorInfo.from_node(index, "Index is unused, use `for x in y` instead")) if is_name_unused_in_contexts(value, contexts): errors.append( ErrorInfo.from_node(value, "Value is unused, use `for x in range(len(y))` instead") ) # TODO: allow for any type that supports the Sequence protocol def is_sequence_type(ty: str) -> bool: return ty.startswith(("builtins.list[", "Tuple[", "builtins.tuple[", "tuple[")) refurb-1.27.0/refurb/checks/builtin/no_is_type_none.py000066400000000000000000000021351454672660200230370ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, ComparisonExpr, NameExpr from refurb.checks.common import is_type_none_call from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use `type(None)` to check if the type of an object is `None`, use an `is` comparison instead. Bad: ``` x = 123 if type(x) is type(None): pass ``` Good: ``` x = 123 if x is None: pass ``` """ name = "no-is-type-none" code = 169 categories = ("pythonic", "readability") def check(node: ComparisonExpr, errors: list[Error]) -> None: match node: case ComparisonExpr( operators=["is" | "is not" | "==" | "!=" as oper], operands=[ CallExpr(callee=NameExpr(fullname="builtins.type")), rhs, ], ) if is_type_none_call(rhs): new = "is" if oper in {"is", "=="} else "is not" msg = f"Replace `type(x) {oper} type(None)` with `x {new} None`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/builtin/no_isinstance_type_none.py000066400000000000000000000046361454672660200245740ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, Expression, NameExpr, OpExpr, TupleExpr from refurb.checks.common import is_type_none_call from refurb.error import Error @dataclass class ErrorInfo(Error): """ Checking if an object is `None` using `isinstance()` is un-pythonic: use an `is` comparison instead. Bad: ``` x = 123 if isinstance(x, type(None)): pass ``` Good: ``` x = 123 if x is None: pass ``` """ name = "no-isinstance-type-none" code = 168 categories = ("pythonic", "readability") def get_type_none_index(node: Expression, index: int = 0) -> int: match node: case _ if is_type_none_call(node): return index case OpExpr(op="|"): lhs_index = get_type_none_index(node.left, index) if lhs_index != -1: return lhs_index return get_type_none_index(node.right, index + 1) return -1 def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=NameExpr(fullname="builtins.isinstance"), args=[_, ty], ): match ty: case _ if is_type_none_call(ty): msg = "Replace `isinstance(x, type(None))` with `x is None`" # noqa: E501 errors.append(ErrorInfo.from_node(node, msg)) return case OpExpr(op="|"): type_none_index = get_type_none_index(ty) if type_none_index == -1: return if type_none_index == 0: types = "type(None) | ..." else: types = "... | type(None)" case TupleExpr(items=items): for i, item in enumerate(items): if is_type_none_call(item): if i == 0: types = "(type(None), ...)" else: types = "(..., type(None))" break else: return case _: return msg = f"Replace `isinstance(x, {types})` with `x is None or isinstance(x, ...)`" # noqa: E501 errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/builtin/no_set_for_loop.py000066400000000000000000000040131454672660200230330ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import Block, CallExpr, ExpressionStmt, ForStmt, MemberExpr, NameExpr, Var from refurb.checks.common import unmangle_name from refurb.error import Error @dataclass class ErrorInfo(Error): """ When you want to add/remove a bunch of items to/from a set, don't use a for loop, call the appropriate method on the set itself. Bad: ``` sentence = "hello world" vowels = "aeiou" letters = set(sentence) for vowel in vowels: letters.discard(vowel) ``` Good: ``` sentence = "hello world" vowels = "aeiou" letters = set(sentence) letters.difference_update(vowels) ``` """ name = "no-set-for-loop" code = 142 categories = ("builtin",) def check(node: ForStmt, errors: list[Error]) -> None: match node: case ForStmt( index=NameExpr(name=for_name), body=Block( body=[ ExpressionStmt( expr=CallExpr( callee=MemberExpr( expr=NameExpr(node=Var(type=ty)) as set_name, name=("add" | "discard") as name, ), args=[arg], ) ) ] ), ) if str(ty).startswith("builtins.set[") and set_name.name != for_name: new_func = "update" if name == "add" else "difference_update" if isinstance(arg, NameExpr): expr = unmangle_name(arg.name) new_expr = "y" if unmangle_name(for_name) != expr: return else: expr = "..." new_expr = "... for x in y" errors.append( ErrorInfo.from_node( node, f"Replace `for x in y: s.{name}({expr})` with `s.{new_func}({new_expr})`", # noqa: E501 ) ) refurb-1.27.0/refurb/checks/builtin/no_slice_copy.py000066400000000000000000000033651454672660200225030ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import AssignmentStmt, DelStmt, IndexExpr, MypyFile, RefExpr, SliceExpr, Var from refurb.error import Error from refurb.visitor import TraverserVisitor @dataclass class ErrorInfo(Error): """ Don't use a slice expression (with no bounds) to make a copy of something, use the more readable `.copy()` method instead: Bad: ``` nums = [3.1415, 1234] copy = nums[:] ``` Good: ``` nums = [3.1415, 1234] copy = nums.copy() ``` """ name = "no-slice-copy" code = 145 msg: str = "Replace `x[:]` with `x.copy()`" categories = ("readability",) SEQUENCE_BUILTINS = ( "builtins.bytearray", "builtins.list[", "builtins.tuple[", "tuple[", ) class SliceExprVisitor(TraverserVisitor): errors: list[Error] def __init__(self, errors: list[Error]) -> None: super().__init__() self.errors = errors def visit_assignment_stmt(self, node: AssignmentStmt) -> None: self.accept(node.rvalue) def visit_del_stmt(self, node: DelStmt) -> None: if not isinstance(node.expr, IndexExpr): self.accept(node.expr) def visit_index_expr(self, node: IndexExpr) -> None: index = node.index match node.base: case RefExpr(node=Var(type=ty)): if not str(ty).startswith(SEQUENCE_BUILTINS): return case _: return if ( isinstance(index, SliceExpr) and index.begin_index is index.end_index is index.stride is None ): self.errors.append(ErrorInfo.from_node(node)) def check(node: MypyFile, errors: list[Error]) -> None: SliceExprVisitor(errors).accept(node) refurb-1.27.0/refurb/checks/builtin/print.py000066400000000000000000000011531454672660200210030ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, NameExpr, StrExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ `print("")` can be simplified to just `print()`. """ name = "simplify-print" code = 105 msg: str = 'Replace `print("")` with `print()`' categories = ("builtin", "readability") def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=NameExpr(fullname="builtins.print"), args=[StrExpr(value="")], ): errors.append(ErrorInfo.from_node(node)) refurb-1.27.0/refurb/checks/builtin/set_discard.py000066400000000000000000000032051454672660200221330ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( Block, CallExpr, ComparisonExpr, ExpressionStmt, IfStmt, MemberExpr, NameExpr, Var, ) from refurb.checks.common import is_equivalent from refurb.error import Error @dataclass class ErrorInfo(Error): """ If you want to remove a value from a set regardless of whether it exists or not, use the `discard()` method instead of `remove()`: Bad: ``` nums = {123, 456} if 123 in nums: nums.remove(123) ``` Good: ``` nums = {123, 456} nums.discard(123) ``` """ name = "use-set-discard" code = 132 msg: str = "Replace `if x in s: s.remove(x)` with `s.discard(x)`" categories = ("readability", "set") def check(node: IfStmt, errors: list[Error]) -> None: match node: case IfStmt( expr=[ComparisonExpr(operators=["in"], operands=[lhs, rhs])], body=[ Block( body=[ ExpressionStmt( expr=CallExpr( callee=MemberExpr( expr=NameExpr(node=Var(type=ty)) as expr, name="remove", ), args=[arg], ) ) ] ) ], ) if ( is_equivalent(lhs, arg) and is_equivalent(rhs, expr) and str(ty).startswith("builtins.set[") ): errors.append(ErrorInfo.from_node(node)) refurb-1.27.0/refurb/checks/builtin/simplify_comprehension.py000066400000000000000000000042631454672660200244410ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, GeneratorExpr, ListComprehension, NameExpr, SetComprehension from refurb.error import Error @dataclass class ErrorInfo(Error): """ Often times generator expressions and list/set/dict comprehensions can be written more succinctly. For example, passing a list comprehension to a function when a generator expression would suffice, or using the shorthand notation in the case of `list` and `set`. For example: Bad: ``` nums = [1, 1, 2, 3] nums_times_10 = list(num * 10 for num in nums) unique_squares = set(num ** 2 for num in nums) number_tuple = tuple([num ** 2 for num in nums]) ``` Good: ``` nums = [1, 1, 2, 3] nums_times_10 = [num * 10 for num in nums] unique_squares = {num ** 2 for num in nums} number_tuple = tuple(num ** 2 for num in nums) ``` """ name = "simplify-comprehension" enabled = False code = 137 categories = ("builtin", "iterable", "readability") FUNCTION_MAPPINGS = { "builtins.list": "[...]", "builtins.set": "{...}", "builtins.frozenset": "frozenset(...)", "builtins.tuple": "tuple(...)", } SET_TYPES = ("frozenset", "set") COMPREHENSION_SHORTHAND_TYPES = ("list", "set") NODE_TYPE_TO_FUNC_NAME = { ListComprehension: "builtins.list", SetComprehension: "builtins.set", GeneratorExpr: "", } def format_func_name(fullname: str) -> str: return FUNCTION_MAPPINGS.get(fullname, "...") def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=NameExpr(name=name, fullname=fullname), args=[GeneratorExpr() | ListComprehension() | SetComprehension() as arg], ) if fullname in FUNCTION_MAPPINGS: if isinstance(arg, GeneratorExpr) and name not in COMPREHENSION_SHORTHAND_TYPES: return if isinstance(arg, SetComprehension) and name not in SET_TYPES: return old = format_func_name(NODE_TYPE_TO_FUNC_NAME[type(arg)]) new = format_func_name(fullname) errors.append(ErrorInfo.from_node(node, f"Replace `{name}({old})` with `{new}`")) refurb-1.27.0/refurb/checks/builtin/simplify_global_and_nonlocal.py000066400000000000000000000031471454672660200255370ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import Block, GlobalDecl, NonlocalDecl from refurb.error import Error @dataclass class ErrorInfo(Error): """ The `global` and `nonlocal` keywords can take multiple comma-separated names, removing the need for multiple lines. Bad: ``` def some_func(): global x global y print(x, y) ``` Good: ``` def some_func(): global x, y print(x, y) ``` """ name = "simplify-global-and-nonlocal" code = 154 categories = ("builtin", "readability") def emit_error_if_needed(found: list[GlobalDecl | NonlocalDecl], errors: list[Error]) -> None: if len(found) < 2: return name = "global" if isinstance(found[0], GlobalDecl) else "nonlocal" replace_lines = [f"{name} x", f"{name} y"] new_args = ["x", "y"] if len(found) >= 3: replace_lines.append("...") new_args.append("...") replace = "; ".join(replace_lines) new = f"{name} {', '.join(new_args)}" errors.append(ErrorInfo.from_node(found[0], f"Replace `{replace}` with `{new}`")) def check(node: Block, errors: list[Error]) -> None: found: list[GlobalDecl | NonlocalDecl] = [] for stmt in node.body: if isinstance(stmt, GlobalDecl | NonlocalDecl): if not found or isinstance(stmt, type(found[0])): found.append(stmt) continue emit_error_if_needed(found, errors) found = [] if isinstance(stmt, GlobalDecl | NonlocalDecl): found.append(stmt) emit_error_if_needed(found, errors) refurb-1.27.0/refurb/checks/builtin/use_bit_count.py000066400000000000000000000040671454672660200225200ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( CallExpr, IndexExpr, IntExpr, MemberExpr, NameExpr, RefExpr, SliceExpr, StrExpr, ) from refurb.error import Error from refurb.settings import Settings @dataclass class ErrorInfo(Error): """ Python 3.10 adds a very helpful `bit_count()` function for integers which counts the number of set bits. This new function is more descriptive and faster compared to converting/counting characters in a string. Bad: ``` x = bin(0b1010).count("1") assert x == 2 ``` Good: ``` x = 0b1010.bit_count() assert x == 2 ``` """ name = "use-bit-count" code = 161 categories = ("builtin", "performance", "python310", "readability") def check(node: CallExpr, errors: list[Error], settings: Settings) -> None: if settings.get_python_version() < (3, 10): return # pragma: no cover match node: case CallExpr( callee=MemberExpr( expr=IndexExpr( base=bin_func, index=SliceExpr( begin_index=IntExpr(value=2), end_index=None, stride=None, ), ) | bin_func, name="count", ), args=[StrExpr(value="1")], ): match bin_func: case CallExpr( callee=NameExpr(fullname="builtins.bin"), args=[arg], ): pass case _: return if isinstance(node.callee.expr, IndexExpr): # type: ignore old = "bin(x)[2:]" else: old = "bin(x)" if isinstance(arg, IntExpr | RefExpr | CallExpr | IndexExpr): x = "x" else: x = "(x)" errors.append( ErrorInfo.from_node(node, f'Replace `{old}.count("1")` with `{x}.bit_count()`') ) refurb-1.27.0/refurb/checks/builtin/use_int_base_zero.py000066400000000000000000000036661454672660200233610ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ArgKind, CallExpr, IndexExpr, IntExpr, RefExpr, SliceExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ When converting a string starting with `0b`, `0o`, or `0x` to an int, you don't need to slice the string and set the base yourself: just call `int()` with a base of zero. Doing this will autodeduce the correct base to use based on the string prefix. Bad: ``` num = "0xABC" if num.startswith("0b"): i = int(num[2:], 2) elif num.startswith("0o"): i = int(num[2:], 8) elif num.startswith("0x"): i = int(num[2:], 16) print(i) ``` Good: ``` num = "0xABC" i = int(num, 0) print(i) ``` This check is disabled by default because there is no way for Refurb to detect whether the prefixes that are being stripped are valid Python int prefixes (like `0x`) or some other prefix which would fail if parsed using this method. """ enabled = False name = "use-int-base-zero" code = 166 categories = ("builtin", "readability") def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=RefExpr(fullname="builtins.int"), args=[ IndexExpr( index=SliceExpr( begin_index=IntExpr(value=2), end_index=None, stride=None, ), ), IntExpr(value=2 | 8 | 16 as base), ], arg_kinds=arg_kinds, arg_names=[_, "base" | None], ): kw = "base=" if arg_kinds[1] == ArgKind.ARG_NAMED else "" errors.append( ErrorInfo.from_node( node, f"Replace `int(x[2:], {kw}{base})` with `int(x, {kw}0)`", ) ) refurb-1.27.0/refurb/checks/builtin/use_isinstance_tuple.py000066400000000000000000000033041454672660200240740ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, NameExpr, OpExpr from refurb.checks.common import extract_binary_oper, is_equivalent from refurb.error import Error from refurb.settings import Settings @dataclass class ErrorInfo(Error): """ `isinstance()` and `issubclass()` both take tuple arguments, so instead of calling them multiple times for the same object, you can check all of them at once: Bad: ``` if isinstance(num, float) or isinstance(num, int): pass ``` Good: ``` if isinstance(num, (float, int)): pass ``` Note: In Python 3.10+, you can also pass type unions as the second param to these functions: ``` if isinstance(num, float | int): pass ``` """ name = "use-isinstance-issubclass-tuple" code = 121 categories = ("python310", "readability") def check(node: OpExpr, errors: list[Error], settings: Settings) -> None: match extract_binary_oper("or", node): case ( CallExpr(callee=NameExpr() as lhs, args=lhs_args), CallExpr(callee=NameExpr() as rhs, args=rhs_args), ) if ( lhs.fullname == rhs.fullname and lhs.fullname in {"builtins.isinstance", "builtins.issubclass"} and len(lhs_args) == 2 and is_equivalent(lhs_args[0], rhs_args[0]) ): type_args = "y | z" if settings.get_python_version() >= (3, 10) else "(y, z)" errors.append( ErrorInfo.from_node( lhs_args[1], f"Replace `{lhs.name}(x, y) or {lhs.name}(x, z)` with `{lhs.name}(x, {type_args})`", # noqa: E501 ) ) refurb-1.27.0/refurb/checks/builtin/use_max.py000066400000000000000000000037121454672660200213130ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ComparisonExpr, ConditionalExpr from refurb.checks.common import is_equivalent from refurb.error import Error @dataclass class ErrorInfo(Error): """ Certain ternary expressions can be written more succinctly using the builtin `min`/`max` functions: Bad: ``` score1 = 90 score2 = 99 highest_score = score1 if score1 > score2 else score2 ``` Good: ``` score1 = 90 score2 = 99 highest_score = max(score1, score2) ``` """ name = "use-min-max" code = 136 categories = ("builtin", "logical", "readability") FUNC_TABLE = { "<": "min", "<=": "min", ">": "max", ">=": "max", } def flip_comparison_oper(oper: str) -> str: return { "<": ">", "<=": ">=", ">": "<", ">=": "<=", }.get(oper, oper) def check(node: ConditionalExpr, errors: list[Error]) -> None: match node: case ConditionalExpr( if_expr=if_expr, cond=ComparisonExpr(operators=[oper], operands=[lhs, rhs]), else_expr=else_expr, ): if ( is_equivalent(if_expr, lhs) and is_equivalent(rhs, else_expr) and (func := FUNC_TABLE.get(oper)) ): errors.append( ErrorInfo.from_node( node, f"Replace `x if x {oper} y else y` with `{func}(x, y)`", # noqa: E501 ) ) if ( is_equivalent(if_expr, rhs) and is_equivalent(lhs, else_expr) and (func := FUNC_TABLE.get(flip_comparison_oper(oper))) ): errors.append( ErrorInfo.from_node( node, f"Replace `x if y {oper} x else y` with `{func}(y, x)`", # noqa: E501 ) ) refurb-1.27.0/refurb/checks/builtin/writelines.py000066400000000000000000000040121454672660200220310ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( Block, CallExpr, ExpressionStmt, ForStmt, MemberExpr, NameExpr, Var, WithStmt, ) from refurb.error import Error @dataclass class ErrorInfo(Error): r""" When you want to write a list of lines to a file, don't call `.write()` for every line, use `.writelines()` instead: Bad: ``` lines = ["line 1\n", "line 2\n", "line 3\n"] with open("file") as f: for line in lines: f.write(line) ``` Good: ``` lines = ["line 1\n", "line 2\n", "line 3\n"] with open("file") as f: f.writelines(lines) ``` Note: If you have a more complex expression then just `lines`, you may need to use a list comprehension instead. For example: ``` f.writelines(f"{line}\n" for line in lines) ``` """ name = "use-writelines" code = 122 msg: str = "Replace `for line in lines: f.write(line)` with `f.writelines(lines)`" categories = ("builtin", "readability") def check(node: WithStmt, errors: list[Error]) -> None: match node: case WithStmt( target=[NameExpr(node=Var(type=ty)) as resource], body=Block( body=[ ForStmt( index=NameExpr(), body=Block( body=[ ExpressionStmt( expr=CallExpr( callee=MemberExpr( expr=NameExpr() as file, name="write", ) ) ) ] ), ) as for_stmt ] ), ) if str(ty).startswith("io.") and resource.fullname == file.fullname: errors.append(ErrorInfo.from_node(for_stmt)) refurb-1.27.0/refurb/checks/common.py000066400000000000000000000230761454672660200175010ustar00rootroot00000000000000from collections.abc import Callable from itertools import chain, combinations, starmap from mypy.nodes import ( ArgKind, Block, BytesExpr, CallExpr, ComparisonExpr, DictExpr, DictionaryComprehension, Expression, ForStmt, GeneratorExpr, IndexExpr, IntExpr, LambdaExpr, ListExpr, MemberExpr, MypyFile, NameExpr, Node, OpExpr, ReturnStmt, SetExpr, SliceExpr, StarExpr, Statement, TupleExpr, UnaryExpr, ) from refurb.error import Error from refurb.visitor import TraverserVisitor def extract_binary_oper(oper: str, node: OpExpr) -> tuple[Expression, Expression] | None: match node: case OpExpr( op=op, left=lhs, right=rhs, ) if op == oper: match rhs: case OpExpr(op=op, left=rhs) if op == oper: return lhs, rhs case OpExpr(): return None case Expression(): return lhs, rhs return None def check_block_like( func: Callable[[list[Statement], list[Error]], None], node: Block | MypyFile, errors: list[Error], ) -> None: match node: case Block(): func(node.body, errors) case MypyFile(): func(node.defs, errors) def check_for_loop_like( func: Callable[[Node, Node, list[Node], list[Error]], None], node: ForStmt | GeneratorExpr | DictionaryComprehension, errors: list[Error], ) -> None: match node: case ForStmt(index=index, expr=expr): func(index, expr, [node.body], errors) case GeneratorExpr( indices=[index], sequences=[expr], condlists=condlists, ): func( index, expr, list(chain([node.left_expr], *condlists)), errors, ) case DictionaryComprehension( indices=[index], sequences=[expr], condlists=condlists, ): func( index, expr, list(chain([node.key, node.value], *condlists)), errors, ) def unmangle_name(name: str | None) -> str: return (name or "").replace("'", "") def is_equivalent(lhs: Node | None, rhs: Node | None) -> bool: match (lhs, rhs): case None, None: return True case NameExpr() as lhs, NameExpr() as rhs: return unmangle_name(lhs.fullname) == unmangle_name(rhs.fullname) case MemberExpr() as lhs, MemberExpr() as rhs: return ( lhs.name == rhs.name and unmangle_name(lhs.fullname) == unmangle_name(rhs.fullname) and is_equivalent(lhs.expr, rhs.expr) ) case IndexExpr() as lhs, IndexExpr() as rhs: return is_equivalent(lhs.base, rhs.base) and is_equivalent(lhs.index, rhs.index) case CallExpr() as lhs, CallExpr() as rhs: return ( is_equivalent(lhs.callee, rhs.callee) and all(starmap(is_equivalent, zip(lhs.args, rhs.args))) and lhs.arg_kinds == rhs.arg_kinds and lhs.arg_names == rhs.arg_names ) case ( (ListExpr() as lhs, ListExpr() as rhs) | (TupleExpr() as lhs, TupleExpr() as rhs) | (SetExpr() as lhs, SetExpr() as rhs) ): return len(lhs.items) == len(rhs.items) and all( # type: ignore starmap(is_equivalent, zip(lhs.items, rhs.items)) # type: ignore ) case DictExpr() as lhs, DictExpr() as rhs: return len(lhs.items) == len(rhs.items) and all( is_equivalent(lhs_item[0], rhs_item[0]) and is_equivalent(lhs_item[1], rhs_item[1]) for lhs_item, rhs_item in zip(lhs.items, rhs.items) ) case StarExpr() as lhs, StarExpr() as rhs: return is_equivalent(lhs.expr, rhs.expr) case UnaryExpr() as lhs, UnaryExpr() as rhs: return lhs.op == rhs.op and is_equivalent(lhs.expr, rhs.expr) case OpExpr() as lhs, OpExpr() as rhs: return ( lhs.op == rhs.op and is_equivalent(lhs.left, rhs.left) and is_equivalent(lhs.right, rhs.right) ) case ComparisonExpr() as lhs, ComparisonExpr() as rhs: return lhs.operators == rhs.operators and all( starmap(is_equivalent, zip(lhs.operands, rhs.operands)) ) case SliceExpr() as lhs, SliceExpr() as rhs: return ( is_equivalent(lhs.begin_index, rhs.begin_index) and is_equivalent(lhs.end_index, rhs.end_index) and is_equivalent(lhs.stride, rhs.stride) ) return str(lhs) == str(rhs) def get_common_expr_positions(*exprs: Expression) -> tuple[int, int] | None: for lhs, rhs in combinations(exprs, 2): if is_equivalent(lhs, rhs): return exprs.index(lhs), exprs.index(rhs) return None def get_common_expr_in_comparison_chain( node: OpExpr, oper: str, cmp_oper: str = "==" ) -> tuple[Expression, tuple[int, int]] | None: """ This function finds the first expression shared between 2 comparison expressions in the binary operator `oper`. For example, an OpExpr that looks like the following: 1 == 2 or 3 == 1 Will return a tuple containing the first common expression (`IntExpr(1)` in this case), and the indices of the common expressions as they appear in the source (`0` and `3` in this case). The indices are to be used for display purposes by the caller. If the binary operator is not composed of 2 comparison operators, or if there are no common expressions, `None` is returned. """ match extract_binary_oper(oper, node): case ( ComparisonExpr(operators=[lhs_oper], operands=[a, b]), ComparisonExpr(operators=[rhs_oper], operands=[c, d]), ) if ( lhs_oper == rhs_oper == cmp_oper and (indices := get_common_expr_positions(a, b, c, d)) ): return a, indices return None # pragma: no cover class ReadCountVisitor(TraverserVisitor): name: NameExpr read_count: int def __init__(self, name: NameExpr) -> None: self.name = name self.read_count = 0 def visit_name_expr(self, node: NameExpr) -> None: if node.fullname == self.name.fullname: self.read_count += 1 @property def was_read(self) -> int: return self.read_count > 0 def is_name_unused_in_contexts(name: NameExpr, contexts: list[Node]) -> bool: for ctx in contexts: visitor = ReadCountVisitor(name) visitor.accept(ctx) if visitor.was_read: return False return True def normalize_os_path(module: str | None) -> str: """ Mypy turns "os.path" module names into their respective platform, such as "ntpath" for windows, "posixpath" if they are POSIX only, or "genericpath" if they apply to both (I assume). To make life easier for us though, we turn those module names into their original form. """ # Used for compatibility with older versions of Mypy. if not module: return "" segments = module.split(".") if segments[0].startswith(("genericpath", "ntpath", "posixpath")): return ".".join(["os", "path"] + segments[1:]) return module def is_type_none_call(node: Expression) -> bool: match node: case CallExpr( callee=NameExpr(fullname="builtins.type"), args=[NameExpr(fullname="builtins.None")], ): return True return False def stringify(node: Node) -> str: try: return _stringify(node) except ValueError: # pragma: no cover return "x" def _stringify(node: Node) -> str: match node: case MemberExpr(expr=expr, name=name): return f"{_stringify(expr)}.{name}" case NameExpr(name=name): return unmangle_name(name) case BytesExpr(value=value): # TODO: use same formatting as source line return repr(value.encode()) case IntExpr(value=value): # TODO: use same formatting as source line return str(value) case CallExpr(): name = _stringify(node.callee) args = ", ".join(_stringify(arg) for arg in node.args) return f"{name}({args})" case OpExpr(left=left, op=op, right=right): return f"{_stringify(left)} {op} {_stringify(right)}" case ComparisonExpr(): parts: list[str] = [] for op, operand in zip(node.operators, node.operands): parts.extend((_stringify(operand), op)) parts.append(_stringify(node.operands[-1])) return " ".join(parts) case UnaryExpr(op=op, expr=expr): return f"{op} {_stringify(expr)}" case LambdaExpr( arg_names=arg_names, arg_kinds=arg_kinds, body=Block(body=[ReturnStmt(expr=Expression() as expr)]), ) if (all(kind == ArgKind.ARG_POS for kind in arg_kinds) and all(arg_names)): if arg_names: args = " " args += ", ".join(arg_names) # type: ignore else: args = "" body = _stringify(expr) return f"lambda{args}: {body}" case ListExpr(items=items): inner = ", ".join(stringify(x) for x in items) return f"[{inner}]" raise ValueError refurb-1.27.0/refurb/checks/contextlib/000077500000000000000000000000001454672660200200025ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/contextlib/__init__.py000066400000000000000000000000001454672660200221010ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/contextlib/with_suppress.py000066400000000000000000000036171454672660200233020ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import Block, NameExpr, PassStmt, TryStmt, TupleExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Often times you want to handle an exception and just ignore it. You can do this with a `try`/`except` block with a single `pass` in the `except` block, but there is a simpler and more concise way using the `suppress()` function from `contextlib`: Bad: ``` try: f() except FileNotFoundError: pass ``` Good: ``` with suppress(FileNotFoundError): f() ``` Note: `suppress()` is slower than using `try`/`except`, so for performance critical code you might consider ignoring this check. """ name = "use-with-suppress" code = 107 categories = ("contextlib", "readability") def check(node: TryStmt, errors: list[Error]) -> None: match node: case TryStmt( handlers=[Block(body=[PassStmt()])], types=[types], else_body=None, finally_body=None, ): match types: case NameExpr(name=name): inner = name except_inner = f" {inner}" case TupleExpr(items=items): if any(not isinstance(item, NameExpr) for item in items): return inner = ", ".join(item.name for item in items) # type: ignore except_inner = f" ({inner})" case None: inner = "BaseException" except_inner = "" case _: return errors.append( ErrorInfo.from_node( node, f"Replace `try: ... except{except_inner}: pass` with `with suppress({inner}): ...`", # noqa: E501 ) ) refurb-1.27.0/refurb/checks/datetime/000077500000000000000000000000001454672660200174235ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/datetime/__init__.py000066400000000000000000000000001454672660200215220ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/datetime/simplify_fromisoformat.py000066400000000000000000000061371454672660200246070ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( CallExpr, Expression, IndexExpr, IntExpr, MemberExpr, NameExpr, OpExpr, SliceExpr, StrExpr, UnaryExpr, Var, ) from refurb.error import Error from refurb.settings import Settings @dataclass class ErrorInfo(Error): """ Python 3.11 adds support for parsing UTC timestamps that end with `Z`, thus removing the need to strip and append the `+00:00` timezone. Bad: ``` date = "2023-02-21T02:23:15Z" start_date = datetime.fromisoformat(date.replace("Z", "+00:00")) ``` Good: ``` date = "2023-02-21T02:23:15Z" start_date = datetime.fromisoformat(date) ``` """ name = "simplify-fromisoformat" code = 162 categories = ("datetime", "python311", "readability") def is_string(node: Expression) -> bool: match node: case StrExpr(): return True case NameExpr(node=Var(type=ty)) if str(ty) == "builtins.str": return True return False def is_utc_timezone(timezone: str) -> bool: return timezone.startswith(("+", "-")) and timezone.strip("+-") in { "00:00", "0000", "00", } def check(node: CallExpr, errors: list[Error], settings: Settings) -> None: if settings.get_python_version() < (3, 11): return match node: case CallExpr( callee=MemberExpr( expr=NameExpr(fullname="datetime.datetime"), name="fromisoformat", ), args=[arg], ): match arg: case CallExpr( callee=MemberExpr(expr=date, name="replace"), args=[ StrExpr(value="Z"), StrExpr(value=timezone), ], ) if is_string(date) and is_utc_timezone(timezone): old = f'fromisoformat(x.replace("Z", "{timezone}"))' case OpExpr( left=IndexExpr( base=date, index=SliceExpr( begin_index=None, end_index=UnaryExpr(op="-", expr=IntExpr(value=1)), stride=None, ), ), op="+", right=StrExpr(value=timezone), ) if is_string(date) and is_utc_timezone(timezone): old = f'fromisoformat(x[:-1] + "{timezone}")' case OpExpr( left=CallExpr( callee=MemberExpr(expr=date, name="strip" | "rstrip" as func_name), args=[StrExpr(value="Z")], ), op="+", right=StrExpr(value=timezone), ) if is_string(date) and is_utc_timezone(timezone): old = f'fromisoformat(x.{func_name}("Z") + "{timezone}")' case _: return errors.append(ErrorInfo.from_node(node, f"Replace `{old}` with `fromisoformat(x)`")) refurb-1.27.0/refurb/checks/datetime/unreliable_utc_usage.py000066400000000000000000000027211454672660200241600ustar00rootroot00000000000000from dataclasses import dataclass from typing import Final from mypy.nodes import CallExpr, MemberExpr, RefExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Because naive `datetime` objects are treated by many `datetime` methods as local times, it is preferred to use aware datetimes to represent times in UTC. This check affects `datetime.utcnow` and `datetime.utcfromtimestamp`. Bad: ``` from datetime import datetime now = datetime.utcnow() past_date = datetime.utcfromtimestamp(some_timestamp) ``` Good: ``` from datetime import datetime, timezone datetime.now(timezone.utc) datetime.fromtimestamp(some_timestamp, tz=timezone.utc) ``` """ name = "unreliable-utc-usage" code = 176 categories = ("datetime",) _replacements: Final = { "utcnow": ("()", "now(tz=timezone.utc)"), "utcfromtimestamp": ("(...)", "fromtimestamp(..., tz=timezone.utc)"), } def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=RefExpr(fullname="datetime.datetime"), ) as func, ) if replacements := _replacements.get(func.name): parens, replaced = replacements errors.append( ErrorInfo.from_node( node, f"Replace `{func.name}{parens}` with `{replaced}`", ) ) refurb-1.27.0/refurb/checks/decimal/000077500000000000000000000000001454672660200172255ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/decimal/__init__.py000066400000000000000000000000001454672660200213240ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/decimal/simplify_ctor.py000066400000000000000000000037361454672660200224730ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, NameExpr, RefExpr, StrExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Under certain circumstances the `Decimal()` constructor can be made more succinct. Bad: ``` if x == Decimal("0"): pass if y == Decimal(float("Infinity")): pass ``` Good: ``` if x == Decimal(0): pass if y == Decimal("Infinity"): pass ``` """ name = "simplify-decimal-ctor" code = 157 categories = ("decimal",) FLOAT_LITERALS = ["inf", "-inf", "infinity", "-infinity", "nan"] def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=RefExpr(fullname="_decimal.Decimal") as ref, args=[arg], ): match arg: case StrExpr(value=value): old = repr(value)[1:-1] try: new = value.strip().lstrip("+") if int(value) != 0: new = new.lstrip("0") except ValueError: return func_name = stringify_decimal_expr(ref) msg = f'Replace `{func_name}("{old}")` with `{func_name}({new})`' # noqa: E501 errors.append(ErrorInfo.from_node(node, msg)) case CallExpr( callee=NameExpr(fullname="builtins.float"), args=[StrExpr(value=value)], ) if value.lower() in FLOAT_LITERALS: func_name = stringify_decimal_expr(ref) msg = f'Replace `{func_name}(float("{value}"))` with `{func_name}("{value}")`' # noqa: E501 errors.append(ErrorInfo.from_node(node, msg)) def stringify_decimal_expr(node: RefExpr) -> str: return "decimal.Decimal" if isinstance(node, MemberExpr) else "Decimal" refurb-1.27.0/refurb/checks/flow/000077500000000000000000000000001454672660200165765ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/flow/__init__.py000066400000000000000000000000001454672660200206750ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/flow/no_trailing_continue.py000066400000000000000000000042741454672660200233700ustar00rootroot00000000000000from collections.abc import Generator from dataclasses import dataclass from mypy.nodes import ( Block, ContinueStmt, ForStmt, IfStmt, MatchStmt, Statement, WhileStmt, WithStmt, ) from mypy.patterns import AsPattern from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't explicitly continue if you are already at the end of the control flow for the current for/while loop: Bad: ``` def func(): for _ in range(10): print("hello world!") continue def func2(x): for x in range(10): if x == 1: print("x is 1") else: print("x is not 1") continue ``` Good: ``` def func(): for _ in range(10): print("hello world!") def func2(x): for x in range(10): if x == 1: print("x is 1") else: print("x is not 1") ``` """ name = "no-redundant-continue" code = 133 msg: str = "Continue is redundant here" categories = ("control-flow", "readability") def get_trailing_continue(node: Statement) -> Generator[Statement, None, None]: match node: case ContinueStmt(): yield node case MatchStmt(bodies=bodies, patterns=patterns): for body, pattern in zip(bodies, patterns): match (body.body, pattern): case _, AsPattern(pattern=None, name=None): pass case [ContinueStmt()], _: continue yield from get_trailing_continue(body.body[-1]) case (IfStmt(else_body=Block(body=[*_, stmt])) | WithStmt(body=Block(body=[*_, stmt]))): yield from get_trailing_continue(stmt) return None def check(node: ForStmt | WhileStmt, errors: list[Error]) -> None: match node: case (ForStmt(body=Block(body=[*prev, stmt])) | WhileStmt(body=Block(body=[*prev, stmt]))): if not prev and isinstance(stmt, ContinueStmt): return errors.extend(ErrorInfo.from_node(x) for x in get_trailing_continue(stmt)) refurb-1.27.0/refurb/checks/flow/no_trailing_return.py000066400000000000000000000036361454672660200230640ustar00rootroot00000000000000from collections.abc import Generator from dataclasses import dataclass from mypy.nodes import Block, FuncItem, IfStmt, MatchStmt, ReturnStmt, Statement, WithStmt from mypy.patterns import AsPattern from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't explicitly return if you are already at the end of the control flow for the current function: Bad: ``` def func(): print("hello world!") return def func2(x): if x == 1: print("x is 1") else: print("x is not 1") return ``` Good: ``` def func(): print("hello world!") def func2(x): if x == 1: print("x is 1") else: print("x is not 1") ``` """ name = "no-redundant-return" code = 125 msg: str = "Return is redundant here" categories = ("control-flow", "readability") def get_trailing_return(node: Statement) -> Generator[Statement, None, None]: match node: case ReturnStmt(expr=None): yield node case MatchStmt(bodies=bodies, patterns=patterns): for body, pattern in zip(bodies, patterns): match (body.body, pattern): case _, AsPattern(pattern=None, name=None): pass case [ReturnStmt()], _: continue yield from get_trailing_return(body.body[-1]) case (IfStmt(else_body=Block(body=[*_, stmt])) | WithStmt(body=Block(body=[*_, stmt]))): yield from get_trailing_return(stmt) return None def check(node: FuncItem, errors: list[Error]) -> None: match node: case FuncItem(body=Block(body=[*prev, stmt])): if not prev and isinstance(stmt, ReturnStmt): return errors.extend(ErrorInfo.from_node(x) for x in get_trailing_return(stmt)) refurb-1.27.0/refurb/checks/flow/no_with_assign.py000066400000000000000000000040151454672660200221630ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( AssignmentStmt, Block, CallExpr, MypyFile, NameExpr, RefExpr, Statement, WithStmt, ) from refurb.checks.common import check_block_like from refurb.error import Error @dataclass class ErrorInfo(Error): """ Due to Python's scoping rules, you can use a variable that has gone "out of scope" so long as all previous code paths can bind to it. Long story short, you don't need to declare a variable before you assign it in a `with` statement: Bad: ``` x = "" with open("file.txt") as f: x = f.read() ``` Good: ``` with open("file.txt") as f: x = f.read() ``` """ name = "no-with-assign" code = 127 msg: str = "This variable is redeclared later, and can be removed here" categories = ("readability", "scoping") def check(node: Block | MypyFile, errors: list[Error]) -> None: check_block_like(check_stmts, node, errors) def check_stmts(body: list[Statement], errors: list[Error]) -> None: assign: AssignmentStmt | None = None for stmt in body: if assign: match stmt: case WithStmt( body=Block(body=[AssignmentStmt(lvalues=[NameExpr() as name])]), expr=resources, ) if ( name.fullname and name.fullname == assign.lvalues[0].fullname # type: ignore ): # Skip if suppress() is one of the resources # see https://github.com/dosisod/refurb/issues/47 for resource in resources: match resource: case CallExpr(callee=RefExpr(fullname="contextlib.suppress")): break else: errors.append(ErrorInfo.from_node(assign)) assign = None match stmt: case AssignmentStmt(lvalues=[NameExpr()]): assign = stmt refurb-1.27.0/refurb/checks/flow/simplify_return.py000066400000000000000000000042271454672660200224100ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import Block, Expression, FuncItem, IfStmt, MatchStmt, ReturnStmt, Statement from mypy.patterns import AsPattern from refurb.error import Error @dataclass class ErrorInfo(Error): """ Sometimes a return statement can be written more succinctly: Bad: ``` def index_or_default(nums: list[Any], index: int, default: Any): if index >= len(nums): return default else: return nums[index] def is_on_axis(position: tuple[int, int]) -> bool: match position: case (0, _) | (_, 0): return True case _: return False ``` Good: ``` def index_or_default(nums: list[Any], index: int, default: Any): if index >= len(nums): return default return nums[index] def is_on_axis(position: tuple[int, int]) -> bool: match position: case (0, _) | (_, 0): return True return False ``` """ name = "simplify-return" code = 126 categories = ("control-flow", "readability") def get_trailing_return(node: Statement) -> Statement | None: match node: case ReturnStmt(expr=Expression()): return node case MatchStmt( bodies=[*bodies, Block(body=[stmt])], patterns=[*_, AsPattern(pattern=None)], ) if all(isinstance(block.body[-1], ReturnStmt) for block in bodies): return get_trailing_return(stmt) case IfStmt(body=[Block(body=[*_, ReturnStmt()])], else_body=Block(body=[stmt])): return get_trailing_return(stmt) return None def check(node: FuncItem, errors: list[Error]) -> None: match node: case FuncItem(body=Block(body=[*_, IfStmt() | MatchStmt() as stmt])): if return_node := get_trailing_return(stmt): name = "case _" if type(stmt) is MatchStmt else "else" errors.append( ErrorInfo.from_node( return_node, f"Replace `{name}: return x` with `return x`", ) ) refurb-1.27.0/refurb/checks/function/000077500000000000000000000000001454672660200174545ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/function/__init__.py000066400000000000000000000000001454672660200215530ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/function/use_implicit_default.py000066400000000000000000000131111454672660200242150ustar00rootroot00000000000000from collections.abc import Iterator from dataclasses import dataclass from mypy.nodes import ( ArgKind, Argument, CallExpr, Decorator, Expression, FuncDef, IntExpr, MemberExpr, NameExpr, OverloadedFuncDef, StrExpr, SymbolNode, TypeInfo, Var, ) from mypy.types import Instance from refurb.checks.common import is_equivalent from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't pass an argument if it is the same as the default value: Bad: ``` def greet(name: str = "bob") -> None: print(f"Hello {name}") greet("bob") {}.get("some key", None) ``` Good: ``` def greet(name: str = "bob") -> None: print(f"Hello {name}") greet() {}.get("some key") ``` """ name = "use-implicit-default" enabled = False code = 120 msg: str = "Don't pass an argument if it is the same as the default value" NoneNode = NameExpr("None") NoneNode.fullname = "builtins.None" BUILTIN_MAPPINGS = { "builtins.dict.fromkeys": (..., NoneNode), "builtins.dict.get": (..., NoneNode), "builtins.dict.setdefault": (..., NoneNode), "builtins.round": (..., IntExpr(0)), "builtins.input": (StrExpr(""),), "builtins.int": (..., IntExpr(10)), } def get_full_type_name(node: CallExpr) -> str: match node: case CallExpr(callee=NameExpr() as name): return name.fullname or "" case CallExpr( callee=MemberExpr( expr=NameExpr( node=(Var(type=Instance(type=TypeInfo() as ty)) | (TypeInfo() as ty)) ), name=name, ), ): return f"{ty.fullname}.{name}" return "" def inject_stdlib_defaults(node: CallExpr, args: list[Argument]) -> None: if defaults := BUILTIN_MAPPINGS.get(get_full_type_name(node)): for default, arg in zip(defaults, args): if default == Ellipsis: continue arg.initializer = default # type: ignore ZippedArg = tuple[str | None, Expression, ArgKind] def strip_caller_var_args(start: int, args: Iterator[ZippedArg]) -> Iterator[ZippedArg]: for i, arg in enumerate(args): if i < start: continue if arg[2] == ArgKind.ARG_NAMED: yield arg def check_func(caller: CallExpr, func: FuncDef, errors: list[Error]) -> None: args = list(zip(func.arg_names, func.arguments)) if isinstance(caller.callee, MemberExpr) and args and func.arg_names[0] in {"self", "cls"}: args.pop(0) lookup = dict(args) inject_stdlib_defaults(caller, [x[1] for x in args]) caller_args = zip(caller.arg_names, caller.args, caller.arg_kinds) for i, arg in enumerate(args): if arg[1].kind == ArgKind.ARG_STAR: caller_args = strip_caller_var_args(i, caller_args) # type: ignore temp_errors: list[Error] = [] for i, (name, value, kind) in enumerate(caller_args): if i >= len(args): break if kind == ArgKind.ARG_NAMED: try: default = lookup[name].initializer except KeyError: continue elif kind == ArgKind.ARG_POS: default = args[i][1].initializer else: return # pragma: no cover if default and is_equivalent(value, default): temp_errors.append(ErrorInfo.from_node(value)) elif kind == ArgKind.ARG_POS: # Since this arg is not a default value and cannot be deleted, # deleting previous default args would cause this arg to become # misaligned. If this was a kwarg it wouldn't be an issue because # the position would not be affected during deletion. temp_errors = [] errors.extend(temp_errors) def check_symbol(node: CallExpr, symbol: SymbolNode | None, errors: list[Error]) -> None: match symbol: case Decorator(func=FuncDef() as func) | (FuncDef() as func): check_func(node, func, errors) case OverloadedFuncDef(items=items): error_count = len(errors) for item in items: if len(errors) > error_count: break if isinstance(item, Decorator): check_func(node, item.func, errors) if symbol.impl: if isinstance(symbol.impl, FuncDef): check_func(node, symbol.impl, errors) else: check_func(node, symbol.impl.func, errors) case TypeInfo(): for func_name in ("__new__", "__init__"): if new_symbol := symbol.names.get(func_name): assert new_symbol.node check_symbol(node, new_symbol.node, errors) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr(callee=NameExpr(node=symbol)): check_symbol(node, symbol, errors) # TODO: find a way to make this look nicer case CallExpr( callee=MemberExpr( expr=( NameExpr(node=(Var(type=Instance(type=TypeInfo() as ty)) | (TypeInfo() as ty))) | CallExpr( callee=NameExpr( node=(Var(type=Instance(type=TypeInfo() as ty)) | (TypeInfo() as ty)) ) ) ), name=name, ), ) if symbol := ty.names.get( name ): # type: ignore check_symbol(node, symbol.node, errors) # type: ignore refurb-1.27.0/refurb/checks/functools/000077500000000000000000000000001454672660200176435ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/functools/__init__.py000066400000000000000000000000001454672660200217420ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/functools/use_cache.py000066400000000000000000000030561454672660200221400ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ArgKind, CallExpr, Decorator, MemberExpr, NameExpr, RefExpr from refurb.error import Error from refurb.settings import Settings @dataclass class ErrorInfo(Error): """ Python 3.9 introduces the `@cache` decorator which can be used as a short-hand for `@lru_cache(maxsize=None)`. Bad: ``` from functools import lru_cache @lru_cache(maxsize=None) def f(x: int) -> int: return x + 1 ``` Good: ``` from functools import cache @cache def f(x: int) -> int: return x + 1 ``` """ name = "use-cache" code = 134 msg: str = "Replace `@lru_cache(maxsize=None)` with `@cache`" categories = ("functools", "python39", "readability") def check(node: Decorator, errors: list[Error], settings: Settings) -> None: if settings.get_python_version() < (3, 9): return # pragma: no cover match node: case Decorator( decorators=[ CallExpr( callee=RefExpr(fullname="functools.lru_cache") as ref, arg_names=["maxsize"], arg_kinds=[ArgKind.ARG_NAMED], args=[NameExpr(fullname="builtins.None")], ) ] ): prefix = "functools." if isinstance(ref, MemberExpr) else "" old = f"@{prefix}lru_cache(maxsize=None)" new = f"@{prefix}cache" msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/hashlib/000077500000000000000000000000001454672660200172415ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/hashlib/__init__.py000066400000000000000000000000001454672660200213400ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/hashlib/simplify_ctor.py000066400000000000000000000042171454672660200225020ustar00rootroot00000000000000from dataclasses import dataclass from typing import cast from mypy.nodes import ( AssignmentStmt, Block, CallExpr, ExpressionStmt, MemberExpr, MypyFile, NameExpr, RefExpr, Statement, ) from refurb.checks.common import check_block_like, stringify from refurb.checks.hashlib.use_hexdigest import HASHLIB_ALGOS from refurb.error import Error @dataclass class ErrorInfo(Error): """ You can pass data into `hashlib` constructors, so instead of creating a hash object and immediately updating it, pass the data directly. Bad: ``` from hashlib import sha512 h = sha512() h.update(b"data) ``` Good: ``` from hashlib import sha512 h = sha512(b"data") ``` """ name = "simplify-hashlib-ctor" categories = ("hashlib", "readability") code = 182 def check(node: Block | MypyFile, errors: list[Error]) -> None: check_block_like(check_stmts, node, errors) def check_stmts(stmts: list[Statement], errors: list[Error]) -> None: assignment: AssignmentStmt | None = None var: RefExpr | None = None for stmt in stmts: match stmt: case AssignmentStmt( lvalues=[NameExpr() as lhs], rvalue=CallExpr(callee=RefExpr(fullname=fn), args=[]), ) if fn in HASHLIB_ALGOS: assignment = stmt var = lhs case ExpressionStmt( expr=CallExpr( callee=MemberExpr( expr=RefExpr(fullname=fullname, name=lhs), # type: ignore name="update", ), args=[arg], ) ) if assignment and var and var.fullname == fullname: func_name = stringify(cast(CallExpr, assignment.rvalue).callee) data = stringify(arg) old = f"{lhs} = {func_name}(); {lhs}.update({data})" new = f"{lhs} = {func_name}({data})" msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(assignment, msg)) case _: assignment = None refurb-1.27.0/refurb/checks/hashlib/use_hexdigest.py000066400000000000000000000036771454672660200224700ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, Expression, MemberExpr, NameExpr, RefExpr, Var from refurb.checks.common import stringify from refurb.error import Error @dataclass class ErrorInfo(Error): """ Use `.hexdigest()` to get a hex digest from a hash. Bad: ``` from hashlib import sha512 hashed = sha512(b"some data").digest().hex() ``` Good: ``` from hashlib import sha512 hashed = sha512(b"some data").hexdigest() ``` """ name = "use-hexdigest-hashlib" categories = ("hashlib", "readability") code = 181 HASHLIB_ALGOS = { "hashlib.md5", "hashlib.sha1", "hashlib.sha224", "hashlib.sha256", "hashlib.sha384", "hashlib.sha512", "hashlib.blake2b", "hashlib.blake2s", "hashlib.sha3_224", "hashlib.sha3_256", "hashlib.sha3_384", "hashlib.sha3_512", "hashlib.shake_128", "hashlib.shake_256", "hashlib._Hash", } def is_hashlib_algo(expr: Expression) -> bool: match expr: case CallExpr(callee=RefExpr(fullname=fn)) if fn in HASHLIB_ALGOS: return True case NameExpr(node=Var(type=ty)) if str(ty) in HASHLIB_ALGOS: return True return False def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=CallExpr( callee=MemberExpr(expr=expr, name="digest"), args=[] | [_] as digest_args, ), name="hex", ), args=[], ): if is_hashlib_algo(expr): root = stringify(expr) arg = stringify(digest_args[0]) if digest_args else "" old = f"{root}.digest({arg}).hex()" new = f"{root}.hexdigest({arg})" msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/iterable/000077500000000000000000000000001454672660200174165ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/iterable/__init__.py000066400000000000000000000000001454672660200215150ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/iterable/implicit_readlines.py000066400000000000000000000026711454672660200236360ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, Expression, ForStmt, GeneratorExpr, MemberExpr, NameExpr, Var from refurb.error import Error @dataclass class ErrorInfo(Error): """ When iterating over a file object line-by-line you don't need to add `.readlines()`, simply iterate over the object itself. This assumes you aren't passing an argument to readlines(). Bad: ``` with open("file.txt") as f: for line in f.readlines(): ... ``` Good: ``` with open("file.txt") as f: for line in f: ... ``` """ name = "simplify-readlines" code = 129 msg: str = "Replace `f.readlines()` with `f`" categories = ("builtin", "readability") def get_readline_file_object(expr: Expression) -> NameExpr | None: match expr: case CallExpr( callee=MemberExpr(expr=NameExpr(node=Var(type=ty)) as f, name="readlines"), args=[], ) if str(ty) in {"io.TextIOWrapper", "io.BufferedReader"}: return f return None def check(node: ForStmt | GeneratorExpr, errors: list[Error]) -> None: if isinstance(node, ForStmt): if f := get_readline_file_object(node.expr): errors.append(ErrorInfo.from_node(f)) else: errors.extend( ErrorInfo.from_node(f) for expr in node.sequences if (f := get_readline_file_object(expr)) ) refurb-1.27.0/refurb/checks/iterable/in_tuple.py000066400000000000000000000030321454672660200216050ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ComparisonExpr, ForStmt, GeneratorExpr, ListExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Since tuple, list, and set literals can be used with the `in` operator, it is best to pick one and stick with it. Bad: ``` for x in (1, 2, 3): pass nums = [str(x) for x in [1, 2, 3]] ``` Good: ``` for x in (1, 2, 3): pass nums = [str(x) for x in (1, 2, 3)] ``` """ # Currently this check is hard-coded for tuples, but once we have the # ability to pass parameters into checks this check will be able to work # with a variety of bracket types. name = "use-consistent-in-bracket" code = 109 categories = ("iterable", "readability") def error_msg(oper: str) -> str: return f"Replace `{oper} [x, y, z]` with `{oper} (x, y, z)`" def check(node: ComparisonExpr | ForStmt | GeneratorExpr, errors: list[Error]) -> None: match node: case ComparisonExpr( operators=["in" | "not in" as oper], operands=[_, ListExpr() as expr], ): errors.append(ErrorInfo.from_node(expr, error_msg(oper))) case ForStmt(expr=ListExpr() as expr): errors.append(ErrorInfo.from_node(expr, error_msg("in"))) case GeneratorExpr(): errors.extend( ErrorInfo.from_node(expr, error_msg("in")) for expr in node.sequences if isinstance(expr, ListExpr) ) refurb-1.27.0/refurb/checks/iterable/no_single_item_in.py000066400000000000000000000020161454672660200234500ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ComparisonExpr, ListExpr, TupleExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use `in` to check against a single value, use `==` instead: Bad: ``` if name in ("bob",): pass ``` Good: ``` if name == "bob": pass ``` """ name = "no-single-item-in" code = 171 categories = ("iterable", "readability") def check(node: ComparisonExpr, errors: list[Error]) -> None: match node: case ComparisonExpr( operators=["in" | "not in" as oper], operands=[_, ListExpr() | TupleExpr() as expr], ) if len(expr.items) == 1: new_oper = "==" if oper == "in" else "!=" if isinstance(expr, ListExpr): msg = f"Replace `x {oper} [y]` with `x {new_oper} y`" else: msg = f"Replace `x {oper} (y,)` with `x {new_oper} y`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/itertools/000077500000000000000000000000001454672660200176535ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/itertools/__init__.py000066400000000000000000000000001454672660200217520ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/itertools/use_chain_from_iterable.py000066400000000000000000000076211454672660200250630ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( ArgKind, CallExpr, GeneratorExpr, ListComprehension, ListExpr, NameExpr, RefExpr, SetComprehension, ) from refurb.checks.common import stringify from refurb.error import Error @dataclass class ErrorInfo(Error): """ When flattening a list of lists, use the `chain.from_iterable()` function from the `itertools` stdlib package. This function is faster than native list/generator comprehensions or using `sum()` with a list default. Bad: ``` from itertools import chain rows = [[1, 2], [3, 4]] # using list comprehension flat = [col for row in rows for col in row] # using sum() flat = sum(rows, []) # using chain(*x) flat = chain(*rows) ``` Good: ``` from itertools import chain rows = [[1, 2], [3, 4]] flat = chain.from_iterable(rows) ``` Note: `chain.from_iterable()` returns an iterator, which means you might need to wrap it in `list()` depending on your use case. Refurb cannot detect this (yet), so this is something you will need to keep in mind. Note: `chain(*x)` may be marginally faster/slower depending on the length of `x`. Since `*` might potentially expand to a lot of arguments, it is better to use `chain.from_iterable()` when you are unsure. """ name = "use-chain-from-iterable" categories = ("itertools", "performance", "readability") code = 179 def is_flatten_generator(node: GeneratorExpr) -> bool: match node: case GeneratorExpr( left_expr=RefExpr(fullname=expr), sequences=[_, RefExpr(fullname=inner_source)], indices=[RefExpr(fullname=outer), RefExpr(fullname=inner)], is_async=[False, False], condlists=[[], []], ) if expr == inner and inner_source == outer: return True return False # List of nodes we have already emitted errors for, since list comprehensions # and their inner generators will emit 2 errors. ignore = set[int]() def check( node: ListComprehension | SetComprehension | GeneratorExpr | CallExpr, errors: list[Error], ) -> None: if id(node) in ignore: return match node: case ListComprehension(generator=g) if is_flatten_generator(g): old = "[... for ... in x for ... in ...]" new = "list(chain.from_iterable(x))" ignore.add(id(g)) case SetComprehension(generator=g) if is_flatten_generator(g): old = "{... for ... in x for ... in ...}" new = "set(chain.from_iterable(x))" ignore.add(id(g)) case GeneratorExpr() if is_flatten_generator(node): old = "... for ... in x for ... in ..." new = "chain.from_iterable(x)" case CallExpr( callee=RefExpr(fullname="builtins.sum"), args=[arg, ListExpr(items=[])], ): old = f"sum({stringify(arg)}, [])" new = f"chain.from_iterable({stringify(arg)})" case CallExpr( callee=RefExpr(fullname="functools.reduce"), args=[op, arg] | [op, arg, ListExpr(items=[])], ): match op: case RefExpr(fullname="_operator.add" | "_operator.concat"): pass case _: return old = stringify(node) new = f"chain.from_iterable({stringify(arg)})" case CallExpr( callee=RefExpr(fullname="itertools.chain") as callee, args=[arg], arg_kinds=[ArgKind.ARG_STAR], ): chain = "chain" if isinstance(callee, NameExpr) else "itertools.chain" old = f"{chain}(*{stringify(arg)})" new = f"{chain}.from_iterable({stringify(arg)})" case _: return msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/itertools/use_starmap.py000066400000000000000000000056111454672660200225530ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( ArgKind, CallExpr, GeneratorExpr, ListComprehension, NameExpr, SetComprehension, TupleExpr, ) from refurb.error import Error @dataclass class ErrorInfo(Error): """ If you only want to iterate and unpack values so that you can pass them to a function (in the same order and with no modifications), you should use the more performant `starmap` function: Bad: ``` scores = [85, 100, 60] passing_scores = [60, 80, 70] def passed_test(score: int, passing_score: int) -> bool: return score >= passing_score passed_all_tests = all( passed_test(score, passing_score) for score, passing_score in zip(scores, passing_scores) ) ``` Good: ``` from itertools import starmap scores = [85, 100, 60] passing_scores = [60, 80, 70] def passed_test(score: int, passing_score: int) -> bool: return score >= passing_score passed_all_tests = all(starmap(passed_test, zip(scores, passing_scores))) ``` """ name = "use-starmap" code = 140 msg: str = "Replace `f(...) for ... in x` with `starmap(f, x)`" categories = ("itertools", "performance") ignore = set[int]() def check_generator( node: GeneratorExpr, errors: list[Error], old_wrapper: str = "{}", new_wrapper: str = "{}", ) -> None: match node: case GeneratorExpr( left_expr=CallExpr(args=args, arg_kinds=arg_kinds), indices=[TupleExpr(items=names)], ) if ( names and len(names) == len(args) and all(kind == ArgKind.ARG_POS for kind in arg_kinds) ): for lhs, rhs in zip(args, names): if not ( isinstance(lhs, NameExpr) and isinstance(rhs, NameExpr) and lhs.name == rhs.name ): return ignore.add(id(node)) old = "f(...) for ... in x" old = old_wrapper.format(old) new = "starmap(f, x)" new = new_wrapper.format(new) msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(node, msg)) def check( node: GeneratorExpr | ListComprehension | SetComprehension, errors: list[Error], ) -> None: if id(node) in ignore: return match node: case GeneratorExpr(): check_generator(node, errors) case ListComprehension(generator=g): check_generator( g, errors, old_wrapper="[{}]", new_wrapper="list({})", ) case SetComprehension(generator=g): check_generator( g, errors, old_wrapper="{{{}}}", new_wrapper="set({})", ) refurb-1.27.0/refurb/checks/logical/000077500000000000000000000000001454672660200172415ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/logical/__init__.py000066400000000000000000000000001454672660200213400ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/logical/use_equal_chain.py000066400000000000000000000024771454672660200227520ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import OpExpr from refurb.checks.common import get_common_expr_in_comparison_chain from refurb.error import Error @dataclass class ErrorInfo(Error): """ When checking that multiple objects are equal to each other, don't use an `and` expression. Use a comparison chain instead, for example: Bad: ``` if x == y and x == z: pass # and if x is None and y is None pass ``` Good: ``` if x == y == z: pass # and if x is y is None: pass ``` Note: if `x` depends on side-effects, then this check should be ignored. """ name = "use-comparison-chain" code = 124 categories = ("logical", "readability") def create_message(indices: tuple[int, int], oper: str = "==") -> str: names = ["x", "y", "z"] names.insert(indices[1], names[indices[0]]) expr = f"{names[0]} {oper} {names[1]} and {names[2]} {oper} {names[3]}" return f"Replace `{expr}` with `x {oper} y {oper} z`" def check(node: OpExpr, errors: list[Error]) -> None: for cmp_oper in ("==", "is"): if data := get_common_expr_in_comparison_chain(node, "and", cmp_oper): expr, indices = data errors.append(ErrorInfo.from_node(expr, create_message(indices, cmp_oper))) refurb-1.27.0/refurb/checks/logical/use_in.py000066400000000000000000000025671454672660200211070ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import OpExpr from refurb.checks.common import get_common_expr_in_comparison_chain from refurb.error import Error @dataclass class ErrorInfo(Error): """ When comparing a value to multiple possible options, don't `or` multiple comparison checks, use a single `in` expr: Bad: ``` if x == "abc" or x == "def": pass ``` Good: ``` if x in ("abc", "def"): pass ``` Note: This should not be used if the operands depend on boolean short circuiting, since the operands will be eagerly evaluated. This is primarily useful for comparing against a range of constant values. """ name = "use-in-oper" code = 108 categories = ("logical", "readability") def create_message(indices: tuple[int, int]) -> str: names = ["x", "y", "z"] common_name = names[indices[0]] names.insert(indices[1], common_name) old = f"{names[0]} == {names[1]} or {names[2]} == {names[3]}" names = [name for name in names if name != common_name] new = f"{common_name} in ({', '.join(names)})" return f"Replace `{old}` with `{new}`" def check(node: OpExpr, errors: list[Error]) -> None: if data := get_common_expr_in_comparison_chain(node, oper="or"): expr, indices = data errors.append(ErrorInfo.from_node(expr, create_message(indices))) refurb-1.27.0/refurb/checks/logical/use_or.py000066400000000000000000000014271454672660200211130ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ConditionalExpr from refurb.checks.common import is_equivalent from refurb.error import Error @dataclass class ErrorInfo(Error): """ Sometimes the ternary operator (aka, inline if statements) can be simplified to a single `or` expression. Bad: ``` z = x if x else y ``` Good: ``` z = x or y ``` Note: if `x` depends on side-effects, then this check should be ignored. """ name = "use-or-oper" code = 110 msg: str = "Replace `x if x else y` with `x or y`" categories = ("logical", "readability") def check(node: ConditionalExpr, errors: list[Error]) -> None: if is_equivalent(node.if_expr, node.cond): errors.append(ErrorInfo.from_node(node)) refurb-1.27.0/refurb/checks/math/000077500000000000000000000000001454672660200165605ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/math/__init__.py000066400000000000000000000000001454672660200206570ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/math/simplify_log.py000066400000000000000000000023671454672660200216370ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, FloatExpr, IntExpr, RefExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Use the shorthand `log2` and `log10` functions instead of passing 2 or 10 as the second argument to the `log` function. If `math.e` is used as the second argument, just use `math.log(x)` instead, since `e` is the default. Bad: ``` power = math.log(x, 10) ``` Good: ``` power = math.log10(x) ``` """ name = "simplify-math-log" code = 163 categories = ("math", "readability") def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=RefExpr(fullname="math.log"), args=[_, arg], ): match arg: case IntExpr(value=2 | 10) | FloatExpr(value=2.0 | 10.0): base = str(arg.value) new = f"math.log{int(arg.value)}(x)" case RefExpr(fullname="math.e"): base = "math.e" new = "math.log(x)" case _: return errors.append(ErrorInfo.from_node(node, f"Replace `math.log(x, {base})` with `{new}`")) refurb-1.27.0/refurb/checks/math/use_constants.py000066400000000000000000000016411454672660200220240ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import FloatExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't hardcode math constants like pi, tau, or e, use the `math.pi`, `math.tau`, or `math.e` constants respectively. Bad: ``` def area(r: float) -> float: return 3.1415 * r * r ``` Good: ``` import math def area(r: float) -> float: return math.pi * r * r ``` """ name = "use-math-constant" code = 152 categories = ("math", "readability") CONSTANTS = { "pi": "3.14", "e": "2.71", "tau": "6.28", } def check(node: FloatExpr, errors: list[Error]) -> None: num = str(node.value) if len(num) <= 3: return for name, value in CONSTANTS.items(): if num.startswith(value): errors.append(ErrorInfo.from_node(node, f"Replace `{num}` with `math.{name}`")) refurb-1.27.0/refurb/checks/pathlib/000077500000000000000000000000001454672660200172525ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/pathlib/__init__.py000066400000000000000000000000001454672660200213510ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/pathlib/cwd.py000066400000000000000000000013101454672660200203740ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, RefExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ A modern alternative to `os.getcwd()` is the `Path.cwd()` method: Bad: ``` cwd = os.getcwd() ``` Good: ``` cwd = Path.cwd() ``` """ name = "use-pathlib-cwd" code = 104 categories = ("pathlib",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr(callee=RefExpr(fullname=fullname)) if fullname in { "os.getcwd", "os.getcwdb", }: errors.append(ErrorInfo.from_node(node, f"Replace `{fullname}()` with `Path.cwd()`")) refurb-1.27.0/refurb/checks/pathlib/exists.py000066400000000000000000000021131454672660200211400ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, RefExpr from refurb.checks.common import normalize_os_path from refurb.checks.pathlib.util import is_pathlike from refurb.error import Error @dataclass class ErrorInfo(Error): """ When checking whether a file exists or not, try and use the more modern `pathlib` module instead of `os.path`. Bad: ``` import os if os.path.exists("filename"): pass ``` Good: ``` from pathlib import Path if Path("filename").exists(): pass ``` """ name = "use-pathlib-exists" code = 141 categories = ("pathlib",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=RefExpr() as expr, args=[arg], ) if normalize_os_path(expr.fullname or "") == "os.path.exists": replace = "x.exists()" if is_pathlike(arg) else "Path(x).exists()" errors.append( ErrorInfo.from_node(node, f"Replace `os.path.exists(x)` with `{replace}`") ) refurb-1.27.0/refurb/checks/pathlib/getsize.py000066400000000000000000000034261454672660200213030ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import BytesExpr, CallExpr, NameExpr, RefExpr, StrExpr, Var from refurb.checks.common import normalize_os_path from refurb.checks.pathlib.util import is_pathlike from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use the `os.path.getsize` (or similar) functions, use the more modern `pathlib` module instead: Bad: ``` if os.path.getsize("file.txt"): pass ``` Good: ``` if Path("file.txt").stat().st_size: pass ``` """ name = "use-pathlib-stat" code = 155 categories = ("pathlib",) PATH_TO_PATHLIB_NAMES = { "os.stat": "stat()", "os.path.getsize": "stat().st_size", "os.path.getatime": "stat().st_atime", "os.path.getmtime": "stat().st_mtime", "os.path.getctime": "stat().st_ctime", } def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr(callee=RefExpr(fullname=fullname), args=[arg]): normalized_name = normalize_os_path(fullname) new_name = PATH_TO_PATHLIB_NAMES.get(normalized_name) if not new_name: return if is_pathlike(arg): replace = f"x.{new_name}" else: match arg: case BytesExpr() | StrExpr(): pass case NameExpr(node=Var(type=ty)) if ( str(ty) in {"builtins.str", "builtins.bytes"} ): pass case _: return replace = f"Path(x).{new_name}" errors.append( ErrorInfo.from_node(node, f"Replace `{normalized_name}(x)` with `{replace}`") ) refurb-1.27.0/refurb/checks/pathlib/is_file.py000066400000000000000000000033341454672660200212410ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import BytesExpr, CallExpr, NameExpr, RefExpr, StrExpr, Var from refurb.checks.common import normalize_os_path from refurb.checks.pathlib.util import is_pathlike from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use the `os.path.isfile` (or similar) functions, use the more modern `pathlib` module instead: Bad: ``` if os.path.isfile("file.txt"): pass ``` Good: ``` if Path("file.txt").is_file(): pass ``` """ name = "use-pathlib-is-funcs" code = 146 categories = ("pathlib",) PATH_TO_PATHLIB_NAMES = { "os.path.isabs": "is_absolute", "os.path.isdir": "is_dir", "os.path.isfile": "is_file", "os.path.islink": "is_symlink", } def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr(callee=RefExpr(fullname=fullname), args=[arg]): normalized_name = normalize_os_path(fullname) new_name = PATH_TO_PATHLIB_NAMES.get(normalized_name) if not new_name: return if is_pathlike(arg): replace = f"x.{new_name}()" else: match arg: case BytesExpr() | StrExpr(): pass case NameExpr(node=Var(type=ty)) if ( str(ty) in {"builtins.str", "builtins.bytes"} ): pass case _: return replace = f"Path(x).{new_name}()" errors.append( ErrorInfo.from_node(node, f"Replace `{normalized_name}(x)` with `{replace}`") ) refurb-1.27.0/refurb/checks/pathlib/mkdir.py000066400000000000000000000032521454672660200207340ustar00rootroot00000000000000from dataclasses import dataclass from typing import cast from mypy.nodes import CallExpr, RefExpr from refurb.error import Error from .util import is_pathlike @dataclass class ErrorInfo(Error): """ Use the `mkdir` method from the pathlib library instead of using the `mkdir` and `makedirs` functions from the `os` library: the pathlib library is more modern and provides better flexibility over the construction and manipulation of file paths. Bad: ``` import os os.mkdir("new_folder") ``` Good: ``` from pathlib import Path Path("new_folder").mkdir() ``` """ name = "use-pathlib-mkdir" code = 150 categories = ("pathlib",) def create_error(node: CallExpr) -> list[Error]: old_args = ["x"] new_args = [] fullname = cast(RefExpr, node.callee).fullname is_makedirs = fullname == "os.makedirs" allowed_names = [None, "mode"] if is_makedirs: allowed_names.append("exist_ok") if len(node.args) > 1: if any(name not in allowed_names for name in node.arg_names): return [] old_args.append("...") new_args.append("...") if is_makedirs: new_args.append("parents=True") new_args = ", ".join(new_args) expr = f"x.mkdir({new_args})" if is_pathlike(node.args[0]) else f"Path(x).mkdir({new_args})" return [ ErrorInfo.from_node(node, f"Replace `{fullname}({', '.join(old_args)})` with `{expr}`") ] def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr(callee=RefExpr(fullname="os.mkdir" | "os.makedirs"), args=args) if args: errors.extend(create_error(node)) refurb-1.27.0/refurb/checks/pathlib/no_cwd_resolve.py000066400000000000000000000020671454672660200226410ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, RefExpr, StrExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ If you want to get the current working directory don't call `resolve()` on an empty `Path()` object, use `Path.cwd()` instead. Bad: ``` cwd = Path().resolve() ``` Good: ``` cwd = Path.cwd() ``` """ name = "no-implicit-cwd" code = 177 categories = ("pathlib",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=CallExpr( callee=RefExpr(fullname="pathlib.Path"), args=[] | [StrExpr(value="" | ".")] as args, ), name="resolve", ), args=[], ): arg = f'"{args[0].value}"' if args else "" # type: ignore msg = f"Replace `Path({arg}).resolve()` with `Path.cwd()`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/pathlib/no_join.py000066400000000000000000000045771454672660200212740ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import BytesExpr, CallExpr, RefExpr, StrExpr from refurb.checks.common import normalize_os_path from refurb.error import Error @dataclass class ErrorInfo(Error): """ When joining strings to make a filepath, use the more modern and flexible `Path()` object instead of `os.path.join`: Bad: ``` with open(os.path.join("folder", "file"), "w") as f: f.write("hello world!") ``` Good: ``` from pathlib import Path with open(Path("folder", "file"), "w") as f: f.write("hello world!") # even better ... with Path("folder", "file").open("w") as f: f.write("hello world!") # even better ... Path("folder", "file").write_text("hello world!") ``` Note that this check is disabled by default because `Path()` returns a Path object, not a string, meaning that the Path object will propagate through your code. This might be what you want, and might encourage you to use the pathlib module in more places, but since it is not a drop-in replacement it is disabled by default. """ name = "no-path-join" enabled = False code = 147 categories = ("pathlib",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=RefExpr(fullname=fullname), args=args, ) if args and normalize_os_path(fullname) == "os.path.join": trailing_dot_dot_args: list[str] = [] for arg in reversed(args): if isinstance(arg, StrExpr | BytesExpr) and arg.value == "..": trailing_dot_dot_args.append('".."' if isinstance(arg, StrExpr) else 'b".."') else: break normal_arg_count = len(args) - len(trailing_dot_dot_args) if normal_arg_count <= 3: placeholders = ["x", "y", "z"][:normal_arg_count] join_args = ", ".join(placeholders + trailing_dot_dot_args) path_args = ", ".join(placeholders) parents = ".parent" * len(trailing_dot_dot_args) new = f"Path({path_args}){parents}" else: join_args = "..." new = "Path(...)" errors.append( ErrorInfo.from_node(node, f"Replace `os.path.join({join_args})` with `{new}`") ) refurb-1.27.0/refurb/checks/pathlib/open.py000066400000000000000000000026751454672660200205770ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, NameExpr, StrExpr from refurb.checks.pathlib.util import is_pathlike from refurb.error import Error @dataclass class ErrorInfo(Error): """ When you want to open a Path object, don't pass it to `open()`, just call `.open()` on the Path object itself: Bad: ``` path = Path("filename") with open(path) as f: pass ``` Good: ``` path = Path("filename") with path.open() as f: pass ``` """ name = "use-pathlib-open" code = 117 categories = ("pathlib",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=NameExpr(fullname="builtins.open") as open_node, args=[ CallExpr( callee=NameExpr(fullname="builtins.str"), args=[arg], ) | arg, *rest, ], ) if is_pathlike(arg): mode = args = "" match rest: case [StrExpr(value=value), *_]: mode = f'"{value}"' args = f", {mode}" expr = "x" if arg == node.args[0] else "str(x)" errors.append( ErrorInfo.from_node( open_node, f"Replace `open({expr}{args})` with `x.open({mode})`", ) ) refurb-1.27.0/refurb/checks/pathlib/read_text.py000066400000000000000000000042341454672660200216060ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import AssignmentStmt, Block, CallExpr, MemberExpr, NameExpr, StrExpr, WithStmt from refurb.error import Error @dataclass class ErrorInfo(Error): """ When you just want to save the contents of a file to a variable, using a `with` block is a bit overkill. A simpler alternative is to use pathlib's `read_text()` function: Bad: ``` with open(filename) as f: contents = f.read() ``` Good: ``` contents = Path(filename).read_text() ``` """ name = "use-pathlib-read-text-read-bytes" code = 101 categories = ("pathlib",) def check(node: WithStmt, errors: list[Error]) -> None: match node: case WithStmt( expr=[ CallExpr( callee=NameExpr(name="open"), args=args, arg_names=arg_names, ) ], target=[NameExpr(name=with_name)], body=Block( body=[ AssignmentStmt( rvalue=CallExpr( callee=MemberExpr(expr=NameExpr(name=read_name), name="read"), args=[], ) ) ] ), ) if with_name == read_name: func = "read_text" read_text_params = "" with_params = "" for i, name in enumerate(arg_names[1:], start=1): if name in {None, "mode"}: with_params = ", ..." match args[i]: case StrExpr(value=mode) if "b" in mode: func = "read_bytes" elif name in {"encoding", "errors"}: read_text_params = "..." with_params = ", ..." else: return errors.append( ErrorInfo.from_node( node, f"Replace `with open(x{with_params}) as f: y = f.read()` with `y = Path(x).{func}({read_text_params})`", # noqa: E501 ) ) refurb-1.27.0/refurb/checks/pathlib/simplify_ctor.py000066400000000000000000000027501454672660200225130ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, RefExpr, StrExpr from refurb.checks.common import normalize_os_path, stringify from refurb.error import Error @dataclass class ErrorInfo(Error): """ The Path() constructor defaults to the current directory, so don't pass the current directory explicitly. Bad: ``` file = Path(".") ``` Good: ``` file = Path() ``` Note: Lots of different values can trigger this check, including `"."`, `""`, `os.curdir`, and `os.path.curdir`. """ name = "simplify-path-constructor" code = 153 categories = ("pathlib", "readability") def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( args=[StrExpr(value="." | "" as value)], callee=RefExpr(fullname="pathlib.Path") as ref, ): func_name = stringify(ref) errors.append( ErrorInfo.from_node(node, f'Replace `{func_name}("{value}")` with `Path()`') ) match node: case CallExpr( args=[RefExpr(fullname=arg) as arg_ref], callee=RefExpr(fullname="pathlib.Path") as func_ref, ) if ((arg := normalize_os_path(arg)) in {"os.curdir", "os.path.curdir"}): func_name = stringify(func_ref) arg_name = stringify(arg_ref) msg = f"Replace `{func_name}({arg_name})` with `Path()`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/pathlib/touch.py000066400000000000000000000030721454672660200207500ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, NameExpr, StrExpr from refurb.error import Error from .util import is_pathlike @dataclass class ErrorInfo(Error): """ Don't use `open(x, "w").close()` if you just want to create an empty file, use the less confusing `Path.touch()` method instead. Bad: ``` open("file.txt", "w").close() ``` Good: ``` from pathlib import Path Path("file.txt").touch() ``` This check is disabled by default because `touch()` will throw a `FileExistsError` if the file already exists, and (at least on Linux) it sets different file permissions, meaning it is not a drop-in replacement. If you don't care about the file permissions or know that the file doesn't exist beforehand this check may be for you. """ name = "use-pathlib-touch" enabled = False code = 151 categories = ("pathlib",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=CallExpr( callee=NameExpr(fullname="builtins.open"), args=[arg, StrExpr(value=mode)], arg_names=[_, None | "mode"], ), name="close", ), args=[], ) if "w" in mode: new = "x.touch()" if is_pathlike(arg) else "Path(x).touch()" errors.append( ErrorInfo.from_node(node, f'Replace `open(x, "{mode}").close()` with `{new}`') ) refurb-1.27.0/refurb/checks/pathlib/unlink.py000066400000000000000000000022201454672660200211200ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, NameExpr from refurb.checks.pathlib.util import is_pathlike from refurb.error import Error @dataclass class ErrorInfo(Error): """ When removing a file, use the more modern `Path.unlink()` method instead of `os.remove()` or `os.unlink()`: The `pathlib` module allows for more flexibility when it comes to traversing folders, building file paths, and accessing/modifying files. Bad: ``` import os os.remove("filename") ``` Good: ``` from pathlib import Path Path("filename").unlink() ``` """ name = "use-pathlib-unlink" code = 144 categories = ("pathlib",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=(MemberExpr() | NameExpr()) as expr, args=[arg], ) if expr.fullname in {"os.remove", "os.unlink"}: replace = "x.unlink()" if is_pathlike(arg) else "Path(x).unlink()" errors.append( ErrorInfo.from_node(node, f"Replace `os.{expr.name}(x)` with `{replace}`") ) refurb-1.27.0/refurb/checks/pathlib/use_suffix.py000066400000000000000000000031641454672660200220100ustar00rootroot00000000000000import re from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, StrExpr from refurb.checks.pathlib.util import is_pathlike from refurb.error import Error @dataclass class ErrorInfo(Error): """ When checking the file extension for a Path object don't call `endswith()` on the `name` field, directly check against `suffix` instead. Bad: ``` from pathlib import Path def is_markdown_file(file: Path) -> bool: return file.name.endswith(".md") ``` Good: ``` from pathlib import Path def is_markdown_file(file: Path) -> bool: return file.suffix == ".md" ``` Note: The `suffix` field will only contain the last file extension, so don't use `suffix` if you are checking for an extension like `.tar.gz`. Refurb won't warn in those cases, but it is good to remember in case you plan to use this in other places. """ enabled = False name = "use-suffix" code = 172 categories = ("pathlib",) FILE_EXTENSION = re.compile(r"^\.[a-zA-Z0-9_-]+$") def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=MemberExpr( expr=file, name="name", ), name="endswith", ), args=[StrExpr(value=suffix)], ) if FILE_EXTENSION.match(suffix) and is_pathlike(file): old = f'x.name.endswith("{suffix}")' new = f'x.suffix == "{suffix}"' errors.append(ErrorInfo.from_node(node, f"Replace `{old}` with `{new}`")) refurb-1.27.0/refurb/checks/pathlib/util.py000066400000000000000000000010261454672660200206000ustar00rootroot00000000000000from mypy.nodes import CallExpr, Expression, NameExpr, OpExpr, RefExpr, Var def is_pathlike(expr: Expression) -> bool: # TODO: just check that the expression is of type `Path` once we actually # get proper type checking match expr: case CallExpr(callee=RefExpr(fullname="pathlib.Path")): return True case NameExpr(node=Var(type=ty)) if str(ty) == "pathlib.Path": return True case OpExpr(left=left, op="/") if is_pathlike(left): return True return False refurb-1.27.0/refurb/checks/pathlib/with_suffix.py000066400000000000000000000023161454672660200221650ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, IndexExpr, NameExpr, OpExpr, SliceExpr, StrExpr from refurb.error import Error from .util import is_pathlike @dataclass class ErrorInfo(Error): """ A common operation is changing the extension of a file. If you have an existing `Path` object, you don't need to convert it to a string, slice it, and append a new extension. Instead, use the `with_suffix()` method: Bad: ``` new_filepath = str(Path("file.txt"))[:4] + ".md" ``` Good: ``` new_filepath = Path("file.txt").with_suffix(".md") ``` """ name = "use-pathlib-with-suffix" code = 100 msg: str = "Use `Path(x).with_suffix(y)` instead of slice and concat" categories = ("pathlib",) def check(node: OpExpr, errors: list[Error]) -> None: match node: case OpExpr( op="+", left=IndexExpr( base=CallExpr( callee=NameExpr(name="str"), args=[arg], ), index=SliceExpr(begin_index=None), ), right=StrExpr(), ) if is_pathlike(arg): errors.append(ErrorInfo.from_node(arg)) refurb-1.27.0/refurb/checks/pathlib/write_text.py000066400000000000000000000027471454672660200220340ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import Block, CallExpr, ExpressionStmt, MemberExpr, NameExpr, StrExpr, WithStmt from refurb.error import Error @dataclass class ErrorInfo(Error): """ When you just want to save some contents to a file, using a `with` block is a bit overkill. Instead you can use pathlib's `write_text()` method: Bad: ``` with open(filename, "w") as f: f.write("hello world") ``` Good: ``` Path(filename).write_text("hello world") ``` """ name = "use-pathlib-write-text-write-bytes" code = 103 categories = ("pathlib",) def check(node: WithStmt, errors: list[Error]) -> None: match node: case WithStmt( expr=[CallExpr(callee=NameExpr(name="open"), args=[_, StrExpr(value=mode)])], target=[NameExpr(name=with_name)], body=Block( body=[ ExpressionStmt( expr=CallExpr( callee=MemberExpr(expr=NameExpr(name=write_name), name="write") ) ) ] ), ) if with_name == write_name and "w" in mode: func = "write_bytes" if ("b" in mode) else "write_text" errors.append( ErrorInfo.from_node( node, f"Replace `with open(x, ...) as f: f.write(y)` with `Path(x).{func}(y)`", # noqa: E501 ) ) refurb-1.27.0/refurb/checks/pattern_matching/000077500000000000000000000000001454672660200211565ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/pattern_matching/__init__.py000066400000000000000000000000001454672660200232550ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/pattern_matching/simplify_as_builtin.py000066400000000000000000000026601454672660200256010ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import NameExpr from mypy.patterns import AsPattern, ClassPattern from refurb.error import Error @dataclass class ErrorInfo(Error): """ When pattern matching builtin classes such as `int()` and `str()`, don't use an `as` pattern to bind to the value, since the most common builtin classes can use positional patterns instead. Bad: ``` match x: case str() as name: print(f"Hello {name}") ``` Good: ``` match x: case str(name): print(f"Hello {name}") ``` """ name = "simplify-as-pattern-with-builtin" code = 158 categories = ("pattern-matching", "readability") BUILTIN_PATTERN_CLASSES = ( "builtins.bool", "builtins.bytearray", "builtins.bytes", "builtins.dict", "builtins.float", "builtins.frozenset", "builtins.int", "builtins.list", "builtins.set", "builtins.str", "builtins.tuple", ) def check(node: AsPattern, errors: list[Error]) -> None: match node: case AsPattern( pattern=ClassPattern( class_ref=NameExpr(name=name, fullname=fullname), positionals=[], keyword_keys=[], keyword_values=[], ) ) if fullname in BUILTIN_PATTERN_CLASSES: errors.append(ErrorInfo.from_node(node, f"Replace `{name}() as x` with `{name}(x)`")) refurb-1.27.0/refurb/checks/readability/000077500000000000000000000000001454672660200201205ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/readability/__init__.py000066400000000000000000000000001454672660200222170ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/readability/fluid_interface.py000066400000000000000000000116011454672660200236140ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( AssignmentStmt, CallExpr, Expression, FuncDef, MemberExpr, NameExpr, ReturnStmt, Statement, ) from refurb.checks.common import ReadCountVisitor, check_block_like from refurb.error import Error from refurb.visitor import TraverserVisitor @dataclass class ErrorInfo(Error): r""" When an API has a Fluent Interface (the ability to chain multiple calls together), you should chain those calls instead of repeatedly assigning and using the value. Sometimes a return statement can be written more succinctly: Bad: ```python def get_tensors(device: str) -> torch.Tensor: t1 = torch.ones(2, 1) t2 = t1.long() t3 = t2.to(device) return t3 def process(file_name: str): common_columns = ["col1_renamed", "col2_renamed", "custom_col"] df = spark.read.parquet(file_name) df = df \ .withColumnRenamed('col1', 'col1_renamed') \ .withColumnRenamed('col2', 'col2_renamed') df = df \ .select(common_columns) \ .withColumn('service_type', F.lit('green')) return df ``` Good: ```python def get_tensors(device: str) -> torch.Tensor: t3 = ( torch.ones(2, 1) .long() .to(device) ) return t3 def process(file_name: str): common_columns = ["col1_renamed", "col2_renamed", "custom_col"] df = ( spark.read.parquet(file_name) .withColumnRenamed('col1', 'col1_renamed') .withColumnRenamed('col2', 'col2_renamed') .select(common_columns) .withColumn('service_type', F.lit('green')) ) return df ``` """ name = "use-fluid-interface" code = 184 categories = ("readability",) def check(node: FuncDef, errors: list[Error]) -> None: check_block_like(check_stmts, node.body, errors) def check_call(node: Expression, name: str | None = None) -> bool: match node: # Single chain case CallExpr(callee=MemberExpr(expr=NameExpr(name=x), name=_)): if name is None or name == x: # Exclude other references x_expr = NameExpr(x) x_expr.fullname = x visitor = ReadCountVisitor(x_expr) visitor.accept(node) return visitor.read_count == 1 return False # Nested case CallExpr(callee=MemberExpr(expr=call_node, name=_)): return check_call(call_node, name=name) return False class NameReferenceVisitor(TraverserVisitor): name: NameExpr referenced: bool def __init__(self, name: NameExpr, stmt: Statement | None = None) -> None: super().__init__() self.name = name self.stmt = stmt self.referenced = False def visit_name_expr(self, node: NameExpr) -> None: if not self.referenced and node.fullname == self.name.fullname: self.referenced = True def check_stmts(stmts: list[Statement], errors: list[Error]) -> None: last = "" visitors: list[NameReferenceVisitor] = [] for stmt in stmts: for visitor in visitors: visitor.accept(stmt) # No need to track referenced variables anymore visitors = [visitor for visitor in visitors if not visitor.referenced] match stmt: case AssignmentStmt(lvalues=[NameExpr(name=name)], rvalue=rvalue): if last and check_call(rvalue, name=last): if f"{last}'" == name: errors.append( ErrorInfo.from_node( stmt, "Assignment statement should be chained", ) ) else: # We need to ensure that the variable is not referenced somewhere else name_expr = NameExpr(name=last) name_expr.fullname = last visitors.append(NameReferenceVisitor(name_expr, stmt)) last = name if name != "_" else "" case ReturnStmt(expr=rvalue): if last and rvalue is not None and check_call(rvalue, name=last): errors.append( ErrorInfo.from_node( stmt, "Return statement should be chained", ) ) case _: last = "" # Ensure that variables are not referenced errors.extend( [ ErrorInfo.from_node( visitor.stmt, "Assignment statement should be chained", ) for visitor in visitors if not visitor.referenced and visitor.stmt is not None ] ) refurb-1.27.0/refurb/checks/readability/in_keys.py000066400000000000000000000022321454672660200221320ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, ComparisonExpr, MemberExpr, NameExpr, Var from refurb.error import Error @dataclass class ErrorInfo(Error): """ If you only want to check if a key exists in a dictionary, you don't need to call `.keys()` first, just use `in` on the dictionary itself: Bad: ``` d = {"key": "value"} if "key" in d.keys(): ... ``` Good: ``` d = {"key": "value"} if "key" in d: ... ``` """ name = "no-in-dict-keys" code = 130 categories = ("dict", "readability") def check(node: ComparisonExpr, errors: list[Error]) -> None: match node: case ComparisonExpr( operators=["in" | "not in" as oper], operands=[ _, CallExpr( callee=MemberExpr( expr=NameExpr(node=Var(type=ty)), name="keys", ), ) as expr, ], ) if str(ty).startswith("builtins.dict"): errors.append(ErrorInfo.from_node(expr, f"Replace `{oper} d.keys()` with `{oper} d`")) refurb-1.27.0/refurb/checks/readability/no_copy_with_merge.py000066400000000000000000000026221454672660200243540ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, Expression, MemberExpr, OpExpr, RefExpr, Var from refurb.checks.common import stringify from refurb.error import Error @dataclass class ErrorInfo(Error): """ You don't need to call `.copy()` on a dict/set when using it in a union since the original dict/set is not modified. Bad: ``` d = {"a": 1} merged = d.copy() | {"b": 2} ``` Good: ``` d = {"a": 1} merged = d | {"b": 2} ``` """ name = "no-copy-with-merge" categories = ("readability",) code = 185 UNIONABLE_TYPES = ("builtins.dict[", "builtins.set[") ignored_nodes = set[int]() def check_expr(expr: Expression, errors: list[Error]) -> None: if id(expr) in ignored_nodes: return match expr: case CallExpr( callee=MemberExpr( expr=RefExpr(node=Var(type=ty)) as ref, name="copy", ), args=[], ) if str(ty).startswith(UNIONABLE_TYPES): msg = f"Replace `{stringify(ref)}.copy()` with `{stringify(ref)}`" errors.append(ErrorInfo.from_node(expr, msg)) case OpExpr(left=lhs, op="|", right=rhs): check_expr(lhs, errors) check_expr(rhs, errors) ignored_nodes.add(id(expr)) def check(node: OpExpr, errors: list[Error]) -> None: check_expr(node, errors) refurb-1.27.0/refurb/checks/readability/no_double_not.py000066400000000000000000000012541454672660200233220ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import UnaryExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Double negatives are confusing, so use `bool(x)` instead of `not not x`. Bad: ``` if not not value: pass ``` Good: ``` if value: pass ``` """ name = "no-double-not" code = 114 msg: str = "Replace `not not x` with `bool(x)`" categories = ("builtin", "readability", "truthy") def check(node: UnaryExpr, errors: list[Error]) -> None: match node: case UnaryExpr(op="not", expr=UnaryExpr(op="not")): errors.append(ErrorInfo.from_node(node)) refurb-1.27.0/refurb/checks/readability/no_from_float.py000066400000000000000000000027351454672660200233250ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, RefExpr from refurb.checks.common import stringify from refurb.error import Error @dataclass class ErrorInfo(Error): """ When constructing a Fraction or Decimal using a float, don't use the `from_float()` or `from_decimal()` class methods: Just use the more concise `Fraction()` and `Decimal()` class constructors instead. Bad: ``` ratio = Fraction.from_float(1.2) score = Decimal.from_float(98.0) ``` Good: ``` ratio = Fraction(1.2) score = Decimal(98.0) ``` """ name = "no-from-float" code = 164 categories = ("decimal", "fractions", "readability") KNOWN_FUNCS = { "_decimal.Decimal.from_float", "fractions.Fraction.from_float", "fractions.Fraction.from_decimal", } def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=RefExpr(fullname="_decimal.Decimal" | "fractions.Fraction") as ref, name="from_float" | "from_decimal" as ctor, ), args=[arg], ): if f"{ref.fullname}.{ctor}" not in KNOWN_FUNCS: return base = stringify(ref) arg = stringify(arg) # type: ignore old = f"{base}.{ctor}({arg})" new = f"{base}({arg})" errors.append(ErrorInfo.from_node(node, f"Replace `{old}` with `{new}`")) refurb-1.27.0/refurb/checks/readability/no_is_bool_compare.py000066400000000000000000000034041454672660200243230ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ComparisonExpr, Expression, NameExpr, Var from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use `is` or `==` to check if a boolean is True or False, simply use the name itself: Bad: ``` failed = True if failed is True: print("You failed") ``` Good: ``` failed = True if failed: print("You failed") ``` """ name = "no-bool-literal-compare" code = 149 categories = ("logical", "readability", "truthy") def is_bool_literal(expr: Expression) -> bool: match expr: case NameExpr(fullname="builtins.True" | "builtins.False"): return True return False def is_bool_variable(expr: Expression) -> bool: match expr: case NameExpr(node=Var(type=ty)) if str(ty) == "builtins.bool": return True return False def is_truthy(oper: str, name: str) -> bool: value = name == "True" return not value if oper in {"is not", "!="} else value def check(node: ComparisonExpr, errors: list[Error]) -> None: match node: case ComparisonExpr( operators=["is" | "is not" | "==" | "!=" as oper], operands=[NameExpr() as lhs, NameExpr() as rhs], ): if is_bool_literal(lhs) and is_bool_variable(rhs): old = f"{lhs.name} {oper} x" new = "x" if is_truthy(oper, lhs.name) else "not x" elif is_bool_variable(lhs) and is_bool_literal(rhs): old = f"x {oper} {rhs.name}" new = "x" if is_truthy(oper, rhs.name) else "not x" else: return errors.append(ErrorInfo.from_node(node, f"Replace `{old}` with `{new}`")) refurb-1.27.0/refurb/checks/readability/no_len_cmp.py000066400000000000000000000121061454672660200226030ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( AssertStmt, CallExpr, ComparisonExpr, ConditionalExpr, DictExpr, DictionaryComprehension, Expression, GeneratorExpr, IfStmt, IntExpr, ListExpr, MatchStmt, NameExpr, Node, OpExpr, StrExpr, TupleExpr, UnaryExpr, Var, WhileStmt, ) from refurb.error import Error from refurb.visitor import METHOD_NODE_MAPPINGS, TraverserVisitor @dataclass class ErrorInfo(Error): """ Don't check a container's length to determine if it is empty or not, use a truthiness check instead: Bad: ``` name = "bob" if len(name) == 0: pass nums = [1, 2, 3] if len(nums) >= 1: pass ``` Good: ``` name = "bob" if not name: pass nums = [1, 2, 3] if nums: pass ``` """ name = "no-len-compare" code = 115 categories = ("iterable", "truthy") CONTAINER_TYPES = { "builtins.list", "builtins.tuple", "tuple[", "builtins.dict", "builtins.set", "builtins.frozenset", "builtins.str", "Tuple", } def is_builtin_container_type(ty: str | None) -> bool: # Kept for compatibility with older Mypy versions if not ty: return False # pragma: no cover return any(ty.startswith(x) for x in CONTAINER_TYPES) def is_builtin_container_like(node: Expression) -> bool: match node: case NameExpr(node=Var(type=ty)) if is_builtin_container_type(str(ty)): return True case CallExpr(callee=NameExpr(fullname=name)) if is_builtin_container_type(name): return True case DictExpr() | ListExpr() | StrExpr() | TupleExpr(): return True return False def is_len_call(node: CallExpr) -> bool: match node: case CallExpr( callee=NameExpr(fullname="builtins.len"), args=[arg], ) if is_builtin_container_like(arg): return True return False IS_INT_COMPARISON_TRUTHY: dict[tuple[str, int], bool] = { ("==", 0): False, ("<=", 0): False, (">", 0): True, ("!=", 0): True, (">=", 1): True, } class LenComparisonVisitor(TraverserVisitor): errors: list[Error] def __init__(self, errors: list[Error]) -> None: super().__init__() self.errors = errors for name, ty in METHOD_NODE_MAPPINGS.items(): if ty in {ComparisonExpr, UnaryExpr, OpExpr, CallExpr}: continue def inner(self: "LenComparisonVisitor", _: Node) -> None: return setattr(self, name, inner.__get__(self)) def visit_op_expr(self, o: OpExpr) -> None: if o.op in {"and", "or"}: super().visit_op_expr(o) def visit_comparison_expr(self, node: ComparisonExpr) -> None: match node: case ComparisonExpr( operators=[oper], operands=[CallExpr() as call, IntExpr(value=num)], ) if is_len_call(call): is_truthy = IS_INT_COMPARISON_TRUTHY.get((oper, num)) if is_truthy is None: return expr = "x" if is_truthy else "not x" self.errors.append( ErrorInfo.from_node(node, f"Replace `len(x) {oper} {num}` with `{expr}`") ) case ComparisonExpr( operators=["==" | "!=" as oper], operands=[ NameExpr() as name, (ListExpr() | DictExpr()) as expr, ], ) if is_builtin_container_like(name): if expr.items: # type: ignore return old_expr = "[]" if isinstance(expr, ListExpr) else "{}" expr = "not x" if oper == "==" else "x" self.errors.append( ErrorInfo.from_node(node, f"Replace `x {oper} {old_expr}` with `{expr}`") ) def visit_call_expr(self, node: CallExpr) -> None: if is_len_call(node): self.errors.append(ErrorInfo.from_node(node, "Replace `len(x)` with `x`")) ConditionLikeNode = ( IfStmt | MatchStmt | GeneratorExpr | DictionaryComprehension | ConditionalExpr | WhileStmt | AssertStmt ) def check(node: ConditionLikeNode, errors: list[Error]) -> None: check_condition_like(LenComparisonVisitor(errors), node) def check_condition_like( visitor: TraverserVisitor, node: ConditionLikeNode, ) -> None: match node: case IfStmt(expr=exprs): for expr in exprs: visitor.accept(expr) case MatchStmt(guards=guards) if guards: for guard in guards: if guard: visitor.accept(guard) case (GeneratorExpr(condlists=conditions) | DictionaryComprehension(condlists=conditions)): for condition in conditions: for expr in condition: visitor.accept(expr) case (ConditionalExpr(cond=expr) | WhileStmt(expr=expr) | AssertStmt(expr=expr)): visitor.accept(expr) refurb-1.27.0/refurb/checks/readability/no_or_default.py000066400000000000000000000050441454672660200233150ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( BytesExpr, CallExpr, DictExpr, IntExpr, ListExpr, NameExpr, OpExpr, StrExpr, TupleExpr, Var, ) from refurb.checks.common import extract_binary_oper from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't check an expression to see if it is falsey then assign the same falsey value to it. For example, if an expression used to be of type `int | None`, checking if the expression is falsey would make sense, since it could be `None` or `0`. But, if the expression is changed to be of type `int`, the falsey value is just `0`, so setting it to `0` if it is falsey (`0`) is redundant. Bad: ``` def is_markdown_header(line: str) -> bool: return (line or "").startswith("#") ``` Good: ``` def is_markdown_header(line: str) -> bool: return line.startswith("#") ``` """ name = "no-default-or" code = 143 categories = ("logical", "readability") def check(node: OpExpr, errors: list[Error]) -> None: match extract_binary_oper("or", node): case (NameExpr(node=Var(type=ty)), arg): match arg: case CallExpr(callee=NameExpr(name=name, fullname=fullname), args=[]): expr = f"{name}()" case ListExpr(items=[]): fullname = "builtins.list" expr = "[]" case DictExpr(items=[]): fullname = "builtins.dict" expr = "{}" case TupleExpr(items=[]): fullname = "builtins.tuple" expr = "()" case StrExpr(value=""): fullname = "builtins.str" expr = '""' case BytesExpr(value=""): fullname = "builtins.bytes" expr = 'b""' case IntExpr(value=0): fullname = "builtins.int" expr = "0" case NameExpr(fullname="builtins.False"): fullname = "builtins.bool" expr = "False" case _: return type_name = "builtins.tuple" if str(ty).lower().startswith("tuple[") else str(ty) # Must check fullname for compatibility with older Mypy versions if fullname and type_name.startswith(fullname): errors.append(ErrorInfo.from_node(node, f"Replace `x or {expr}` with `x`")) refurb-1.27.0/refurb/checks/readability/no_redundant_assign.py000066400000000000000000000017771454672660200245320ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import AssignmentStmt, NameExpr from refurb.checks.common import unmangle_name from refurb.error import Error @dataclass class ErrorInfo(Error): """ Sometimes when you are debugging (or copy-pasting code) you will end up with a variable that is assigning itself to itself. These lines can be removed. Bad: ``` name = input("What is your name? ") name = name ``` Good: ``` name = input("What is your name? ") ``` """ code = 160 name = "no-redundant-assignment" categories = ("readability",) msg: str = "Remove redundant assignment of variable to itself" def check(node: AssignmentStmt, errors: list[Error]) -> None: match node: case AssignmentStmt( lvalues=[NameExpr(fullname=lhs_name)], rvalue=NameExpr(fullname=rhs_name), ) if lhs_name and unmangle_name(lhs_name) == unmangle_name(rhs_name): errors.append(ErrorInfo.from_node(node)) refurb-1.27.0/refurb/checks/readability/no_temp_class_object.py000066400000000000000000000031771454672660200246560ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, Decorator, FuncDef, MemberExpr, NameExpr, TypeInfo from refurb.error import Error @dataclass class ErrorInfo(Error): """ You don't need to construct a class object to call a static method or a class method, just invoke the method on the class directly: Bad: ``` cwd = Path().cwd() ``` Good: ``` cwd = Path.cwd() ``` """ name = "no-temp-class-object" code = 165 categories = ("readability",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=CallExpr( callee=NameExpr(node=TypeInfo() as klass), args=class_args, ), name=func_name, ), args=func_args, ): for func in klass.defn.defs.body: if isinstance(func, Decorator): func = func.func # noqa: PLW2901 elif not isinstance(func, FuncDef): continue if func.name == func_name and (func.is_class or func.is_static): class_name = klass.defn.name class_args = "..." if class_args else "" # type: ignore func_args = "..." if func_args else "" # type: ignore old = f"{class_name}({class_args}).{func_name}({func_args})" # noqa: E501 new = f"{class_name}.{func_name}({func_args})" errors.append(ErrorInfo.from_node(node, f"Replace `{old}` with `{new}`")) refurb-1.27.0/refurb/checks/readability/no_unnecessary_cast.py000066400000000000000000000052071454672660200245430ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( ArgKind, BytesExpr, CallExpr, ComplexExpr, DictExpr, Expression, FloatExpr, IntExpr, ListExpr, NameExpr, StrExpr, TupleExpr, Var, ) from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't cast a variable or literal if it is already of that type. This usually is the result of not realizing a type is already the type you want, or artifacts of some debugging code. One example of where this might be intentional is when using container types like `dict` or `list`, which will create a shallow copy. If that is the case, it might be preferable to use `.copy()` instead, since it makes it more explicit that a copy is taking place. Examples: Bad: ``` name = str("bob") num = int(123) ages = {"bob": 123} copy = dict(ages) ``` Good: ``` name = "bob" num = 123 ages = {"bob": 123} copy = ages.copy() ``` """ name = "no-redundant-cast" code = 123 categories = ("readability",) FUNC_NAMES = { "builtins.bool": (None, "x"), "builtins.bytes": (BytesExpr, "x"), "builtins.complex": (ComplexExpr, "x"), "builtins.dict": (DictExpr, "x.copy()"), "builtins.float": (FloatExpr, "x"), "builtins.int": (IntExpr, "x"), "builtins.list": (ListExpr, "x.copy()"), "builtins.str": (StrExpr, "x"), "builtins.tuple": (TupleExpr, "x"), "tuple[]": (TupleExpr, "x"), } def is_boolean_literal(node: Expression) -> bool: return isinstance(node, NameExpr) and node.fullname in { "builtins.True", "builtins.False", } def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=NameExpr(fullname=fullname, name=name), args=[arg], arg_kinds=[arg_kind], ) if arg_kind != ArgKind.ARG_STAR2 and fullname in FUNC_NAMES: node_type, msg = FUNC_NAMES[fullname] if type(arg) == node_type: if isinstance(arg, DictExpr | ListExpr): msg = "x" elif is_boolean_literal(arg) and name == "bool": pass else: match arg: case NameExpr(node=Var(type=ty)) if ( str(ty).startswith(fullname) or (str(ty).lower().startswith("tuple[") and name == "tuple") ): pass case _: return errors.append(ErrorInfo.from_node(node, f"Replace `{name}(x)` with `{msg}`")) refurb-1.27.0/refurb/checks/readability/use_abc_shorthand.py000066400000000000000000000026231454672660200241500ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ClassDef, NameExpr, RefExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Instead of setting `metaclass` directly, inherit from the `ABC` wrapper class. This is semantically the same thing, but more succinct. Bad: ``` class C(metaclass=ABCMeta): pass ``` Good: ``` class C(ABC): pass ``` """ name = "use-abc-shorthand" code = 180 categories = ("abc", "readability") def check(node: ClassDef, errors: list[Error]) -> None: match node: case ClassDef(metaclass=RefExpr(fullname="abc.ABCMeta") as ref): metaclass = node.metaclass assert metaclass # HACK: attempt to calculate the start of the metaclass keyword # from the position of its argument column = metaclass.column - 10 column_end = metaclass.end_column - 10 if metaclass.end_column else None prefix = "" if isinstance(ref, NameExpr) else "abc." msg = f"Replace `metaclass={prefix}ABCMeta` with `{prefix}ABC`" errors.append( ErrorInfo( line=metaclass.line, column=column, line_end=metaclass.end_line, column_end=column_end, msg=msg, ) ) refurb-1.27.0/refurb/checks/readability/use_comprehension.py000066400000000000000000000056751454672660200242340ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( AssignmentExpr, AssignmentStmt, Block, CallExpr, ExpressionStmt, ForStmt, IfStmt, ListExpr, MemberExpr, MypyFile, NameExpr, Statement, ) from refurb.checks.common import ReadCountVisitor, check_block_like from refurb.error import Error @dataclass class ErrorInfo(Error): """ When constructing a new list it is usually more performant to use a list comprehension, and in some cases, it can be more readable. Bad: ``` nums = [1, 2, 3, 4] odds = [] for num in nums: if num % 2: odds.append(num) ``` Good: ``` nums = [1, 2, 3, 4] odds = [num for num in nums if num % 2] ``` """ name = "use-list-comprehension" code = 138 msg: str = "Consider using list comprehension" categories = ("performance", "readability") def check(node: Block | MypyFile, errors: list[Error]) -> None: check_block_like(check_stmts, node, errors) def get_append_func_callee_name(expr: Statement) -> NameExpr | None: match expr: case ExpressionStmt( expr=CallExpr( callee=MemberExpr( expr=NameExpr() as name, name="append", ) ) ): return name return None def check_stmts(stmts: list[Statement], errors: list[Error]) -> None: assign: NameExpr | None = None for stmt in stmts: if assign: match stmt: case ForStmt( body=Block( body=[ IfStmt( expr=[if_expr], body=[Block(body=[stmt])], else_body=None, ) ] ) ) if ( (name := get_append_func_callee_name(stmt)) and name.fullname == assign.fullname and not isinstance(if_expr, AssignmentExpr) ): name_visitor = ReadCountVisitor(name) name_visitor.accept(stmt) if name_visitor.read_count == 1: errors.append(ErrorInfo.from_node(assign)) case ForStmt(body=Block(body=[stmt])) if ( (name := get_append_func_callee_name(stmt)) and name.fullname == assign.fullname ): name_visitor = ReadCountVisitor(name) name_visitor.accept(stmt) if name_visitor.read_count == 1: errors.append(ErrorInfo.from_node(assign)) assign = None match stmt: case AssignmentStmt( lvalues=[NameExpr() as name], rvalue=ListExpr(items=[]), ): assign = name refurb-1.27.0/refurb/checks/readability/use_dict_union.py000066400000000000000000000114671454672660200235120ustar00rootroot00000000000000from dataclasses import dataclass from itertools import groupby from mypy.nodes import ArgKind, CallExpr, DictExpr, Expression, RefExpr, Var from refurb.checks.common import stringify from refurb.error import Error from refurb.settings import Settings @dataclass class ErrorInfo(Error): """ Dicts can be created/combined in many ways, one of which is the `**` operator (inside the dict), and another is the `|` operator (used outside the dict). While they both have valid uses, the `|` operator allows for more flexibility, including using `|=` to update an existing dict. See PEP 584 for more info. Bad: ``` def add_defaults(settings: dict[str, str]) -> dict[str, str]: return {"color": "1", **settings} ``` Good: ``` def add_defaults(settings: dict[str, str]) -> dict[str, str]: return {"color": "1"} | settings ``` """ name = "use-dict-union" code = 173 categories = ("dict", "readability") MAPPING_TYPES = ( "builtins.dict[", "collections.ChainMap[", "collections.Counter[", "collections.OrderedDict[", "collections.defaultdict[", "collections.UserDict[", ) def is_builtin_mapping(expr: Expression) -> bool: match expr: case RefExpr(node=Var(type=ty)): return str(ty).startswith(MAPPING_TYPES) return False def check(node: DictExpr | CallExpr, errors: list[Error], settings: Settings) -> None: if settings.get_python_version() < (3, 9): return # pragma: no cover match node: case DictExpr(items=items): groups = [(k, list(v)) for k, v in groupby(items, lambda x: x[0] is None)] if len(groups) not in {1, 2}: # Only allow groups of 1 and 2 because a group of 0 means the # dict is empty, and 3 or more means that there are 3 or more # alternations of star and non-star patterns in the dict, # which would look like `x | {"k": "v"} | z`, for example, and # to me this looks less readable. I might change this later. return if len(groups) == 1 and (not groups[0][0] or len(groups[0][1]) == 1): return old: list[str] = [] new: list[str] = [] index = 1 for group in groups: is_star, pairs = group for pair in pairs: if is_star: _, star_expr = pair if not is_builtin_mapping(star_expr): return old.append(f"**{stringify(star_expr)}") new.append(stringify(star_expr)) index += 1 else: old.append("...") new.append("{...}") old_msg = ", ".join(old) new_msg = " | ".join(new) msg = f"Replace `{{{old_msg}}}` with `{new_msg}`" errors.append(ErrorInfo.from_node(node, msg)) case CallExpr(callee=RefExpr(fullname="builtins.dict")): old = [] args: list[str] = [] kwargs: dict[str, str] = {} # ignore dict(x) since that is covered by FURB123 match node.arg_kinds: case []: return case [ArgKind.ARG_POS]: return # TODO: move dict(a=1, b=2) to FURB112 if all(x == ArgKind.ARG_NAMED for x in node.arg_kinds): return for arg, name, kind in zip(node.args, node.arg_names, node.arg_kinds): # ignore dict(*x) if kind == ArgKind.ARG_STAR: return if kind == ArgKind.ARG_STAR2: old.append(f"**{stringify(arg)}") stringified_arg = stringify(arg) if len(node.args) == 1: # TODO: dict(**x) can be replaced with x.copy() if we know x has a copy() # method. stringified_arg = f"{{**{stringified_arg}}}" args.append(stringified_arg) elif name: old.append(f"{name}={stringify(arg)}") kwargs[name] = stringify(arg) else: old.append(stringify(arg)) args.append(stringify(arg)) inner = ", ".join(old) old_msg = f"dict({inner})" if kwargs: kwargs2 = ", ".join(f'"{name}": {expr}' for name, expr in kwargs.items()) kwargs2 = f"{{{kwargs2}}}" args.append(kwargs2) new_msg = " | ".join(args) msg = f"Replace `{old_msg}` with `{new_msg}`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/readability/use_func_name.py000066400000000000000000000051301454672660200233000ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( ArgKind, Argument, Block, CallExpr, DictExpr, Expression, LambdaExpr, ListExpr, NameExpr, RefExpr, ReturnStmt, TupleExpr, ) from refurb.checks.common import stringify from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use a lambda if it is just forwarding its arguments to a function verbatim: Bad: ``` predicate = lambda x: bool(x) some_func(lambda x, y: print(x, y)) ``` Good: ``` predicate = bool some_func(print) ``` """ name = "use-func-name" code = 111 categories = ("readability",) def get_lambda_arg_names(args: list[Argument]) -> list[str]: return [arg.variable.name for arg in args] def get_func_arg_names(args: list[Expression]) -> list[str | None]: return [arg.name if isinstance(arg, NameExpr) else None for arg in args] def check(node: LambdaExpr, errors: list[Error]) -> None: match node: case LambdaExpr( arguments=lambda_args, body=Block( body=[ ReturnStmt(expr=CallExpr(callee=RefExpr() as ref) as func), ] ), ) if ( get_lambda_arg_names(lambda_args) == get_func_arg_names(func.args) and all(kind == ArgKind.ARG_POS for kind in func.arg_kinds) ): func_name = stringify(ref) arg_names = get_lambda_arg_names(lambda_args) arg_names = ", ".join(arg_names) if arg_names else "" _lambda = f"lambda {arg_names}" if arg_names else "lambda" errors.append( ErrorInfo.from_node( node, f"Replace `{_lambda}: {func_name}({arg_names})` with `{func_name}`", # noqa: E501 ) ) case LambdaExpr( arguments=[], body=Block( body=[ ReturnStmt( expr=ListExpr(items=[]) | DictExpr(items=[]) | TupleExpr(items=[]) as expr, ) ], ), ): if isinstance(expr, ListExpr): old = "[]" new = "list" elif isinstance(expr, DictExpr): old = "{}" new = "dict" else: old = "()" new = "tuple" errors.append( ErrorInfo.from_node( node, f"Replace `lambda: {old}` with `{new}`", ) ) refurb-1.27.0/refurb/checks/readability/use_literal.py000066400000000000000000000020551454672660200230040ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, NameExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Using `list` and `dict` without any arguments is slower, and not Pythonic. Use `[]` and `{}` instead: Bad: ``` nums = list() books = dict() ``` Good: ``` nums = [] books = {} ``` """ name = "use-literal" code = 112 categories = ("pythonic", "readability") FUNC_NAMES = { "builtins.bool": "False", "builtins.bytes": 'b""', "builtins.complex": "0j", "builtins.dict": "{}", "builtins.float": "0.0", "builtins.int": "0", "builtins.list": "[]", "builtins.str": '""', "builtins.tuple": "()", } def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=NameExpr(fullname=fullname, name=name), args=[], ) if literal := FUNC_NAMES.get(fullname): errors.append(ErrorInfo.from_node(node, f"Replace `{name}()` with `{literal}`")) refurb-1.27.0/refurb/checks/readability/use_operators.py000066400000000000000000000067571454672660200234030ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( ArgKind, Block, ComparisonExpr, FuncItem, LambdaExpr, NameExpr, OpExpr, ReturnStmt, UnaryExpr, ) from refurb.checks.common import _stringify from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't write lambdas/functions to wrap builtin operators, use the `operator` module instead: Bad: ``` from functools import reduce nums = [1, 2, 3] print(reduce(lambda x, y: x + y, nums)) # 6 ``` Good: ``` from functools import reduce from operator import add nums = [1, 2, 3] print(reduce(add, nums)) # 6 ``` """ name = "use-operator" code = 118 categories = ("operator",) BINARY_OPERATORS = { "+": "add", "in": "contains", "/": "truediv", "//": "floordiv", "&": "and_", "^": "xor", "|": "or_", "**": "pow", "is": "is_", "is not": "is_not", "<<": "lshift", "%": "mod", "*": "mul", "@": "matmul", ">>": "rshift", "-": "sub", "<": "lt", "<=": "le", "==": "eq", "!=": "ne", ">=": "ge", ">": "gt", } UNARY_OPERATORS = { "~": "invert", "-": "neg", "not": "not_", "+": "pos", } def check(node: FuncItem, errors: list[Error]) -> None: func_type = get_function_type(node) match node: case FuncItem( arg_names=[lhs_name, rhs_name], arg_kinds=[ArgKind.ARG_POS, ArgKind.ARG_POS], body=Block( body=[ ReturnStmt( expr=OpExpr( op=op, left=NameExpr(name=expr_lhs), right=NameExpr(name=expr_rhs), ) | ComparisonExpr( operators=[op], operands=[ NameExpr(name=expr_lhs), NameExpr(name=expr_rhs), ], ), ) ] ), ) if func_name := BINARY_OPERATORS.get(op): if func_name == "contains": # operator.contains has reversed parameters expr_lhs, expr_rhs = expr_rhs, expr_lhs if lhs_name == expr_lhs and rhs_name == expr_rhs: errors.append( ErrorInfo.from_node( node, f"Replace {func_type} with `operator.{func_name}`", ) ) case FuncItem( arg_names=[name], arg_kinds=[ArgKind.ARG_POS], body=Block( body=[ ReturnStmt( expr=UnaryExpr( op=op, expr=NameExpr(name=expr_name), ) ) ] ), ) if name == expr_name: if func_name := UNARY_OPERATORS.get(op): errors.append( ErrorInfo.from_node( node, f"Replace {func_type} with `operator.{func_name}`", ) ) def get_function_type(node: FuncItem) -> str: if isinstance(node, LambdaExpr): try: return f"`{_stringify(node)}`" except ValueError: return "lambda" return "function" refurb-1.27.0/refurb/checks/readability/use_sort.py000066400000000000000000000036571454672660200223500ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ArgKind, AssignmentStmt, CallExpr, NameExpr, Var from refurb.checks.common import stringify, unmangle_name from refurb.error import Error @dataclass class ErrorInfo(Error): """ Don't use `sorted()` to sort a list and reassign it to itself, use the faster in-place `.sort()` method instead. Bad: ``` names = ["Bob", "Alice", "Charlie"] names = sorted(names) ``` Good: ``` names = ["Bob", "Alice", "Charlie"] names.sort() ``` """ name = "use-sort" categories = ("performance", "readability") code = 186 def check(node: AssignmentStmt, errors: list[Error]) -> None: match node: case AssignmentStmt( lvalues=[NameExpr(fullname=assign_name) as assign_ref], rvalue=CallExpr( callee=NameExpr(fullname="builtins.sorted"), args=[ NameExpr(fullname=sort_name, node=Var(type=ty)), *rest, ], arg_names=[_, *arg_names], arg_kinds=[_, *arg_kinds], ), ) if ( unmangle_name(assign_name) == unmangle_name(sort_name) and str(ty).startswith("builtins.list[") and all(arg_kind == ArgKind.ARG_NAMED for arg_kind in arg_kinds) ): old_args: list[str] = [] new_args: list[str] = [] name = stringify(assign_ref) old_args.append(name) if rest: for arg_name, expr in zip(arg_names, rest): arg = f"{arg_name}={stringify(expr)}" old_args.append(arg) new_args.append(arg) old = f"{name} = sorted({', '.join(old_args)})" new = f"{name}.sort({', '.join(new_args)})" msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/readability/use_str_func.py000066400000000000000000000040641454672660200231750ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, ListExpr, MemberExpr, NameExpr, StrExpr from refurb.checks.common import stringify from refurb.checks.string.use_fstring_fmt import CONVERSIONS as FURB_119_FUNCS from refurb.error import Error from refurb.visitor import TraverserVisitor @dataclass class ErrorInfo(Error): """ If you want to stringify a single value without concatenating anything, use the `str()` function instead. Bad: ``` nums = [123, 456] num = f"{num[0]}") ``` Good: ``` nums = [123, 456] num = str(num[0]) ``` """ name = "use-str-func" code = 183 categories = ("readability",) ignore = set[int]() # TODO: add support for returning False from check to indicate it shouldnt prapogate class NestedFstringIgnorer(TraverserVisitor): def visit_call_expr(self, o: CallExpr) -> None: ignore.add(id(o)) super().visit_call_expr(o) def check(node: CallExpr, errors: list[Error]) -> None: if id(node) in ignore: return match node: case CallExpr( callee=MemberExpr( expr=StrExpr(value=""), name="join", ), args=[ListExpr(items=items)], ): visitor = NestedFstringIgnorer() for item in items: visitor.accept(item) case CallExpr( callee=MemberExpr( expr=StrExpr(value="{:{}}"), name="format", ), args=[arg, StrExpr(value="")], ): match arg: case CallExpr(callee=NameExpr(fullname=fn)) if fn in FURB_119_FUNCS: return x = stringify(arg) msg = f'Replace `f"{{{x}}}"` with `str({x})`' errors.append(ErrorInfo.from_node(node, msg)) case CallExpr( callee=MemberExpr( expr=StrExpr(value="{:{}}"), name="format", ), args=[_, arg], ): NestedFstringIgnorer().accept(arg) refurb-1.27.0/refurb/checks/readability/use_tuple_swap.py000066400000000000000000000030721454672660200235330ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import AssignmentStmt, Block, MypyFile, NameExpr, Statement from refurb.checks.common import check_block_like from refurb.error import Error @dataclass class ErrorInfo(Error): """ You don't need to use a temporary variable to swap 2 variables, you can use tuple unpacking instead: Bad: ``` temp = x x = y y = temp ``` Good: ``` x, y = y, x ``` """ name = "use-tuple-unpack-swap" code = 128 msg: str = "Use tuple unpacking instead of temporary variables to swap values" # noqa: E501 categories = ("readability",) def check(node: Block | MypyFile, errors: list[Error]) -> None: check_block_like(check_stmts, node, errors) def check_stmts(stmts: list[Statement], errors: list[Error]) -> None: assignments = [] for stmt in stmts: if isinstance(stmt, AssignmentStmt): assignments.append(stmt) else: assignments = [] if len(assignments) == 3: match assignments: case [ AssignmentStmt(lvalues=[NameExpr() as a], rvalue=NameExpr() as b), AssignmentStmt(lvalues=[NameExpr() as c], rvalue=NameExpr() as d), AssignmentStmt(lvalues=[NameExpr() as e], rvalue=NameExpr() as f), ] if (a.name == f.name and b.name == c.name and d.name == e.name): errors.append(ErrorInfo.from_node(a)) assignments = [] case _: assignments.pop(0) refurb-1.27.0/refurb/checks/regex/000077500000000000000000000000001454672660200167415ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/regex/__init__.py000066400000000000000000000000001454672660200210400ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/regex/use_long_flag.py000066400000000000000000000024041454672660200221170ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import MemberExpr, NameExpr, RefExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Regex operations can be changed using flags such as `re.I`, which will make the regex case-insensitive. These single-character flag names can be harder to read/remember, and should be replaced with the longer aliases so that they are more descriptive. Bad: ``` if re.match("^hello", "hello world", re.I): pass ``` Good: ``` if re.match("^hello", "hello world", re.IGNORECASE): pass ``` """ name = "use-long-regex-flag" code = 167 categories = ("readability", "regex") SHORT_TO_LONG_FLAG = { "re.A": "re.ASCII", "re.I": "re.IGNORECASE", "re.L": "re.LOCALE", "re.M": "re.MULTILINE", "re.S": "re.DOTALL", "re.T": "re.TEMPLATE", "re.U": "re.UNICODE", "re.X": "re.VERBOSE", } def check(node: NameExpr | MemberExpr, errors: list[Error]) -> None: match node: case RefExpr(fullname=fullname): if long_name := SHORT_TO_LONG_FLAG.get(fullname): errors.append( ErrorInfo.from_node(node, f"Replace `{fullname}` with `{long_name}`") ) refurb-1.27.0/refurb/checks/regex/use_pattern_method.py000066400000000000000000000050171454672660200232070ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, RefExpr, Var from refurb.error import Error @dataclass class ErrorInfo(Error): """ If you are passing a compiled regular expression to a regex function, consider calling the regex method on the pattern itself: It is faster, and can improve readability. Bad: ``` import re COMMENT = re.compile(".*(#.*)") found_comment = re.match(COMMENT, "this is a # comment") ``` Good: ``` import re COMMENT = re.compile(".*(#.*)") found_comment = COMMENT.match("this is a # comment") ``` """ name = "use-regex-pattern-methods" code = 170 categories = ("readability", "regex") # This table represents the function calls that we will emit errors for. The # ellipsis are positional args, and the strings are optional args/kwargs. # The number of required/optional args must match, and if an optional arg # is used, it must either be unnamed (positional), or named (kwarg), and if # so, must match the string name. REGEX_FUNC_ARGS = { "re.search": (..., ...), "re.match": (..., ...), "re.fullmatch": (..., ...), "re.split": (..., ..., "maxsplit"), "re.findall": (..., ...), "re.finditer": (..., ...), "re.sub": (..., ..., ..., "count"), "re.subn": (..., ..., ..., "count"), } def build_args(arg_names: list[str | None]) -> str: args = ["..." if arg is None else f"{arg}=..." for arg in arg_names] return ", ".join(args) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=RefExpr(fullname=fullname, name=name), # type: ignore args=[pattern, *_] as args, arg_names=arg_names, ): arg_format = REGEX_FUNC_ARGS.get(fullname) if not arg_format: return match pattern: case RefExpr(node=Var(type=ty)) if (str(ty).startswith("re.Pattern[")): pass case _: return min_len = len([arg for arg in arg_format if arg is ...]) if len(args) < min_len or len(args) > len(arg_format): return if isinstance(arg_format[-1], str): if arg_names[-1] and arg_names[-1] != arg_format[-1]: return params = build_args(arg_names[1:]) msg = f"Replace `{fullname}(x, {params})` with `x.{name}({params})`" # noqa: E501 errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/secrets/000077500000000000000000000000001454672660200172775ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/secrets/__init__.py000066400000000000000000000000001454672660200213760ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/secrets/simplify_token_function.py000066400000000000000000000054131454672660200246150ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, IndexExpr, IntExpr, MemberExpr, NameExpr, RefExpr, SliceExpr from refurb.checks.common import stringify from refurb.error import Error @dataclass class ErrorInfo(Error): """ Depending on how you are using the `secrets` module, there might be more expressive ways of writing what it is you're trying to write. Bad: ``` random_hex = token_bytes().hex() random_url = token_urlsafe()[:16] ``` Good: ``` random_hex = token_hex() random_url = token_urlsafe(16) ``` """ name = "simplify-token-function" code = 174 categories = ("readability", "secrets") def check(node: CallExpr | IndexExpr, errors: list[Error]) -> None: match node: # Detects `token_bytes().hex()` case CallExpr( callee=MemberExpr( expr=CallExpr( callee=RefExpr(fullname="secrets.token_bytes") as ref, args=token_args, ), name="hex", ), args=[], ): match token_args: case [IntExpr(value=value)]: arg = str(value) case [NameExpr(fullname="builtins.None")]: arg = "None" case []: arg = "" case _: return new_arg = "" if arg == "None" else arg prefix = "secrets." if isinstance(ref, MemberExpr) else "" old = f"{prefix}token_bytes({arg}).hex()" new = f"{prefix}token_hex({new_arg})" msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(node, msg)) # Detects `token_xyz()[:x]` case IndexExpr( base=CallExpr( callee=RefExpr( fullname=fullname, name=name, # type: ignore[misc] ) as ref, args=[] | [NameExpr(fullname="builtins.None")] as args, ), index=SliceExpr( begin_index=None, end_index=IntExpr(value=size), stride=None, ), ) if fullname in {"secrets.token_hex", "secrets.token_bytes"}: arg = "None" if args else "" func_name = stringify(ref) old = f"{func_name}({arg})[:{size}]" # size must be multiple of 2 for hex functions since each hex digit # takes up 2 bytes. if name == "token_hex": if size % 2 == 1: return size //= 2 new = f"{func_name}({size})" msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(node, msg)) refurb-1.27.0/refurb/checks/shlex/000077500000000000000000000000001454672660200167525ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/shlex/__init__.py000066400000000000000000000000001454672660200210510ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/shlex/use_join.py000066400000000000000000000040041454672660200211350ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import ( CallExpr, Expression, GeneratorExpr, ListComprehension, MemberExpr, NameExpr, Node, RefExpr, StrExpr, ) from refurb.error import Error @dataclass class ErrorInfo(Error): """ When using `shlex` to escape and join a bunch of strings consider using the `shlex.join` method instead. Bad: ``` args = ["hello", "world!"] cmd = " ".join(shlex.quote(arg) for arg in args) ``` Good: ``` args = ["hello", "world!"] cmd = shlex.join(args) ``` """ name = "use-shlex-join" code = 178 categories = ("readability", "shlex") def handle_join_arg(root: Node, arg: Expression) -> list[Error]: match arg: case GeneratorExpr( left_expr=CallExpr( callee=RefExpr(fullname="shlex.quote") as ref, args=[quote_arg], ), condlists=[condlist], ): if isinstance(ref, MemberExpr): quote = "shlex.quote" join = "shlex.join" else: quote = ref.name # type: ignore join = "join" if isinstance(quote_arg, NameExpr) and not condlist: old = f'" ".join({quote}(x) for x in y)' new = f"{join}(y)" else: _if = " if ..." if condlist else "" old = f'" ".join({quote}(...) for x in y{_if})' new = f"{join}(... for x in y{_if})" msg = f"Replace `{old}` with `{new}`" return [ErrorInfo.from_node(root, msg)] case ListComprehension(): return handle_join_arg(root, arg.generator) return [] def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=StrExpr(value=" "), name="join", ), args=[arg], ): errors += handle_join_arg(node, arg) refurb-1.27.0/refurb/checks/string/000077500000000000000000000000001454672660200171355ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/string/__init__.py000066400000000000000000000000001454672660200212340ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/string/charsets.py000066400000000000000000000042251454672660200213260ustar00rootroot00000000000000import string from dataclasses import dataclass from mypy.nodes import ComparisonExpr, StrExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Python includes some pre-defined charsets such as digits (0-9), upper and lower case alpha characters, and so on. You don't have to define them yourself, and they are usually more readable. Bad: ``` digits = "0123456789" if c in digits: pass if c in "0123456789abcdefABCDEF": pass ``` Good: ``` if c in string.digits: pass if c in string.hexdigits: pass ``` Note that when using a literal string, the corresponding `string.xyz` value must be exact, but when used in an `in` comparison, the characters can be out of order since `in` will compare every character in the string. """ name = "use-string-charsets" code = 156 categories = ("readability", "string") _CHARSETS = [ "ascii_letters", "ascii_lowercase", "ascii_uppercase", "digits", "hexdigits", "octdigits", "printable", "punctuation", "whitespace", ] CHARSETS_EXACT = {f"string.{name}": getattr(string, name) for name in _CHARSETS} CHARSET_PERMUTATIONS = {name: frozenset(value) for name, value in CHARSETS_EXACT.items()} def format_error(value: str, name: str) -> str: # Escape and pretty print control chars, remove surrounding quotes value = repr(value)[1:-1].replace("\\x0b", "\\v").replace("\\x0c", "\\f") return f"Replace `{value}` with `{name}`" def check(node: ComparisonExpr | StrExpr, errors: list[Error]) -> None: match node: case ComparisonExpr(operators=["in"], operands=[_, StrExpr(value=value)]): value_set = set(value) for name, charset in CHARSET_PERMUTATIONS.items(): if value_set == charset: errors.append(ErrorInfo.from_node(node, format_error(value, name))) case StrExpr(value=value): for name, charset in CHARSETS_EXACT.items(): if value == charset: # type: ignore errors.append(ErrorInfo.from_node(node, format_error(value, name))) refurb-1.27.0/refurb/checks/string/expandtabs.py000066400000000000000000000071551454672660200216500ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import BytesExpr, CallExpr, IntExpr, MemberExpr, NameExpr, OpExpr, StrExpr from refurb.error import Error @dataclass class ErrorInfo(Error): r""" If you want to expand the tabs at the start of a string, don't use `.replace("\t", " " * 8)`, use `.expandtabs()` instead. Note that this only works if the tabs are at the start of the string, since `expandtabs()` will expand each tab to the nearest tab column. Bad: ``` spaces_8 = "\thello world".replace("\t", " " * 8) spaces_4 = "\thello world".replace("\t", " ") ``` Good: ``` spaces_8 = "\thello world".expandtabs() spaces_4 = "\thello world".expandtabs(4) ``` """ name = "use-expandtabs" enabled = False code = 106 categories = ("string",) def check_str(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr(name="replace") as func, args=[StrExpr(value="\t"), replace], ): match replace: case StrExpr(value=s) if all(c == " " for c in s): tabsize = str(len(s)) expr_value = f'"{s}"' case OpExpr( op="*", left=StrExpr(value=" "), right=IntExpr(value=value) | NameExpr(name=value), ): tabsize = str(value) expr_value = f'" " * {value}' case OpExpr( op="*", left=IntExpr(value=value) | NameExpr(name=value), right=StrExpr(value=" "), ): tabsize = str(value) expr_value = f'{value} * " "' case _: return if tabsize == "8": tabsize = "" errors.append( ErrorInfo( func.line, (func.end_column or 0) - len("replace"), f'Replace `x.replace("\\t", {expr_value})` with `x.expandtabs({tabsize})`', # noqa: E501 ) ) def check_bytes(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr(name="replace") as func, args=[BytesExpr(value="\\t"), replace], ): match replace: case BytesExpr(value=s) if all(c == " " for c in s): tabsize = str(len(s)) expr_value = f'b"{s}"' case OpExpr( op="*", left=BytesExpr(value=" "), right=IntExpr(value=value) | NameExpr(name=value), ): tabsize = str(value) expr_value = f'b" " * {value}' case OpExpr( op="*", left=IntExpr(value=value) | NameExpr(name=value), right=BytesExpr(value=" "), ): tabsize = str(value) expr_value = f'{value} * b" "' case _: return if tabsize == "8": tabsize = "" errors.append( ErrorInfo( func.line, (func.end_column or 0) - len("replace"), f'Replace `x.replace(b"\\t", {expr_value})` with `x.expandtabs({tabsize})`', # noqa: E501 ) ) def check(node: CallExpr, errors: list[Error]) -> None: check_str(node, errors) check_bytes(node, errors) refurb-1.27.0/refurb/checks/string/fstring_number.py000066400000000000000000000025551454672660200225420ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, IndexExpr, IntExpr, NameExpr, SliceExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ The `bin()`, `oct()`, and `hex()` functions return the string representation of a number but with a prefix attached. If you don't want the prefix, you might be tempted to just slice it off, but using an f-string will give you more flexibility and let you work with negative numbers: Bad: ``` print(bin(1337)[2:]) ``` Good: ``` print(f"{1337:b}") ``` """ name = "use-fstring-number-format" code = 116 categories = ("builtin", "fstring") FUNC_CONVERSIONS = { "builtins.bin": "b", "builtins.oct": "o", "builtins.hex": "x", } def check(node: IndexExpr, errors: list[Error]) -> None: match node: case IndexExpr( base=CallExpr(callee=NameExpr() as name_node), index=SliceExpr(begin_index=IntExpr(value=2), end_index=None), ) if name_node.fullname in FUNC_CONVERSIONS: format = FUNC_CONVERSIONS[name_node.fullname or ""] fstring = f'f"{{num:{format}}}"' errors.append( ErrorInfo.from_node( node, f"Replace `{name_node.name}(num)[2:]` with `{fstring}`", ) ) refurb-1.27.0/refurb/checks/string/no_multiline_lstrip.py000066400000000000000000000037421454672660200236100ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, StrExpr from refurb.error import Error @dataclass class ErrorInfo(Error): r''' If you want to define a multi-line string but don't want a leading/trailing newline, use a continuation character ('\') instead of calling `lstrip()`, `rstrip()`, or `strip()`. Bad: ``` """ This is some docstring """.lstrip() """ This is another docstring """.strip() ``` Good: ``` """\ This is some docstring """ """\ This is another docstring\ """ ``` ''' name = "no-multiline-strip" code = 139 categories = ("readability",) def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=StrExpr(value=value), name="lstrip" | "rstrip" | "strip" as func, ), args=[] | [StrExpr(value="\n")] as args, ) if node.line != node.end_line and len(value) > 1: leading_newline = value.startswith("\n") and not value[1].isspace() trailing_newline = value.endswith("\n") and not value[-2].isspace() if func == "strip" and (leading_newline or trailing_newline): pass elif func == "lstrip" and leading_newline: trailing_newline = False elif func == "rstrip" and trailing_newline: leading_newline = False else: return func_expr: str = func func_expr += '("\\n")' if args else "()" parts = [ '"""', "\\n" if leading_newline else "", "...", "\\n" if trailing_newline else "", '"""', ] old = "".join(parts) new = old.replace("n", "") errors.append(ErrorInfo.from_node(node, f"Replace `{old}.{func_expr}` with `{new}`")) refurb-1.27.0/refurb/checks/string/simplify_strip.py000066400000000000000000000052761454672660200225760ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, NameExpr, StrExpr, Var from refurb.error import Error @dataclass class ErrorInfo(Error): """ In some situations the `.lstrip()`, `.rstrip()` and `.strip()` string methods can be written more succinctly: `strip()` is the same thing as calling both `lstrip()` and `rstrip()` together, and all the strip functions take an iterable argument of the characters to strip, meaning you don't need to call strip methods multiple times with different arguments, you can just concatenate them and call it once. Bad: ``` name = input().lstrip().rstrip() num = " -123".lstrip(" ").lstrip("-") ``` Good: ``` name = input().strip() num = " -123".lstrip(" -") ``` """ name = "simplify-strip" code = 159 categories = ("readability", "string") STRIP_FUNCS = ("lstrip", "rstrip", "strip") def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr( expr=CallExpr( callee=MemberExpr(expr=expr, name=lhs_func), args=lhs_args, ), name=rhs_func, ), args=rhs_args, ) if rhs_func in STRIP_FUNCS and lhs_func in STRIP_FUNCS: match expr: case StrExpr(): pass case NameExpr(node=Var(type=ty)) if str(ty) == "builtins.str": pass case _: return exprs: list[str] match lhs_args, rhs_args: case [], []: lhs_arg = rhs_arg = "" if lhs_func == rhs_func: exprs = [f"{lhs_func}()"] else: exprs = ["strip()"] case ( [StrExpr(value=lhs_arg)], [StrExpr(value=rhs_arg)], ): if lhs_func == rhs_func: combined = "".join(sorted(set(lhs_arg + rhs_arg))) exprs = [f"{lhs_func}({combined!r})"] elif lhs_arg == rhs_arg: exprs = [f"strip({lhs_arg!r})"] else: return lhs_arg = repr(lhs_arg) rhs_arg = repr(rhs_arg) case _: return lhs = f"{lhs_func}({lhs_arg})" rhs = f"{rhs_func}({rhs_arg})" new = f"x.{'.'.join(exprs)}" errors.append(ErrorInfo.from_node(node, f"Replace `x.{lhs}.{rhs}` with `{new}`")) refurb-1.27.0/refurb/checks/string/startswith.py000066400000000000000000000044001454672660200217210ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, Expression, MemberExpr, NameExpr, OpExpr, UnaryExpr, Var from refurb.checks.common import extract_binary_oper from refurb.error import Error @dataclass class ErrorInfo(Error): """ `startswith()` and `endswith()` both take a tuple, so instead of calling `startswith()` multiple times on the same string, you can check them all at once: Bad: ``` name = "bob" if name.startswith("b") or name.startswith("B"): pass ``` Good: ``` name = "bob" if name.startswith(("b", "B")): pass ``` """ name = "use-startswith-endswith-tuple" code = 102 categories = ("string",) def are_startswith_or_endswith_calls( lhs: Expression, rhs: Expression ) -> tuple[str, Expression] | None: match lhs, rhs: case ( CallExpr( callee=MemberExpr(expr=NameExpr(node=Var(type=ty)) as lhs, name=lhs_func), args=args, ), CallExpr(callee=MemberExpr(expr=NameExpr() as rhs, name=rhs_func)), ) if ( lhs.fullname == rhs.fullname and str(ty) in {"builtins.str", "builtins.bytes"} and lhs_func == rhs_func and lhs_func in {"startswith", "endswith"} and args ): return lhs_func, args[0] return None def check(node: OpExpr, errors: list[Error]) -> None: match extract_binary_oper("or", node): case (lhs, rhs) if data := are_startswith_or_endswith_calls(lhs, rhs): func, arg = data old = f"x.{func}(y) or x.{func}(z)" new = f"x.{func}((y, z))" errors.append(ErrorInfo.from_node(arg, msg=f"Replace `{old}` with `{new}`")) match extract_binary_oper("and", node): case ( UnaryExpr(op="not", expr=lhs), UnaryExpr(op="not", expr=rhs), ) if data := are_startswith_or_endswith_calls(lhs, rhs): func, arg = data old = f"not x.{func}(y) and not x.{func}(z)" new = f"not x.{func}((y, z))" errors.append( ErrorInfo.from_node( arg, msg=f"Replace `{old}` with `{new}`", ) ) refurb-1.27.0/refurb/checks/string/use_fstring_fmt.py000066400000000000000000000030461454672660200227100ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, MemberExpr, NameExpr, StrExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ Certain expressions which are passed to f-strings are redundant because the f-string itself is capable of formatting it. For example: Bad: ``` print(f"{bin(1337)}") print(f"{ascii(input())}") print(f"{str(123)}") ``` Good: ``` print(f"{1337:#b}") print(f"{input()!a}") print(f"{123}") ``` """ name = "use-fstring-format" code = 119 categories = ("builtin", "fstring") CONVERSIONS = { "builtins.str": "x", "builtins.repr": "x!r", "builtins.ascii": "x!a", "builtins.bin": "x:#b", "builtins.oct": "x:#o", "builtins.hex": "x:#x", "builtins.chr": "x:c", "builtins.format": "x", } def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr( callee=MemberExpr(expr=StrExpr(value="{:{}}"), name="format"), args=[inner, _], ): match inner: case CallExpr( callee=NameExpr(fullname=fullname) as func, args=[_], ) if fullname in CONVERSIONS: func_name = f"{{{func.name}(x)}}" conversion = f"{{{CONVERSIONS[fullname or '']}}}" # noqa: FURB143, E501 errors.append( ErrorInfo.from_node(node, f"Replace `{func_name}` with `{conversion}`") ) refurb-1.27.0/refurb/checks/third_party/000077500000000000000000000000001454672660200201605ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/third_party/__init__.py000066400000000000000000000000001454672660200222570ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/third_party/fastapi/000077500000000000000000000000001454672660200216075ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/third_party/fastapi/__init__.py000066400000000000000000000000001454672660200237060ustar00rootroot00000000000000refurb-1.27.0/refurb/checks/third_party/fastapi/simplify_query.py000066400000000000000000000041051454672660200252420ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr, EllipsisExpr, FuncDef, NameExpr from refurb.error import Error @dataclass class ErrorInfo(Error): """ FastAPI will automatically pass along query parameters to your function, so you only need to use `Query()` when you use params other than `default`. Bad: ``` @app.get("/") def index(name: str = Query()) -> str: return f"Your name is {name}" ``` Good: ``` @app.get("/") def index(name: str) -> str: return f"Your name is {name}" ``` """ name = "simplify-fastapi-query" code = 175 categories = ("fastapi", "readability") def check(node: FuncDef, errors: list[Error]) -> None: for arg in node.arguments: name = arg.variable.fullname match arg.initializer: case CallExpr( callee=NameExpr(fullname="fastapi.param_functions.Query"), args=query_args, arg_names=query_arg_names, ): if len(query_arg_names) > 1: continue ty = ": T" if arg.type_annotation else "" is_ellipsis = False if query_arg_names: # Query(...) is special in that it acts the same as Query() # so keep track of this so we can emit a better message. is_ellipsis = isinstance(query_args[0], EllipsisExpr) query_arg = "..." if is_ellipsis else "x" if query_arg_names[0] == "default": query = f"Query(default={query_arg})" elif query_arg_names[0] is None: query = f"Query({query_arg})" else: continue else: query = "Query()" old = f"{name}{ty} = {query}" new = f"{name}{ty}" if is_ellipsis else f"{name}{ty} = x" msg = f"Replace `{old}` with `{new}`" errors.append(ErrorInfo.from_node(arg, msg)) refurb-1.27.0/refurb/error.py000066400000000000000000000032101454672660200160660ustar00rootroot00000000000000from __future__ import annotations from dataclasses import dataclass from typing import TYPE_CHECKING, ClassVar if TYPE_CHECKING: from pathlib import Path from mypy.nodes import Node @dataclass(frozen=True) class ErrorCode: """ This class represents an error code id which can be used to enable and disable errors in Refurb. The `path` field is used to tell Refurb that a particular error should only apply to a given path instead of all paths, which is the default. """ id: int prefix: str = "FURB" path: Path | None = None @classmethod def from_error(cls, err: type[Error]) -> ErrorCode: return ErrorCode(err.code, err.prefix) def __str__(self) -> str: return f"{self.prefix}{self.id}" @dataclass(frozen=True) class ErrorCategory: value: str path: Path | None = None ErrorClassifier = ErrorCategory | ErrorCode @dataclass class Error: enabled: ClassVar[bool] = True name: ClassVar[str | None] = None prefix: ClassVar[str] = "FURB" categories: ClassVar[tuple[str, ...]] = () code: ClassVar[int] line: int column: int msg: str filename: str | None = None line_end: int | None = None column_end: int | None = None def __str__(self) -> str: return f"{self.filename}:{self.line}:{self.column + 1} [{self.prefix}{self.code}]: {self.msg}" # noqa: E501 @classmethod def from_node(cls, node: Node, msg: str | None = None) -> Error: return cls( node.line, node.column, line_end=node.end_line, column_end=node.end_column, msg=msg or cls.msg, ) refurb-1.27.0/refurb/explain.py000066400000000000000000000022521454672660200164020ustar00rootroot00000000000000from pathlib import Path from textwrap import dedent from refurb.loader import get_error_class, get_modules from refurb.settings import Settings from .error import ErrorCode def explain(settings: Settings) -> str: lookup = settings.explain for module in get_modules(settings.load): error = get_error_class(module) if error and ErrorCode.from_error(error) == lookup: docstring = error.__doc__ or "" if docstring.startswith(f"{error.__name__}("): return f'refurb: Explanation for "{lookup}" not found' output = "" if settings.verbose: root = Path(__file__).parent.parent file = Path(module.__file__ or "").relative_to(root) output += f"Filename: {file}\n\n" docstring = dedent(error.__doc__ or "").strip() name = error.name or "" error_code = ErrorCode.from_error(error) categories = " ".join(f"[{x}]" for x in error.categories) output += f"{error_code}: {name} {categories}\n\n{docstring}" return output return f'refurb: Error code "{lookup}" not found' refurb-1.27.0/refurb/gen.py000066400000000000000000000071641454672660200155220ustar00rootroot00000000000000import os import sys from collections import defaultdict from contextlib import suppress from pathlib import Path from subprocess import PIPE, run from .error import ErrorCode from .loader import get_error_class, get_modules from .visitor import METHOD_NODE_MAPPINGS FILE_TEMPLATE = '''\ from dataclasses import dataclass {imports} from refurb.error import Error @dataclass class ErrorInfo(Error): """ TODO: fill this in Bad: ``` # TODO: fill this in ``` Good: ``` # TODO: fill this in ``` """ prefix = "{prefix}" code = {id} msg: str = "Your message here" def check(node: {accept_type}, errors: list[Error]) -> None: match node: case {pattern}: errors.append(ErrorInfo.from_node(node)) ''' def fzf(data: list[str] | None, args: list[str]) -> str: env = os.environ | { "SHELL": "/bin/bash", "FZF_DEFAULT_COMMAND": "find refurb -name '*.py' -not -path '*__*' 2> /dev/null || true", # noqa: E501 } process = run( # noqa: PLW1510 ["fzf", "--height=20", *args], # noqa: S603, S607 env=env, stdout=PIPE, input=bytes("\n".join(data), "utf8") if data else None, ) fzf_error_codes = (2, 130) if process.returncode in fzf_error_codes: sys.exit(1) return process.stdout[:-1].decode() def folders_needing_init_file(path: Path) -> list[Path]: path = path.resolve() cwd = Path.cwd().resolve() if path.is_relative_to(cwd): to_remove = len(cwd.parents) + 1 return [path, *list(path.parents)[:-to_remove]] return [] def get_next_error_id(prefix: str) -> int: highest = 0 for module in get_modules([]): if error := get_error_class(module): error_code = ErrorCode.from_error(error) if error_code.prefix == prefix: highest = max(highest, error_code.id + 1) return highest NODES: dict[str, type] = {x.__name__: x for x in METHOD_NODE_MAPPINGS.values()} def node_type_prompt() -> list[str]: return sorted(fzf(list(NODES.keys()), args=["--prompt", "type> ", "--multi"]).splitlines()) def filename_prompt() -> Path: return Path( fzf( None, args=[ "--prompt", "filename> ", "--print-query", "--query", "refurb/checks/", ], ).splitlines()[0] ) def prefix_prompt() -> str: return fzf([""], args=["--prompt", "prefix> ", "--print-query", "--query", "FURB"]) def build_imports(names: list[str]) -> str: modules: defaultdict[str, list[str]] = defaultdict(list) for name in names: modules[NODES[name].__module__].append(name) return "\n".join( f"from {module} import {', '.join(names)}" for module, names in sorted(modules.items(), key=lambda x: x[0]) ) def main() -> None: selected = node_type_prompt() file = filename_prompt() if file.suffix != ".py": print('refurb: File must end in ".py"') sys.exit(1) prefix = prefix_prompt() template = FILE_TEMPLATE.format( accept_type=" | ".join(selected), imports=build_imports(selected), prefix=prefix, id=get_next_error_id(prefix) or 100, pattern=" | ".join(f"{x}()" for x in selected), ) with suppress(FileExistsError): file.parent.mkdir(parents=True, exist_ok=True) for folder in folders_needing_init_file(file.parent): (folder / "__init__.py").touch(exist_ok=True) file.write_text(template, "utf8") print(f"Generated {file}") if __name__ == "__main__": main() refurb-1.27.0/refurb/loader.py000066400000000000000000000123121454672660200162060ustar00rootroot00000000000000import importlib import pkgutil import sys from collections import defaultdict from collections.abc import Generator from importlib.metadata import entry_points from inspect import getsourcefile, getsourcelines, signature from pathlib import Path from types import GenericAlias, ModuleType, UnionType from typing import Any, TypeGuard from mypy.nodes import Node from refurb.visitor.mapping import METHOD_NODE_MAPPINGS from . import checks as checks_module from .error import Error, ErrorCategory, ErrorCode from .settings import Settings from .types import Check def get_modules(paths: list[str]) -> Generator[ModuleType, None, None]: sys.path.append(str(Path.cwd())) plugins = [x.value for x in entry_points(group="refurb.plugins")] extra_modules = (importlib.import_module(x) for x in paths + plugins) loaded: set[ModuleType] = set() for pkg in (checks_module, *extra_modules): if pkg in loaded: continue if not hasattr(pkg, "__path__"): module = importlib.import_module(pkg.__name__) if module not in loaded: loaded.add(module) yield module continue for info in pkgutil.walk_packages(pkg.__path__, f"{pkg.__name__}."): if info.ispkg: continue module = importlib.import_module(info.name) if module not in loaded: loaded.add(module) yield module loaded.add(pkg) def is_valid_error_class(obj: Any) -> TypeGuard[type[Error]]: # type: ignore if not hasattr(obj, "__name__"): return False name = obj.__name__ ignored_names = ("Error", "ErrorCode", "ErrorCategory") return name.startswith("Error") and name not in ignored_names and issubclass(obj, Error) def get_error_class(module: ModuleType) -> type[Error] | None: for name in dir(module): if name.startswith("Error") and name not in {"Error", "ErrorCode"}: error = getattr(module, name) if is_valid_error_class(error): return error return None def should_load_check(settings: Settings, error: type[Error]) -> bool: error_code = ErrorCode.from_error(error) if error_code in settings.enable: return True if error_code in (settings.disable | settings.ignore): return False categories = {ErrorCategory(cat) for cat in error.categories} if settings.enable & categories: return True if settings.disable & categories or settings.disable_all: return False return error.enabled or settings.enable_all VALID_NODE_TYPES = set(METHOD_NODE_MAPPINGS.values()) VALID_OPTIONAL_ARGS = (("settings", Settings),) def type_error_with_line_info(func: Any, msg: str) -> TypeError: # type: ignore filename = getsourcefile(func) line = getsourcelines(func)[1] if not filename: return TypeError(msg) # pragma: no cover return TypeError(f"{filename}:{line}: {msg}") def extract_function_types( # type: ignore func: Any, ) -> Generator[type[Node], None, None]: if not callable(func): raise TypeError("Check function must be callable") params = list(signature(func).parameters.values()) if len(params) not in {2, 3}: raise type_error_with_line_info(func, "Check function must take 2-3 parameters") node_param = params[0].annotation error_param = params[1].annotation optional_params = params[2:] if not ( type(error_param) == GenericAlias and error_param.__origin__ is list and error_param.__args__[0] is Error ): raise type_error_with_line_info(func, '"error" param must be of type list[Error]') for param in optional_params: if (param.name, param.annotation) not in VALID_OPTIONAL_ARGS: raise type_error_with_line_info( func, f'"{param.name}: {param.annotation.__name__}" is not a valid service', # noqa: E501 ) match node_param: case UnionType() as types: for ty in types.__args__: if ty not in VALID_NODE_TYPES: raise type_error_with_line_info( func, f'"{ty.__name__}" is not a valid Mypy node type', ) yield ty case ty if ty in VALID_NODE_TYPES: yield ty case _: raise type_error_with_line_info( func, f'"{ty.__name__}" is not a valid Mypy node type', ) def load_checks(settings: Settings) -> defaultdict[type[Node], list[Check]]: found: defaultdict[type[Node], list[Check]] = defaultdict(list) enabled_errors: set[str] = set() for module in get_modules(settings.load): error = get_error_class(module) if error and should_load_check(settings, error): if func := getattr(module, "check", None): for ty in extract_function_types(func): found[ty].append(func) enabled_errors.add(str(ErrorCode.from_error(error))) if settings.verbose: msg = ", ".join(sorted(enabled_errors)) if enabled_errors else "No checks enabled" print(f"Enabled checks: {msg}\n") return found refurb-1.27.0/refurb/main.py000066400000000000000000000245311454672660200156720ustar00rootroot00000000000000import json import re import time from collections.abc import Callable, Sequence from contextlib import suppress from functools import cache, partial from importlib import metadata from io import StringIO from pathlib import Path from tempfile import mkstemp from mypy.build import build from mypy.errors import CompileError from mypy.main import process_options from .error import Error, ErrorCode from .explain import explain from .gen import main as generate from .loader import load_checks from .settings import Settings, load_settings from .visitor import RefurbVisitor def usage() -> None: print( """\ usage: refurb [--ignore err] [--load path] [--debug] [--quiet] [--enable err] [--disable err] [--enable-all] [--disable-all] [--config-file path] [--python-version version] [--verbose | -v] [--format format] [--sort sort] [--timing-stats file] SRC [SRCS...] [-- MYPY_ARGS] refurb [--help | -h] refurb [--version] refurb --explain err refurb gen Command Line Options: --help, -h This help menu. --version Print version information. --ignore err Ignore an error. Can be repeated. --load module Add a module to the list of paths to be searched when looking for checks. Can be repeated. --debug Print the AST representation of all files that where checked. --quiet Suppress default "--explain" suggestion when an error occurs. --enable err Load a check which is disabled by default. --disable err Disable loading a check which is enabled by default. --config-file file Load "file" instead of the default config file. --explain err Print the explanation/documentation from a given error code. --disable-all Disable all checks by default. --enable-all Enable all checks by default. --python-version x.y Version of the Python code being checked. --verbose Increase verbosity. --format format Output errors in specified format. Can be "text" or "github". --sort sort Sort errors by sort. Can be "filename" or "error". --timing-stats file Export timing information (as JSON) to file. Positional Args: SRC A list of files or folders to check. MYPY_ARGS Extra args to be passed directly to Mypy. Subcommands: gen Generate boilerplate code for a new check. Useful for developers. """ ) def version() -> str: # pragma: no cover refurb_version = metadata.version("refurb") mypy_version = metadata.version("mypy") return f"Refurb: v{refurb_version}\nMypy: v{mypy_version}" @cache def get_source_lines(filepath: str) -> list[str]: return Path(filepath).read_text("utf8").splitlines() def is_ignored_via_comment(error: Error) -> bool: assert error.filename line = get_source_lines(error.filename)[error.line - 1].rstrip() if comment := re.search(r"""# noqa(: [^'"]*)?$""", line): ignore = str(ErrorCode.from_error(type(error))) error_codes = comment.group(1) return not error_codes or any( error_code == ignore for error_code in error_codes[2:].replace(",", " ").split(" ") ) return False def is_ignored_via_amend(error: Error, settings: Settings) -> bool: assert error.filename path = Path(error.filename).resolve() error_code = ErrorCode.from_error(type(error)) config_root = Path(settings.config_file).parent if settings.config_file else Path() for ignore in settings.ignore: if ignore.path: ignore_path = (config_root / ignore.path).resolve() if path.is_relative_to(ignore_path): if isinstance(ignore, ErrorCode): return str(ignore) == str(error_code) return ignore.value in error.categories return False def should_ignore_error(error: Error | str, settings: Settings) -> bool: if isinstance(error, str): return False return ( not error.filename or is_ignored_via_comment(error) or is_ignored_via_amend(error, settings) ) def run_refurb(settings: Settings) -> Sequence[Error | str]: stdout = StringIO() stderr = StringIO() try: args = [ *settings.files, *settings.mypy_args, "--exclude", ".*\\.pyi", "--explicit-package-bases", "--namespace-packages", ] files, opt = process_options(args, stdout=stdout, stderr=stderr) except SystemExit: lines = ["refurb: " + err for err in stderr.getvalue().splitlines()] return lines + stdout.getvalue().splitlines() finally: stdout.close() stderr.close() opt.incremental = True opt.fine_grained_incremental = True opt.cache_fine_grained = True opt.allow_redefinition = True opt.local_partial_types = True opt.python_version = settings.get_python_version() mypy_timing_stats = Path(mkstemp()[1]) if settings.timing_stats else None opt.timing_stats = str(mypy_timing_stats) if mypy_timing_stats else None try: start = time.time() result = build(files, options=opt) mypy_build_time = time.time() - start except CompileError as e: return [re.sub("^mypy: ", "refurb: ", msg) for msg in e.messages] errors: list[Error | str] = [] checks = load_checks(settings) refurb_timing_stats_in_ms: dict[str, int] = {} for file in files: tree = result.graph[file.module].tree assert tree if settings.debug: errors.append(str(tree)) start = time.time() visitor = RefurbVisitor(checks, settings) # See: https://github.com/dosisod/refurb/issues/302 with suppress(RecursionError): visitor.accept(tree) elapsed = time.time() - start refurb_timing_stats_in_ms[file.module] = int(elapsed * 1_000) for error in visitor.errors: error.filename = file.path errors += visitor.errors output_timing_stats( settings, mypy_build_time, mypy_timing_stats, refurb_timing_stats_in_ms, ) if mypy_timing_stats: mypy_timing_stats.unlink() return sorted( [error for error in errors if not should_ignore_error(error, settings)], key=partial(sort_errors, settings=settings), ) def sort_errors(error: Error | str, settings: Settings) -> tuple[str | int, ...]: if isinstance(error, str): return ("", error) if settings.sort_by == "error": return ( error.prefix, error.code, error.filename or "", error.line, error.column, ) return ( error.filename or "", error.line, error.column, error.prefix, error.code, ) def format_as_github_annotation(error: Error | str) -> str: if isinstance(error, str): return f"::error title=Refurb Error::{error}" assert error.filename file = Path(error.filename).resolve().relative_to(Path.cwd()) return "::error " + ",".join( [ f"line={error.line}", f"col={error.column + 1}", f"title=Refurb {error.prefix}{error.code}", f"file={file}::{error.msg}", ] ) ERROR_DIFF_PATTERN = re.compile(r"`([^`]*)`([^`]*)`([^`]*)`") def format_with_color(error: Error | str) -> str: if isinstance(error, str): return error blue = "\x1b[94m" yellow = "\x1b[33m" gray = "\x1b[90m" green = "\x1b[92m" red = "\x1b[91m" reset = "\x1b[0m" # Add red/green color for diffs, assuming the 2 pairs of backticks are in the form: # Replace `old` with `new` if error.msg.count("`") == 4: parts = [ f"{gray}`{red}\\1{gray}`{reset}", "\\2", f"{gray}`{green}\\3{gray}`{reset}", ] error.msg = ERROR_DIFF_PATTERN.sub("".join(parts), error.msg) parts = [ f"{blue}{error.filename}{reset}", f"{gray}:{error.line}:{error.column + 1}{reset}", " ", f"{yellow}[{error.prefix}{error.code}]{reset}", f"{gray}:{reset}", " ", error.msg, ] return "".join(parts) def format_errors(errors: Sequence[Error | str], settings: Settings) -> str: if settings.format == "github": formatter: Callable[[Error | str], str] = format_as_github_annotation elif settings.color: formatter = format_with_color else: formatter = str done = "\n".join(formatter(error) for error in errors) if not settings.quiet and any(isinstance(err, Error) for err in errors): done += "\n\nRun `refurb --explain ERR` to further explain an error. Use `--quiet` to silence this message" return done def output_timing_stats( settings: Settings, mypy_total_time_spent: float, mypy_timing_stats: Path | None, refurb_timing_stats_in_ms: dict[str, int], ) -> None: if not settings.timing_stats: return assert mypy_timing_stats mypy_stats: dict[str, int] = {} lines = mypy_timing_stats.read_text().splitlines() for line in lines: module, micro_seconds = line.split() mypy_stats[module] = int(micro_seconds) // 1_000 data = { "mypy_total_time_spent_in_ms": int(mypy_total_time_spent * 1_000), "mypy_time_spent_parsing_modules_in_ms": dict( sorted(mypy_stats.items(), key=lambda x: x[1], reverse=True) ), "refurb_time_spent_checking_file_in_ms": dict( sorted( refurb_timing_stats_in_ms.items(), key=lambda x: x[1], reverse=True, ) ), } settings.timing_stats.write_text(json.dumps(data, separators=(",", ":"))) def main(args: list[str]) -> int: try: settings = load_settings(args) except ValueError as e: print(e) return 1 if settings.help: usage() return 0 if settings.version: print(version()) return 0 if settings.generate: generate() return 0 if settings.explain: print(explain(settings)) return 0 try: errors = run_refurb(settings) except TypeError as e: print(e) return 1 if formatted_errors := format_errors(errors, settings): print(formatted_errors) return 1 if errors else 0 refurb-1.27.0/refurb/py.typed000066400000000000000000000000001454672660200160540ustar00rootroot00000000000000refurb-1.27.0/refurb/settings.py000066400000000000000000000256321454672660200166110ustar00rootroot00000000000000from __future__ import annotations import os import re import sys from dataclasses import dataclass, field, replace from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, TypeVar if TYPE_CHECKING: from collections.abc import Callable, Iterator if sys.version_info >= (3, 11): import tomllib # pragma: no cover else: import tomli as tomllib # pragma: no cover from .error import ErrorCategory, ErrorClassifier, ErrorCode def get_python_version() -> tuple[int, int]: return sys.version_info[:2] @dataclass class Settings: files: list[str] = field(default_factory=list) explain: ErrorCode | None = None ignore: set[ErrorClassifier] = field(default_factory=set) load: list[str] = field(default_factory=list) enable: set[ErrorClassifier] = field(default_factory=set) disable: set[ErrorClassifier] = field(default_factory=set) debug: bool = False generate: bool = False help: bool = False version: bool = False quiet: bool = False enable_all: bool = False disable_all: bool = False config_file: str | None = None python_version: tuple[int, int] | None = None mypy_args: list[str] = field(default_factory=list) format: Literal["text", "github"] | None = None sort_by: Literal["filename", "error"] | None = None verbose: bool = False timing_stats: Path | None = None color: bool = True def __post_init__(self) -> None: if self.enable_all and self.disable_all: raise ValueError( 'refurb: "enable all" and "disable all" can\'t be used at the same time' # noqa: E501 ) if os.getenv("NO_COLOR") or not sys.stdout.isatty(): self.color = False @staticmethod def merge(old: Settings, new: Settings) -> Settings: if not old.disable_all and new.disable_all: enable = new.enable disable = set() elif not old.enable_all and new.enable_all: disable = new.disable enable = set() else: disable = old.disable | new.disable enable = (old.enable | new.enable) - disable return Settings( files=old.files + new.files, explain=old.explain or new.explain, ignore=old.ignore | new.ignore, enable=enable, disable=disable, load=old.load + new.load, debug=old.debug or new.debug, generate=old.generate or new.generate, help=old.help or new.help, version=old.version or new.version, disable_all=old.disable_all or new.disable_all, enable_all=old.enable_all or new.enable_all, quiet=old.quiet or new.quiet, config_file=old.config_file or new.config_file, python_version=new.python_version or old.python_version, mypy_args=new.mypy_args or old.mypy_args, format=new.format or old.format, sort_by=new.sort_by or old.sort_by, verbose=old.verbose or new.verbose, timing_stats=old.timing_stats or new.timing_stats, color=old.color and new.color, ) def get_python_version(self) -> tuple[int, int]: return self.python_version or get_python_version() ERROR_ID_REGEX = re.compile("^([A-Z]{3,4})?(\\d{3})$") def parse_error_classifier(err: str) -> ErrorCategory | ErrorCode: return parse_error_category(err) or parse_error_id(err) def parse_error_category(err: str) -> ErrorCategory | None: return ErrorCategory(err[1:]) if err.startswith("#") else None def parse_error_id(err: str) -> ErrorCode: if match := ERROR_ID_REGEX.match(err): groups = match.groups() return ErrorCode(prefix=groups[0] or "FURB", id=int(groups[1])) raise ValueError(f'refurb: "{err}" must be in form FURB123 or 123') def parse_python_version(version: str) -> tuple[int, int]: nums = version.split(".") if len(nums) == 2 and all(num.isnumeric() for num in nums): return tuple(int(num) for num in nums)[:2] # type: ignore raise ValueError("refurb: version must be in form `x.y`") def validate_format(format: str) -> Literal["github", "text"]: if format in {"github", "text"}: return format # type: ignore raise ValueError(f'refurb: "{format}" is not a valid format') def validate_sort_by(sort_by: str) -> Literal["filename", "error"]: if sort_by in {"filename", "error"}: return sort_by # type: ignore raise ValueError(f'refurb: cannot sort by "{sort_by}"') def parse_amend_error(err: str, path: Path) -> ErrorClassifier: classifier = parse_error_classifier(err) return replace(classifier, path=path) def parse_amendment(amendment: dict[str, Any]) -> set[ErrorClassifier]: # type: ignore match amendment: case {"path": str(path), "ignore": list(ignored), **extra}: if extra: raise ValueError('refurb: only "path" and "ignore" fields are supported') return {parse_amend_error(str(error), Path(path)) for error in ignored} raise ValueError('refurb: "path" or "ignore" fields are missing or malformed') T = TypeVar("T") def pop_type(ty: type[T], type_name: str = "") -> Callable[..., T]: # type: ignore[misc] def inner(config: dict[str, Any], name: str, *, default: T | None = None) -> T: # type: ignore[misc] x = config.pop(name, default or ty()) if isinstance(x, ty): return x raise ValueError(f'refurb: "{name}" must be a {type_name or ty.__name__}') return inner pop_list = pop_type(list) pop_bool = pop_type(bool) pop_str = pop_type(str, "string") def parse_config_file(contents: str) -> Settings: tool = tomllib.loads(contents).get("tool") if not tool: return Settings() config = tool.get("refurb") if not config: return Settings() settings = Settings() settings.load = pop_list(config, "load") settings.quiet = pop_bool(config, "quiet") settings.disable_all = pop_bool(config, "disable_all") settings.enable_all = pop_bool(config, "enable_all") settings.color = pop_bool(config, "color", default=True) enable = pop_list(config, "enable") disable = pop_list(config, "disable") settings.enable = {parse_error_classifier(str(x)) for x in enable} settings.disable = {parse_error_classifier(str(x)) for x in disable} settings.enable -= settings.disable ignore = pop_list(config, "ignore") settings.ignore = {parse_error_classifier(str(x)) for x in ignore} mypy_args = pop_list(config, "mypy_args") settings.mypy_args = [str(x) for x in mypy_args] if "python_version" in config: version = pop_str(config, "python_version") settings.python_version = parse_python_version(version) if "format" in config: settings.format = validate_format(pop_str(config, "format")) if "sort_by" in config: settings.sort_by = validate_sort_by(pop_str(config, "sort_by")) amendments: list[dict[str, Any]] = config.pop("amend", []) # type: ignore if not isinstance(amendments, list): raise ValueError('refurb: "amend" field(s) must be a TOML table') for amendment in amendments: settings.ignore.update(parse_amendment(amendment)) if config: raise ValueError(f"refurb: unknown field(s): {', '.join(config.keys())}") return settings def parse_command_line_args(args: list[str]) -> Settings: if not args: return Settings(help=True) if len(args) == 1 and args[0] == "gen": return Settings(generate=True) iargs = iter(args) settings = Settings() def get_next_arg(arg: str, args: Iterator[str]) -> str: if (value := next(args, None)) is not None: return value raise ValueError(f'refurb: missing argument after "{arg}"') for arg in iargs: if arg == "--debug": settings.debug = True elif arg in {"--help", "-h"}: settings.help = True elif arg == "--version": settings.version = True elif arg == "--quiet": settings.quiet = True elif arg == "--disable-all": settings.enable.clear() settings.disable_all = True elif arg == "--enable-all": settings.disable.clear() settings.enable_all = True elif arg == "--explain": settings.explain = parse_error_id(get_next_arg(arg, iargs)) elif arg == "--ignore": classifiers = get_next_arg(arg, iargs).split(",") settings.ignore.update(map(parse_error_classifier, classifiers)) elif arg == "--enable": error_codes = { parse_error_classifier(classifier) for classifier in get_next_arg(arg, iargs).split(",") } settings.enable |= error_codes settings.disable -= error_codes elif arg == "--disable": error_codes = { parse_error_classifier(classifier) for classifier in get_next_arg(arg, iargs).split(",") } settings.disable |= error_codes settings.enable -= error_codes elif arg == "--load": settings.load.append(get_next_arg(arg, iargs)) elif arg == "--config-file": settings.config_file = get_next_arg(arg, iargs) elif arg == "--python-version": version = get_next_arg(arg, iargs) settings.python_version = parse_python_version(version) elif arg == "--format": settings.format = validate_format(get_next_arg(arg, iargs)) elif arg == "--sort": settings.sort_by = validate_sort_by(get_next_arg(arg, iargs)) elif arg in {"--verbose", "-v"}: settings.verbose = True elif arg == "--timing-stats": settings.timing_stats = Path(get_next_arg(arg, iargs)) elif arg == "--no-color": settings.color = False elif arg == "--": settings.mypy_args = list(iargs) elif arg.startswith("-"): raise ValueError(f'refurb: unsupported option "{arg}"') elif arg: settings.files.append(arg) else: raise ValueError("refurb: argument cannot be empty") if len(args) > 1 and (settings.help or settings.version): msg = f"refurb: unexpected value before/after `{args[0]}`" raise ValueError(msg) return settings def load_settings(args: list[str]) -> Settings: cli_args = parse_command_line_args(args) file = Path(cli_args.config_file or "pyproject.toml") try: config_file = parse_config_file(file.read_text()) except IsADirectoryError as ex: raise ValueError(f'refurb: "{file}" is a directory') from ex except FileNotFoundError as ex: if cli_args.config_file: raise ValueError(f'refurb: "{file}" was not found') from ex config_file = Settings() # pragma: no cover return Settings.merge(config_file, cli_args) refurb-1.27.0/refurb/types.py000066400000000000000000000004671454672660200161140ustar00rootroot00000000000000from collections import defaultdict from collections.abc import Callable from mypy.nodes import Node from refurb.error import Error from refurb.settings import Settings Check = Callable[[Node, list[Error]], None] | Callable[[Node, list[Error], Settings], None] Checks = defaultdict[type[Node], list[Check]] refurb-1.27.0/refurb/visitor/000077500000000000000000000000001454672660200160665ustar00rootroot00000000000000refurb-1.27.0/refurb/visitor/__init__.py000066400000000000000000000002761454672660200202040ustar00rootroot00000000000000from .mapping import METHOD_NODE_MAPPINGS from .traverser import TraverserVisitor from .visitor import RefurbVisitor __all__ = ("METHOD_NODE_MAPPINGS", "RefurbVisitor", "TraverserVisitor") refurb-1.27.0/refurb/visitor/mapping.py000066400000000000000000000102341454672660200200730ustar00rootroot00000000000000import mypy.nodes import mypy.patterns VisitorNodeTypeMap = dict[str, type[mypy.nodes.Node]] METHOD_NODE_MAPPINGS: VisitorNodeTypeMap = { "visit_as_pattern": mypy.patterns.AsPattern, "visit_assert_stmt": mypy.nodes.AssertStmt, "visit_assert_type_expr": mypy.nodes.AssertTypeExpr, "visit_assignment_expr": mypy.nodes.AssignmentExpr, "visit_assignment_stmt": mypy.nodes.AssignmentStmt, "visit_await_expr": mypy.nodes.AwaitExpr, "visit_block": mypy.nodes.Block, "visit_break_stmt": mypy.nodes.BreakStmt, "visit_bytes_expr": mypy.nodes.BytesExpr, "visit_call_expr": mypy.nodes.CallExpr, "visit_cast_expr": mypy.nodes.CastExpr, "visit_class_def": mypy.nodes.ClassDef, "visit_class_pattern": mypy.patterns.ClassPattern, "visit_comparison_expr": mypy.nodes.ComparisonExpr, "visit_complex_expr": mypy.nodes.ComplexExpr, "visit_conditional_expr": mypy.nodes.ConditionalExpr, "visit_continue_stmt": mypy.nodes.ContinueStmt, "visit_decorator": mypy.nodes.Decorator, "visit_del_stmt": mypy.nodes.DelStmt, "visit_dict_expr": mypy.nodes.DictExpr, "visit_dictionary_comprehension": mypy.nodes.DictionaryComprehension, "visit_ellipsis": mypy.nodes.EllipsisExpr, "visit_enum_call_expr": mypy.nodes.EnumCallExpr, "visit_expression_stmt": mypy.nodes.ExpressionStmt, "visit_float_expr": mypy.nodes.FloatExpr, "visit_for_stmt": mypy.nodes.ForStmt, "visit_func_def": mypy.nodes.FuncDef, "visit_func": mypy.nodes.FuncItem, "visit_generator_expr": mypy.nodes.GeneratorExpr, "visit_global_decl": mypy.nodes.GlobalDecl, "visit_if_stmt": mypy.nodes.IfStmt, "visit_import_all": mypy.nodes.ImportAll, "visit_import_from": mypy.nodes.ImportFrom, "visit_import": mypy.nodes.Import, "visit_index_expr": mypy.nodes.IndexExpr, "visit_int_expr": mypy.nodes.IntExpr, "visit_lambda_expr": mypy.nodes.LambdaExpr, "visit_list_comprehension": mypy.nodes.ListComprehension, "visit_list_expr": mypy.nodes.ListExpr, "visit_mapping_pattern": mypy.patterns.MappingPattern, "visit_match_stmt": mypy.nodes.MatchStmt, "visit_member_expr": mypy.nodes.MemberExpr, "visit_mypy_file": mypy.nodes.MypyFile, "visit_namedtuple_expr": mypy.nodes.NamedTupleExpr, "visit_name_expr": mypy.nodes.NameExpr, "visit_newtype_expr": mypy.nodes.NewTypeExpr, "visit_nonlocal_decl": mypy.nodes.NonlocalDecl, "visit_operator_assignment_stmt": mypy.nodes.OperatorAssignmentStmt, "visit_op_expr": mypy.nodes.OpExpr, "visit_or_pattern": mypy.patterns.OrPattern, "visit_overloaded_func_def": mypy.nodes.OverloadedFuncDef, "visit_paramspec_expr": mypy.nodes.ParamSpecExpr, "visit_pass_stmt": mypy.nodes.PassStmt, "visit_placeholder_node": mypy.nodes.PlaceholderNode, "visit__promote_expr": mypy.nodes.PromoteExpr, "visit_raise_stmt": mypy.nodes.RaiseStmt, "visit_return_stmt": mypy.nodes.ReturnStmt, "visit_reveal_expr": mypy.nodes.RevealExpr, "visit_sequence_pattern": mypy.patterns.SequencePattern, "visit_set_comprehension": mypy.nodes.SetComprehension, "visit_set_expr": mypy.nodes.SetExpr, "visit_singleton_pattern": mypy.patterns.SingletonPattern, "visit_slice_expr": mypy.nodes.SliceExpr, "visit_star_expr": mypy.nodes.StarExpr, "visit_starred_pattern": mypy.patterns.StarredPattern, "visit_str_expr": mypy.nodes.StrExpr, "visit_super_expr": mypy.nodes.SuperExpr, "visit_temp_node": mypy.nodes.TempNode, "visit_try_stmt": mypy.nodes.TryStmt, "visit_tuple_expr": mypy.nodes.TupleExpr, "visit_type_alias_expr": mypy.nodes.TypeAliasExpr, "visit_type_alias": mypy.nodes.TypeAlias, "visit_type_application": mypy.nodes.TypeApplication, "visit_typeddict_expr": mypy.nodes.TypedDictExpr, "visit_type_var_expr": mypy.nodes.TypeVarExpr, "visit_type_var_tuple_expr": mypy.nodes.TypeVarTupleExpr, "visit_unary_expr": mypy.nodes.UnaryExpr, "visit_value_pattern": mypy.patterns.ValuePattern, "visit_var": mypy.nodes.Var, "visit_while_stmt": mypy.nodes.WhileStmt, "visit_with_stmt": mypy.nodes.WithStmt, "visit_yield_expr": mypy.nodes.YieldExpr, "visit_yield_from_expr": mypy.nodes.YieldFromExpr, } refurb-1.27.0/refurb/visitor/traverser.py000066400000000000000000000661531454672660200204700ustar00rootroot00000000000000# This work is substantially derived from mypy (https://mypy-lang.org/), and # is licensed under the same terms # (https://github.com/python/mypy/blob/master/LICENSE) with all credits to the # original author(s) and contributor(s), reproduced below. # # = = = = = # # The MIT License # # Copyright (c) 2012-2023 Jukka Lehtosalo and contributors # Copyright (c) 2015-2023 Dropbox, Inc. # # 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. # # = = = = = from __future__ import annotations import functools import mypy.nodes import mypy.traverser from mypy.nodes import AssertStmt as AssertStmt from mypy.nodes import AssertTypeExpr as AssertTypeExpr from mypy.nodes import AssignmentExpr as AssignmentExpr from mypy.nodes import AssignmentStmt as AssignmentStmt from mypy.nodes import AwaitExpr as AwaitExpr from mypy.nodes import Block as Block from mypy.nodes import BreakStmt as BreakStmt from mypy.nodes import BytesExpr as BytesExpr from mypy.nodes import CallExpr as CallExpr from mypy.nodes import CastExpr as CastExpr from mypy.nodes import ClassDef as ClassDef from mypy.nodes import ComparisonExpr as ComparisonExpr from mypy.nodes import ComplexExpr as ComplexExpr from mypy.nodes import ConditionalExpr as ConditionalExpr from mypy.nodes import Context as Context from mypy.nodes import ContinueStmt as ContinueStmt from mypy.nodes import Decorator as Decorator from mypy.nodes import DelStmt as DelStmt from mypy.nodes import DictExpr as DictExpr from mypy.nodes import DictionaryComprehension as DictionaryComprehension from mypy.nodes import EllipsisExpr as EllipsisExpr from mypy.nodes import EnumCallExpr as EnumCallExpr from mypy.nodes import ExpressionStmt as ExpressionStmt from mypy.nodes import FloatExpr as FloatExpr from mypy.nodes import ForStmt as ForStmt from mypy.nodes import FuncDef as FuncDef from mypy.nodes import GeneratorExpr as GeneratorExpr from mypy.nodes import GlobalDecl as GlobalDecl from mypy.nodes import IfStmt as IfStmt from mypy.nodes import Import as Import from mypy.nodes import ImportAll as ImportAll from mypy.nodes import ImportFrom as ImportFrom from mypy.nodes import IndexExpr as IndexExpr from mypy.nodes import IntExpr as IntExpr from mypy.nodes import LambdaExpr as LambdaExpr from mypy.nodes import ListComprehension as ListComprehension from mypy.nodes import ListExpr as ListExpr from mypy.nodes import MatchStmt as MatchStmt from mypy.nodes import MemberExpr as MemberExpr from mypy.nodes import MypyFile as MypyFile from mypy.nodes import NamedTupleExpr as NamedTupleExpr from mypy.nodes import NameExpr as NameExpr from mypy.nodes import NewTypeExpr as NewTypeExpr from mypy.nodes import NonlocalDecl as NonlocalDecl from mypy.nodes import OperatorAssignmentStmt as OperatorAssignmentStmt from mypy.nodes import OpExpr as OpExpr from mypy.nodes import OverloadedFuncDef as OverloadedFuncDef from mypy.nodes import ParamSpecExpr as ParamSpecExpr from mypy.nodes import PassStmt as PassStmt from mypy.nodes import PlaceholderNode as PlaceholderNode from mypy.nodes import PromoteExpr as PromoteExpr from mypy.nodes import RaiseStmt as RaiseStmt from mypy.nodes import ReturnStmt as ReturnStmt from mypy.nodes import RevealExpr as RevealExpr from mypy.nodes import SetComprehension as SetComprehension from mypy.nodes import SetExpr as SetExpr from mypy.nodes import SliceExpr as SliceExpr from mypy.nodes import StarExpr as StarExpr from mypy.nodes import StrExpr as StrExpr from mypy.nodes import SuperExpr as SuperExpr from mypy.nodes import TempNode as TempNode from mypy.nodes import TryStmt as TryStmt from mypy.nodes import TupleExpr as TupleExpr from mypy.nodes import TypeAlias as TypeAlias from mypy.nodes import TypeAliasExpr as TypeAliasExpr from mypy.nodes import TypeApplication as TypeApplication from mypy.nodes import TypedDictExpr as TypedDictExpr from mypy.nodes import TypeVarExpr as TypeVarExpr from mypy.nodes import TypeVarTupleExpr as TypeVarTupleExpr from mypy.nodes import UnaryExpr as UnaryExpr from mypy.nodes import Var as Var from mypy.nodes import WhileStmt as WhileStmt from mypy.nodes import WithStmt as WithStmt from mypy.nodes import YieldExpr as YieldExpr from mypy.nodes import YieldFromExpr as YieldFromExpr from mypy.patterns import AsPattern as AsPattern from mypy.patterns import ClassPattern as ClassPattern from mypy.patterns import MappingPattern as MappingPattern from mypy.patterns import OrPattern as OrPattern from mypy.patterns import SequencePattern as SequencePattern from mypy.patterns import SingletonPattern as SingletonPattern from mypy.patterns import StarredPattern as StarredPattern from mypy.patterns import ValuePattern as ValuePattern from mypy.types import RequiredType as RequiredType class TraverserVisitor: """A parse tree visitor that traverses the parse tree during visiting. It does not perform any actions outside the traversal. Subclasses should override visit methods to perform actions during traversal. Calling the superclass method allows reusing the traversal implementation. """ def __init__(self) -> None: pass def accept(self, o: Context) -> None: return accept(o, self) def visit_func(self, o: mypy.nodes.FuncItem) -> None: if o.arguments is not None: for arg in o.arguments: init = arg.initializer if init is not None: accept(init, self) for arg in o.arguments: self.visit_var(arg.variable) accept(o.body, self) def visit_mypy_file(self, o: MypyFile) -> None: for d in o.defs: accept(d, self) def visit_var(self, o: Var) -> None: pass def visit_type_alias(self, o: TypeAlias) -> None: pass def visit_placeholder_node(self, o: PlaceholderNode) -> None: pass def visit_int_expr(self, o: IntExpr) -> None: pass def visit_str_expr(self, o: StrExpr) -> None: pass def visit_bytes_expr(self, o: BytesExpr) -> None: pass def visit_float_expr(self, o: FloatExpr) -> None: pass def visit_complex_expr(self, o: ComplexExpr) -> None: pass def visit_ellipsis(self, o: EllipsisExpr) -> None: pass def visit_star_expr(self, o: StarExpr) -> None: accept(o.expr, self) def visit_name_expr(self, o: NameExpr) -> None: pass def visit_member_expr(self, o: MemberExpr) -> None: accept(o.expr, self) def visit_yield_from_expr(self, o: YieldFromExpr) -> None: accept(o.expr, self) def visit_yield_expr(self, o: YieldExpr) -> None: if o.expr: accept(o.expr, self) def visit_call_expr(self, o: CallExpr) -> None: accept(o.callee, self) for a in o.args: accept(a, self) if o.analyzed: accept(o.analyzed, self) def visit_op_expr(self, o: OpExpr) -> None: accept(o.left, self) accept(o.right, self) if o.analyzed is not None: accept(o.analyzed, self) def visit_comparison_expr(self, o: ComparisonExpr) -> None: for operand in o.operands: accept(operand, self) def visit_cast_expr(self, o: CastExpr) -> None: accept(o.expr, self) def visit_assert_type_expr(self, o: AssertTypeExpr) -> None: accept(o.expr, self) def visit_reveal_expr(self, o: RevealExpr) -> None: if o.kind == mypy.nodes.REVEAL_TYPE: assert o.expr is not None accept(o.expr, self) else: pass def visit_super_expr(self, o: SuperExpr) -> None: accept(o.call, self) def visit_unary_expr(self, o: UnaryExpr) -> None: accept(o.expr, self) def visit_assignment_expr(self, o: AssignmentExpr) -> None: accept(o.target, self) accept(o.value, self) def visit_list_expr(self, o: ListExpr) -> None: for item in o.items: accept(item, self) def visit_dict_expr(self, o: DictExpr) -> None: for k, v in o.items: if k is not None: accept(k, self) accept(v, self) def visit_tuple_expr(self, o: TupleExpr) -> None: for item in o.items: accept(item, self) def visit_set_expr(self, o: SetExpr) -> None: for item in o.items: accept(item, self) def visit_index_expr(self, o: IndexExpr) -> None: accept(o.base, self) accept(o.index, self) if o.analyzed: accept(o.analyzed, self) def visit_type_application(self, o: TypeApplication) -> None: accept(o.expr, self) def visit_lambda_expr(self, o: LambdaExpr) -> None: self.visit_func(o) def visit_list_comprehension(self, o: ListComprehension) -> None: accept(o.generator, self) def visit_set_comprehension(self, o: SetComprehension) -> None: accept(o.generator, self) def visit_dictionary_comprehension(self, o: DictionaryComprehension) -> None: for index, sequence, conditions in zip(o.indices, o.sequences, o.condlists): accept(sequence, self) accept(index, self) for cond in conditions: accept(cond, self) accept(o.key, self) accept(o.value, self) def visit_generator_expr(self, o: GeneratorExpr) -> None: for index, sequence, conditions in zip(o.indices, o.sequences, o.condlists): accept(sequence, self) accept(index, self) for cond in conditions: accept(cond, self) accept(o.left_expr, self) def visit_slice_expr(self, o: SliceExpr) -> None: if o.begin_index is not None: accept(o.begin_index, self) if o.end_index is not None: accept(o.end_index, self) if o.stride is not None: accept(o.stride, self) def visit_conditional_expr(self, o: ConditionalExpr) -> None: accept(o.cond, self) accept(o.if_expr, self) accept(o.else_expr, self) def visit_type_var_expr(self, o: TypeVarExpr) -> None: pass def visit_paramspec_expr(self, o: ParamSpecExpr) -> None: pass def visit_type_var_tuple_expr(self, o: TypeVarTupleExpr) -> None: pass def visit_type_alias_expr(self, o: TypeAliasExpr) -> None: pass def visit_namedtuple_expr(self, o: NamedTupleExpr) -> None: pass def visit_enum_call_expr(self, o: EnumCallExpr) -> None: pass def visit_typeddict_expr(self, o: TypedDictExpr) -> None: pass def visit_newtype_expr(self, o: NewTypeExpr) -> None: pass def visit__promote_expr(self, o: PromoteExpr) -> None: pass def visit_await_expr(self, o: AwaitExpr) -> None: accept(o.expr, self) def visit_temp_node(self, o: TempNode) -> None: pass def visit_assignment_stmt(self, o: AssignmentStmt) -> None: accept(o.rvalue, self) for l in o.lvalues: accept(l, self) def visit_for_stmt(self, o: ForStmt) -> None: accept(o.index, self) accept(o.expr, self) accept(o.body, self) if o.else_body: accept(o.else_body, self) def visit_with_stmt(self, o: WithStmt) -> None: for i in range(len(o.expr)): accept(o.expr[i], self) targ = o.target[i] if targ is not None: accept(targ, self) accept(o.body, self) def visit_del_stmt(self, o: DelStmt) -> None: if o.expr is not None: accept(o.expr, self) def visit_func_def(self, o: FuncDef) -> None: self.visit_func(o) def visit_overloaded_func_def(self, o: OverloadedFuncDef) -> None: for item in o.items: accept(item, self) if o.impl: accept(o.impl, self) def visit_class_def(self, o: ClassDef) -> None: for d in o.decorators: accept(d, self) for base in o.base_type_exprs: accept(base, self) if o.metaclass: accept(o.metaclass, self) for v in o.keywords.values(): accept(v, self) accept(o.defs, self) if o.analyzed: accept(o.analyzed, self) def visit_global_decl(self, o: GlobalDecl) -> None: pass def visit_nonlocal_decl(self, o: NonlocalDecl) -> None: pass def visit_decorator(self, o: Decorator) -> None: accept(o.func, self) accept(o.var, self) for decorator in o.decorators: accept(decorator, self) def visit_import(self, o: Import) -> None: for a in o.assignments: accept(a, self) def visit_import_from(self, o: ImportFrom) -> None: for a in o.assignments: accept(a, self) def visit_import_all(self, o: ImportAll) -> None: pass def visit_block(self, block: Block) -> None: for s in block.body: accept(s, self) def visit_expression_stmt(self, o: ExpressionStmt) -> None: accept(o.expr, self) def visit_operator_assignment_stmt(self, o: OperatorAssignmentStmt) -> None: accept(o.rvalue, self) accept(o.lvalue, self) def visit_while_stmt(self, o: WhileStmt) -> None: accept(o.expr, self) accept(o.body, self) if o.else_body: accept(o.else_body, self) def visit_return_stmt(self, o: ReturnStmt) -> None: if o.expr is not None: accept(o.expr, self) def visit_assert_stmt(self, o: AssertStmt) -> None: if o.expr is not None: accept(o.expr, self) if o.msg is not None: accept(o.msg, self) def visit_if_stmt(self, o: IfStmt) -> None: for e in o.expr: accept(e, self) for b in o.body: accept(b, self) if o.else_body: accept(o.else_body, self) def visit_break_stmt(self, o: BreakStmt) -> None: pass def visit_continue_stmt(self, o: ContinueStmt) -> None: pass def visit_pass_stmt(self, o: PassStmt) -> None: pass def visit_raise_stmt(self, o: RaiseStmt) -> None: if o.expr is not None: accept(o.expr, self) if o.from_expr is not None: accept(o.from_expr, self) def visit_try_stmt(self, o: TryStmt) -> None: accept(o.body, self) for i in range(len(o.types)): tp = o.types[i] if tp is not None: accept(tp, self) accept(o.handlers[i], self) for v in o.vars: if v is not None: accept(v, self) if o.else_body is not None: accept(o.else_body, self) if o.finally_body is not None: accept(o.finally_body, self) def visit_match_stmt(self, o: MatchStmt) -> None: accept(o.subject, self) for i in range(len(o.patterns)): accept(o.patterns[i], self) guard = o.guards[i] if guard is not None: accept(guard, self) accept(o.bodies[i], self) def visit_as_pattern(self, o: AsPattern) -> None: if o.pattern is not None: accept(o.pattern, self) if o.name is not None: accept(o.name, self) def visit_or_pattern(self, o: OrPattern) -> None: for p in o.patterns: accept(p, self) def visit_value_pattern(self, o: ValuePattern) -> None: accept(o.expr, self) def visit_singleton_pattern(self, o: SingletonPattern) -> None: pass def visit_sequence_pattern(self, o: SequencePattern) -> None: for p in o.patterns: accept(p, self) def visit_starred_pattern(self, o: StarredPattern) -> None: if o.capture is not None: accept(o.capture, self) def visit_mapping_pattern(self, o: MappingPattern) -> None: for key in o.keys: accept(key, self) for value in o.values: accept(value, self) if o.rest is not None: accept(o.rest, self) def visit_class_pattern(self, o: ClassPattern) -> None: accept(o.class_ref, self) for p in o.positionals: accept(p, self) for v in o.keyword_values: accept(v, self) @functools.singledispatch def accept(node: Context, visitor: TraverserVisitor) -> None: raise NotImplementedError(f"No `visit_*` overload available for `{type(node).__qualname__}`") @accept.register def _(node: MypyFile, visitor: TraverserVisitor) -> None: return visitor.visit_mypy_file(node) @accept.register def _(node: Import, visitor: TraverserVisitor) -> None: return visitor.visit_import(node) @accept.register def _(node: ImportFrom, visitor: TraverserVisitor) -> None: return visitor.visit_import_from(node) @accept.register def _(node: ImportAll, visitor: TraverserVisitor) -> None: return visitor.visit_import_all(node) @accept.register def _(node: OverloadedFuncDef, visitor: TraverserVisitor) -> None: return visitor.visit_overloaded_func_def(node) @accept.register def _(node: FuncDef, visitor: TraverserVisitor) -> None: return visitor.visit_func_def(node) @accept.register def _(node: Decorator, visitor: TraverserVisitor) -> None: return visitor.visit_decorator(node) @accept.register def _(node: Var, visitor: TraverserVisitor) -> None: return visitor.visit_var(node) @accept.register def _(node: ClassDef, visitor: TraverserVisitor) -> None: return visitor.visit_class_def(node) @accept.register def _(node: GlobalDecl, visitor: TraverserVisitor) -> None: return visitor.visit_global_decl(node) @accept.register def _(node: NonlocalDecl, visitor: TraverserVisitor) -> None: return visitor.visit_nonlocal_decl(node) @accept.register def _(node: Block, visitor: TraverserVisitor) -> None: return visitor.visit_block(node) @accept.register def _(node: ExpressionStmt, visitor: TraverserVisitor) -> None: return visitor.visit_expression_stmt(node) @accept.register def _(node: AssignmentStmt, visitor: TraverserVisitor) -> None: return visitor.visit_assignment_stmt(node) @accept.register def _(node: OperatorAssignmentStmt, visitor: TraverserVisitor) -> None: return visitor.visit_operator_assignment_stmt(node) @accept.register def _(node: WhileStmt, visitor: TraverserVisitor) -> None: return visitor.visit_while_stmt(node) @accept.register def _(node: ForStmt, visitor: TraverserVisitor) -> None: return visitor.visit_for_stmt(node) @accept.register def _(node: ReturnStmt, visitor: TraverserVisitor) -> None: return visitor.visit_return_stmt(node) @accept.register def _(node: AssertStmt, visitor: TraverserVisitor) -> None: return visitor.visit_assert_stmt(node) @accept.register def _(node: DelStmt, visitor: TraverserVisitor) -> None: return visitor.visit_del_stmt(node) @accept.register def _(node: BreakStmt, visitor: TraverserVisitor) -> None: return visitor.visit_break_stmt(node) @accept.register def _(node: ContinueStmt, visitor: TraverserVisitor) -> None: return visitor.visit_continue_stmt(node) @accept.register def _(node: PassStmt, visitor: TraverserVisitor) -> None: return visitor.visit_pass_stmt(node) @accept.register def _(node: IfStmt, visitor: TraverserVisitor) -> None: return visitor.visit_if_stmt(node) @accept.register def _(node: RaiseStmt, visitor: TraverserVisitor) -> None: return visitor.visit_raise_stmt(node) @accept.register def _(node: TryStmt, visitor: TraverserVisitor) -> None: return visitor.visit_try_stmt(node) @accept.register def _(node: WithStmt, visitor: TraverserVisitor) -> None: return visitor.visit_with_stmt(node) @accept.register def _(node: MatchStmt, visitor: TraverserVisitor) -> None: return visitor.visit_match_stmt(node) @accept.register def _(node: IntExpr, visitor: TraverserVisitor) -> None: return visitor.visit_int_expr(node) @accept.register def _(node: StrExpr, visitor: TraverserVisitor) -> None: return visitor.visit_str_expr(node) @accept.register def _(node: BytesExpr, visitor: TraverserVisitor) -> None: return visitor.visit_bytes_expr(node) @accept.register def _(node: FloatExpr, visitor: TraverserVisitor) -> None: return visitor.visit_float_expr(node) @accept.register def _(node: ComplexExpr, visitor: TraverserVisitor) -> None: return visitor.visit_complex_expr(node) @accept.register def _(node: EllipsisExpr, visitor: TraverserVisitor) -> None: return visitor.visit_ellipsis(node) @accept.register def _(node: StarExpr, visitor: TraverserVisitor) -> None: return visitor.visit_star_expr(node) @accept.register def _(node: NameExpr, visitor: TraverserVisitor) -> None: return visitor.visit_name_expr(node) @accept.register def _(node: MemberExpr, visitor: TraverserVisitor) -> None: return visitor.visit_member_expr(node) @accept.register def _(node: CallExpr, visitor: TraverserVisitor) -> None: return visitor.visit_call_expr(node) @accept.register def _(node: YieldFromExpr, visitor: TraverserVisitor) -> None: return visitor.visit_yield_from_expr(node) @accept.register def _(node: YieldExpr, visitor: TraverserVisitor) -> None: return visitor.visit_yield_expr(node) @accept.register def _(node: IndexExpr, visitor: TraverserVisitor) -> None: return visitor.visit_index_expr(node) @accept.register def _(node: UnaryExpr, visitor: TraverserVisitor) -> None: return visitor.visit_unary_expr(node) @accept.register def _(node: AssignmentExpr, visitor: TraverserVisitor) -> None: return visitor.visit_assignment_expr(node) @accept.register def _(node: OpExpr, visitor: TraverserVisitor) -> None: return visitor.visit_op_expr(node) @accept.register def _(node: ComparisonExpr, visitor: TraverserVisitor) -> None: return visitor.visit_comparison_expr(node) @accept.register def _(node: SliceExpr, visitor: TraverserVisitor) -> None: return visitor.visit_slice_expr(node) @accept.register def _(node: CastExpr, visitor: TraverserVisitor) -> None: return visitor.visit_cast_expr(node) @accept.register def _(node: AssertTypeExpr, visitor: TraverserVisitor) -> None: return visitor.visit_assert_type_expr(node) @accept.register def _(node: RevealExpr, visitor: TraverserVisitor) -> None: return visitor.visit_reveal_expr(node) @accept.register def _(node: SuperExpr, visitor: TraverserVisitor) -> None: return visitor.visit_super_expr(node) @accept.register def _(node: LambdaExpr, visitor: TraverserVisitor) -> None: return visitor.visit_lambda_expr(node) @accept.register def _(node: ListExpr, visitor: TraverserVisitor) -> None: return visitor.visit_list_expr(node) @accept.register def _(node: DictExpr, visitor: TraverserVisitor) -> None: return visitor.visit_dict_expr(node) @accept.register def _(node: TupleExpr, visitor: TraverserVisitor) -> None: return visitor.visit_tuple_expr(node) @accept.register def _(node: SetExpr, visitor: TraverserVisitor) -> None: return visitor.visit_set_expr(node) @accept.register def _(node: GeneratorExpr, visitor: TraverserVisitor) -> None: return visitor.visit_generator_expr(node) @accept.register def _(node: ListComprehension, visitor: TraverserVisitor) -> None: return visitor.visit_list_comprehension(node) @accept.register def _(node: SetComprehension, visitor: TraverserVisitor) -> None: return visitor.visit_set_comprehension(node) @accept.register def _(node: DictionaryComprehension, visitor: TraverserVisitor) -> None: return visitor.visit_dictionary_comprehension(node) @accept.register def _(node: ConditionalExpr, visitor: TraverserVisitor) -> None: return visitor.visit_conditional_expr(node) @accept.register def _(node: TypeApplication, visitor: TraverserVisitor) -> None: return visitor.visit_type_application(node) @accept.register def _(node: TypeVarExpr, visitor: TraverserVisitor) -> None: return visitor.visit_type_var_expr(node) @accept.register def _(node: ParamSpecExpr, visitor: TraverserVisitor) -> None: return visitor.visit_paramspec_expr(node) @accept.register def _(node: TypeVarTupleExpr, visitor: TraverserVisitor) -> None: return visitor.visit_type_var_tuple_expr(node) @accept.register def _(node: TypeAliasExpr, visitor: TraverserVisitor) -> None: return visitor.visit_type_alias_expr(node) @accept.register def _(node: NamedTupleExpr, visitor: TraverserVisitor) -> None: return visitor.visit_namedtuple_expr(node) @accept.register def _(node: TypedDictExpr, visitor: TraverserVisitor) -> None: return visitor.visit_typeddict_expr(node) @accept.register def _(node: EnumCallExpr, visitor: TraverserVisitor) -> None: return visitor.visit_enum_call_expr(node) @accept.register def _(node: PromoteExpr, visitor: TraverserVisitor) -> None: return visitor.visit__promote_expr(node) @accept.register def _(node: NewTypeExpr, visitor: TraverserVisitor) -> None: return visitor.visit_newtype_expr(node) @accept.register def _(node: AwaitExpr, visitor: TraverserVisitor) -> None: return visitor.visit_await_expr(node) @accept.register def _(node: TempNode, visitor: TraverserVisitor) -> None: return visitor.visit_temp_node(node) @accept.register def _(node: TypeAlias, visitor: TraverserVisitor) -> None: return visitor.visit_type_alias(node) @accept.register def _(node: PlaceholderNode, visitor: TraverserVisitor) -> None: return visitor.visit_placeholder_node(node) @accept.register def _(node: AsPattern, visitor: TraverserVisitor) -> None: return visitor.visit_as_pattern(node) @accept.register def _(node: OrPattern, visitor: TraverserVisitor) -> None: return visitor.visit_or_pattern(node) @accept.register def _(node: ValuePattern, visitor: TraverserVisitor) -> None: return visitor.visit_value_pattern(node) @accept.register def _(node: SingletonPattern, visitor: TraverserVisitor) -> None: return visitor.visit_singleton_pattern(node) @accept.register def _(node: SequencePattern, visitor: TraverserVisitor) -> None: return visitor.visit_sequence_pattern(node) @accept.register def _(node: StarredPattern, visitor: TraverserVisitor) -> None: return visitor.visit_starred_pattern(node) @accept.register def _(node: MappingPattern, visitor: TraverserVisitor) -> None: return visitor.visit_mapping_pattern(node) @accept.register def _(node: ClassPattern, visitor: TraverserVisitor) -> None: return visitor.visit_class_pattern(node) @accept.register def _(node: RequiredType, visitor: TraverserVisitor) -> None: return accept(node.item, visitor) refurb-1.27.0/refurb/visitor/visitor.py000066400000000000000000000036071454672660200201450ustar00rootroot00000000000000from collections import defaultdict from collections.abc import Callable from mypy.nodes import CallExpr, Node from refurb.error import Error from refurb.settings import Settings from refurb.types import Check, Checks from refurb.visitor import TraverserVisitor from .mapping import METHOD_NODE_MAPPINGS VisitorMethod = Callable[["RefurbVisitor", Node], None] def build_visitor(name: str, ty: type[Node], checks: Checks) -> VisitorMethod: def inner(self: RefurbVisitor, o: Node) -> None: for check in checks[ty]: self.run_check(o, check) getattr(TraverserVisitor, name)(self, o) inner.__name__ = name inner.__annotations__["o"] = ty return inner class RefurbVisitor(TraverserVisitor): errors: list[Error] settings: Settings _dont_build = ("visit_call_expr",) def __init__(self, checks: defaultdict[type[Node], list[Check]], settings: Settings) -> None: self.errors = [] self.checks = checks self.settings = settings types = set(self.checks.keys()) for name, type in METHOD_NODE_MAPPINGS.items(): if type in types and name not in self._dont_build: func = build_visitor(name, type, self.checks) setattr(self, name, func.__get__(self)) def visit_call_expr(self, o: CallExpr) -> None: for check in self.checks[CallExpr]: self.run_check(o, check) for arg in o.args: self.accept(arg) self.accept(o.callee) def run_check(self, node: Node, check: Check) -> None: # Hack: use the type annotations to check if the function takes 2 or # 3 arguments. There is an extra field for return types, hence why we # use 4. if len(check.__annotations__) == 4: check(node, self.errors, self.settings) # type: ignore else: check(node, self.errors) # type: ignore refurb-1.27.0/test/000077500000000000000000000000001454672660200140615ustar00rootroot00000000000000refurb-1.27.0/test/__init__.py000066400000000000000000000000001454672660200161600ustar00rootroot00000000000000refurb-1.27.0/test/config/000077500000000000000000000000001454672660200153265ustar00rootroot00000000000000refurb-1.27.0/test/config/amend_config.toml000066400000000000000000000000741454672660200206350ustar00rootroot00000000000000[[tool.refurb.amend]] path = "../data" ignore = ["FURB123"] refurb-1.27.0/test/config/config.toml000066400000000000000000000000351454672660200174660ustar00rootroot00000000000000[tool.refurb] ignore = [101] refurb-1.27.0/test/conftest.py000066400000000000000000000007071454672660200162640ustar00rootroot00000000000000from collections.abc import Generator from unittest.mock import Mock, patch import pytest @pytest.fixture(autouse=True) def fake_tty() -> Generator[Mock, None, None]: # Pytest doesnt run in a TTY, so the new TTY detection code is causing a lot of color related # tests to fail. This hack makes it so color is always enabled, like it would in a normal TTY. with patch("sys.stdout.isatty") as p: p.return_value = True yield p refurb-1.27.0/test/custom_checks/000077500000000000000000000000001454672660200167135ustar00rootroot00000000000000refurb-1.27.0/test/custom_checks/__init__.py000066400000000000000000000000001454672660200210120ustar00rootroot00000000000000refurb-1.27.0/test/custom_checks/disabled_check.py000066400000000000000000000005421454672660200221720ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import MypyFile from refurb.error import Error @dataclass class ErrorInfo(Error): enabled = False prefix = "XYZ" code = 101 msg: str = "This message is disabled by default" def check(node: MypyFile, errors: list[Error]) -> None: errors.append(ErrorInfo(node.line, node.column)) refurb-1.27.0/test/custom_checks/disallow_call.py000066400000000000000000000007311454672660200220770ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import CallExpr from refurb.error import Error @dataclass class ErrorDisallowCall(Error): """ This check will simply emit an error whenever a `CallExpr` node is hit """ prefix = "XYZ" code = 100 msg: str = "Your message here" def check(node: CallExpr, errors: list[Error]) -> None: match node: case CallExpr(): errors.append(ErrorDisallowCall(node.line, node.column)) refurb-1.27.0/test/custom_checks/no_docstring.py000066400000000000000000000005711454672660200217600ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import EllipsisExpr from refurb.error import Error @dataclass class ErrorInfo(Error): prefix = "XYZ" code = 102 msg: str = "Your message here" def check(node: EllipsisExpr, errors: list[Error]) -> None: match node: case EllipsisExpr(): errors.append(ErrorInfo(node.line, node.column)) refurb-1.27.0/test/custom_checks/settings.py000066400000000000000000000010771454672660200211320ustar00rootroot00000000000000from dataclasses import dataclass from mypy.nodes import MypyFile from refurb.error import Error from refurb.settings import Settings @dataclass class ErrorInfo(Error): """ TODO: fill this in Bad: ``` # TODO: fill this in ``` Good: ``` # TODO: fill this in ``` """ prefix = "XYZ" code = 103 msg: str = "Your message here" def check(node: MypyFile, errors: list[Error], settings: Settings) -> None: msg = f"Files being checked: {settings.files}" errors.append(ErrorInfo(node.line, node.column, msg)) refurb-1.27.0/test/data/000077500000000000000000000000001454672660200147725ustar00rootroot00000000000000refurb-1.27.0/test/data/bug_cast.py000066400000000000000000000003101454672660200171250ustar00rootroot00000000000000from typing import cast # Due to how Mypy's default traverser works, this expression will emit 2 # errors instead of just one. This is fixed now, and should only return 1 # error. cast(int, int(0)) refurb-1.27.0/test/data/bug_cast.txt000066400000000000000000000001001454672660200173110ustar00rootroot00000000000000test/data/bug_cast.py:7:11 [FURB123]: Replace `int(x)` with `x` refurb-1.27.0/test/data/bug_equivalent_nodes.py000066400000000000000000000043521454672660200215520ustar00rootroot00000000000000# These tests ensure that when comparing nodes to make sure that they are # similar, extraneous info such as line numbers don't interfere. In short, if # two nodes are semanticaly similar, but only differ in line number, they # should still be considered equivalent. # See issue #97 class Person: name: str age: int def __init__(self, name: str) -> None: self.name = name bob = Person("bob") # The following examples should all emit errors # member expr _ = ( bob.name if bob.name else "alice" ) nums = [1, 2, 3] # index expr _ = ( nums[0] if nums[0] else 123 ) def f(x: int) -> int: return x # func call expr _ = ( f(1) if f(1) else 2 ) # list expr _ = ( [1, 2, 3] if [1, 2, 3] else [] ) # star "*" expr _ = ( [*nums, 4, 5, 6] if [*nums, 4, 5, 6] else [] ) # unary oper _ = ( not False if not False else False ) # binary oper _ = ( 1 + 2 if 1 + 2 else 3 ) # comparison expr _ = ( 1 < 2 if 1 < 2 else 3 ) # slice expr _ = ( nums[1:] if nums[1:] else nums ) # dict expr _ = ( {"k": "v"} if {"k": "v"} else {} ) # tuple expr _ = ( (1, 2, 3) if (1, 2, 3) else () ) # set expr _ = ( {1, 2, 3} if {1, 2, 3} else set() ) # These should not _ = bob.age if bob.name else 123 _ = f(1) if f(2) else 2 _ = f(1) if f(x=1) else 2 _ = [1, 2, 3] if [1, 2, 3, 4] else [] _ = [1, 2, 3] if [1, 2, 4] else [] _ = [*nums, 1, 2, 3] if [*nums, 4, 5, 6] else [] _ = [*nums, 1, 2, 3] if [*nums, 1, 2, 3, 4] else [] nums2 = [] _ = [*nums, 1, 2, 3] if [*nums2, 1, 2, 3] else [] _ = - 1 if - 2 else 3 _ = - 1 if + 1 else 2 _ = 1 + 2 if 1 - 2 else 3 _ = 1 + 2 if 1 + 3 else 3 _ = 1 + 2 if 2 + 2 else 3 _ = 1 < 2 if 1 > 2 else 3 _ = 1 < 2 if 1 < 1 else 3 _ = 1 < 2 if 2 < 2 else 3 _ = 1 < 2 if 1 < 2 < 3 else 3 _ = nums[1:] if nums[2:] else nums _ = nums[:1] if nums[:2] else nums _ = nums[::1] if nums[::2] else nums _ = {"k": "v"} if {"k": "not v"} else {} _ = {"k": "v"} if {"not k": "v"} else {} _ = {"k": "v"} if {"k": "v", "extra": "items"} else {} _ = (1, 2, 3) if (1, 2, 3, 4) else () _ = (1, 2, 3) if (4, 5, 6) else () _ = {1, 2, 3} if {1, 2, 3, 4} else set() _ = {1, 2, 3} if {4, 5, 6} else set() refurb-1.27.0/test/data/bug_equivalent_nodes.txt000066400000000000000000000020411454672660200217320ustar00rootroot00000000000000test/data/bug_equivalent_nodes.py:21:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:30:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:42:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:49:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:56:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:63:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:70:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:77:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:84:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:91:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:98:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/bug_equivalent_nodes.py:105:5 [FURB110]: Replace `x if x else y` with `x or y` refurb-1.27.0/test/data/bug_recursion_error.py000066400000000000000000001171301454672660200214260ustar00rootroot00000000000000# File taken from the sympy python package. License is available here: # https://github.com/sympy/sympy/blob/master/LICENSE """Lookup table for Galois resolvents for polys of degree 4 through 6. """ # This table was generated by a call to # `sympy.polys.numberfields.galois_resolvents.generate_lambda_lookup()`. # The entire job took 543.23s. # Of this, Case (6, 1) took 539.03s. # The final polynomial of Case (6, 1) alone took 455.09s. resolvent_coeff_lambdas = { (4, 0): [ lambda s1, s2, s3, s4: (-2*s1*s2 + 6*s3), lambda s1, s2, s3, s4: (2*s1**3*s3 + s1**2*s2**2 + s1**2*s4 - 17*s1*s2*s3 + 2*s2**3 - 8*s2*s4 + 24*s3**2), lambda s1, s2, s3, s4: (-2*s1**5*s4 - 2*s1**4*s2*s3 + 10*s1**3*s2*s4 + 8*s1**3*s3**2 + 10*s1**2*s2**2*s3 - 12*s1**2*s3*s4 - 2*s1*s2**4 - 54*s1*s2*s3**2 + 32*s1*s4**2 + 8*s2**3*s3 - 32*s2*s3*s4 + 56*s3**3), lambda s1, s2, s3, s4: (2*s1**6*s2*s4 + s1**6*s3**2 - 5*s1**5*s3*s4 - 11*s1**4*s2**2*s4 - 13*s1**4*s2*s3**2 + 7*s1**4*s4**2 + 3*s1**3*s2**3*s3 + 30*s1**3*s2*s3*s4 + 22*s1**3*s3**3 + 10*s1**2*s2**3*s4 + 33*s1**2*s2**2*s3**2 - 72*s1**2*s2*s4**2 - 36*s1**2*s3**2*s4 - 13*s1*s2**4*s3 + 48*s1*s2**2*s3*s4 - 116*s1*s2*s3**3 + 144*s1*s3*s4**2 + s2**6 - 12*s2**4*s4 + 22*s2**3*s3**2 + 48*s2**2*s4**2 - 120*s2*s3**2*s4 + 96*s3**4 - 64*s4**3), lambda s1, s2, s3, s4: (-2*s1**8*s3*s4 - s1**7*s4**2 + 22*s1**6*s2*s3*s4 + 2*s1**6*s3**3 - 2*s1**5*s2**3*s4 - s1**5*s2**2*s3**2 - 29*s1**5*s3**2*s4 - 60*s1**4*s2**2*s3*s4 - 19*s1**4*s2*s3**3 + 38*s1**4*s3*s4**2 + 9*s1**3*s2**4*s4 + 10*s1**3*s2**3*s3**2 + 24*s1**3*s2**2*s4**2 + 134*s1**3*s2*s3**2*s4 + 28*s1**3*s3**4 + 16*s1**3*s4**3 - s1**2*s2**5*s3 - 4*s1**2*s2**3*s3*s4 + 34*s1**2*s2**2*s3**3 - 288*s1**2*s2*s3*s4**2 - 104*s1**2*s3**3*s4 - 19*s1*s2**4*s3**2 + 120*s1*s2**2*s3**2*s4 - 128*s1*s2*s3**4 + 336*s1*s3**2*s4**2 + 2*s2**6*s3 - 24*s2**4*s3*s4 + 28*s2**3*s3**3 + 96*s2**2*s3*s4**2 - 176*s2*s3**3*s4 + 96*s3**5 - 128*s3*s4**3), lambda s1, s2, s3, s4: (s1**10*s4**2 - 11*s1**8*s2*s4**2 - 2*s1**8*s3**2*s4 + s1**7*s2**2*s3*s4 + 15*s1**7*s3*s4**2 + 45*s1**6*s2**2*s4**2 + 17*s1**6*s2*s3**2*s4 + s1**6*s3**4 - 5*s1**6*s4**3 - 12*s1**5*s2**3*s3*s4 - 133*s1**5*s2*s3*s4**2 - 22*s1**5*s3**3*s4 + s1**4*s2**5*s4 - 76*s1**4*s2**3*s4**2 - 6*s1**4*s2**2*s3**2*s4 - 12*s1**4*s2*s3**4 + 32*s1**4*s2*s4**3 + 128*s1**4*s3**2*s4**2 + 29*s1**3*s2**4*s3*s4 + 2*s1**3*s2**3*s3**3 + 344*s1**3*s2**2*s3*s4**2 + 48*s1**3*s2*s3**3*s4 + 16*s1**3*s3**5 - 48*s1**3*s3*s4**3 - 4*s1**2*s2**6*s4 + 32*s1**2*s2**4*s4**2 - 134*s1**2*s2**3*s3**2*s4 + 36*s1**2*s2**2*s3**4 - 64*s1**2*s2**2*s4**3 - 648*s1**2*s2*s3**2*s4**2 - 48*s1**2*s3**4*s4 + 16*s1*s2**5*s3*s4 - 12*s1*s2**4*s3**3 - 128*s1*s2**3*s3*s4**2 + 296*s1*s2**2*s3**3*s4 - 96*s1*s2*s3**5 + 256*s1*s2*s3*s4**3 + 416*s1*s3**3*s4**2 + s2**6*s3**2 - 28*s2**4*s3**2*s4 + 16*s2**3*s3**4 + 176*s2**2*s3**2*s4**2 - 224*s2*s3**4*s4 + 64*s3**6 - 320*s3**2*s4**3) ], (4, 1): [ lambda s1, s2, s3, s4: (-s2), lambda s1, s2, s3, s4: (s1*s3 - 4*s4), lambda s1, s2, s3, s4: (-s1**2*s4 + 4*s2*s4 - s3**2) ], (5, 1): [ lambda s1, s2, s3, s4, s5: (-2*s1*s3 + 8*s4), lambda s1, s2, s3, s4, s5: (-8*s1**3*s5 + 2*s1**2*s2*s4 + s1**2*s3**2 + 30*s1*s2*s5 - 14*s1*s3*s4 - 6*s2**2*s4 + 2*s2*s3**2 - 50*s3*s5 + 40*s4**2), lambda s1, s2, s3, s4, s5: (16*s1**4*s3*s5 - 2*s1**4*s4**2 - 2*s1**3*s2**2*s5 - 2*s1**3*s2*s3*s4 - 44*s1**3*s4*s5 - 66*s1**2*s2*s3*s5 + 21*s1**2*s2*s4**2 + 6*s1**2*s3**2*s4 - 50*s1**2*s5**2 + 9*s1*s2**3*s5 + 5*s1*s2**2*s3*s4 - 2*s1*s2*s3**3 + 190*s1*s2*s4*s5 + 120*s1*s3**2*s5 - 80*s1*s3*s4**2 - 15*s2**2*s3*s5 - 40*s2**2*s4**2 + 21*s2*s3**2*s4 + 125*s2*s5**2 - 2*s3**4 - 400*s3*s4*s5 + 160*s4**3), lambda s1, s2, s3, s4, s5: (16*s1**6*s5**2 - 8*s1**5*s2*s4*s5 - 8*s1**5*s3**2*s5 + 2*s1**5*s3*s4**2 + 2*s1**4*s2**2*s3*s5 + s1**4*s2**2*s4**2 - 120*s1**4*s2*s5**2 + 68*s1**4*s3*s4*s5 - 8*s1**4*s4**3 + 46*s1**3*s2**2*s4*s5 + 28*s1**3*s2*s3**2*s5 - 19*s1**3*s2*s3*s4**2 + 250*s1**3*s3*s5**2 - 144*s1**3*s4**2*s5 - 9*s1**2*s2**3*s3*s5 - 6*s1**2*s2**3*s4**2 + 3*s1**2*s2**2*s3**2*s4 + 225*s1**2*s2**2*s5**2 - 354*s1**2*s2*s3*s4*s5 + 76*s1**2*s2*s4**3 - 70*s1**2*s3**3*s5 + 41*s1**2*s3**2*s4**2 - 200*s1**2*s4*s5**2 - 54*s1*s2**3*s4*s5 + 45*s1*s2**2*s3**2*s5 + 30*s1*s2**2*s3*s4**2 - 19*s1*s2*s3**3*s4 - 875*s1*s2*s3*s5**2 + 640*s1*s2*s4**2*s5 + 2*s1*s3**5 + 630*s1*s3**2*s4*s5 - 264*s1*s3*s4**3 + 9*s2**4*s4**2 - 6*s2**3*s3**2*s4 + s2**2*s3**4 + 90*s2**2*s3*s4*s5 - 136*s2**2*s4**3 - 50*s2*s3**3*s5 + 76*s2*s3**2*s4**2 + 500*s2*s4*s5**2 - 8*s3**4*s4 + 625*s3**2*s5**2 - 1400*s3*s4**2*s5 + 400*s4**4), lambda s1, s2, s3, s4, s5: (-32*s1**7*s3*s5**2 + 8*s1**7*s4**2*s5 + 8*s1**6*s2**2*s5**2 + 8*s1**6*s2*s3*s4*s5 - 2*s1**6*s2*s4**3 + 48*s1**6*s4*s5**2 - 2*s1**5*s2**3*s4*s5 + 264*s1**5*s2*s3*s5**2 - 94*s1**5*s2*s4**2*s5 - 24*s1**5*s3**2*s4*s5 + 6*s1**5*s3*s4**3 - 56*s1**5*s5**3 - 66*s1**4*s2**3*s5**2 - 50*s1**4*s2**2*s3*s4*s5 + 19*s1**4*s2**2*s4**3 + 8*s1**4*s2*s3**3*s5 - 2*s1**4*s2*s3**2*s4**2 - 318*s1**4*s2*s4*s5**2 - 352*s1**4*s3**2*s5**2 + 166*s1**4*s3*s4**2*s5 + 3*s1**4*s4**4 + 15*s1**3*s2**4*s4*s5 - 2*s1**3*s2**3*s3**2*s5 - s1**3*s2**3*s3*s4**2 - 574*s1**3*s2**2*s3*s5**2 + 347*s1**3*s2**2*s4**2*s5 + 194*s1**3*s2*s3**2*s4*s5 - 89*s1**3*s2*s3*s4**3 + 350*s1**3*s2*s5**3 - 8*s1**3*s3**4*s5 + 4*s1**3*s3**3*s4**2 + 1090*s1**3*s3*s4*s5**2 - 364*s1**3*s4**3*s5 + 162*s1**2*s2**4*s5**2 + 33*s1**2*s2**3*s3*s4*s5 - 51*s1**2*s2**3*s4**3 - 32*s1**2*s2**2*s3**3*s5 + 28*s1**2*s2**2*s3**2*s4**2 + 305*s1**2*s2**2*s4*s5**2 - 2*s1**2*s2*s3**4*s4 + 1340*s1**2*s2*s3**2*s5**2 - 901*s1**2*s2*s3*s4**2*s5 + 76*s1**2*s2*s4**4 - 234*s1**2*s3**3*s4*s5 + 102*s1**2*s3**2*s4**3 - 750*s1**2*s3*s5**3 - 550*s1**2*s4**2*s5**2 - 27*s1*s2**5*s4*s5 + 9*s1*s2**4*s3**2*s5 + 3*s1*s2**4*s3*s4**2 - s1*s2**3*s3**3*s4 + 180*s1*s2**3*s3*s5**2 - 366*s1*s2**3*s4**2*s5 - 231*s1*s2**2*s3**2*s4*s5 + 212*s1*s2**2*s3*s4**3 - 375*s1*s2**2*s5**3 + 112*s1*s2*s3**4*s5 - 89*s1*s2*s3**3*s4**2 - 3075*s1*s2*s3*s4*s5**2 + 1640*s1*s2*s4**3*s5 + 6*s1*s3**5*s4 - 850*s1*s3**3*s5**2 + 1220*s1*s3**2*s4**2*s5 - 384*s1*s3*s4**4 + 2500*s1*s4*s5**3 - 108*s2**5*s5**2 + 117*s2**4*s3*s4*s5 + 32*s2**4*s4**3 - 31*s2**3*s3**3*s5 - 51*s2**3*s3**2*s4**2 + 525*s2**3*s4*s5**2 + 19*s2**2*s3**4*s4 - 325*s2**2*s3**2*s5**2 + 260*s2**2*s3*s4**2*s5 - 256*s2**2*s4**4 - 2*s2*s3**6 + 105*s2*s3**3*s4*s5 + 76*s2*s3**2*s4**3 + 625*s2*s3*s5**3 - 500*s2*s4**2*s5**2 - 58*s3**5*s5 + 3*s3**4*s4**2 + 2750*s3**2*s4*s5**2 - 2400*s3*s4**3*s5 + 512*s4**5 - 3125*s5**4), lambda s1, s2, s3, s4, s5: (16*s1**8*s3**2*s5**2 - 8*s1**8*s3*s4**2*s5 + s1**8*s4**4 - 8*s1**7*s2**2*s3*s5**2 + 2*s1**7*s2**2*s4**2*s5 - 48*s1**7*s3*s4*s5**2 + 12*s1**7*s4**3*s5 + s1**6*s2**4*s5**2 + 12*s1**6*s2**2*s4*s5**2 - 144*s1**6*s2*s3**2*s5**2 + 88*s1**6*s2*s3*s4**2*s5 - 13*s1**6*s2*s4**4 + 56*s1**6*s3*s5**3 + 86*s1**6*s4**2*s5**2 + 72*s1**5*s2**3*s3*s5**2 - 22*s1**5*s2**3*s4**2*s5 - 4*s1**5*s2**2*s3**2*s4*s5 + s1**5*s2**2*s3*s4**3 - 14*s1**5*s2**2*s5**3 + 304*s1**5*s2*s3*s4*s5**2 - 148*s1**5*s2*s4**3*s5 + 152*s1**5*s3**3*s5**2 - 54*s1**5*s3**2*s4**2*s5 + 5*s1**5*s3*s4**4 - 468*s1**5*s4*s5**3 - 9*s1**4*s2**5*s5**2 + s1**4*s2**4*s3*s4*s5 - 76*s1**4*s2**3*s4*s5**2 + 370*s1**4*s2**2*s3**2*s5**2 - 287*s1**4*s2**2*s3*s4**2*s5 + 65*s1**4*s2**2*s4**4 - 28*s1**4*s2*s3**3*s4*s5 + 5*s1**4*s2*s3**2*s4**3 - 200*s1**4*s2*s3*s5**3 - 294*s1**4*s2*s4**2*s5**2 + 8*s1**4*s3**5*s5 - 2*s1**4*s3**4*s4**2 - 676*s1**4*s3**2*s4*s5**2 + 180*s1**4*s3*s4**3*s5 + 17*s1**4*s4**5 + 625*s1**4*s5**4 - 210*s1**3*s2**4*s3*s5**2 + 76*s1**3*s2**4*s4**2*s5 + 43*s1**3*s2**3*s3**2*s4*s5 - 15*s1**3*s2**3*s3*s4**3 + 50*s1**3*s2**3*s5**3 - 6*s1**3*s2**2*s3**4*s5 + 2*s1**3*s2**2*s3**3*s4**2 - 397*s1**3*s2**2*s3*s4*s5**2 + 514*s1**3*s2**2*s4**3*s5 - 700*s1**3*s2*s3**3*s5**2 + 447*s1**3*s2*s3**2*s4**2*s5 - 118*s1**3*s2*s3*s4**4 + 2300*s1**3*s2*s4*s5**3 - 12*s1**3*s3**4*s4*s5 + 6*s1**3*s3**3*s4**3 + 250*s1**3*s3**2*s5**3 + 1470*s1**3*s3*s4**2*s5**2 - 276*s1**3*s4**4*s5 + 27*s1**2*s2**6*s5**2 - 9*s1**2*s2**5*s3*s4*s5 + s1**2*s2**5*s4**3 + s1**2*s2**4*s3**3*s5 + 141*s1**2*s2**4*s4*s5**2 - 185*s1**2*s2**3*s3**2*s5**2 + 168*s1**2*s2**3*s3*s4**2*s5 - 128*s1**2*s2**3*s4**4 + 93*s1**2*s2**2*s3**3*s4*s5 + 19*s1**2*s2**2*s3**2*s4**3 - 125*s1**2*s2**2*s3*s5**3 - 610*s1**2*s2**2*s4**2*s5**2 - 36*s1**2*s2*s3**5*s5 + 5*s1**2*s2*s3**4*s4**2 + 1995*s1**2*s2*s3**2*s4*s5**2 - 1174*s1**2*s2*s3*s4**3*s5 - 16*s1**2*s2*s4**5 - 3125*s1**2*s2*s5**4 + 375*s1**2*s3**4*s5**2 - 172*s1**2*s3**3*s4**2*s5 + 82*s1**2*s3**2*s4**4 - 3500*s1**2*s3*s4*s5**3 - 1450*s1**2*s4**3*s5**2 + 198*s1*s2**5*s3*s5**2 - 78*s1*s2**5*s4**2*s5 - 95*s1*s2**4*s3**2*s4*s5 + 44*s1*s2**4*s3*s4**3 + 25*s1*s2**3*s3**4*s5 - 15*s1*s2**3*s3**3*s4**2 + 15*s1*s2**3*s3*s4*s5**2 - 384*s1*s2**3*s4**3*s5 + s1*s2**2*s3**5*s4 + 525*s1*s2**2*s3**3*s5**2 - 528*s1*s2**2*s3**2*s4**2*s5 + 384*s1*s2**2*s3*s4**4 - 1750*s1*s2**2*s4*s5**3 - 29*s1*s2*s3**4*s4*s5 - 118*s1*s2*s3**3*s4**3 + 625*s1*s2*s3**2*s5**3 - 850*s1*s2*s3*s4**2*s5**2 + 1760*s1*s2*s4**4*s5 + 38*s1*s3**6*s5 + 5*s1*s3**5*s4**2 - 2050*s1*s3**3*s4*s5**2 + 780*s1*s3**2*s4**3*s5 - 192*s1*s3*s4**5 + 3125*s1*s3*s5**4 + 7500*s1*s4**2*s5**3 - 27*s2**7*s5**2 + 18*s2**6*s3*s4*s5 - 4*s2**6*s4**3 - 4*s2**5*s3**3*s5 + s2**5*s3**2*s4**2 - 99*s2**5*s4*s5**2 - 150*s2**4*s3**2*s5**2 + 196*s2**4*s3*s4**2*s5 + 48*s2**4*s4**4 + 12*s2**3*s3**3*s4*s5 - 128*s2**3*s3**2*s4**3 + 1200*s2**3*s4**2*s5**2 - 12*s2**2*s3**5*s5 + 65*s2**2*s3**4*s4**2 - 725*s2**2*s3**2*s4*s5**2 - 160*s2**2*s3*s4**3*s5 - 192*s2**2*s4**5 + 3125*s2**2*s5**4 - 13*s2*s3**6*s4 - 125*s2*s3**4*s5**2 + 590*s2*s3**3*s4**2*s5 - 16*s2*s3**2*s4**4 - 1250*s2*s3*s4*s5**3 - 2000*s2*s4**3*s5**2 + s3**8 - 124*s3**5*s4*s5 + 17*s3**4*s4**3 + 3250*s3**2*s4**2*s5**2 - 1600*s3*s4**4*s5 + 256*s4**6 - 9375*s4*s5**4) ], (6, 1): [ lambda s1, s2, s3, s4, s5, s6: (8*s1*s5 - 2*s2*s4 - 18*s6), lambda s1, s2, s3, s4, s5, s6: (-50*s1**2*s4*s6 + 40*s1**2*s5**2 + 30*s1*s2*s3*s6 - 14*s1*s2*s4*s5 - 6*s1*s3**2*s5 + 2*s1*s3*s4**2 - 30*s1*s5*s6 - 8*s2**3*s6 + 2*s2**2*s3*s5 + s2**2*s4**2 + 114*s2*s4*s6 - 50*s2*s5**2 - 54*s3**2*s6 + 30*s3*s4*s5 - 8*s4**3 - 135*s6**2), lambda s1, s2, s3, s4, s5, s6: (125*s1**3*s3*s6**2 - 400*s1**3*s4*s5*s6 + 160*s1**3*s5**3 - 50*s1**2*s2**2*s6**2 + 190*s1**2*s2*s3*s5*s6 + 120*s1**2*s2*s4**2*s6 - 80*s1**2*s2*s4*s5**2 - 15*s1**2*s3**2*s4*s6 - 40*s1**2*s3**2*s5**2 + 21*s1**2*s3*s4**2*s5 - 2*s1**2*s4**4 + 900*s1**2*s4*s6**2 - 80*s1**2*s5**2*s6 - 44*s1*s2**3*s5*s6 - 66*s1*s2**2*s3*s4*s6 + 21*s1*s2**2*s3*s5**2 + 6*s1*s2**2*s4**2*s5 + 9*s1*s2*s3**3*s6 + 5*s1*s2*s3**2*s4*s5 - 2*s1*s2*s3*s4**3 - 990*s1*s2*s3*s6**2 + 920*s1*s2*s4*s5*s6 - 400*s1*s2*s5**3 - 135*s1*s3**2*s5*s6 - 126*s1*s3*s4**2*s6 + 190*s1*s3*s4*s5**2 - 44*s1*s4**3*s5 - 2070*s1*s5*s6**2 + 16*s2**4*s4*s6 - 2*s2**4*s5**2 - 2*s2**3*s3**2*s6 - 2*s2**3*s3*s4*s5 + 304*s2**3*s6**2 - 126*s2**2*s3*s5*s6 - 232*s2**2*s4**2*s6 + 120*s2**2*s4*s5**2 + 198*s2*s3**2*s4*s6 - 15*s2*s3**2*s5**2 - 66*s2*s3*s4**2*s5 + 16*s2*s4**4 - 1440*s2*s4*s6**2 + 900*s2*s5**2*s6 - 27*s3**4*s6 + 9*s3**3*s4*s5 - 2*s3**2*s4**3 + 1350*s3**2*s6**2 - 990*s3*s4*s5*s6 + 125*s3*s5**3 + 304*s4**3*s6 - 50*s4**2*s5**2 + 3240*s6**3), lambda s1, s2, s3, s4, s5, s6: (500*s1**4*s3*s5*s6**2 + 625*s1**4*s4**2*s6**2 - 1400*s1**4*s4*s5**2*s6 + 400*s1**4*s5**4 - 200*s1**3*s2**2*s5*s6**2 - 875*s1**3*s2*s3*s4*s6**2 + 640*s1**3*s2*s3*s5**2*s6 + 630*s1**3*s2*s4**2*s5*s6 - 264*s1**3*s2*s4*s5**3 + 90*s1**3*s3**2*s4*s5*s6 - 136*s1**3*s3**2*s5**3 - 50*s1**3*s3*s4**3*s6 + 76*s1**3*s3*s4**2*s5**2 - 1125*s1**3*s3*s6**3 - 8*s1**3*s4**4*s5 + 2550*s1**3*s4*s5*s6**2 - 200*s1**3*s5**3*s6 + 250*s1**2*s2**3*s4*s6**2 - 144*s1**2*s2**3*s5**2*s6 + 225*s1**2*s2**2*s3**2*s6**2 - 354*s1**2*s2**2*s3*s4*s5*s6 + 76*s1**2*s2**2*s3*s5**3 - 70*s1**2*s2**2*s4**3*s6 + 41*s1**2*s2**2*s4**2*s5**2 + 450*s1**2*s2**2*s6**3 - 54*s1**2*s2*s3**3*s5*s6 + 45*s1**2*s2*s3**2*s4**2*s6 + 30*s1**2*s2*s3**2*s4*s5**2 - 19*s1**2*s2*s3*s4**3*s5 - 2880*s1**2*s2*s3*s5*s6**2 + 2*s1**2*s2*s4**5 - 3480*s1**2*s2*s4**2*s6**2 + 4692*s1**2*s2*s4*s5**2*s6 - 1400*s1**2*s2*s5**4 + 9*s1**2*s3**4*s5**2 - 6*s1**2*s3**3*s4**2*s5 + s1**2*s3**2*s4**4 + 1485*s1**2*s3**2*s4*s6**2 - 522*s1**2*s3**2*s5**2*s6 - 1257*s1**2*s3*s4**2*s5*s6 + 640*s1**2*s3*s4*s5**3 + 218*s1**2*s4**4*s6 - 144*s1**2*s4**3*s5**2 + 1350*s1**2*s4*s6**3 - 5175*s1**2*s5**2*s6**2 - 120*s1*s2**4*s3*s6**2 + 68*s1*s2**4*s4*s5*s6 - 8*s1*s2**4*s5**3 + 46*s1*s2**3*s3**2*s5*s6 + 28*s1*s2**3*s3*s4**2*s6 - 19*s1*s2**3*s3*s4*s5**2 + 868*s1*s2**3*s5*s6**2 - 9*s1*s2**2*s3**3*s4*s6 - 6*s1*s2**2*s3**3*s5**2 + 3*s1*s2**2*s3**2*s4**2*s5 + 2484*s1*s2**2*s3*s4*s6**2 - 1257*s1*s2**2*s3*s5**2*s6 - 1356*s1*s2**2*s4**2*s5*s6 + 630*s1*s2**2*s4*s5**3 - 891*s1*s2*s3**3*s6**2 + 882*s1*s2*s3**2*s4*s5*s6 + 90*s1*s2*s3**2*s5**3 + 84*s1*s2*s3*s4**3*s6 - 354*s1*s2*s3*s4**2*s5**2 + 3240*s1*s2*s3*s6**3 + 68*s1*s2*s4**4*s5 - 4392*s1*s2*s4*s5*s6**2 + 2550*s1*s2*s5**3*s6 + 54*s1*s3**4*s5*s6 - 54*s1*s3**3*s4**2*s6 - 54*s1*s3**3*s4*s5**2 + 46*s1*s3**2*s4**3*s5 + 2727*s1*s3**2*s5*s6**2 - 8*s1*s3*s4**5 + 756*s1*s3*s4**2*s6**2 - 2880*s1*s3*s4*s5**2*s6 + 500*s1*s3*s5**4 + 868*s1*s4**3*s5*s6 - 200*s1*s4**2*s5**3 + 8100*s1*s5*s6**3 + 16*s2**6*s6**2 - 8*s2**5*s3*s5*s6 - 8*s2**5*s4**2*s6 + 2*s2**5*s4*s5**2 + 2*s2**4*s3**2*s4*s6 + s2**4*s3**2*s5**2 - 688*s2**4*s4*s6**2 + 218*s2**4*s5**2*s6 + 234*s2**3*s3**2*s6**2 + 84*s2**3*s3*s4*s5*s6 - 50*s2**3*s3*s5**3 + 168*s2**3*s4**3*s6 - 70*s2**3*s4**2*s5**2 - 1224*s2**3*s6**3 - 54*s2**2*s3**3*s5*s6 - 144*s2**2*s3**2*s4**2*s6 + 45*s2**2*s3**2*s4*s5**2 + 28*s2**2*s3*s4**3*s5 + 756*s2**2*s3*s5*s6**2 - 8*s2**2*s4**5 + 4320*s2**2*s4**2*s6**2 - 3480*s2**2*s4*s5**2*s6 + 625*s2**2*s5**4 + 27*s2*s3**4*s4*s6 - 9*s2*s3**3*s4**2*s5 + 2*s2*s3**2*s4**4 - 4752*s2*s3**2*s4*s6**2 + 1485*s2*s3**2*s5**2*s6 + 2484*s2*s3*s4**2*s5*s6 - 875*s2*s3*s4*s5**3 - 688*s2*s4**4*s6 + 250*s2*s4**3*s5**2 - 4536*s2*s4*s6**3 + 1350*s2*s5**2*s6**2 + 972*s3**4*s6**2 - 891*s3**3*s4*s5*s6 + 234*s3**2*s4**3*s6 + 225*s3**2*s4**2*s5**2 - 1944*s3**2*s6**3 - 120*s3*s4**4*s5 + 3240*s3*s4*s5*s6**2 - 1125*s3*s5**3*s6 + 16*s4**6 - 1224*s4**3*s6**2 + 450*s4**2*s5**2*s6), lambda s1, s2, s3, s4, s5, s6: (-3125*s1**6*s6**4 + 2500*s1**5*s2*s5*s6**3 + 625*s1**5*s3*s4*s6**3 - 500*s1**5*s3*s5**2*s6**2 + 2750*s1**5*s4**2*s5*s6**2 - 2400*s1**5*s4*s5**3*s6 + 512*s1**5*s5**5 - 750*s1**4*s2**2*s4*s6**3 - 550*s1**4*s2**2*s5**2*s6**2 - 375*s1**4*s2*s3**2*s6**3 - 3075*s1**4*s2*s3*s4*s5*s6**2 + 1640*s1**4*s2*s3*s5**3*s6 - 850*s1**4*s2*s4**3*s6**2 + 1220*s1**4*s2*s4**2*s5**2*s6 - 384*s1**4*s2*s4*s5**4 + 22500*s1**4*s2*s6**4 + 525*s1**4*s3**3*s5*s6**2 - 325*s1**4*s3**2*s4**2*s6**2 + 260*s1**4*s3**2*s4*s5**2*s6 - 256*s1**4*s3**2*s5**4 + 105*s1**4*s3*s4**3*s5*s6 + 76*s1**4*s3*s4**2*s5**3 + 375*s1**4*s3*s5*s6**3 - 58*s1**4*s4**5*s6 + 3*s1**4*s4**4*s5**2 - 12750*s1**4*s4**2*s6**3 + 3700*s1**4*s4*s5**2*s6**2 + 640*s1**4*s5**4*s6 + 350*s1**3*s2**3*s3*s6**3 + 1090*s1**3*s2**3*s4*s5*s6**2 - 364*s1**3*s2**3*s5**3*s6 + 305*s1**3*s2**2*s3**2*s5*s6**2 + 1340*s1**3*s2**2*s3*s4**2*s6**2 - 901*s1**3*s2**2*s3*s4*s5**2*s6 + 76*s1**3*s2**2*s3*s5**4 - 234*s1**3*s2**2*s4**3*s5*s6 + 102*s1**3*s2**2*s4**2*s5**3 - 16650*s1**3*s2**2*s5*s6**3 + 180*s1**3*s2*s3**3*s4*s6**2 - 366*s1**3*s2*s3**3*s5**2*s6 - 231*s1**3*s2*s3**2*s4**2*s5*s6 + 212*s1**3*s2*s3**2*s4*s5**3 + 112*s1**3*s2*s3*s4**4*s6 - 89*s1**3*s2*s3*s4**3*s5**2 + 10950*s1**3*s2*s3*s4*s6**3 + 1555*s1**3*s2*s3*s5**2*s6**2 + 6*s1**3*s2*s4**5*s5 - 9540*s1**3*s2*s4**2*s5*s6**2 + 9016*s1**3*s2*s4*s5**3*s6 - 2400*s1**3*s2*s5**5 - 108*s1**3*s3**5*s6**2 + 117*s1**3*s3**4*s4*s5*s6 + 32*s1**3*s3**4*s5**3 - 31*s1**3*s3**3*s4**3*s6 - 51*s1**3*s3**3*s4**2*s5**2 - 2025*s1**3*s3**3*s6**3 + 19*s1**3*s3**2*s4**4*s5 + 2955*s1**3*s3**2*s4*s5*s6**2 - 1436*s1**3*s3**2*s5**3*s6 - 2*s1**3*s3*s4**6 + 2770*s1**3*s3*s4**3*s6**2 - 5123*s1**3*s3*s4**2*s5**2*s6 + 1640*s1**3*s3*s4*s5**4 - 40500*s1**3*s3*s6**4 + 914*s1**3*s4**4*s5*s6 - 364*s1**3*s4**3*s5**3 + 53550*s1**3*s4*s5*s6**3 - 17930*s1**3*s5**3*s6**2 - 56*s1**2*s2**5*s6**3 - 318*s1**2*s2**4*s3*s5*s6**2 - 352*s1**2*s2**4*s4**2*s6**2 + 166*s1**2*s2**4*s4*s5**2*s6 + 3*s1**2*s2**4*s5**4 - 574*s1**2*s2**3*s3**2*s4*s6**2 + 347*s1**2*s2**3*s3**2*s5**2*s6 + 194*s1**2*s2**3*s3*s4**2*s5*s6 - 89*s1**2*s2**3*s3*s4*s5**3 - 8*s1**2*s2**3*s4**4*s6 + 4*s1**2*s2**3*s4**3*s5**2 + 560*s1**2*s2**3*s4*s6**3 + 3662*s1**2*s2**3*s5**2*s6**2 + 162*s1**2*s2**2*s3**4*s6**2 + 33*s1**2*s2**2*s3**3*s4*s5*s6 - 51*s1**2*s2**2*s3**3*s5**3 - 32*s1**2*s2**2*s3**2*s4**3*s6 + 28*s1**2*s2**2*s3**2*s4**2*s5**2 + 270*s1**2*s2**2*s3**2*s6**3 - 2*s1**2*s2**2*s3*s4**4*s5 + 4872*s1**2*s2**2*s3*s4*s5*s6**2 - 5123*s1**2*s2**2*s3*s5**3*s6 + 2144*s1**2*s2**2*s4**3*s6**2 - 2812*s1**2*s2**2*s4**2*s5**2*s6 + 1220*s1**2*s2**2*s4*s5**4 - 37800*s1**2*s2**2*s6**4 - 27*s1**2*s2*s3**5*s5*s6 + 9*s1**2*s2*s3**4*s4**2*s6 + 3*s1**2*s2*s3**4*s4*s5**2 - s1**2*s2*s3**3*s4**3*s5 - 3078*s1**2*s2*s3**3*s5*s6**2 - 4014*s1**2*s2*s3**2*s4**2*s6**2 + 5412*s1**2*s2*s3**2*s4*s5**2*s6 + 260*s1**2*s2*s3**2*s5**4 - 310*s1**2*s2*s3*s4**3*s5*s6 - 901*s1**2*s2*s3*s4**2*s5**3 - 3780*s1**2*s2*s3*s5*s6**3 + 166*s1**2*s2*s4**4*s5**2 + 40320*s1**2*s2*s4**2*s6**3 - 25344*s1**2*s2*s4*s5**2*s6**2 + 3700*s1**2*s2*s5**4*s6 + 918*s1**2*s3**4*s4*s6**2 + 27*s1**2*s3**4*s5**2*s6 - 342*s1**2*s3**3*s4**2*s5*s6 - 366*s1**2*s3**3*s4*s5**3 + 32*s1**2*s3**2*s4**4*s6 + 347*s1**2*s3**2*s4**3*s5**2 - 4590*s1**2*s3**2*s4*s6**3 + 594*s1**2*s3**2*s5**2*s6**2 - 94*s1**2*s3*s4**5*s5 + 3618*s1**2*s3*s4**2*s5*s6**2 + 1555*s1**2*s3*s4*s5**3*s6 - 500*s1**2*s3*s5**5 + 8*s1**2*s4**7 - 7192*s1**2*s4**4*s6**2 + 3662*s1**2*s4**3*s5**2*s6 - 550*s1**2*s4**2*s5**4 - 48600*s1**2*s4*s6**4 + 1080*s1**2*s5**2*s6**3 + 48*s1*s2**6*s5*s6**2 + 264*s1*s2**5*s3*s4*s6**2 - 94*s1*s2**5*s3*s5**2*s6 - 24*s1*s2**5*s4**2*s5*s6 + 6*s1*s2**5*s4*s5**3 - 66*s1*s2**4*s3**3*s6**2 - 50*s1*s2**4*s3**2*s4*s5*s6 + 19*s1*s2**4*s3**2*s5**3 + 8*s1*s2**4*s3*s4**3*s6 - 2*s1*s2**4*s3*s4**2*s5**2 - 552*s1*s2**4*s3*s6**3 - 2560*s1*s2**4*s4*s5*s6**2 + 914*s1*s2**4*s5**3*s6 + 15*s1*s2**3*s3**4*s5*s6 - 2*s1*s2**3*s3**3*s4**2*s6 - s1*s2**3*s3**3*s4*s5**2 + 1602*s1*s2**3*s3**2*s5*s6**2 - 608*s1*s2**3*s3*s4**2*s6**2 - 310*s1*s2**3*s3*s4*s5**2*s6 + 105*s1*s2**3*s3*s5**4 + 600*s1*s2**3*s4**3*s5*s6 - 234*s1*s2**3*s4**2*s5**3 + 31368*s1*s2**3*s5*s6**3 + 756*s1*s2**2*s3**3*s4*s6**2 - 342*s1*s2**2*s3**3*s5**2*s6 + 216*s1*s2**2*s3**2*s4**2*s5*s6 - 231*s1*s2**2*s3**2*s4*s5**3 - 192*s1*s2**2*s3*s4**4*s6 + 194*s1*s2**2*s3*s4**3*s5**2 - 39096*s1*s2**2*s3*s4*s6**3 + 3618*s1*s2**2*s3*s5**2*s6**2 - 24*s1*s2**2*s4**5*s5 + 9408*s1*s2**2*s4**2*s5*s6**2 - 9540*s1*s2**2*s4*s5**3*s6 + 2750*s1*s2**2*s5**5 - 162*s1*s2*s3**5*s6**2 - 378*s1*s2*s3**4*s4*s5*s6 + 117*s1*s2*s3**4*s5**3 + 150*s1*s2*s3**3*s4**3*s6 + 33*s1*s2*s3**3*s4**2*s5**2 + 10044*s1*s2*s3**3*s6**3 - 50*s1*s2*s3**2*s4**4*s5 - 8640*s1*s2*s3**2*s4*s5*s6**2 + 2955*s1*s2*s3**2*s5**3*s6 + 8*s1*s2*s3*s4**6 + 6144*s1*s2*s3*s4**3*s6**2 + 4872*s1*s2*s3*s4**2*s5**2*s6 - 3075*s1*s2*s3*s4*s5**4 + 174960*s1*s2*s3*s6**4 - 2560*s1*s2*s4**4*s5*s6 + 1090*s1*s2*s4**3*s5**3 - 148824*s1*s2*s4*s5*s6**3 + 53550*s1*s2*s5**3*s6**2 + 81*s1*s3**6*s5*s6 - 27*s1*s3**5*s4**2*s6 - 27*s1*s3**5*s4*s5**2 + 15*s1*s3**4*s4**3*s5 + 2430*s1*s3**4*s5*s6**2 - 2*s1*s3**3*s4**5 - 2052*s1*s3**3*s4**2*s6**2 - 3078*s1*s3**3*s4*s5**2*s6 + 525*s1*s3**3*s5**4 + 1602*s1*s3**2*s4**3*s5*s6 + 305*s1*s3**2*s4**2*s5**3 + 18144*s1*s3**2*s5*s6**3 - 104*s1*s3*s4**5*s6 - 318*s1*s3*s4**4*s5**2 - 33696*s1*s3*s4**2*s6**3 - 3780*s1*s3*s4*s5**2*s6**2 + 375*s1*s3*s5**4*s6 + 48*s1*s4**6*s5 + 31368*s1*s4**3*s5*s6**2 - 16650*s1*s4**2*s5**3*s6 + 2500*s1*s4*s5**5 + 77760*s1*s5*s6**4 - 32*s2**7*s4*s6**2 + 8*s2**7*s5**2*s6 + 8*s2**6*s3**2*s6**2 + 8*s2**6*s3*s4*s5*s6 - 2*s2**6*s3*s5**3 + 96*s2**6*s6**3 - 2*s2**5*s3**3*s5*s6 - 104*s2**5*s3*s5*s6**2 + 416*s2**5*s4**2*s6**2 - 58*s2**5*s5**4 - 312*s2**4*s3**2*s4*s6**2 + 32*s2**4*s3**2*s5**2*s6 - 192*s2**4*s3*s4**2*s5*s6 + 112*s2**4*s3*s4*s5**3 - 8*s2**4*s4**3*s5**2 + 4224*s2**4*s4*s6**3 - 7192*s2**4*s5**2*s6**2 + 54*s2**3*s3**4*s6**2 + 150*s2**3*s3**3*s4*s5*s6 - 31*s2**3*s3**3*s5**3 - 32*s2**3*s3**2*s4**2*s5**2 - 864*s2**3*s3**2*s6**3 + 8*s2**3*s3*s4**4*s5 + 6144*s2**3*s3*s4*s5*s6**2 + 2770*s2**3*s3*s5**3*s6 - 4032*s2**3*s4**3*s6**2 + 2144*s2**3*s4**2*s5**2*s6 - 850*s2**3*s4*s5**4 - 16416*s2**3*s6**4 - 27*s2**2*s3**5*s5*s6 + 9*s2**2*s3**4*s4*s5**2 - 2*s2**2*s3**3*s4**3*s5 - 2052*s2**2*s3**3*s5*s6**2 + 2376*s2**2*s3**2*s4**2*s6**2 - 4014*s2**2*s3**2*s4*s5**2*s6 - 325*s2**2*s3**2*s5**4 - 608*s2**2*s3*s4**3*s5*s6 + 1340*s2**2*s3*s4**2*s5**3 - 33696*s2**2*s3*s5*s6**3 + 416*s2**2*s4**5*s6 - 352*s2**2*s4**4*s5**2 - 6048*s2**2*s4**2*s6**3 + 40320*s2**2*s4*s5**2*s6**2 - 12750*s2**2*s5**4*s6 - 324*s2*s3**4*s4*s6**2 + 918*s2*s3**4*s5**2*s6 + 756*s2*s3**3*s4**2*s5*s6 + 180*s2*s3**3*s4*s5**3 - 312*s2*s3**2*s4**4*s6 - 574*s2*s3**2*s4**3*s5**2 + 43416*s2*s3**2*s4*s6**3 - 4590*s2*s3**2*s5**2*s6**2 + 264*s2*s3*s4**5*s5 - 39096*s2*s3*s4**2*s5*s6**2 + 10950*s2*s3*s4*s5**3*s6 + 625*s2*s3*s5**5 - 32*s2*s4**7 + 4224*s2*s4**4*s6**2 + 560*s2*s4**3*s5**2*s6 - 750*s2*s4**2*s5**4 + 85536*s2*s4*s6**4 - 48600*s2*s5**2*s6**3 - 162*s3**5*s4*s5*s6 - 108*s3**5*s5**3 + 54*s3**4*s4**3*s6 + 162*s3**4*s4**2*s5**2 - 11664*s3**4*s6**3 - 66*s3**3*s4**4*s5 + 10044*s3**3*s4*s5*s6**2 - 2025*s3**3*s5**3*s6 + 8*s3**2*s4**6 - 864*s3**2*s4**3*s6**2 + 270*s3**2*s4**2*s5**2*s6 - 375*s3**2*s4*s5**4 - 163296*s3**2*s6**4 - 552*s3*s4**4*s5*s6 + 350*s3*s4**3*s5**3 + 174960*s3*s4*s5*s6**3 - 40500*s3*s5**3*s6**2 + 96*s4**6*s6 - 56*s4**5*s5**2 - 16416*s4**3*s6**3 - 37800*s4**2*s5**2*s6**2 + 22500*s4*s5**4*s6 - 3125*s5**6 - 93312*s6**5), lambda s1, s2, s3, s4, s5, s6: (-9375*s1**7*s5*s6**4 + 3125*s1**6*s2*s4*s6**4 + 7500*s1**6*s2*s5**2*s6**3 + 3125*s1**6*s3**2*s6**4 - 1250*s1**6*s3*s4*s5*s6**3 - 2000*s1**6*s3*s5**3*s6**2 + 3250*s1**6*s4**2*s5**2*s6**2 - 1600*s1**6*s4*s5**4*s6 + 256*s1**6*s5**6 + 40625*s1**6*s6**5 - 3125*s1**5*s2**2*s3*s6**4 - 3500*s1**5*s2**2*s4*s5*s6**3 - 1450*s1**5*s2**2*s5**3*s6**2 - 1750*s1**5*s2*s3**2*s5*s6**3 + 625*s1**5*s2*s3*s4**2*s6**3 - 850*s1**5*s2*s3*s4*s5**2*s6**2 + 1760*s1**5*s2*s3*s5**4*s6 - 2050*s1**5*s2*s4**3*s5*s6**2 + 780*s1**5*s2*s4**2*s5**3*s6 - 192*s1**5*s2*s4*s5**5 + 35000*s1**5*s2*s5*s6**4 + 1200*s1**5*s3**3*s5**2*s6**2 - 725*s1**5*s3**2*s4**2*s5*s6**2 - 160*s1**5*s3**2*s4*s5**3*s6 - 192*s1**5*s3**2*s5**5 - 125*s1**5*s3*s4**4*s6**2 + 590*s1**5*s3*s4**3*s5**2*s6 - 16*s1**5*s3*s4**2*s5**4 - 20625*s1**5*s3*s4*s6**4 + 17250*s1**5*s3*s5**2*s6**3 - 124*s1**5*s4**5*s5*s6 + 17*s1**5*s4**4*s5**3 - 20250*s1**5*s4**2*s5*s6**3 + 1900*s1**5*s4*s5**3*s6**2 + 1344*s1**5*s5**5*s6 + 625*s1**4*s2**4*s6**4 + 2300*s1**4*s2**3*s3*s5*s6**3 + 250*s1**4*s2**3*s4**2*s6**3 + 1470*s1**4*s2**3*s4*s5**2*s6**2 - 276*s1**4*s2**3*s5**4*s6 - 125*s1**4*s2**2*s3**2*s4*s6**3 - 610*s1**4*s2**2*s3**2*s5**2*s6**2 + 1995*s1**4*s2**2*s3*s4**2*s5*s6**2 - 1174*s1**4*s2**2*s3*s4*s5**3*s6 - 16*s1**4*s2**2*s3*s5**5 + 375*s1**4*s2**2*s4**4*s6**2 - 172*s1**4*s2**2*s4**3*s5**2*s6 + 82*s1**4*s2**2*s4**2*s5**4 - 7750*s1**4*s2**2*s4*s6**4 - 46650*s1**4*s2**2*s5**2*s6**3 + 15*s1**4*s2*s3**3*s4*s5*s6**2 - 384*s1**4*s2*s3**3*s5**3*s6 + 525*s1**4*s2*s3**2*s4**3*s6**2 - 528*s1**4*s2*s3**2*s4**2*s5**2*s6 + 384*s1**4*s2*s3**2*s4*s5**4 - 10125*s1**4*s2*s3**2*s6**4 - 29*s1**4*s2*s3*s4**4*s5*s6 - 118*s1**4*s2*s3*s4**3*s5**3 + 36700*s1**4*s2*s3*s4*s5*s6**3 + 2410*s1**4*s2*s3*s5**3*s6**2 + 38*s1**4*s2*s4**6*s6 + 5*s1**4*s2*s4**5*s5**2 + 5550*s1**4*s2*s4**3*s6**3 - 10040*s1**4*s2*s4**2*s5**2*s6**2 + 5800*s1**4*s2*s4*s5**4*s6 - 1600*s1**4*s2*s5**6 - 292500*s1**4*s2*s6**5 - 99*s1**4*s3**5*s5*s6**2 - 150*s1**4*s3**4*s4**2*s6**2 + 196*s1**4*s3**4*s4*s5**2*s6 + 48*s1**4*s3**4*s5**4 + 12*s1**4*s3**3*s4**3*s5*s6 - 128*s1**4*s3**3*s4**2*s5**3 - 6525*s1**4*s3**3*s5*s6**3 - 12*s1**4*s3**2*s4**5*s6 + 65*s1**4*s3**2*s4**4*s5**2 + 225*s1**4*s3**2*s4**2*s6**3 + 80*s1**4*s3**2*s4*s5**2*s6**2 - 13*s1**4*s3*s4**6*s5 + 5145*s1**4*s3*s4**3*s5*s6**2 - 6746*s1**4*s3*s4**2*s5**3*s6 + 1760*s1**4*s3*s4*s5**5 - 103500*s1**4*s3*s5*s6**4 + s1**4*s4**8 + 954*s1**4*s4**5*s6**2 + 449*s1**4*s4**4*s5**2*s6 - 276*s1**4*s4**3*s5**4 + 70125*s1**4*s4**2*s6**4 + 58900*s1**4*s4*s5**2*s6**3 - 23310*s1**4*s5**4*s6**2 - 468*s1**3*s2**5*s5*s6**3 - 200*s1**3*s2**4*s3*s4*s6**3 - 294*s1**3*s2**4*s3*s5**2*s6**2 - 676*s1**3*s2**4*s4**2*s5*s6**2 + 180*s1**3*s2**4*s4*s5**3*s6 + 17*s1**3*s2**4*s5**5 + 50*s1**3*s2**3*s3**3*s6**3 - 397*s1**3*s2**3*s3**2*s4*s5*s6**2 + 514*s1**3*s2**3*s3**2*s5**3*s6 - 700*s1**3*s2**3*s3*s4**3*s6**2 + 447*s1**3*s2**3*s3*s4**2*s5**2*s6 - 118*s1**3*s2**3*s3*s4*s5**4 + 11700*s1**3*s2**3*s3*s6**4 - 12*s1**3*s2**3*s4**4*s5*s6 + 6*s1**3*s2**3*s4**3*s5**3 + 10360*s1**3*s2**3*s4*s5*s6**3 + 11404*s1**3*s2**3*s5**3*s6**2 + 141*s1**3*s2**2*s3**4*s5*s6**2 - 185*s1**3*s2**2*s3**3*s4**2*s6**2 + 168*s1**3*s2**2*s3**3*s4*s5**2*s6 - 128*s1**3*s2**2*s3**3*s5**4 + 93*s1**3*s2**2*s3**2*s4**3*s5*s6 + 19*s1**3*s2**2*s3**2*s4**2*s5**3 + 5895*s1**3*s2**2*s3**2*s5*s6**3 - 36*s1**3*s2**2*s3*s4**5*s6 + 5*s1**3*s2**2*s3*s4**4*s5**2 - 12020*s1**3*s2**2*s3*s4**2*s6**3 - 5698*s1**3*s2**2*s3*s4*s5**2*s6**2 - 6746*s1**3*s2**2*s3*s5**4*s6 + 5064*s1**3*s2**2*s4**3*s5*s6**2 - 762*s1**3*s2**2*s4**2*s5**3*s6 + 780*s1**3*s2**2*s4*s5**5 + 93900*s1**3*s2**2*s5*s6**4 + 198*s1**3*s2*s3**5*s4*s6**2 - 78*s1**3*s2*s3**5*s5**2*s6 - 95*s1**3*s2*s3**4*s4**2*s5*s6 + 44*s1**3*s2*s3**4*s4*s5**3 + 25*s1**3*s2*s3**3*s4**4*s6 - 15*s1**3*s2*s3**3*s4**3*s5**2 + 1935*s1**3*s2*s3**3*s4*s6**3 - 2808*s1**3*s2*s3**3*s5**2*s6**2 + s1**3*s2*s3**2*s4**5*s5 - 4844*s1**3*s2*s3**2*s4**2*s5*s6**2 + 8996*s1**3*s2*s3**2*s4*s5**3*s6 - 160*s1**3*s2*s3**2*s5**5 - 3616*s1**3*s2*s3*s4**4*s6**2 + 500*s1**3*s2*s3*s4**3*s5**2*s6 - 1174*s1**3*s2*s3*s4**2*s5**4 + 72900*s1**3*s2*s3*s4*s6**4 - 55665*s1**3*s2*s3*s5**2*s6**3 + 128*s1**3*s2*s4**5*s5*s6 + 180*s1**3*s2*s4**4*s5**3 + 16240*s1**3*s2*s4**2*s5*s6**3 - 9330*s1**3*s2*s4*s5**3*s6**2 + 1900*s1**3*s2*s5**5*s6 - 27*s1**3*s3**7*s6**2 + 18*s1**3*s3**6*s4*s5*s6 - 4*s1**3*s3**6*s5**3 - 4*s1**3*s3**5*s4**3*s6 + s1**3*s3**5*s4**2*s5**2 + 54*s1**3*s3**5*s6**3 + 1143*s1**3*s3**4*s4*s5*s6**2 - 820*s1**3*s3**4*s5**3*s6 + 923*s1**3*s3**3*s4**3*s6**2 + 57*s1**3*s3**3*s4**2*s5**2*s6 - 384*s1**3*s3**3*s4*s5**4 + 29700*s1**3*s3**3*s6**4 - 547*s1**3*s3**2*s4**4*s5*s6 + 514*s1**3*s3**2*s4**3*s5**3 - 10305*s1**3*s3**2*s4*s5*s6**3 - 7405*s1**3*s3**2*s5**3*s6**2 + 108*s1**3*s3*s4**6*s6 - 148*s1**3*s3*s4**5*s5**2 - 11360*s1**3*s3*s4**3*s6**3 + 22209*s1**3*s3*s4**2*s5**2*s6**2 + 2410*s1**3*s3*s4*s5**4*s6 - 2000*s1**3*s3*s5**6 + 432000*s1**3*s3*s6**5 + 12*s1**3*s4**7*s5 - 22624*s1**3*s4**4*s5*s6**2 + 11404*s1**3*s4**3*s5**3*s6 - 1450*s1**3*s4**2*s5**5 - 242100*s1**3*s4*s5*s6**4 + 58430*s1**3*s5**3*s6**3 + 56*s1**2*s2**6*s4*s6**3 + 86*s1**2*s2**6*s5**2*s6**2 - 14*s1**2*s2**5*s3**2*s6**3 + 304*s1**2*s2**5*s3*s4*s5*s6**2 - 148*s1**2*s2**5*s3*s5**3*s6 + 152*s1**2*s2**5*s4**3*s6**2 - 54*s1**2*s2**5*s4**2*s5**2*s6 + 5*s1**2*s2**5*s4*s5**4 - 2472*s1**2*s2**5*s6**4 - 76*s1**2*s2**4*s3**3*s5*s6**2 + 370*s1**2*s2**4*s3**2*s4**2*s6**2 - 287*s1**2*s2**4*s3**2*s4*s5**2*s6 + 65*s1**2*s2**4*s3**2*s5**4 - 28*s1**2*s2**4*s3*s4**3*s5*s6 + 5*s1**2*s2**4*s3*s4**2*s5**3 - 8092*s1**2*s2**4*s3*s5*s6**3 + 8*s1**2*s2**4*s4**5*s6 - 2*s1**2*s2**4*s4**4*s5**2 + 1096*s1**2*s2**4*s4**2*s6**3 - 5144*s1**2*s2**4*s4*s5**2*s6**2 + 449*s1**2*s2**4*s5**4*s6 - 210*s1**2*s2**3*s3**4*s4*s6**2 + 76*s1**2*s2**3*s3**4*s5**2*s6 + 43*s1**2*s2**3*s3**3*s4**2*s5*s6 - 15*s1**2*s2**3*s3**3*s4*s5**3 - 6*s1**2*s2**3*s3**2*s4**4*s6 + 2*s1**2*s2**3*s3**2*s4**3*s5**2 + 1962*s1**2*s2**3*s3**2*s4*s6**3 + 3181*s1**2*s2**3*s3**2*s5**2*s6**2 + 1684*s1**2*s2**3*s3*s4**2*s5*s6**2 + 500*s1**2*s2**3*s3*s4*s5**3*s6 + 590*s1**2*s2**3*s3*s5**5 - 168*s1**2*s2**3*s4**4*s6**2 - 494*s1**2*s2**3*s4**3*s5**2*s6 - 172*s1**2*s2**3*s4**2*s5**4 - 22080*s1**2*s2**3*s4*s6**4 + 58894*s1**2*s2**3*s5**2*s6**3 + 27*s1**2*s2**2*s3**6*s6**2 - 9*s1**2*s2**2*s3**5*s4*s5*s6 + s1**2*s2**2*s3**5*s5**3 + s1**2*s2**2*s3**4*s4**3*s6 - 486*s1**2*s2**2*s3**4*s6**3 + 1071*s1**2*s2**2*s3**3*s4*s5*s6**2 + 57*s1**2*s2**2*s3**3*s5**3*s6 + 2262*s1**2*s2**2*s3**2*s4**3*s6**2 - 2742*s1**2*s2**2*s3**2*s4**2*s5**2*s6 - 528*s1**2*s2**2*s3**2*s4*s5**4 - 29160*s1**2*s2**2*s3**2*s6**4 + 772*s1**2*s2**2*s3*s4**4*s5*s6 + 447*s1**2*s2**2*s3*s4**3*s5**3 - 96732*s1**2*s2**2*s3*s4*s5*s6**3 + 22209*s1**2*s2**2*s3*s5**3*s6**2 - 160*s1**2*s2**2*s4**6*s6 - 54*s1**2*s2**2*s4**5*s5**2 - 7992*s1**2*s2**2*s4**3*s6**3 + 8634*s1**2*s2**2*s4**2*s5**2*s6**2 - 10040*s1**2*s2**2*s4*s5**4*s6 + 3250*s1**2*s2**2*s5**6 + 529200*s1**2*s2**2*s6**5 - 351*s1**2*s2*s3**5*s5*s6**2 - 1215*s1**2*s2*s3**4*s4**2*s6**2 - 360*s1**2*s2*s3**4*s4*s5**2*s6 + 196*s1**2*s2*s3**4*s5**4 + 741*s1**2*s2*s3**3*s4**3*s5*s6 + 168*s1**2*s2*s3**3*s4**2*s5**3 + 11718*s1**2*s2*s3**3*s5*s6**3 - 106*s1**2*s2*s3**2*s4**5*s6 - 287*s1**2*s2*s3**2*s4**4*s5**2 + 22572*s1**2*s2*s3**2*s4**2*s6**3 - 8892*s1**2*s2*s3**2*s4*s5**2*s6**2 + 80*s1**2*s2*s3**2*s5**4*s6 + 88*s1**2*s2*s3*s4**6*s5 + 22144*s1**2*s2*s3*s4**3*s5*s6**2 - 5698*s1**2*s2*s3*s4**2*s5**3*s6 - 850*s1**2*s2*s3*s4*s5**5 + 169560*s1**2*s2*s3*s5*s6**4 - 8*s1**2*s2*s4**8 + 3032*s1**2*s2*s4**5*s6**2 - 5144*s1**2*s2*s4**4*s5**2*s6 + 1470*s1**2*s2*s4**3*s5**4 - 249480*s1**2*s2*s4**2*s6**4 - 105390*s1**2*s2*s4*s5**2*s6**3 + 58900*s1**2*s2*s5**4*s6**2 + 162*s1**2*s3**6*s4*s6**2 + 216*s1**2*s3**6*s5**2*s6 - 216*s1**2*s3**5*s4**2*s5*s6 - 78*s1**2*s3**5*s4*s5**3 + 36*s1**2*s3**4*s4**4*s6 + 76*s1**2*s3**4*s4**3*s5**2 - 3564*s1**2*s3**4*s4*s6**3 + 8802*s1**2*s3**4*s5**2*s6**2 - 22*s1**2*s3**3*s4**5*s5 - 11475*s1**2*s3**3*s4**2*s5*s6**2 - 2808*s1**2*s3**3*s4*s5**3*s6 + 1200*s1**2*s3**3*s5**5 + 2*s1**2*s3**2*s4**7 + 222*s1**2*s3**2*s4**4*s6**2 + 3181*s1**2*s3**2*s4**3*s5**2*s6 - 610*s1**2*s3**2*s4**2*s5**4 - 165240*s1**2*s3**2*s4*s6**4 + 118260*s1**2*s3**2*s5**2*s6**3 + 572*s1**2*s3*s4**5*s5*s6 - 294*s1**2*s3*s4**4*s5**3 - 32616*s1**2*s3*s4**2*s5*s6**3 - 55665*s1**2*s3*s4*s5**3*s6**2 + 17250*s1**2*s3*s5**5*s6 - 232*s1**2*s4**7*s6 + 86*s1**2*s4**6*s5**2 + 48408*s1**2*s4**4*s6**3 + 58894*s1**2*s4**3*s5**2*s6**2 - 46650*s1**2*s4**2*s5**4*s6 + 7500*s1**2*s4*s5**6 - 129600*s1**2*s4*s6**5 + 41040*s1**2*s5**2*s6**4 - 48*s1*s2**7*s4*s5*s6**2 + 12*s1*s2**7*s5**3*s6 + 12*s1*s2**6*s3**2*s5*s6**2 - 144*s1*s2**6*s3*s4**2*s6**2 + 88*s1*s2**6*s3*s4*s5**2*s6 - 13*s1*s2**6*s3*s5**4 + 1680*s1*s2**6*s5*s6**3 + 72*s1*s2**5*s3**3*s4*s6**2 - 22*s1*s2**5*s3**3*s5**2*s6 - 4*s1*s2**5*s3**2*s4**2*s5*s6 + s1*s2**5*s3**2*s4*s5**3 - 144*s1*s2**5*s3*s4*s6**3 + 572*s1*s2**5*s3*s5**2*s6**2 + 736*s1*s2**5*s4**2*s5*s6**2 + 128*s1*s2**5*s4*s5**3*s6 - 124*s1*s2**5*s5**5 - 9*s1*s2**4*s3**5*s6**2 + s1*s2**4*s3**4*s4*s5*s6 + 36*s1*s2**4*s3**3*s6**3 - 2028*s1*s2**4*s3**2*s4*s5*s6**2 - 547*s1*s2**4*s3**2*s5**3*s6 - 480*s1*s2**4*s3*s4**3*s6**2 + 772*s1*s2**4*s3*s4**2*s5**2*s6 - 29*s1*s2**4*s3*s4*s5**4 + 6336*s1*s2**4*s3*s6**4 - 12*s1*s2**4*s4**3*s5**3 + 4368*s1*s2**4*s4*s5*s6**3 - 22624*s1*s2**4*s5**3*s6**2 + 441*s1*s2**3*s3**4*s5*s6**2 + 336*s1*s2**3*s3**3*s4**2*s6**2 + 741*s1*s2**3*s3**3*s4*s5**2*s6 + 12*s1*s2**3*s3**3*s5**4 - 868*s1*s2**3*s3**2*s4**3*s5*s6 + 93*s1*s2**3*s3**2*s4**2*s5**3 + 11016*s1*s2**3*s3**2*s5*s6**3 + 176*s1*s2**3*s3*s4**5*s6 - 28*s1*s2**3*s3*s4**4*s5**2 + 14784*s1*s2**3*s3*s4**2*s6**3 + 22144*s1*s2**3*s3*s4*s5**2*s6**2 + 5145*s1*s2**3*s3*s5**4*s6 - 11344*s1*s2**3*s4**3*s5*s6**2 + 5064*s1*s2**3*s4**2*s5**3*s6 - 2050*s1*s2**3*s4*s5**5 - 346896*s1*s2**3*s5*s6**4 - 54*s1*s2**2*s3**5*s4*s6**2 - 216*s1*s2**2*s3**5*s5**2*s6 + 324*s1*s2**2*s3**4*s4**2*s5*s6 - 95*s1*s2**2*s3**4*s4*s5**3 - 80*s1*s2**2*s3**3*s4**4*s6 + 43*s1*s2**2*s3**3*s4**3*s5**2 - 12204*s1*s2**2*s3**3*s4*s6**3 - 11475*s1*s2**2*s3**3*s5**2*s6**2 - 4*s1*s2**2*s3**2*s4**5*s5 - 3888*s1*s2**2*s3**2*s4**2*s5*s6**2 - 4844*s1*s2**2*s3**2*s4*s5**3*s6 - 725*s1*s2**2*s3**2*s5**5 - 1312*s1*s2**2*s3*s4**4*s6**2 + 1684*s1*s2**2*s3*s4**3*s5**2*s6 + 1995*s1*s2**2*s3*s4**2*s5**4 + 139104*s1*s2**2*s3*s4*s6**4 - 32616*s1*s2**2*s3*s5**2*s6**3 + 736*s1*s2**2*s4**5*s5*s6 - 676*s1*s2**2*s4**4*s5**3 + 131040*s1*s2**2*s4**2*s5*s6**3 + 16240*s1*s2**2*s4*s5**3*s6**2 - 20250*s1*s2**2*s5**5*s6 - 27*s1*s2*s3**6*s4*s5*s6 + 18*s1*s2*s3**6*s5**3 + 9*s1*s2*s3**5*s4**3*s6 - 9*s1*s2*s3**5*s4**2*s5**2 + 1944*s1*s2*s3**5*s6**3 + s1*s2*s3**4*s4**4*s5 + 6156*s1*s2*s3**4*s4*s5*s6**2 + 1143*s1*s2*s3**4*s5**3*s6 + 324*s1*s2*s3**3*s4**3*s6**2 + 1071*s1*s2*s3**3*s4**2*s5**2*s6 + 15*s1*s2*s3**3*s4*s5**4 - 7776*s1*s2*s3**3*s6**4 - 2028*s1*s2*s3**2*s4**4*s5*s6 - 397*s1*s2*s3**2*s4**3*s5**3 + 112860*s1*s2*s3**2*s4*s5*s6**3 - 10305*s1*s2*s3**2*s5**3*s6**2 + 336*s1*s2*s3*s4**6*s6 + 304*s1*s2*s3*s4**5*s5**2 - 68976*s1*s2*s3*s4**3*s6**3 - 96732*s1*s2*s3*s4**2*s5**2*s6**2 + 36700*s1*s2*s3*s4*s5**4*s6 - 1250*s1*s2*s3*s5**6 - 1477440*s1*s2*s3*s6**5 - 48*s1*s2*s4**7*s5 + 4368*s1*s2*s4**4*s5*s6**2 + 10360*s1*s2*s4**3*s5**3*s6 - 3500*s1*s2*s4**2*s5**5 + 935280*s1*s2*s4*s5*s6**4 - 242100*s1*s2*s5**3*s6**3 - 972*s1*s3**6*s5*s6**2 - 351*s1*s3**5*s4*s5**2*s6 - 99*s1*s3**5*s5**4 + 441*s1*s3**4*s4**3*s5*s6 + 141*s1*s3**4*s4**2*s5**3 - 36936*s1*s3**4*s5*s6**3 - 84*s1*s3**3*s4**5*s6 - 76*s1*s3**3*s4**4*s5**2 + 17496*s1*s3**3*s4**2*s6**3 + 11718*s1*s3**3*s4*s5**2*s6**2 - 6525*s1*s3**3*s5**4*s6 + 12*s1*s3**2*s4**6*s5 + 11016*s1*s3**2*s4**3*s5*s6**2 + 5895*s1*s3**2*s4**2*s5**3*s6 - 1750*s1*s3**2*s4*s5**5 - 252720*s1*s3**2*s5*s6**4 - 2544*s1*s3*s4**5*s6**2 - 8092*s1*s3*s4**4*s5**2*s6 + 2300*s1*s3*s4**3*s5**4 + 536544*s1*s3*s4**2*s6**4 + 169560*s1*s3*s4*s5**2*s6**3 - 103500*s1*s3*s5**4*s6**2 + 1680*s1*s4**6*s5*s6 - 468*s1*s4**5*s5**3 - 346896*s1*s4**3*s5*s6**3 + 93900*s1*s4**2*s5**3*s6**2 + 35000*s1*s4*s5**5*s6 - 9375*s1*s5**7 + 108864*s1*s5*s6**5 + 16*s2**8*s4**2*s6**2 - 8*s2**8*s4*s5**2*s6 + s2**8*s5**4 - 8*s2**7*s3**2*s4*s6**2 + 2*s2**7*s3**2*s5**2*s6 - 96*s2**7*s4*s6**3 - 232*s2**7*s5**2*s6**2 + s2**6*s3**4*s6**2 + 24*s2**6*s3**2*s6**3 + 336*s2**6*s3*s4*s5*s6**2 + 108*s2**6*s3*s5**3*s6 - 32*s2**6*s4**3*s6**2 - 160*s2**6*s4**2*s5**2*s6 + 38*s2**6*s4*s5**4 + 144*s2**6*s6**4 - 84*s2**5*s3**3*s5*s6**2 + 8*s2**5*s3**2*s4**2*s6**2 - 106*s2**5*s3**2*s4*s5**2*s6 - 12*s2**5*s3**2*s5**4 + 176*s2**5*s3*s4**3*s5*s6 - 36*s2**5*s3*s4**2*s5**3 - 2544*s2**5*s3*s5*s6**3 - 32*s2**5*s4**5*s6 + 8*s2**5*s4**4*s5**2 - 3072*s2**5*s4**2*s6**3 + 3032*s2**5*s4*s5**2*s6**2 + 954*s2**5*s5**4*s6 + 36*s2**4*s3**4*s5**2*s6 - 80*s2**4*s3**3*s4**2*s5*s6 + 25*s2**4*s3**3*s4*s5**3 + 16*s2**4*s3**2*s4**4*s6 - 6*s2**4*s3**2*s4**3*s5**2 + 2520*s2**4*s3**2*s4*s6**3 + 222*s2**4*s3**2*s5**2*s6**2 - 1312*s2**4*s3*s4**2*s5*s6**2 - 3616*s2**4*s3*s4*s5**3*s6 - 125*s2**4*s3*s5**5 + 1296*s2**4*s4**4*s6**2 - 168*s2**4*s4**3*s5**2*s6 + 375*s2**4*s4**2*s5**4 + 19296*s2**4*s4*s6**4 + 48408*s2**4*s5**2*s6**3 + 9*s2**3*s3**5*s4*s5*s6 - 4*s2**3*s3**5*s5**3 - 2*s2**3*s3**4*s4**3*s6 + s2**3*s3**4*s4**2*s5**2 - 432*s2**3*s3**4*s6**3 + 324*s2**3*s3**3*s4*s5*s6**2 + 923*s2**3*s3**3*s5**3*s6 - 752*s2**3*s3**2*s4**3*s6**2 + 2262*s2**3*s3**2*s4**2*s5**2*s6 + 525*s2**3*s3**2*s4*s5**4 - 9936*s2**3*s3**2*s6**4 - 480*s2**3*s3*s4**4*s5*s6 - 700*s2**3*s3*s4**3*s5**3 - 68976*s2**3*s3*s4*s5*s6**3 - 11360*s2**3*s3*s5**3*s6**2 - 32*s2**3*s4**6*s6 + 152*s2**3*s4**5*s5**2 + 6912*s2**3*s4**3*s6**3 - 7992*s2**3*s4**2*s5**2*s6**2 + 5550*s2**3*s4*s5**4*s6 - 29376*s2**3*s6**5 + 108*s2**2*s3**4*s4**2*s6**2 - 1215*s2**2*s3**4*s4*s5**2*s6 - 150*s2**2*s3**4*s5**4 + 336*s2**2*s3**3*s4**3*s5*s6 - 185*s2**2*s3**3*s4**2*s5**3 + 17496*s2**2*s3**3*s5*s6**3 + 8*s2**2*s3**2*s4**5*s6 + 370*s2**2*s3**2*s4**4*s5**2 - 864*s2**2*s3**2*s4**2*s6**3 + 22572*s2**2*s3**2*s4*s5**2*s6**2 + 225*s2**2*s3**2*s5**4*s6 - 144*s2**2*s3*s4**6*s5 + 14784*s2**2*s3*s4**3*s5*s6**2 - 12020*s2**2*s3*s4**2*s5**3*s6 + 625*s2**2*s3*s4*s5**5 + 536544*s2**2*s3*s5*s6**4 + 16*s2**2*s4**8 - 3072*s2**2*s4**5*s6**2 + 1096*s2**2*s4**4*s5**2*s6 + 250*s2**2*s4**3*s5**4 - 93744*s2**2*s4**2*s6**4 - 249480*s2**2*s4*s5**2*s6**3 + 70125*s2**2*s5**4*s6**2 + 162*s2*s3**6*s5**2*s6 - 54*s2*s3**5*s4**2*s5*s6 + 198*s2*s3**5*s4*s5**3 - 210*s2*s3**4*s4**3*s5**2 - 3564*s2*s3**4*s5**2*s6**2 + 72*s2*s3**3*s4**5*s5 - 12204*s2*s3**3*s4**2*s5*s6**2 + 1935*s2*s3**3*s4*s5**3*s6 - 8*s2*s3**2*s4**7 + 2520*s2*s3**2*s4**4*s6**2 + 1962*s2*s3**2*s4**3*s5**2*s6 - 125*s2*s3**2*s4**2*s5**4 - 178848*s2*s3**2*s4*s6**4 - 165240*s2*s3**2*s5**2*s6**3 - 144*s2*s3*s4**5*s5*s6 - 200*s2*s3*s4**4*s5**3 + 139104*s2*s3*s4**2*s5*s6**3 + 72900*s2*s3*s4*s5**3*s6**2 - 20625*s2*s3*s5**5*s6 - 96*s2*s4**7*s6 + 56*s2*s4**6*s5**2 + 19296*s2*s4**4*s6**3 - 22080*s2*s4**3*s5**2*s6**2 - 7750*s2*s4**2*s5**4*s6 + 3125*s2*s4*s5**6 + 248832*s2*s4*s6**5 - 129600*s2*s5**2*s6**4 - 27*s3**7*s5**3 + 27*s3**6*s4**2*s5**2 - 9*s3**5*s4**4*s5 + 1944*s3**5*s4*s5*s6**2 + 54*s3**5*s5**3*s6 + s3**4*s4**6 - 432*s3**4*s4**3*s6**2 - 486*s3**4*s4**2*s5**2*s6 + 46656*s3**4*s6**4 + 36*s3**3*s4**4*s5*s6 + 50*s3**3*s4**3*s5**3 - 7776*s3**3*s4*s5*s6**3 + 29700*s3**3*s5**3*s6**2 + 24*s3**2*s4**6*s6 - 14*s3**2*s4**5*s5**2 - 9936*s3**2*s4**3*s6**3 - 29160*s3**2*s4**2*s5**2*s6**2 - 10125*s3**2*s4*s5**4*s6 + 3125*s3**2*s5**6 + 1026432*s3**2*s6**5 + 6336*s3*s4**4*s5*s6**2 + 11700*s3*s4**3*s5**3*s6 - 3125*s3*s4**2*s5**5 - 1477440*s3*s4*s5*s6**4 + 432000*s3*s5**3*s6**3 + 144*s4**6*s6**2 - 2472*s4**5*s5**2*s6 + 625*s4**4*s5**4 - 29376*s4**3*s6**4 + 529200*s4**2*s5**2*s6**3 - 292500*s4*s5**4*s6**2 + 40625*s5**6*s6 - 186624*s6**6) ], (6, 2): [ lambda s1, s2, s3, s4, s5, s6: (-s3), lambda s1, s2, s3, s4, s5, s6: (-s1*s5 + s2*s4 - 9*s6), lambda s1, s2, s3, s4, s5, s6: (s1*s2*s6 + 2*s1*s3*s5 - s1*s4**2 - s2**2*s5 + 6*s3*s6 + s4*s5), lambda s1, s2, s3, s4, s5, s6: (s1**2*s4*s6 - s1**2*s5**2 - 3*s1*s2*s3*s6 + s1*s2*s4*s5 + 9*s1*s5*s6 + s2**3*s6 - 9*s2*s4*s6 + s2*s5**2 + 3*s3**2*s6 - 3*s3*s4*s5 + s4**3 + 27*s6**2), lambda s1, s2, s3, s4, s5, s6: (-2*s1**3*s6**2 + 2*s1**2*s2*s5*s6 + 2*s1**2*s3*s4*s6 - s1**2*s3*s5**2 - s1*s2**2*s4*s6 - 3*s1*s2*s6**2 - 16*s1*s3*s5*s6 + 4*s1*s4**2*s6 + 2*s1*s4*s5**2 + 4*s2**2*s5*s6 + s2*s3*s4*s6 + 2*s2*s3*s5**2 - s2*s4**2*s5 - 9*s3*s6**2 - 3*s4*s5*s6 - 2*s5**3), lambda s1, s2, s3, s4, s5, s6: (s1**3*s3*s6**2 - 3*s1**3*s4*s5*s6 + s1**3*s5**3 - s1**2*s2**2*s6**2 + s1**2*s2*s3*s5*s6 - 2*s1**2*s4*s6**2 + 6*s1**2*s5**2*s6 + 16*s1*s2*s3*s6**2 - 3*s1*s2*s5**3 - s1*s3**2*s5*s6 - 2*s1*s3*s4**2*s6 + s1*s3*s4*s5**2 - 30*s1*s5*s6**2 - 4*s2**3*s6**2 - 2*s2**2*s3*s5*s6 + s2**2*s4**2*s6 + 18*s2*s4*s6**2 - 2*s2*s5**2*s6 - 15*s3**2*s6**2 + 16*s3*s4*s5*s6 + s3*s5**3 - 4*s4**3*s6 - s4**2*s5**2 - 27*s6**3), lambda s1, s2, s3, s4, s5, s6: (s1**4*s5*s6**2 + 2*s1**3*s2*s4*s6**2 - s1**3*s2*s5**2*s6 - s1**3*s3**2*s6**2 + 9*s1**3*s6**3 - 14*s1**2*s2*s5*s6**2 - 11*s1**2*s3*s4*s6**2 + 6*s1**2*s3*s5**2*s6 + 3*s1**2*s4**2*s5*s6 - s1**2*s4*s5**3 + 3*s1*s2**2*s5**2*s6 + 3*s1*s2*s3**2*s6**2 - s1*s2*s3*s4*s5*s6 + 39*s1*s3*s5*s6**2 - 14*s1*s4*s5**2*s6 + s1*s5**4 - 11*s2*s3*s5**2*s6 + 2*s2*s4*s5**3 - 3*s3**3*s6**2 + 3*s3**2*s4*s5*s6 - s3**2*s5**3 + 9*s5**3*s6), lambda s1, s2, s3, s4, s5, s6: (-s1**4*s2*s6**3 + s1**4*s3*s5*s6**2 - 4*s1**3*s3*s6**3 + 10*s1**3*s4*s5*s6**2 - 4*s1**3*s5**3*s6 + 8*s1**2*s2**2*s6**3 - 8*s1**2*s2*s3*s5*s6**2 - 2*s1**2*s2*s4**2*s6**2 + s1**2*s2*s4*s5**2*s6 + s1**2*s3**2*s4*s6**2 - 6*s1**2*s4*s6**3 - 7*s1**2*s5**2*s6**2 - 24*s1*s2*s3*s6**3 - 4*s1*s2*s4*s5*s6**2 + 10*s1*s2*s5**3*s6 + 8*s1*s3**2*s5*s6**2 + 8*s1*s3*s4**2*s6**2 - 8*s1*s3*s4*s5**2*s6 + s1*s3*s5**4 + 36*s1*s5*s6**3 + 8*s2**2*s3*s5*s6**2 - 2*s2**2*s4*s5**2*s6 - 2*s2*s3**2*s4*s6**2 + s2*s3**2*s5**2*s6 - 6*s2*s5**2*s6**2 + 18*s3**2*s6**3 - 24*s3*s4*s5*s6**2 - 4*s3*s5**3*s6 + 8*s4**2*s5**2*s6 - s4*s5**4), lambda s1, s2, s3, s4, s5, s6: (-s1**5*s4*s6**3 - 2*s1**4*s5*s6**3 + 3*s1**3*s2*s5**2*s6**2 + 3*s1**3*s3**2*s6**3 - s1**3*s3*s4*s5*s6**2 - 8*s1**3*s6**4 + 16*s1**2*s2*s5*s6**3 + 8*s1**2*s3*s4*s6**3 - 6*s1**2*s3*s5**2*s6**2 - 8*s1**2*s4**2*s5*s6**2 + 3*s1**2*s4*s5**3*s6 - 8*s1*s2**2*s5**2*s6**2 - 8*s1*s2*s3**2*s6**3 + 8*s1*s2*s3*s4*s5*s6**2 - s1*s2*s3*s5**3*s6 - s1*s3**3*s5*s6**2 - 24*s1*s3*s5*s6**3 + 16*s1*s4*s5**2*s6**2 - 2*s1*s5**4*s6 + 8*s2*s3*s5**2*s6**2 - s2*s5**5 + 8*s3**3*s6**3 - 8*s3**2*s4*s5*s6**2 + 3*s3**2*s5**3*s6 - 8*s5**3*s6**2), lambda s1, s2, s3, s4, s5, s6: (s1**6*s6**4 - 4*s1**4*s2*s6**4 - 2*s1**4*s3*s5*s6**3 + s1**4*s4**2*s6**3 + 8*s1**3*s3*s6**4 - 4*s1**3*s4*s5*s6**3 + 2*s1**3*s5**3*s6**2 + 8*s1**2*s2*s3*s5*s6**3 - 2*s1**2*s2*s4*s5**2*s6**2 - 2*s1**2*s3**2*s4*s6**3 + s1**2*s3**2*s5**2*s6**2 - 4*s1*s2*s5**3*s6**2 - 12*s1*s3**2*s5*s6**3 + 8*s1*s3*s4*s5**2*s6**2 - 2*s1*s3*s5**4*s6 + s2**2*s5**4*s6 - 2*s2*s3**2*s5**2*s6**2 + s3**4*s6**3 + 8*s3*s5**3*s6**2 - 4*s4*s5**4*s6 + s5**6) ], } refurb-1.27.0/test/data/bug_recursion_error.txt000066400000000000000000000000001454672660200216000ustar00rootroot00000000000000refurb-1.27.0/test/data/bug_type_reassignment.py000066400000000000000000000010011454672660200217310ustar00rootroot00000000000000# See https://github.com/dosisod/refurb/issues/18 and https://github.com/dosisod/refurb/issues/53 # This is a regression test to make sure this code doesn't cause an error x = "abc" print(x) # see below x = 1 y = str(x) # The print(x) line is needed because of the overly-strict "allow_redefinition" # option in Mypy, which requires that a variable be read before it allows the # creation of a new instance. The following code should fail, until Mypy fixes # it (if they do). x2 = "abc" x2 = 1 y2 = str(x2) refurb-1.27.0/test/data/bug_type_reassignment.txt000066400000000000000000000001151454672660200221250ustar00rootroot00000000000000test/data/bug_type_reassignment.py:18:6 [FURB123]: Replace `str(x)` with `x` refurb-1.27.0/test/data/err_100.py000066400000000000000000000004101454672660200165070ustar00rootroot00000000000000import pathlib from pathlib import Path # these will match a = str(Path("file.txt"))[:4] + ".pdf" p = Path("file.txt") b = str(p)[:4] + ".pdf" a = str(pathlib.Path("file.txt"))[:4] + ".pdf" # these will not x = str("file.txt")[:4] + ".pdf" # noqa: FURB123 refurb-1.27.0/test/data/err_100.txt000066400000000000000000000004301454672660200167000ustar00rootroot00000000000000test/data/err_100.py:6:9 [FURB100]: Use `Path(x).with_suffix(y)` instead of slice and concat test/data/err_100.py:9:9 [FURB100]: Use `Path(x).with_suffix(y)` instead of slice and concat test/data/err_100.py:11:9 [FURB100]: Use `Path(x).with_suffix(y)` instead of slice and concat refurb-1.27.0/test/data/err_101.py000066400000000000000000000017071454672660200165220ustar00rootroot00000000000000# these should match with open("file.txt") as f: x = f.read() with open("file.txt", "rb") as f: x2 = f.read() with open("file.txt", mode="rb") as f: x2 = f.read() with open("file.txt", encoding="utf8") as f: x = f.read() with open("file.txt", errors="ignore") as f: x = f.read() with open("file.txt", errors="ignore", mode="rb") as f: x2 = f.read() with open("file.txt", mode="r") as f: # noqa: FURB120 x = f.read() # these should not f2 = open("file2.txt") with open("file.txt") as f: x = f2.read() with open("file.txt") as f: # Path.read_text() does not support size, so ignore this x = f.read(100) # enables line buffering, not supported in read_text() with open("file.txt", buffering=1) as f: x = f.read() # force CRLF, not supported in read_text() with open("file.txt", newline="\r\n") as f: x = f.read() # dont mistake "newline" for "mode" with open("file.txt", newline="b") as f: x = f.read() refurb-1.27.0/test/data/err_101.txt000066400000000000000000000014511454672660200167050ustar00rootroot00000000000000test/data/err_101.py:3:1 [FURB101]: Replace `with open(x) as f: y = f.read()` with `y = Path(x).read_text()` test/data/err_101.py:6:1 [FURB101]: Replace `with open(x, ...) as f: y = f.read()` with `y = Path(x).read_bytes()` test/data/err_101.py:9:1 [FURB101]: Replace `with open(x, ...) as f: y = f.read()` with `y = Path(x).read_bytes()` test/data/err_101.py:12:1 [FURB101]: Replace `with open(x, ...) as f: y = f.read()` with `y = Path(x).read_text(...)` test/data/err_101.py:15:1 [FURB101]: Replace `with open(x, ...) as f: y = f.read()` with `y = Path(x).read_text(...)` test/data/err_101.py:18:1 [FURB101]: Replace `with open(x, ...) as f: y = f.read()` with `y = Path(x).read_bytes(...)` test/data/err_101.py:21:1 [FURB101]: Replace `with open(x, ...) as f: y = f.read()` with `y = Path(x).read_text()` refurb-1.27.0/test/data/err_102.py000066400000000000000000000013741454672660200165230ustar00rootroot00000000000000name = "bob" last_name = b"smith" # these should match _ = name.startswith("a") or name.startswith("b") _ = name.endswith("a") or name.endswith("b") _ = last_name.startswith(b"a") or last_name.startswith(b"b") _ = name.startswith("a") or name.startswith("b") or True _ = not name.startswith("a") and not name.startswith("b") # these should not match _ = name.startswith("a") and name.startswith("b") name_copy = name _ = name.startswith("a") or name_copy.startswith("b") _ = name.startswith() or name.startswith("x") # type: ignore _ = name.startswith("x") or name.startswith("y") and True _ = not name.startswith("a") or not name.startswith("b") _ = not name.startswith("a") and name.startswith("b") _ = name.startswith("a") and not name.startswith("b") refurb-1.27.0/test/data/err_102.txt000066400000000000000000000010561454672660200167070ustar00rootroot00000000000000test/data/err_102.py:5:21 [FURB102]: Replace `x.startswith(y) or x.startswith(z)` with `x.startswith((y, z))` test/data/err_102.py:6:19 [FURB102]: Replace `x.endswith(y) or x.endswith(z)` with `x.endswith((y, z))` test/data/err_102.py:7:26 [FURB102]: Replace `x.startswith(y) or x.startswith(z)` with `x.startswith((y, z))` test/data/err_102.py:8:21 [FURB102]: Replace `x.startswith(y) or x.startswith(z)` with `x.startswith((y, z))` test/data/err_102.py:10:25 [FURB102]: Replace `not x.startswith(y) and not x.startswith(z)` with `not x.startswith((y, z))` refurb-1.27.0/test/data/err_103.py000066400000000000000000000004521454672660200165200ustar00rootroot00000000000000# these will match with open("filename", "w") as f: f.write("hello world") with open("filename", "wb") as f: f.write(b"hello world") # these will not with open("filename") as f: f.write("hello world") f2 = open("filename2") with open("filename") as f: f2.write("hello world") refurb-1.27.0/test/data/err_103.txt000066400000000000000000000003351454672660200167070ustar00rootroot00000000000000test/data/err_103.py:3:1 [FURB103]: Replace `with open(x, ...) as f: f.write(y)` with `Path(x).write_text(y)` test/data/err_103.py:6:1 [FURB103]: Replace `with open(x, ...) as f: f.write(y)` with `Path(x).write_bytes(y)` refurb-1.27.0/test/data/err_104.py000066400000000000000000000001171454672660200165170ustar00rootroot00000000000000import os from os import getcwd a = getcwd() b = os.getcwd() c = os.getcwdb() refurb-1.27.0/test/data/err_104.txt000066400000000000000000000003451454672660200167110ustar00rootroot00000000000000test/data/err_104.py:4:5 [FURB104]: Replace `os.getcwd()` with `Path.cwd()` test/data/err_104.py:5:5 [FURB104]: Replace `os.getcwd()` with `Path.cwd()` test/data/err_104.py:6:5 [FURB104]: Replace `os.getcwdb()` with `Path.cwd()` refurb-1.27.0/test/data/err_105.py000066400000000000000000000001461454672660200165220ustar00rootroot00000000000000# this will match print("") # these will not print() print("abc") print("", "") print("", end="") refurb-1.27.0/test/data/err_105.txt000066400000000000000000000001071454672660200167060ustar00rootroot00000000000000test/data/err_105.py:3:1 [FURB105]: Replace `print("")` with `print()` refurb-1.27.0/test/data/err_106.py000066400000000000000000000013141454672660200165210ustar00rootroot00000000000000# these will match tabsize = 8 spaces_8 = "\thello world".replace("\t", " " * 8) spaces_4 = "\thello world".replace("\t", " ") spaces_1 = "\thello world".replace("\t", " ") spaces = "\thello world".replace("\t", " " * tabsize) spaces = "\thello world".replace("\t", tabsize * " ") bspaces_8 = b"\thello world".replace(b"\t", b" " * 8) bspaces_4 = b"\thello world".replace(b"\t", b" ") bspaces_1 = b"\thello world".replace(b"\t", b" ") bspaces = b"\thello world".replace(b"\t", b" " * tabsize) bspaces = b"\thello world".replace(b"\t", tabsize * b" ") # these will not spaces = "\thello world".replace("\t", "x") spaces = "\thello world".replace("x", " ") bspaces = b"\thello world".replace(b"\t", b"x") refurb-1.27.0/test/data/err_106.txt000066400000000000000000000017511454672660200167150ustar00rootroot00000000000000test/data/err_106.py:5:28 [FURB106]: Replace `x.replace("\t", " " * 8)` with `x.expandtabs()` test/data/err_106.py:6:28 [FURB106]: Replace `x.replace("\t", " ")` with `x.expandtabs(4)` test/data/err_106.py:7:28 [FURB106]: Replace `x.replace("\t", " ")` with `x.expandtabs(1)` test/data/err_106.py:8:26 [FURB106]: Replace `x.replace("\t", " " * tabsize)` with `x.expandtabs(tabsize)` test/data/err_106.py:9:26 [FURB106]: Replace `x.replace("\t", tabsize * " ")` with `x.expandtabs(tabsize)` test/data/err_106.py:11:30 [FURB106]: Replace `x.replace(b"\t", b" " * 8)` with `x.expandtabs()` test/data/err_106.py:12:30 [FURB106]: Replace `x.replace(b"\t", b" ")` with `x.expandtabs(4)` test/data/err_106.py:13:30 [FURB106]: Replace `x.replace(b"\t", b" ")` with `x.expandtabs(1)` test/data/err_106.py:14:28 [FURB106]: Replace `x.replace(b"\t", b" " * tabsize)` with `x.expandtabs(tabsize)` test/data/err_106.py:15:28 [FURB106]: Replace `x.replace(b"\t", tabsize * b" ")` with `x.expandtabs(tabsize)` refurb-1.27.0/test/data/err_107.py000066400000000000000000000011651454672660200165260ustar00rootroot00000000000000# these will match try: print() except: pass try: print() print() except Exception: pass try: print() except Exception as e: pass try: print() except (ValueError, FileNotFoundError): pass try: print() except (ValueError, FileNotFoundError) as e: pass # these will not try: print() except Exception: print() try: print() except: pass finally: print("cleanup") try: print() except: pass else: print("no exception thrown") try: print() except ("not", "an", "exception"): pass try: print() except "not an exception": pass refurb-1.27.0/test/data/err_107.txt000066400000000000000000000012201454672660200167050ustar00rootroot00000000000000test/data/err_107.py:3:1 [FURB107]: Replace `try: ... except: pass` with `with suppress(BaseException): ...` test/data/err_107.py:8:1 [FURB107]: Replace `try: ... except Exception: pass` with `with suppress(Exception): ...` test/data/err_107.py:14:1 [FURB107]: Replace `try: ... except Exception: pass` with `with suppress(Exception): ...` test/data/err_107.py:19:1 [FURB107]: Replace `try: ... except (ValueError, FileNotFoundError): pass` with `with suppress(ValueError, FileNotFoundError): ...` test/data/err_107.py:24:1 [FURB107]: Replace `try: ... except (ValueError, FileNotFoundError): pass` with `with suppress(ValueError, FileNotFoundError): ...` refurb-1.27.0/test/data/err_108.py000066400000000000000000000007001454672660200165210ustar00rootroot00000000000000x = y = "abc" class C: y: str = "xyz" c = C() # these should match _ = x == "abc" or x == "def" _ = c.y == "abc" or c.y == "def" _ = x == "abc" or x == "def" or x == "ghi" _ = x == "abc" or x == "def" or y == "ghi" _ = ( x == "abc" or x == "def" ) _ = x == "abc" or "def" == x _ = "abc" == x or "def" == x _ = "abc" == x or x == "def" # these should not _ = x == "abc" or y == "def" _ = x == "abc" or x == "def" and y == "ghi" refurb-1.27.0/test/data/err_108.txt000066400000000000000000000013541454672660200167160ustar00rootroot00000000000000test/data/err_108.py:11:5 [FURB108]: Replace `x == y or x == z` with `x in (y, z)` test/data/err_108.py:12:5 [FURB108]: Replace `x == y or x == z` with `x in (y, z)` test/data/err_108.py:13:5 [FURB108]: Replace `x == y or x == z` with `x in (y, z)` test/data/err_108.py:13:19 [FURB108]: Replace `x == y or x == z` with `x in (y, z)` test/data/err_108.py:14:5 [FURB108]: Replace `x == y or x == z` with `x in (y, z)` test/data/err_108.py:17:5 [FURB108]: Replace `x == y or x == z` with `x in (y, z)` test/data/err_108.py:21:5 [FURB108]: Replace `x == y or z == x` with `x in (y, z)` test/data/err_108.py:22:5 [FURB108]: Replace `x == y or z == y` with `y in (x, z)` test/data/err_108.py:23:5 [FURB108]: Replace `x == y or y == z` with `y in (x, z)` refurb-1.27.0/test/data/err_109.py000066400000000000000000000005741454672660200165330ustar00rootroot00000000000000# these will match for x in [1, 2, 3]: pass [x for x in [1, 2, 3]] (x for x in [1, 2, 3]) [ (x + y) for x in [1, 2, 3] for y in [4, 5, 6] ] if 1 in [1, 2, 3]: pass if 1 not in [1, 2, 3]: pass # these will not nums = [1, 2, 3] for x in nums: pass for x in list((1, 2, 3)): pass [x for x in list((1, 2, 3))] if 1 in list((1, 2, 3)): pass refurb-1.27.0/test/data/err_109.txt000066400000000000000000000010731454672660200167150ustar00rootroot00000000000000test/data/err_109.py:3:10 [FURB109]: Replace `in [x, y, z]` with `in (x, y, z)` test/data/err_109.py:6:13 [FURB109]: Replace `in [x, y, z]` with `in (x, y, z)` test/data/err_109.py:8:13 [FURB109]: Replace `in [x, y, z]` with `in (x, y, z)` test/data/err_109.py:11:22 [FURB109]: Replace `in [x, y, z]` with `in (x, y, z)` test/data/err_109.py:12:14 [FURB109]: Replace `in [x, y, z]` with `in (x, y, z)` test/data/err_109.py:15:9 [FURB109]: Replace `in [x, y, z]` with `in (x, y, z)` test/data/err_109.py:18:13 [FURB109]: Replace `not in [x, y, z]` with `not in (x, y, z)` refurb-1.27.0/test/data/err_110.py000066400000000000000000000003331454672660200165140ustar00rootroot00000000000000x = 123 y = 456 def f(): return 1337 # these will match z = x if x else y z = True if True else y z = ( x if x else y ) z = f() if f() else y # these will not z = x if y else y z = y if x else y refurb-1.27.0/test/data/err_110.txt000066400000000000000000000004541454672660200167070ustar00rootroot00000000000000test/data/err_110.py:10:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/err_110.py:11:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/err_110.py:13:5 [FURB110]: Replace `x if x else y` with `x or y` test/data/err_110.py:18:5 [FURB110]: Replace `x if x else y` with `x or y` refurb-1.27.0/test/data/err_111.py000066400000000000000000000006231454672660200165170ustar00rootroot00000000000000# these will match def f(x, y): pass mod = object lambda: print() lambda x: bool(x) lambda x, y: f(x, y) lambda: [] lambda: {} lambda: () lambda x: mod.cast(x) # these will not lambda: f(True, False) lambda x: f(x, True) lambda x, y: f(y, x) lambda x: bool(x + 1) lambda x: x + 1 lambda x: print(*x) lambda x: print(**x) lambda: True lambda: [1, 2, 3] lambda: {"k": "v"} lambda: (1, 2, 3) refurb-1.27.0/test/data/err_111.txt000066400000000000000000000010151454672660200167020ustar00rootroot00000000000000test/data/err_111.py:9:1 [FURB111]: Replace `lambda: print()` with `print` test/data/err_111.py:10:1 [FURB111]: Replace `lambda x: bool(x)` with `bool` test/data/err_111.py:11:1 [FURB111]: Replace `lambda x, y: f(x, y)` with `f` test/data/err_111.py:13:1 [FURB111]: Replace `lambda: []` with `list` test/data/err_111.py:14:1 [FURB111]: Replace `lambda: {}` with `dict` test/data/err_111.py:15:1 [FURB111]: Replace `lambda: ()` with `tuple` test/data/err_111.py:17:1 [FURB111]: Replace `lambda x: mod.cast(x)` with `mod.cast` refurb-1.27.0/test/data/err_112.py000066400000000000000000000004051454672660200165160ustar00rootroot00000000000000# these will match x = list() y = dict() z = tuple() i = int() s = str() f = float() c = complex() b = bool() by = bytes() # these will not x = [] y = {} z = () i = 0 s = "" x = list((1, 2, 3)) y = dict((("a", 1), ("b", 2))) i2 = int("0xFF") s2 = str(123) refurb-1.27.0/test/data/err_112.txt000066400000000000000000000011011454672660200166770ustar00rootroot00000000000000test/data/err_112.py:3:5 [FURB112]: Replace `list()` with `[]` test/data/err_112.py:4:5 [FURB112]: Replace `dict()` with `{}` test/data/err_112.py:5:5 [FURB112]: Replace `tuple()` with `()` test/data/err_112.py:6:5 [FURB112]: Replace `int()` with `0` test/data/err_112.py:7:5 [FURB112]: Replace `str()` with `""` test/data/err_112.py:8:5 [FURB112]: Replace `float()` with `0.0` test/data/err_112.py:9:5 [FURB112]: Replace `complex()` with `0j` test/data/err_112.py:10:5 [FURB112]: Replace `bool()` with `False` test/data/err_112.py:11:6 [FURB112]: Replace `bytes()` with `b""` refurb-1.27.0/test/data/err_113.py000066400000000000000000000012261454672660200165210ustar00rootroot00000000000000nums = [] nums2 = [] # these will match nums.append(1) nums.append(2) pass nums.append(1) nums2.append(1) nums.append(2) nums.append(3) pass nums.append(1) nums.append(2) nums.append(3) if True: nums.append(1) nums.append(2) if True: nums.append(1) nums.append(2) pass if True: nums.append(1) nums2.append(1) nums.append(2) nums.append(3) # these will not nums.append(1) pass nums.append(2) if True: nums.append(1) pass nums.append(2) nums.append(1) pass nums.append(1) nums2.append(2) nums.copy() nums.copy() class C: def append(self, x): pass c = C() c.append(1) c.append(2) refurb-1.27.0/test/data/err_113.txt000066400000000000000000000011271454672660200167100ustar00rootroot00000000000000test/data/err_113.py:6:1 [FURB113]: Use `x.extend(...)` instead of repeatedly calling `x.append()` test/data/err_113.py:13:1 [FURB113]: Use `x.extend(...)` instead of repeatedly calling `x.append()` test/data/err_113.py:18:1 [FURB113]: Use `x.extend(...)` instead of repeatedly calling `x.append()` test/data/err_113.py:24:5 [FURB113]: Use `x.extend(...)` instead of repeatedly calling `x.append()` test/data/err_113.py:29:5 [FURB113]: Use `x.extend(...)` instead of repeatedly calling `x.append()` test/data/err_113.py:37:5 [FURB113]: Use `x.extend(...)` instead of repeatedly calling `x.append()` refurb-1.27.0/test/data/err_114.py000066400000000000000000000002371454672660200165230ustar00rootroot00000000000000# this will match if not not False: pass value = 123 if not not value: pass # these will not match if bool(123): pass if not False: pass refurb-1.27.0/test/data/err_114.txt000066400000000000000000000002161454672660200167070ustar00rootroot00000000000000test/data/err_114.py:3:4 [FURB114]: Replace `not not x` with `bool(x)` test/data/err_114.py:7:4 [FURB114]: Replace `not not x` with `bool(x)` refurb-1.27.0/test/data/err_115.py000066400000000000000000000034421454672660200165250ustar00rootroot00000000000000# these should match nums = [1, 2, 3] authors = {"Dune": "Frank Herbert"} primes = set((1, 2, 3, 5, 7)) data = (True, "something", 123) name = "bob" fruits = frozenset(("apple", "orange", "banana")) if len(nums) == 0: ... if len(authors) == 0: ... if len(primes) == 0: ... if len(data) == 0: ... if len(name) == 0: ... if len(fruits) == 0: ... if len(nums) <= 0: ... if len(nums) > 0: ... if len(nums) != 0: ... if len(nums) >= 1: ... if len([]) == 0: ... if len({}) == 0: ... if len(()) == 0: ... if len("") == 0: ... if len(set(())) == 0: ... if len(frozenset(())) == 0: ... if True and len(nums) == 0: ... match 1: case 1 if len(nums) == 0: pass _ = [x for x in () if len(nums) == 0] _ = (x for x in () if len(nums) == 0) _ = {"k": v for v in () if len(nums) == 0} _ = 1 if len(nums) == 0 else 2 while len(nums) == 0: pass assert len(nums) == 0 # len(x) if len(nums): ... match 1: case 1 if len(nums): pass _ = [x for x in () if len(nums)] _ = (x for x in () if len(nums)) _ = {"k": v for v in () if len(nums)} _ = 1 if len(nums) else 2 while len(nums): pass assert len(nums) assert nums == [] assert nums != [] assert authors == {} assert authors != {} assert len(nums) and True assert len(nums) or False # these should not if len(nums) == 1: ... if len(nums) != 1: ... x = len(nums) == 0 # We cannot verify all containers. For example, with this container, the length # does not indicate whether it is truthy or not. class Container: def __bool__(self) -> bool: return False def __len__(self) -> int: return 1337 container = Container() if len(container) == 0: ... if print(len(nums) == 0): ... if (lambda: len(nums) == 0)(): ... assert nums == [1, 2, 3] assert authors == {"author": "book"} assert nums <= [] assert len(nums) % 2 refurb-1.27.0/test/data/err_115.txt000066400000000000000000000050741454672660200167170ustar00rootroot00000000000000test/data/err_115.py:11:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:12:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:13:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:14:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:15:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:16:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:18:4 [FURB115]: Replace `len(x) <= 0` with `not x` test/data/err_115.py:19:4 [FURB115]: Replace `len(x) > 0` with `x` test/data/err_115.py:20:4 [FURB115]: Replace `len(x) != 0` with `x` test/data/err_115.py:21:4 [FURB115]: Replace `len(x) >= 1` with `x` test/data/err_115.py:23:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:24:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:25:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:26:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:27:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:28:4 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:30:13 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:33:15 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:36:23 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:37:23 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:38:28 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:40:10 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:42:7 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:45:8 [FURB115]: Replace `len(x) == 0` with `not x` test/data/err_115.py:50:4 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:53:15 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:56:23 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:57:23 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:58:28 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:60:10 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:62:7 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:65:8 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:67:8 [FURB115]: Replace `x == []` with `not x` test/data/err_115.py:68:8 [FURB115]: Replace `x != []` with `x` test/data/err_115.py:70:8 [FURB115]: Replace `x == {}` with `not x` test/data/err_115.py:71:8 [FURB115]: Replace `x != {}` with `x` test/data/err_115.py:73:8 [FURB115]: Replace `len(x)` with `x` test/data/err_115.py:74:8 [FURB115]: Replace `len(x)` with `x` refurb-1.27.0/test/data/err_116.py000066400000000000000000000002101454672660200165140ustar00rootroot00000000000000# these will match _ = bin(1234)[2:] _ = oct(1234)[2:] _ = hex(1234)[2:] # these will not _ = bin(1234) _ = oct(1234) _ = hex(1234) refurb-1.27.0/test/data/err_116.txt000066400000000000000000000003471454672660200167160ustar00rootroot00000000000000test/data/err_116.py:3:5 [FURB116]: Replace `bin(num)[2:]` with `f"{num:b}"` test/data/err_116.py:4:5 [FURB116]: Replace `oct(num)[2:]` with `f"{num:o}"` test/data/err_116.py:5:5 [FURB116]: Replace `hex(num)[2:]` with `f"{num:x}"` refurb-1.27.0/test/data/err_117.py000066400000000000000000000006641454672660200165320ustar00rootroot00000000000000from pathlib import Path # these will match path = Path("filename") with open(str(path)) as f: pass with open(str(Path("filename"))) as f: pass with open(Path("filename")) as f: pass with open(path) as f: pass with open(str(path), "rb") as f: pass with open(path, "rb") as f: pass f = open(str(path)) # these will not with Path("filename").open() as f: pass with open("filename") as f: pass refurb-1.27.0/test/data/err_117.txt000066400000000000000000000010301454672660200167050ustar00rootroot00000000000000test/data/err_117.py:7:6 [FURB117]: Replace `open(str(x))` with `x.open()` test/data/err_117.py:10:6 [FURB117]: Replace `open(str(x))` with `x.open()` test/data/err_117.py:13:6 [FURB117]: Replace `open(x)` with `x.open()` test/data/err_117.py:16:6 [FURB117]: Replace `open(x)` with `x.open()` test/data/err_117.py:19:6 [FURB117]: Replace `open(str(x), "rb")` with `x.open("rb")` test/data/err_117.py:22:6 [FURB117]: Replace `open(x, "rb")` with `x.open("rb")` test/data/err_117.py:25:5 [FURB117]: Replace `open(str(x))` with `x.open()` refurb-1.27.0/test/data/err_118.py000066400000000000000000000012421454672660200165240ustar00rootroot00000000000000# these will match lambda x, y: x + y lambda x, y: x - y lambda x, y: x / y lambda x, y: x // y lambda x, y: x * y lambda x, y: x @ y lambda x, y: x ** y lambda x, y: x is y lambda x, y: x is not y lambda x, y: y in x lambda x, y: x & y lambda x, y: x | y lambda x, y: x ^ y lambda x, y: x << y lambda x, y: x >> y lambda x, y: x % y lambda x, y: x < y lambda x, y: x <= y lambda x, y: x == y lambda x, y: x != y lambda x, y: x >= y lambda x, y: x > y lambda x: ~ x lambda x: - x lambda x: not x lambda x: + x def f(x, y): return x + y def f2(x): return - x # these will not lambda x, y: print(x + y) lambda x, *y: x + y lambda x, y: y + x lambda x, y: 1 + 2 refurb-1.27.0/test/data/err_118.txt000066400000000000000000000045371454672660200167250ustar00rootroot00000000000000test/data/err_118.py:3:1 [FURB118]: Replace `lambda x, y: x + y` with `operator.add` test/data/err_118.py:4:1 [FURB118]: Replace `lambda x, y: x - y` with `operator.sub` test/data/err_118.py:5:1 [FURB118]: Replace `lambda x, y: x / y` with `operator.truediv` test/data/err_118.py:6:1 [FURB118]: Replace `lambda x, y: x // y` with `operator.floordiv` test/data/err_118.py:7:1 [FURB118]: Replace `lambda x, y: x * y` with `operator.mul` test/data/err_118.py:8:1 [FURB118]: Replace `lambda x, y: x @ y` with `operator.matmul` test/data/err_118.py:9:1 [FURB118]: Replace `lambda x, y: x ** y` with `operator.pow` test/data/err_118.py:10:1 [FURB118]: Replace `lambda x, y: x is y` with `operator.is_` test/data/err_118.py:11:1 [FURB118]: Replace `lambda x, y: x is not y` with `operator.is_not` test/data/err_118.py:12:1 [FURB118]: Replace `lambda x, y: y in x` with `operator.contains` test/data/err_118.py:13:1 [FURB118]: Replace `lambda x, y: x & y` with `operator.and_` test/data/err_118.py:14:1 [FURB118]: Replace `lambda x, y: x | y` with `operator.or_` test/data/err_118.py:15:1 [FURB118]: Replace `lambda x, y: x ^ y` with `operator.xor` test/data/err_118.py:16:1 [FURB118]: Replace `lambda x, y: x << y` with `operator.lshift` test/data/err_118.py:17:1 [FURB118]: Replace `lambda x, y: x >> y` with `operator.rshift` test/data/err_118.py:18:1 [FURB118]: Replace `lambda x, y: x % y` with `operator.mod` test/data/err_118.py:19:1 [FURB118]: Replace `lambda x, y: x < y` with `operator.lt` test/data/err_118.py:20:1 [FURB118]: Replace `lambda x, y: x <= y` with `operator.le` test/data/err_118.py:21:1 [FURB118]: Replace `lambda x, y: x == y` with `operator.eq` test/data/err_118.py:22:1 [FURB118]: Replace `lambda x, y: x != y` with `operator.ne` test/data/err_118.py:23:1 [FURB118]: Replace `lambda x, y: x >= y` with `operator.ge` test/data/err_118.py:24:1 [FURB118]: Replace `lambda x, y: x > y` with `operator.gt` test/data/err_118.py:26:1 [FURB118]: Replace `lambda x: ~ x` with `operator.invert` test/data/err_118.py:27:1 [FURB118]: Replace `lambda x: - x` with `operator.neg` test/data/err_118.py:28:1 [FURB118]: Replace `lambda x: not x` with `operator.not_` test/data/err_118.py:29:1 [FURB118]: Replace `lambda x: + x` with `operator.pos` test/data/err_118.py:31:1 [FURB118]: Replace function with `operator.add` test/data/err_118.py:34:1 [FURB118]: Replace function with `operator.neg` refurb-1.27.0/test/data/err_119.py000066400000000000000000000005331454672660200165270ustar00rootroot00000000000000# these will match f"{str('hello world')}" # noqa: FURB123 f"{repr(123)}" f"{ascii('hello world')}" f"{bin(0b1100)}" f"{oct(0o777)}" f"{hex(0xFF)}" f"{chr(0x41)}" f"{format('hello world')}" # these will not f"{123}" # noqa: FURB183 f"{0b1010:b}" f"{str('hello world')!s}" # noqa: FURB123 f"{str(b'hello world', encoding='utf8')}" refurb-1.27.0/test/data/err_119.txt000066400000000000000000000010511454672660200167120ustar00rootroot00000000000000test/data/err_119.py:3:1 [FURB119]: Replace `{str(x)}` with `{x}` test/data/err_119.py:5:1 [FURB119]: Replace `{repr(x)}` with `{x!r}` test/data/err_119.py:7:1 [FURB119]: Replace `{ascii(x)}` with `{x!a}` test/data/err_119.py:9:1 [FURB119]: Replace `{bin(x)}` with `{x:#b}` test/data/err_119.py:11:1 [FURB119]: Replace `{oct(x)}` with `{x:#o}` test/data/err_119.py:13:1 [FURB119]: Replace `{hex(x)}` with `{x:#x}` test/data/err_119.py:15:1 [FURB119]: Replace `{chr(x)}` with `{x:c}` test/data/err_119.py:17:1 [FURB119]: Replace `{format(x)}` with `{x}` refurb-1.27.0/test/data/err_120.py000066400000000000000000000026201454672660200165160ustar00rootroot00000000000000from typing import overload def f(a: int = 1, b: int = 2) -> int: return a + b def f2(a: int, b: int = 2, c: int = 3) -> int: return a + b + c class C: x: int def __init__(self, x: int = 1) -> None: self.x = x def f(self, a: int = 1): return a @classmethod def f2(cls, a: int = 1): return a @staticmethod def f3(x: int = 1): pass @staticmethod def f4(): pass @overload def over() -> None: ... @overload def over(x: int = 1) -> None: ... def over(x: int = 2, y: int = 3) -> None: pass class C2: @overload @staticmethod def over() -> None: ... @overload @staticmethod def over(x: int = 1) -> None: ... @staticmethod def over(x: int = 2, y: int = 3) -> None: pass # these should match f(1) f(1, 2) f2(1, 2) f2(1, 2, 3) f(a=1) f(b=2) c = C() c.f(1) c.f2(1) C.f2(1) c.f(a=1) c.f2(a=1) C.f2(a=1) C().f(1) # noqa: FURB165 C().f2(1) # noqa: FURB165 C(x=1) C.f3(1) d = {} d.get("unknown", None) round(123, 0) input("") int("123", 10) def args(*args, x: int = 1): pass args(x=1) args(None, x=1) args(None, None, x=1) over(1) over(2) C2.over(1) C2.over(2) f(0, 2) f(1, b=3) # these should not f() f(2, 3) f(b=1, a=2) f2(1) int("123") def kw(**kwargs): pass kw(x=1) C.f4() args(1) args(None, 1) args(None, None, 1) args(x=2) args(None, x=2) args(None, None, x=2) refurb-1.27.0/test/data/err_120.txt000066400000000000000000000060031454672660200167040ustar00rootroot00000000000000test/data/err_120.py:56:3 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:57:3 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:57:6 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:59:7 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:60:7 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:60:10 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:61:5 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:62:5 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:65:5 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:66:6 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:67:6 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:68:7 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:69:8 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:70:8 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:71:7 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:72:8 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:73:5 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:74:6 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:77:18 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:79:12 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:80:7 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:81:12 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:86:8 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:87:14 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:88:20 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:90:6 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:91:6 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:93:9 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:94:9 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:96:6 [FURB120]: Don't pass an argument if it is the same as the default value test/data/err_120.py:97:3 [FURB120]: Don't pass an argument if it is the same as the default value refurb-1.27.0/test/data/err_121.py000066400000000000000000000006421454672660200165210ustar00rootroot00000000000000num = num2 = 123 # these will match _ = isinstance(num, float) or isinstance(num, int) _ = isinstance(num, (float, str)) or isinstance(num, int) _ = isinstance(num, (float, str)) or isinstance(num, int) or True # these will not _ = isinstance(num, float) or isinstance(num2, int) def f(x, y): return True _ = f(num, float) or f(num2, int) _ = isinstance(num, (float, str)) or isinstance(num, int) and True refurb-1.27.0/test/data/err_121.txt000066400000000000000000000005201454672660200167030ustar00rootroot00000000000000test/data/err_121.py:5:21 [FURB121]: Replace `isinstance(x, y) or isinstance(x, z)` with `isinstance(x, y | z)` test/data/err_121.py:6:21 [FURB121]: Replace `isinstance(x, y) or isinstance(x, z)` with `isinstance(x, y | z)` test/data/err_121.py:7:21 [FURB121]: Replace `isinstance(x, y) or isinstance(x, z)` with `isinstance(x, y | z)` refurb-1.27.0/test/data/err_122.py000066400000000000000000000007301454672660200165200ustar00rootroot00000000000000lines = ["line 1", "line 2", "line 3"] # these will match with open("file") as f: for line in lines: f.write(line) with open("file", "wb") as f: for line in lines: f.write(line.encode()) with open("file") as f: for line in lines: f.write(line.upper()) # these will not with open("x") as f: pass for line in lines: f.write(line) with open("x") as f: for line in lines: pass f.write(line) refurb-1.27.0/test/data/err_122.txt000066400000000000000000000005001454672660200167020ustar00rootroot00000000000000test/data/err_122.py:6:5 [FURB122]: Replace `for line in lines: f.write(line)` with `f.writelines(lines)` test/data/err_122.py:11:5 [FURB122]: Replace `for line in lines: f.write(line)` with `f.writelines(lines)` test/data/err_122.py:16:5 [FURB122]: Replace `for line in lines: f.write(line)` with `f.writelines(lines)` refurb-1.27.0/test/data/err_123.py000066400000000000000000000011411454672660200165160ustar00rootroot00000000000000# these will match _ = bool(True) _ = bytes(b"hello world") _ = complex(1j) _ = dict({"a": 1}) _ = float(123.456) _ = list([1, 2, 3]) _ = str("hello world") _ = tuple((1, 2, 3)) _ = int(123) a = True _ = bool(a) b = b"hello world" _ = bytes(b) c = 1j _ = complex(c) d = {"a": 1} _ = dict(d) e = 123.456 _ = float(e) f = [1, 2, 3] _ = list(f) g = "hello world" _ = str(g) t = (1, 2, 3) _ = tuple(t) # these will not _ = bool([]) _ = bytes(0xFF) _ = complex(1) _ = dict((("a", 1),)) _ = float(123) _ = list((1, 2, 3)) _ = str(123) _ = tuple([1, 2, 3]) _ = int("0xFF") _ = dict(**d) # noqa: FURB173 refurb-1.27.0/test/data/err_123.txt000066400000000000000000000021201454672660200167030ustar00rootroot00000000000000test/data/err_123.py:3:5 [FURB123]: Replace `bool(x)` with `x` test/data/err_123.py:4:5 [FURB123]: Replace `bytes(x)` with `x` test/data/err_123.py:5:5 [FURB123]: Replace `complex(x)` with `x` test/data/err_123.py:6:5 [FURB123]: Replace `dict(x)` with `x` test/data/err_123.py:7:5 [FURB123]: Replace `float(x)` with `x` test/data/err_123.py:8:5 [FURB123]: Replace `list(x)` with `x` test/data/err_123.py:9:5 [FURB123]: Replace `str(x)` with `x` test/data/err_123.py:10:5 [FURB123]: Replace `tuple(x)` with `x` test/data/err_123.py:11:5 [FURB123]: Replace `int(x)` with `x` test/data/err_123.py:14:5 [FURB123]: Replace `bool(x)` with `x` test/data/err_123.py:17:5 [FURB123]: Replace `bytes(x)` with `x` test/data/err_123.py:20:5 [FURB123]: Replace `complex(x)` with `x` test/data/err_123.py:23:5 [FURB123]: Replace `dict(x)` with `x.copy()` test/data/err_123.py:26:5 [FURB123]: Replace `float(x)` with `x` test/data/err_123.py:29:5 [FURB123]: Replace `list(x)` with `x.copy()` test/data/err_123.py:32:5 [FURB123]: Replace `str(x)` with `x` test/data/err_123.py:35:5 [FURB123]: Replace `tuple(x)` with `x` refurb-1.27.0/test/data/err_124.py000066400000000000000000000007271454672660200165300ustar00rootroot00000000000000x = y = z = 1 # these should match _ = x == y and x == z _ = x == y and y == z _ = x == y and z == y _ = x == y and x == z and True _ = x == y and y == z and z == 1 _ = x == y and z == x _ = x is None and y is None _ = x is None and None is y _ = None is x and y is None _ = x is None and y is None and True # these should not _ = x == y and z == 1 _ = x == y or 1 == z _ = x == y or 1 == z _ = x == y or x <= z _ = x is None and y is 1 _ = x is None or y is None refurb-1.27.0/test/data/err_124.txt000066400000000000000000000016271454672660200167170ustar00rootroot00000000000000test/data/err_124.py:5:5 [FURB124]: Replace `x == y and x == z` with `x == y == z` test/data/err_124.py:6:5 [FURB124]: Replace `x == y and y == z` with `x == y == z` test/data/err_124.py:7:5 [FURB124]: Replace `x == y and z == y` with `x == y == z` test/data/err_124.py:8:5 [FURB124]: Replace `x == y and x == z` with `x == y == z` test/data/err_124.py:9:5 [FURB124]: Replace `x == y and y == z` with `x == y == z` test/data/err_124.py:9:16 [FURB124]: Replace `x == y and y == z` with `x == y == z` test/data/err_124.py:10:5 [FURB124]: Replace `x == y and z == x` with `x == y == z` test/data/err_124.py:12:5 [FURB124]: Replace `x is y and z is y` with `x is y is z` test/data/err_124.py:13:5 [FURB124]: Replace `x is y and y is z` with `x is y is z` test/data/err_124.py:14:5 [FURB124]: Replace `x is y and z is x` with `x is y is z` test/data/err_124.py:15:5 [FURB124]: Replace `x is y and z is y` with `x is y is z` refurb-1.27.0/test/data/err_125.py000066400000000000000000000031431454672660200165240ustar00rootroot00000000000000# these will match def stmt_then_return(): pass return def match_trailing_case(): match 123: case 123: print("it is 123!") case _: return def if_trailing_return(): if False: pass else: return def elif_trailing_return(): if False: pass elif False: pass else: return def nested_match(): match [123]: case [x]: match x: case 123: pass case _: return def with_stmt(): with open("file"): return def match_without_wildcard(): match 1: case 1: return def match_multiple_bodies(): match [123]: case [_]: print("here") return case []: print("there") return case _: return # these will not def just_return(): return def return_value(): return 1 def return_none(): return None def if_with_non_trailing_node(): if False: pass else: pass pass def nested_if_with_non_trailing_node(): if False: pass else: if False: pass else: return pass def nested_match_with_non_trailing_node(): match [123]: case [x]: match x: case 123: pass case _: return pass def match_with_early_return(x): match x: case [_]: return case []: return refurb-1.27.0/test/data/err_125.txt000066400000000000000000000010621454672660200167110ustar00rootroot00000000000000test/data/err_125.py:6:5 [FURB125]: Return is redundant here test/data/err_125.py:14:13 [FURB125]: Return is redundant here test/data/err_125.py:21:9 [FURB125]: Return is redundant here test/data/err_125.py:31:9 [FURB125]: Return is redundant here test/data/err_125.py:41:21 [FURB125]: Return is redundant here test/data/err_125.py:45:9 [FURB125]: Return is redundant here test/data/err_125.py:59:13 [FURB125]: Return is redundant here test/data/err_125.py:64:13 [FURB125]: Return is redundant here test/data/err_125.py:67:13 [FURB125]: Return is redundant here refurb-1.27.0/test/data/err_126.py000066400000000000000000000025171454672660200165310ustar00rootroot00000000000000# these will match def is_even(x): if x % 2 == 0: return True else: return False def is_even_again(x): match x % 2: case 0: return True case _: return False def nested(x): match x % 2: case 0: return True case _: match x % 3: case 0: return True case _: return False def match_multiple_stmts(x): match x: case 1: pass return 1 case 2: pass return 2 case _: return 3 # these will not def func(x): if x == 1: return True else: pass return False def func2(x): match x % 2: case 0: return True case _: pass return False def func3(x): match x % 2: case 0: return True return False def func4(x): if x == 1: return True return False def func5(): return False def func6(x): match x: case 1: return 1 case 2: return 2 def func7(x): match x: case 1: pass case _: return 2 def func8(x): if x == 1: pass else: return 1 refurb-1.27.0/test/data/err_126.txt000066400000000000000000000005001454672660200167060ustar00rootroot00000000000000test/data/err_126.py:8:9 [FURB126]: Replace `else: return x` with `return x` test/data/err_126.py:17:13 [FURB126]: Replace `case _: return x` with `return x` test/data/err_126.py:30:21 [FURB126]: Replace `case _: return x` with `return x` test/data/err_126.py:45:13 [FURB126]: Replace `case _: return x` with `return x` refurb-1.27.0/test/data/err_127.py000066400000000000000000000010041454672660200165200ustar00rootroot00000000000000import contextlib from contextlib import nullcontext, suppress # these will match def func(): x = "" with nullcontext(): x = "some value" x = "" with nullcontext(): x = "some value" # these will not from contextlib import nullcontext from typing import TYPE_CHECKING if not TYPE_CHECKING: x = 1 with nullcontext(): y = 2 # see https://github.com/dosisod/refurb/issues/47 x = 1 with suppress(Exception): x = 2 x = 1 with contextlib.suppress(Exception): x = 2 refurb-1.27.0/test/data/err_127.txt000066400000000000000000000002771454672660200167220ustar00rootroot00000000000000test/data/err_127.py:7:5 [FURB127]: This variable is redeclared later, and can be removed here test/data/err_127.py:13:1 [FURB127]: This variable is redeclared later, and can be removed here refurb-1.27.0/test/data/err_128.py000066400000000000000000000012101454672660200165200ustar00rootroot00000000000000# `if` blocks are used to separate test cases x = 1 y = 2 # these will match if True: tmp = x x = y y = tmp if True: filler = 1 tmp = x x = y y = tmp if True: filler = 1 filler = 2 tmp = x x = y y = tmp if True: tmp = x x = y y = tmp tmp = x x = y y = tmp # these will not if True: tmp = x y = tmp x = y if True: tmp = x x = y tmp = 1 y = tmp if True: tmp = x x = y pass y = tmp from typing import TYPE_CHECKING # See https://github.com/dosisod/refurb/issues/23 if not TYPE_CHECKING: x = tmp x = tmp x = tmp refurb-1.27.0/test/data/err_128.txt000066400000000000000000000010021454672660200167060ustar00rootroot00000000000000test/data/err_128.py:8:5 [FURB128]: Use tuple unpacking instead of temporary variables to swap values test/data/err_128.py:14:5 [FURB128]: Use tuple unpacking instead of temporary variables to swap values test/data/err_128.py:21:5 [FURB128]: Use tuple unpacking instead of temporary variables to swap values test/data/err_128.py:26:5 [FURB128]: Use tuple unpacking instead of temporary variables to swap values test/data/err_128.py:29:5 [FURB128]: Use tuple unpacking instead of temporary variables to swap values refurb-1.27.0/test/data/err_129.py000066400000000000000000000014261454672660200165320ustar00rootroot00000000000000# these should match with open("file.txt") as f: for line in f.readlines(): pass with open("file.txt") as f: lines = [f"{line}!" for line in f.readlines()] with open("file.txt") as f: lines = list(f"{line}!" for line in f.readlines()) # noqa: FURB137 with open("file.txt", "rb") as f: lines = list(f"{line}!" for line in f.readlines()) # noqa: FURB137 f = open("file.txt") for line in f.readlines(): pass # these will not with open("file.txt") as f: for line in f.readlines(1): pass with open("file.txt") as f: for line in f: pass class Reader: @staticmethod def readlines() -> list[str]: return ["hello", "world"] for line in Reader.readlines(): pass file = open("file.txt") x = file.readlines() refurb-1.27.0/test/data/err_129.txt000066400000000000000000000005411454672660200167160ustar00rootroot00000000000000test/data/err_129.py:4:17 [FURB129]: Replace `f.readlines()` with `f` test/data/err_129.py:9:37 [FURB129]: Replace `f.readlines()` with `f` test/data/err_129.py:13:41 [FURB129]: Replace `f.readlines()` with `f` test/data/err_129.py:17:41 [FURB129]: Replace `f.readlines()` with `f` test/data/err_129.py:21:13 [FURB129]: Replace `f.readlines()` with `f` refurb-1.27.0/test/data/err_130.py000066400000000000000000000005371454672660200165240ustar00rootroot00000000000000# these should match d = {} if "key" in d.keys(): pass if "key" not in d.keys(): pass x = "key" if x in d.keys(): pass # these should not if "key" in d: pass if "key" not in d: pass class NotADict: def keys(self) -> list[str]: return ["abc"] not_a_dict = NotADict() if "key" in not_a_dict.keys(): pass refurb-1.27.0/test/data/err_130.txt000066400000000000000000000003351454672660200167070ustar00rootroot00000000000000test/data/err_130.py:5:13 [FURB130]: Replace `in d.keys()` with `in d` test/data/err_130.py:8:17 [FURB130]: Replace `not in d.keys()` with `not in d` test/data/err_130.py:12:9 [FURB130]: Replace `in d.keys()` with `in d` refurb-1.27.0/test/data/err_131.py000066400000000000000000000002341454672660200165170ustar00rootroot00000000000000names = {"key": "value"} nums = [1, 2, 3] # these should match del nums[:] # these should not del names["key"] del nums[0] x = 1 del x del nums[1:2] refurb-1.27.0/test/data/err_131.txt000066400000000000000000000001101454672660200166770ustar00rootroot00000000000000test/data/err_131.py:6:1 [FURB131]: Replace `del x[:]` with `x.clear()` refurb-1.27.0/test/data/err_132.py000066400000000000000000000006411454672660200165220ustar00rootroot00000000000000s = set() # these should match if "x" in s: s.remove("x") # these should not if "x" in s: s.remove("y") s.discard("x") s2 = set() if "x" in s: s2.remove("x") if "x" in s: s.remove("x") print("removed item") class Container: def remove(self, item) -> None: return def __contains__(self, other) -> bool: return True c = Container() if "x" in c: c.remove("x") refurb-1.27.0/test/data/err_132.txt000066400000000000000000000001311454672660200167030ustar00rootroot00000000000000test/data/err_132.py:5:1 [FURB132]: Replace `if x in s: s.remove(x)` with `s.discard(x)` refurb-1.27.0/test/data/err_133.py000066400000000000000000000021461454672660200165250ustar00rootroot00000000000000# these will match def continue_at_end_of_while(): while True: pass continue def continue_at_end_of_for_loop(): for _ in range(10): pass continue def continue_at_end_of_else_block(): for x in range(10): if x: pass else: continue def continue_in_match(): for x in range(10): match x: case 1: pass continue case 2: pass continue case _: continue def continue_in_with_block(): while True: with open("file.txt") as f: continue # these will not def continue_in_match_with_trailing_stmt(): for x in range(10): match x: case 1: continue case _: continue pass def continue_match_with_single_continue(): for x in range(10): match x: case 1: continue case 2: pass def while_loop_with_just_a_continue(): while True: continue refurb-1.27.0/test/data/err_133.txt000066400000000000000000000007041454672660200167120ustar00rootroot00000000000000test/data/err_133.py:7:9 [FURB133]: Continue is redundant here test/data/err_133.py:13:9 [FURB133]: Continue is redundant here test/data/err_133.py:21:13 [FURB133]: Continue is redundant here test/data/err_133.py:29:17 [FURB133]: Continue is redundant here test/data/err_133.py:34:17 [FURB133]: Continue is redundant here test/data/err_133.py:37:17 [FURB133]: Continue is redundant here test/data/err_133.py:42:13 [FURB133]: Continue is redundant here refurb-1.27.0/test/data/err_134.py000066400000000000000000000006301454672660200165220ustar00rootroot00000000000000import functools from functools import cache, lru_cache # these should match @lru_cache(maxsize=None) def f() -> None: pass @functools.lru_cache(maxsize=None) def f2() -> None: pass # these should not @lru_cache(maxsize=None, typed=True) def f3() -> None: pass @lru_cache(maxsize=100) def f4() -> None: pass @lru_cache def f5() -> None: pass @cache def f6() -> None: pass refurb-1.27.0/test/data/err_134.txt000066400000000000000000000002771454672660200167200ustar00rootroot00000000000000test/data/err_134.py:6:2 [FURB134]: Replace `@lru_cache(maxsize=None)` with `@cache` test/data/err_134.py:10:2 [FURB134]: Replace `@functools.lru_cache(maxsize=None)` with `@functools.cache` refurb-1.27.0/test/data/err_135.py000066400000000000000000000022771454672660200165340ustar00rootroot00000000000000# these should match d = {} def f1(): for k, _ in d.items(): print(k) def f2(): for _, v in d.items(): print(v) def f3(): (k for k, _ in d.items()) (v for _, v in d.items()) def f4(): {k: "" for k, _ in d.items()} {v: "" for _, v in d.items()} def f5(): (k for k, v in d.items()) # "v" is unused, warn (v for k, v in d.items()) # "k" is unused, warn {k: "" for k, v in d.items()} # "v" is unused, warn {v: "" for k, v in d.items()} # "k" is unused, warn def f6(): k=v=0 # don't warn because we can't know if "k" or "v" are unused simply by # looking at the for block, we need to account for the surrounding context, # which is not possible currently. for k, v in d.items(): pass print(k, v) # these should not def f7(): for k, v in d.items(): print(k, v) class Shelf: def items(self) -> list[tuple[str, int]]: return [("bagels", 123)] def f8(): shelf = Shelf() for name, count in shelf.items(): pass def f9(): {k: "" for k, v in d.items() if v} {v: "" for k, v in d.items() if k} (k for k, v in d.items() if v) (v for k, v in d.items() if k) refurb-1.27.0/test/data/err_135.txt000066400000000000000000000020131454672660200167070ustar00rootroot00000000000000test/data/err_135.py:6:12 [FURB135]: Value is unused, use `for key in d` instead test/data/err_135.py:11:9 [FURB135]: Key is unused, use `for value in d.values()` instead test/data/err_135.py:16:15 [FURB135]: Value is unused, use `for key in d` instead test/data/err_135.py:17:12 [FURB135]: Key is unused, use `for value in d.values()` instead test/data/err_135.py:21:19 [FURB135]: Value is unused, use `for key in d` instead test/data/err_135.py:22:16 [FURB135]: Key is unused, use `for value in d.values()` instead test/data/err_135.py:26:15 [FURB135]: Value is unused, use `for key in d` instead test/data/err_135.py:27:12 [FURB135]: Key is unused, use `for value in d.values()` instead test/data/err_135.py:29:19 [FURB135]: Value is unused, use `for key in d` instead test/data/err_135.py:30:16 [FURB135]: Key is unused, use `for value in d.values()` instead test/data/err_135.py:39:9 [FURB135]: Key is unused, use `for value in d.values()` instead test/data/err_135.py:39:12 [FURB135]: Value is unused, use `for key in d` instead refurb-1.27.0/test/data/err_136.py000066400000000000000000000004121454672660200165220ustar00rootroot00000000000000x = 1 y = 2 # these should match _ = x if x < y else y _ = x if x <= y else y _ = x if x > y else y _ = x if x >= y else y _ = x if y < x else y _ = x if y <= x else y _ = x if y > x else y _ = x if y >= x else y # these should not z = 3 _ = x if x < y else z refurb-1.27.0/test/data/err_136.txt000066400000000000000000000012201454672660200167070ustar00rootroot00000000000000test/data/err_136.py:6:5 [FURB136]: Replace `x if x < y else y` with `min(x, y)` test/data/err_136.py:7:5 [FURB136]: Replace `x if x <= y else y` with `min(x, y)` test/data/err_136.py:8:5 [FURB136]: Replace `x if x > y else y` with `max(x, y)` test/data/err_136.py:9:5 [FURB136]: Replace `x if x >= y else y` with `max(x, y)` test/data/err_136.py:11:5 [FURB136]: Replace `x if y < x else y` with `max(y, x)` test/data/err_136.py:12:5 [FURB136]: Replace `x if y <= x else y` with `max(y, x)` test/data/err_136.py:13:5 [FURB136]: Replace `x if y > x else y` with `min(y, x)` test/data/err_136.py:14:5 [FURB136]: Replace `x if y >= x else y` with `min(y, x)` refurb-1.27.0/test/data/err_137.py000066400000000000000000000013351454672660200165300ustar00rootroot00000000000000nums = [1, 2, 3] # these should match # Here I am using `num + 1` because `num for num in nums` is basically the same # as `nums` (and I plan to make a check for that in the future). set(num + 1 for num in nums) set([num + 1 for num in nums]) set({num + 1 for num in nums}) list(num + 1 for num in nums) list([num + 1 for num in nums]) frozenset([num + 1 for num in nums]) frozenset({num + 1 for num in nums}) tuple([num + 1 for num in nums]) # these should not _ = {num + 1 for num in nums} _ = [num + 1 for num in nums] list({num + 1 for num in nums}) tuple({num + 1 for num in nums}) set[int](num + 1 for num in nums) list[int](num + 1 for num in nums) frozenset(num + 1 for num in nums) tuple(num + 1 for num in nums) refurb-1.27.0/test/data/err_137.txt000066400000000000000000000011311454672660200167110ustar00rootroot00000000000000test/data/err_137.py:8:1 [FURB137]: Replace `set(...)` with `{...}` test/data/err_137.py:9:1 [FURB137]: Replace `set([...])` with `{...}` test/data/err_137.py:10:1 [FURB137]: Replace `set({...})` with `{...}` test/data/err_137.py:12:1 [FURB137]: Replace `list(...)` with `[...]` test/data/err_137.py:13:1 [FURB137]: Replace `list([...])` with `[...]` test/data/err_137.py:15:1 [FURB137]: Replace `frozenset([...])` with `frozenset(...)` test/data/err_137.py:16:1 [FURB137]: Replace `frozenset({...})` with `frozenset(...)` test/data/err_137.py:18:1 [FURB137]: Replace `tuple([...])` with `tuple(...)` refurb-1.27.0/test/data/err_138.py000066400000000000000000000023541454672660200165330ustar00rootroot00000000000000# these should match def f1(): arr = [] for num in (1, 2, 3): arr.append(num) def f2(): arr = [] for num in (1, 2, 3): arr.append(num + 1) def f3(): arr = [] for num in (1, 2, 3): if num % 2: arr.append(num) nums = [] for num in (1, 2, 3): if num % 2: nums.append(num) # should not match def f4(): arr = [] for num in (1, 2, 3): pass arr.append(num) def f5(): arr = [] for num in (1, 2, 3): arr.pop(num) def f6(): # Although this should be caught, the general case for this is a bit harder # then expected. arr2 = [] arr = [] for num in (1, 2, 3): arr2.append(num) def f7(): arr = [] for num in (1, 2, 3): if x := num + 1: arr.append(x) def f8(): s = "abc" for num in (1, 2, 3): s.append(num) def f9(): arr = [] pass for num in (1, 2, 3): arr.append(num) def f10(): arr = [1, 2, 3] for num in (1, 2, 3): arr.append(num) def f11(): arr = [] for num in (1, 2, 3): if num not in arr: arr.append(arr) def f12(): arr = [] for num in (1, 2, 3): arr.append(arr) refurb-1.27.0/test/data/err_138.txt000066400000000000000000000004331454672660200167160ustar00rootroot00000000000000test/data/err_138.py:4:5 [FURB138]: Consider using list comprehension test/data/err_138.py:11:5 [FURB138]: Consider using list comprehension test/data/err_138.py:18:5 [FURB138]: Consider using list comprehension test/data/err_138.py:25:1 [FURB138]: Consider using list comprehension refurb-1.27.0/test/data/err_139.py000066400000000000000000000010451454672660200165300ustar00rootroot00000000000000# these should match """ Hello world """.lstrip() """\nHello world """.lstrip() """ Hello world """.lstrip("\n") """ Hello world """.rstrip() """\nHello world """.rstrip() """ Hello world """.rstrip("\n") """ Hello world """.strip() """ This is a test """.strip() """\nHello world """.strip() """ Hello world """.strip("\n") # these should not "\n\n".lstrip() """ This is a test """.lstrip() """ This is a test """.rstrip() """ Testing 123 """.lstrip("x") """ Testing 123 """.lstrip() s = "Hello world" s.lstrip() refurb-1.27.0/test/data/err_139.txt000066400000000000000000000015531454672660200167230ustar00rootroot00000000000000test/data/err_139.py:3:1 [FURB139]: Replace `"""\n...""".lstrip()` with `"""\..."""` test/data/err_139.py:8:1 [FURB139]: Replace `"""\n...""".lstrip()` with `"""\..."""` test/data/err_139.py:12:1 [FURB139]: Replace `"""\n...""".lstrip("\n")` with `"""\..."""` test/data/err_139.py:17:1 [FURB139]: Replace `"""...\n""".rstrip()` with `"""...\"""` test/data/err_139.py:22:1 [FURB139]: Replace `"""...\n""".rstrip()` with `"""...\"""` test/data/err_139.py:26:1 [FURB139]: Replace `"""...\n""".rstrip("\n")` with `"""...\"""` test/data/err_139.py:31:1 [FURB139]: Replace `"""\n...\n""".strip()` with `"""\...\"""` test/data/err_139.py:36:1 [FURB139]: Replace `"""\n...""".strip()` with `"""\..."""` test/data/err_139.py:42:1 [FURB139]: Replace `"""\n...\n""".strip()` with `"""\...\"""` test/data/err_139.py:46:1 [FURB139]: Replace `"""\n...\n""".strip("\n")` with `"""\...\"""` refurb-1.27.0/test/data/err_140.py000066400000000000000000000007101454672660200165160ustar00rootroot00000000000000# these should match def zipped(): return zip([1, 2, 3], "ABC") [print(x, y) for x, y in zipped()] (print(x, y) for x, y in zipped()) {print(x, y) for x, y in zipped()} # these should not [print(x, int) for x, _ in zipped()] [print(x, y, 1) for x, y in zipped()] [print(y, x) for x, y in zipped()] [print(x + 1, y) for x, y in zipped()] [print(x) for x in range(100)] [print() for x, y in zipped()] [print(x, end=y) for x, y in zipped()] refurb-1.27.0/test/data/err_140.txt000066400000000000000000000004251454672660200167100ustar00rootroot00000000000000test/data/err_140.py:7:1 [FURB140]: Replace `[f(...) for ... in x]` with `list(starmap(f, x))` test/data/err_140.py:9:1 [FURB140]: Replace `f(...) for ... in x` with `starmap(f, x)` test/data/err_140.py:11:1 [FURB140]: Replace `{f(...) for ... in x}` with `set(starmap(f, x))` refurb-1.27.0/test/data/err_141.py000066400000000000000000000003741454672660200165250ustar00rootroot00000000000000import os from os.path import exists from pathlib import Path # these should match _ = os.path.exists("file") _ = exists("file") p = Path("file") _ = os.path.exists(p) _ = os.path.exists(Path("file")) # these should not _ = Path("file").exists() refurb-1.27.0/test/data/err_141.txt000066400000000000000000000005261454672660200167130ustar00rootroot00000000000000test/data/err_141.py:7:5 [FURB141]: Replace `os.path.exists(x)` with `Path(x).exists()` test/data/err_141.py:8:5 [FURB141]: Replace `os.path.exists(x)` with `Path(x).exists()` test/data/err_141.py:11:5 [FURB141]: Replace `os.path.exists(x)` with `x.exists()` test/data/err_141.py:12:5 [FURB141]: Replace `os.path.exists(x)` with `x.exists()` refurb-1.27.0/test/data/err_142.py000066400000000000000000000005511454672660200165230ustar00rootroot00000000000000# these should match s = set() for x in (1, 2, 3): s.add(x) for x in (1, 2, 3): s.discard(x) for x in (1, 2, 3): s.add(x + 1) # these should not s.update(x for x in (1, 2, 3)) num = 123 for x in (1, 2, 3): s.add(num) for x in (set(),): x.add(x) # TODO: support unpacked tuples here for x, y in ((1, 2), (3, 4)): s.add((x, y)) refurb-1.27.0/test/data/err_142.txt000066400000000000000000000004411454672660200167100ustar00rootroot00000000000000test/data/err_142.py:5:1 [FURB142]: Replace `for x in y: s.add(x)` with `s.update(y)` test/data/err_142.py:8:1 [FURB142]: Replace `for x in y: s.discard(x)` with `s.difference_update(y)` test/data/err_142.py:11:1 [FURB142]: Replace `for x in y: s.add(...)` with `s.update(... for x in y)` refurb-1.27.0/test/data/err_143.py000066400000000000000000000011501454672660200165200ustar00rootroot00000000000000s = set() f = frozenset() l = [] d = {} t = () t2 = tuple((1,)) # noqa: FURB123 r = "" b = b"" n = False i = 0 # these should match _ = s or set() _ = f or frozenset() _ = l or [] _ = d or {} _ = t or () _ = t2 or () _ = r or "" _ = b or b"" _ = n or False _ = i or 0 class C: x: int def __init__(self) -> None: # x could be anything here self.x = 123 c = C() _ = c.x or 0 # these should not _ = s or set((1, 2, 3)) _ = f or frozenset((1, 2, 3)) _ = l or [1, 2, 3] _ = d or {"a": "b"} _ = t or (1, 2, 3) _ = t2 or (1, 2, 3) _ = r or "abc" _ = b or b"abc" _ = n or True _ = i or 123 refurb-1.27.0/test/data/err_143.txt000066400000000000000000000012171454672660200167130ustar00rootroot00000000000000test/data/err_143.py:14:5 [FURB143]: Replace `x or set()` with `x` test/data/err_143.py:15:5 [FURB143]: Replace `x or frozenset()` with `x` test/data/err_143.py:16:5 [FURB143]: Replace `x or []` with `x` test/data/err_143.py:17:5 [FURB143]: Replace `x or {}` with `x` test/data/err_143.py:18:5 [FURB143]: Replace `x or ()` with `x` test/data/err_143.py:19:5 [FURB143]: Replace `x or ()` with `x` test/data/err_143.py:20:5 [FURB143]: Replace `x or ""` with `x` test/data/err_143.py:21:5 [FURB143]: Replace `x or b""` with `x` test/data/err_143.py:22:5 [FURB143]: Replace `x or False` with `x` test/data/err_143.py:23:5 [FURB143]: Replace `x or 0` with `x` refurb-1.27.0/test/data/err_144.py000066400000000000000000000004641454672660200165300ustar00rootroot00000000000000import os from os import remove, unlink from pathlib import Path # these should match os.remove("file") os.unlink("file") file = Path("file") os.remove(file) os.unlink(file) remove("file") unlink("file") # these should not os.remove("file", dir_fd=1) os.unlink("file", dir_fd=1) Path("file").unlink() refurb-1.27.0/test/data/err_144.txt000066400000000000000000000007521454672660200167170ustar00rootroot00000000000000test/data/err_144.py:7:1 [FURB144]: Replace `os.remove(x)` with `Path(x).unlink()` test/data/err_144.py:8:1 [FURB144]: Replace `os.unlink(x)` with `Path(x).unlink()` test/data/err_144.py:11:1 [FURB144]: Replace `os.remove(x)` with `x.unlink()` test/data/err_144.py:12:1 [FURB144]: Replace `os.unlink(x)` with `x.unlink()` test/data/err_144.py:14:1 [FURB144]: Replace `os.remove(x)` with `Path(x).unlink()` test/data/err_144.py:15:1 [FURB144]: Replace `os.unlink(x)` with `Path(x).unlink()` refurb-1.27.0/test/data/err_145.py000066400000000000000000000005361454672660200165310ustar00rootroot00000000000000nums = [1, 2, 3] t = (1, 2, 3) barray = bytearray((0xFF,)) # these should match _ = nums[:] _ = t[:] _ = barray[:] # these should not _ = nums.copy() _ = nums[1:] _ = nums[:1] _ = nums[::1] nums[:] = [4, 5, 6] class C: def __getitem__(self, key): return None _ = C()[:,] c = C() _ = c[:] s = "abc" _ = s[:] b = b"abc" _ = b[:] refurb-1.27.0/test/data/err_145.txt000066400000000000000000000003111454672660200167070ustar00rootroot00000000000000test/data/err_145.py:7:5 [FURB145]: Replace `x[:]` with `x.copy()` test/data/err_145.py:8:5 [FURB145]: Replace `x[:]` with `x.copy()` test/data/err_145.py:9:5 [FURB145]: Replace `x[:]` with `x.copy()` refurb-1.27.0/test/data/err_146.py000066400000000000000000000020251454672660200165250ustar00rootroot00000000000000import os from os.path import isabs, isdir, isfile, islink from pathlib import Path file = Path("filename") filename = "filename" filename2 = b"filename" # these should match os.path.isabs("filename") os.path.isdir("filename") os.path.isfile("filename") os.path.islink("filename") os.path.isabs(file) os.path.isdir(file) os.path.isfile(file) os.path.islink(file) os.path.isabs(b"filename") os.path.isdir(b"filename") os.path.isfile(b"filename") os.path.islink(b"filename") os.path.isabs(filename) os.path.isdir(filename) os.path.isfile(filename) os.path.islink(filename) os.path.isabs(filename2) os.path.isdir(filename2) os.path.isfile(filename2) os.path.islink(filename2) isabs("filename") isdir("filename") isfile("filename") islink("filename") # these should not os.path.ismount("somefile") file.is_absolute() file.is_dir() file.is_file() file.is_symlink() file.is_mount() os.path.isdir(1) os.path.isfile(1) os.path.islink(1) os.path.ismount(1) fd = 1 os.path.isdir(fd) os.path.isfile(fd) os.path.islink(fd) os.path.ismount(fd) refurb-1.27.0/test/data/err_146.txt000066400000000000000000000041601454672660200167160ustar00rootroot00000000000000test/data/err_146.py:11:1 [FURB146]: Replace `os.path.isabs(x)` with `Path(x).is_absolute()` test/data/err_146.py:12:1 [FURB146]: Replace `os.path.isdir(x)` with `Path(x).is_dir()` test/data/err_146.py:13:1 [FURB146]: Replace `os.path.isfile(x)` with `Path(x).is_file()` test/data/err_146.py:14:1 [FURB146]: Replace `os.path.islink(x)` with `Path(x).is_symlink()` test/data/err_146.py:16:1 [FURB146]: Replace `os.path.isabs(x)` with `x.is_absolute()` test/data/err_146.py:17:1 [FURB146]: Replace `os.path.isdir(x)` with `x.is_dir()` test/data/err_146.py:18:1 [FURB146]: Replace `os.path.isfile(x)` with `x.is_file()` test/data/err_146.py:19:1 [FURB146]: Replace `os.path.islink(x)` with `x.is_symlink()` test/data/err_146.py:21:1 [FURB146]: Replace `os.path.isabs(x)` with `Path(x).is_absolute()` test/data/err_146.py:22:1 [FURB146]: Replace `os.path.isdir(x)` with `Path(x).is_dir()` test/data/err_146.py:23:1 [FURB146]: Replace `os.path.isfile(x)` with `Path(x).is_file()` test/data/err_146.py:24:1 [FURB146]: Replace `os.path.islink(x)` with `Path(x).is_symlink()` test/data/err_146.py:26:1 [FURB146]: Replace `os.path.isabs(x)` with `Path(x).is_absolute()` test/data/err_146.py:27:1 [FURB146]: Replace `os.path.isdir(x)` with `Path(x).is_dir()` test/data/err_146.py:28:1 [FURB146]: Replace `os.path.isfile(x)` with `Path(x).is_file()` test/data/err_146.py:29:1 [FURB146]: Replace `os.path.islink(x)` with `Path(x).is_symlink()` test/data/err_146.py:31:1 [FURB146]: Replace `os.path.isabs(x)` with `Path(x).is_absolute()` test/data/err_146.py:32:1 [FURB146]: Replace `os.path.isdir(x)` with `Path(x).is_dir()` test/data/err_146.py:33:1 [FURB146]: Replace `os.path.isfile(x)` with `Path(x).is_file()` test/data/err_146.py:34:1 [FURB146]: Replace `os.path.islink(x)` with `Path(x).is_symlink()` test/data/err_146.py:36:1 [FURB146]: Replace `os.path.isabs(x)` with `Path(x).is_absolute()` test/data/err_146.py:37:1 [FURB146]: Replace `os.path.isdir(x)` with `Path(x).is_dir()` test/data/err_146.py:38:1 [FURB146]: Replace `os.path.isfile(x)` with `Path(x).is_file()` test/data/err_146.py:39:1 [FURB146]: Replace `os.path.islink(x)` with `Path(x).is_symlink()` refurb-1.27.0/test/data/err_147.py000066400000000000000000000014711454672660200165320ustar00rootroot00000000000000import os from os.path import join from pathlib import Path # these should match os.path.join("a") os.path.join("a", "b") os.path.join("a", "b", "c") os.path.join("a", "b", "c", "d") os.path.join("some_path", "..") os.path.join("some", "path", "..") os.path.join("some", "other", "path", "..") os.path.join("some", "path", "..", "..") os.path.join("..", "some", "path") os.path.join(b"a") os.path.join(b"a", b"b") os.path.join(b"a", b"b", b"c") os.path.join(b"a", b"b", b"c", b"d") os.path.join(b"some_path", b"..") os.path.join(b"some", b"path", b"..") os.path.join(b"some", b"other", b"path", b"..") os.path.join(b"some", b"path", b"..", b"..") os.path.join(b"..", b"some", b"path") join("a") # these should not os.path.join() # type: ignore Path("a") Path("a", "b") Path("a", "b", "c") Path("a", "b", "c", "d") refurb-1.27.0/test/data/err_147.txt000066400000000000000000000033021454672660200167140ustar00rootroot00000000000000test/data/err_147.py:7:1 [FURB147]: Replace `os.path.join(x)` with `Path(x)` test/data/err_147.py:8:1 [FURB147]: Replace `os.path.join(x, y)` with `Path(x, y)` test/data/err_147.py:9:1 [FURB147]: Replace `os.path.join(x, y, z)` with `Path(x, y, z)` test/data/err_147.py:10:1 [FURB147]: Replace `os.path.join(...)` with `Path(...)` test/data/err_147.py:12:1 [FURB147]: Replace `os.path.join(x, "..")` with `Path(x).parent` test/data/err_147.py:13:1 [FURB147]: Replace `os.path.join(x, y, "..")` with `Path(x, y).parent` test/data/err_147.py:14:1 [FURB147]: Replace `os.path.join(x, y, z, "..")` with `Path(x, y, z).parent` test/data/err_147.py:15:1 [FURB147]: Replace `os.path.join(x, y, "..", "..")` with `Path(x, y).parent.parent` test/data/err_147.py:16:1 [FURB147]: Replace `os.path.join(x, y, z)` with `Path(x, y, z)` test/data/err_147.py:18:1 [FURB147]: Replace `os.path.join(x)` with `Path(x)` test/data/err_147.py:19:1 [FURB147]: Replace `os.path.join(x, y)` with `Path(x, y)` test/data/err_147.py:20:1 [FURB147]: Replace `os.path.join(x, y, z)` with `Path(x, y, z)` test/data/err_147.py:21:1 [FURB147]: Replace `os.path.join(...)` with `Path(...)` test/data/err_147.py:23:1 [FURB147]: Replace `os.path.join(x, b"..")` with `Path(x).parent` test/data/err_147.py:24:1 [FURB147]: Replace `os.path.join(x, y, b"..")` with `Path(x, y).parent` test/data/err_147.py:25:1 [FURB147]: Replace `os.path.join(x, y, z, b"..")` with `Path(x, y, z).parent` test/data/err_147.py:26:1 [FURB147]: Replace `os.path.join(x, y, b"..", b"..")` with `Path(x, y).parent.parent` test/data/err_147.py:27:1 [FURB147]: Replace `os.path.join(x, y, z)` with `Path(x, y, z)` test/data/err_147.py:29:1 [FURB147]: Replace `os.path.join(x)` with `Path(x)` refurb-1.27.0/test/data/err_148.py000066400000000000000000000020301454672660200165230ustar00rootroot00000000000000from itertools import count nums = (1, 2, 3) # these should match for index, _ in enumerate(nums): print(index) for _, num in enumerate(nums): print(num) _ = (index for index, _ in enumerate(nums)) _ = (num for _, num in enumerate(nums)) _ = {"key": index for index, _ in enumerate(nums)} _ = {"key": num for _, num in enumerate(nums)} _ = (1 for index, num in enumerate(nums)) _ = {"key": "value" for index, num in enumerate(nums)} nums2 = [4, 5, 6] nums3 = tuple((7, 8, 9)) # noqa: FURB123 _ = (index for index, _ in enumerate(nums3)) for index, num in enumerate(nums): pass # these should not # "count" is an infinite generator. In general, we only want to warn on # sequence types because you can call len() on them without exhausting some # iterator. counter = count() for index, _ in enumerate(counter): pass _ = (num for index, num in enumerate(nums) if index) for index, _ in enumerate(nums): print(index, _) _ = ((index, _) for index, _ in enumerate(nums)) _ = ((_, num) for _, num in enumerate(nums)) refurb-1.27.0/test/data/err_148.txt000066400000000000000000000021411454672660200167150ustar00rootroot00000000000000test/data/err_148.py:7:12 [FURB148]: Value is unused, use `for x in range(len(y))` instead test/data/err_148.py:10:5 [FURB148]: Index is unused, use `for x in y` instead test/data/err_148.py:13:23 [FURB148]: Value is unused, use `for x in range(len(y))` instead test/data/err_148.py:14:14 [FURB148]: Index is unused, use `for x in y` instead test/data/err_148.py:16:30 [FURB148]: Value is unused, use `for x in range(len(y))` instead test/data/err_148.py:17:21 [FURB148]: Index is unused, use `for x in y` instead test/data/err_148.py:19:12 [FURB148]: Index is unused, use `for x in y` instead test/data/err_148.py:19:19 [FURB148]: Value is unused, use `for x in range(len(y))` instead test/data/err_148.py:21:25 [FURB148]: Index is unused, use `for x in y` instead test/data/err_148.py:21:32 [FURB148]: Value is unused, use `for x in range(len(y))` instead test/data/err_148.py:26:23 [FURB148]: Value is unused, use `for x in range(len(y))` instead test/data/err_148.py:28:5 [FURB148]: Index is unused, use `for x in y` instead test/data/err_148.py:28:12 [FURB148]: Value is unused, use `for x in range(len(y))` instead refurb-1.27.0/test/data/err_149.py000066400000000000000000000006451454672660200165360ustar00rootroot00000000000000b = True # these should match _ = b is True _ = b is False _ = b is not True _ = b is not False _ = True is b _ = False is b _ = b == True _ = b == False _ = b != True _ = b != False _ = True == b _ = False == b # these should not class C: def __bool__(self) -> bool: return False _ = C() is True def f() -> bool | None: return None x: bool | None = f() _ = x is True _ = b is b _ = b is not b refurb-1.27.0/test/data/err_149.txt000066400000000000000000000014711454672660200167230ustar00rootroot00000000000000test/data/err_149.py:5:5 [FURB149]: Replace `x is True` with `x` test/data/err_149.py:6:5 [FURB149]: Replace `x is False` with `not x` test/data/err_149.py:7:5 [FURB149]: Replace `x is not True` with `not x` test/data/err_149.py:8:5 [FURB149]: Replace `x is not False` with `x` test/data/err_149.py:9:5 [FURB149]: Replace `True is x` with `x` test/data/err_149.py:10:5 [FURB149]: Replace `False is x` with `not x` test/data/err_149.py:12:5 [FURB149]: Replace `x == True` with `x` test/data/err_149.py:13:5 [FURB149]: Replace `x == False` with `not x` test/data/err_149.py:14:5 [FURB149]: Replace `x != True` with `not x` test/data/err_149.py:15:5 [FURB149]: Replace `x != False` with `x` test/data/err_149.py:16:5 [FURB149]: Replace `True == x` with `x` test/data/err_149.py:17:5 [FURB149]: Replace `False == x` with `not x` refurb-1.27.0/test/data/err_150.py000066400000000000000000000011521454672660200165200ustar00rootroot00000000000000import os from os import makedirs, mkdir from pathlib import Path path = Path("folder") # these should match os.mkdir("folder") os.mkdir(b"folder") os.mkdir(path) os.mkdir("folder", mode=0o644) os.mkdir("folder", 0o644) os.makedirs("folder") os.makedirs(b"folder") os.makedirs(path) os.makedirs("folder", mode=0o644) os.makedirs("folder", 0o644) os.makedirs("folder", exist_ok=True) os.makedirs("folder", exist_ok=False) os.makedirs("folder", exist_ok=False, mode=0o644) mkdir("folder") makedirs("folder") # these should not os.mkdir("folder", dir_fd=1) os.mkdir() # type: ignore os.makedirs() # type: ignore refurb-1.27.0/test/data/err_150.txt000066400000000000000000000026121454672660200167110ustar00rootroot00000000000000test/data/err_150.py:9:1 [FURB150]: Replace `os.mkdir(x)` with `Path(x).mkdir()` test/data/err_150.py:10:1 [FURB150]: Replace `os.mkdir(x)` with `Path(x).mkdir()` test/data/err_150.py:11:1 [FURB150]: Replace `os.mkdir(x)` with `x.mkdir()` test/data/err_150.py:12:1 [FURB150]: Replace `os.mkdir(x, ...)` with `Path(x).mkdir(...)` test/data/err_150.py:13:1 [FURB150]: Replace `os.mkdir(x, ...)` with `Path(x).mkdir(...)` test/data/err_150.py:15:1 [FURB150]: Replace `os.makedirs(x)` with `Path(x).mkdir(parents=True)` test/data/err_150.py:16:1 [FURB150]: Replace `os.makedirs(x)` with `Path(x).mkdir(parents=True)` test/data/err_150.py:17:1 [FURB150]: Replace `os.makedirs(x)` with `x.mkdir(parents=True)` test/data/err_150.py:18:1 [FURB150]: Replace `os.makedirs(x, ...)` with `Path(x).mkdir(..., parents=True)` test/data/err_150.py:19:1 [FURB150]: Replace `os.makedirs(x, ...)` with `Path(x).mkdir(..., parents=True)` test/data/err_150.py:20:1 [FURB150]: Replace `os.makedirs(x, ...)` with `Path(x).mkdir(..., parents=True)` test/data/err_150.py:21:1 [FURB150]: Replace `os.makedirs(x, ...)` with `Path(x).mkdir(..., parents=True)` test/data/err_150.py:22:1 [FURB150]: Replace `os.makedirs(x, ...)` with `Path(x).mkdir(..., parents=True)` test/data/err_150.py:24:1 [FURB150]: Replace `os.mkdir(x)` with `Path(x).mkdir()` test/data/err_150.py:25:1 [FURB150]: Replace `os.makedirs(x)` with `Path(x).mkdir(parents=True)` refurb-1.27.0/test/data/err_151.py000066400000000000000000000006101454672660200165170ustar00rootroot00000000000000from pathlib import Path # these should match open("file.txt", "w").close() open("file.txt", "w+").close() open("file.txt", mode="w+").close() path = Path("file.txt") open(path, "w").close() # noqa: FURB117 # these should not open("file.txt", "r").close() # noqa: FURB120 open("file.txt", "w", encoding="utf8").close() open("file.txt", encoding="w").close() open("file.txt").close() refurb-1.27.0/test/data/err_151.txt000066400000000000000000000005451454672660200167150ustar00rootroot00000000000000test/data/err_151.py:5:1 [FURB151]: Replace `open(x, "w").close()` with `Path(x).touch()` test/data/err_151.py:6:1 [FURB151]: Replace `open(x, "w+").close()` with `Path(x).touch()` test/data/err_151.py:7:1 [FURB151]: Replace `open(x, "w+").close()` with `Path(x).touch()` test/data/err_151.py:11:1 [FURB151]: Replace `open(x, "w").close()` with `x.touch()` refurb-1.27.0/test/data/err_152.py000066400000000000000000000003261454672660200165240ustar00rootroot00000000000000# these should match pi = 3.14 pi = 3.1415 pi = 003.1400 pi = -3.14 e = 2.71 e = 2.71828 e = -2.71 tau = 6.28 tau = 6.2831 tau = 2 * 3.14 tau = -6.28 # these should not _ = 3.1 _ = 2.7 _ = 6.2 _ = 1234 _ = 3.0 refurb-1.27.0/test/data/err_152.txt000066400000000000000000000013421454672660200167120ustar00rootroot00000000000000test/data/err_152.py:3:6 [FURB152]: Replace `3.14` with `math.pi` test/data/err_152.py:4:6 [FURB152]: Replace `3.1415` with `math.pi` test/data/err_152.py:5:6 [FURB152]: Replace `3.14` with `math.pi` test/data/err_152.py:6:7 [FURB152]: Replace `3.14` with `math.pi` test/data/err_152.py:7:5 [FURB152]: Replace `2.71` with `math.e` test/data/err_152.py:8:5 [FURB152]: Replace `2.71828` with `math.e` test/data/err_152.py:9:6 [FURB152]: Replace `2.71` with `math.e` test/data/err_152.py:10:7 [FURB152]: Replace `6.28` with `math.tau` test/data/err_152.py:11:7 [FURB152]: Replace `6.2831` with `math.tau` test/data/err_152.py:12:11 [FURB152]: Replace `3.14` with `math.pi` test/data/err_152.py:13:8 [FURB152]: Replace `6.28` with `math.tau` refurb-1.27.0/test/data/err_153.py000066400000000000000000000005111454672660200165210ustar00rootroot00000000000000import os import pathlib from pathlib import Path from os import path, curdir # these should match _ = Path(".") _ = Path("") _ = pathlib.Path(".") _ = Path(os.curdir) _ = Path(curdir) _ = Path(os.path.curdir) _ = Path(path.curdir) _ = pathlib.Path(curdir) # these should not print(".") Path("file.txt") Path(".", "folder") refurb-1.27.0/test/data/err_153.txt000066400000000000000000000011441454672660200167130ustar00rootroot00000000000000test/data/err_153.py:8:5 [FURB153]: Replace `Path(".")` with `Path()` test/data/err_153.py:9:5 [FURB153]: Replace `Path("")` with `Path()` test/data/err_153.py:10:5 [FURB153]: Replace `pathlib.Path(".")` with `Path()` test/data/err_153.py:11:5 [FURB153]: Replace `Path(os.curdir)` with `Path()` test/data/err_153.py:12:5 [FURB153]: Replace `Path(curdir)` with `Path()` test/data/err_153.py:13:5 [FURB153]: Replace `Path(os.path.curdir)` with `Path()` test/data/err_153.py:14:5 [FURB153]: Replace `Path(path.curdir)` with `Path()` test/data/err_153.py:15:5 [FURB153]: Replace `pathlib.Path(curdir)` with `Path()` refurb-1.27.0/test/data/err_154.py000066400000000000000000000016041454672660200165260ustar00rootroot00000000000000# these should match def f1(): global x global y def f3(): global x global y global z def f4(): global x global y pass global x global y def f2(): x = y = z = 1 def inner(): nonlocal x nonlocal y def inner2(): nonlocal x nonlocal y nonlocal z def inner3(): nonlocal x nonlocal y pass nonlocal x nonlocal y def f5(): w = x = y = z = 1 def inner(): global w global x nonlocal y nonlocal z def inner2(): global x nonlocal y nonlocal z # these should not def fx(): x = y = 1 def inner(): global x nonlocal y def inner2(): nonlocal x pass nonlocal y def fy(): global x pass global y def fz(): pass global x refurb-1.27.0/test/data/err_154.txt000066400000000000000000000017351454672660200167220ustar00rootroot00000000000000test/data/err_154.py:4:5 [FURB154]: Replace `global x; global y` with `global x, y` test/data/err_154.py:9:5 [FURB154]: Replace `global x; global y; ...` with `global x, y, ...` test/data/err_154.py:15:5 [FURB154]: Replace `global x; global y` with `global x, y` test/data/err_154.py:18:5 [FURB154]: Replace `global x; global y` with `global x, y` test/data/err_154.py:26:9 [FURB154]: Replace `nonlocal x; nonlocal y` with `nonlocal x, y` test/data/err_154.py:30:9 [FURB154]: Replace `nonlocal x; nonlocal y; ...` with `nonlocal x, y, ...` test/data/err_154.py:35:9 [FURB154]: Replace `nonlocal x; nonlocal y` with `nonlocal x, y` test/data/err_154.py:38:9 [FURB154]: Replace `nonlocal x; nonlocal y` with `nonlocal x, y` test/data/err_154.py:46:9 [FURB154]: Replace `global x; global y` with `global x, y` test/data/err_154.py:48:9 [FURB154]: Replace `nonlocal x; nonlocal y` with `nonlocal x, y` test/data/err_154.py:53:9 [FURB154]: Replace `nonlocal x; nonlocal y` with `nonlocal x, y` refurb-1.27.0/test/data/err_155.py000066400000000000000000000012051454672660200165240ustar00rootroot00000000000000import os from os.path import getatime, getctime, getmtime, getsize from pathlib import Path # these should match os.path.getatime("filename") os.path.getatime(b"filename") os.path.getatime(Path("filename")) getatime("filename") os.path.getmtime("filename") os.path.getmtime(b"filename") os.path.getmtime(Path("filename")) getmtime("filename") os.path.getctime("filename") os.path.getctime(b"filename") os.path.getctime(Path("filename")) getctime("filename") os.path.getsize("filename") os.path.getsize(b"filename") os.path.getsize(Path("filename")) os.path.getsize(__file__) getsize("filename") # this should not match os.path.getsize(1) refurb-1.27.0/test/data/err_155.txt000066400000000000000000000031351454672660200167170ustar00rootroot00000000000000test/data/err_155.py:7:1 [FURB155]: Replace `os.path.getatime(x)` with `Path(x).stat().st_atime` test/data/err_155.py:8:1 [FURB155]: Replace `os.path.getatime(x)` with `Path(x).stat().st_atime` test/data/err_155.py:9:1 [FURB155]: Replace `os.path.getatime(x)` with `x.stat().st_atime` test/data/err_155.py:10:1 [FURB155]: Replace `os.path.getatime(x)` with `Path(x).stat().st_atime` test/data/err_155.py:12:1 [FURB155]: Replace `os.path.getmtime(x)` with `Path(x).stat().st_mtime` test/data/err_155.py:13:1 [FURB155]: Replace `os.path.getmtime(x)` with `Path(x).stat().st_mtime` test/data/err_155.py:14:1 [FURB155]: Replace `os.path.getmtime(x)` with `x.stat().st_mtime` test/data/err_155.py:15:1 [FURB155]: Replace `os.path.getmtime(x)` with `Path(x).stat().st_mtime` test/data/err_155.py:17:1 [FURB155]: Replace `os.path.getctime(x)` with `Path(x).stat().st_ctime` test/data/err_155.py:18:1 [FURB155]: Replace `os.path.getctime(x)` with `Path(x).stat().st_ctime` test/data/err_155.py:19:1 [FURB155]: Replace `os.path.getctime(x)` with `x.stat().st_ctime` test/data/err_155.py:20:1 [FURB155]: Replace `os.path.getctime(x)` with `Path(x).stat().st_ctime` test/data/err_155.py:22:1 [FURB155]: Replace `os.path.getsize(x)` with `Path(x).stat().st_size` test/data/err_155.py:23:1 [FURB155]: Replace `os.path.getsize(x)` with `Path(x).stat().st_size` test/data/err_155.py:24:1 [FURB155]: Replace `os.path.getsize(x)` with `x.stat().st_size` test/data/err_155.py:25:1 [FURB155]: Replace `os.path.getsize(x)` with `Path(x).stat().st_size` test/data/err_155.py:26:1 [FURB155]: Replace `os.path.getsize(x)` with `Path(x).stat().st_size` refurb-1.27.0/test/data/err_156.py000066400000000000000000000005761454672660200165370ustar00rootroot00000000000000# these should match _ = "0123456789" _ = "01234567" _ = "0123456789abcdefABCDEF" _ = "abcdefghijklmnopqrstuvwxyz" _ = "ABCDEFGHIJKLMNOPQRSTUVWXYZ" _ = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" _ = r"""!"#$%&'()*+,-./:;<=>?@[\]^_`{|}~""" _ = " \t\n\r\v\f" _ = "" in "1234567890" _ = "" in "12345670" # these should not _ = "1234567890" _ = "1234" _ = "" in "1234" refurb-1.27.0/test/data/err_156.txt000066400000000000000000000016451454672660200167240ustar00rootroot00000000000000test/data/err_156.py:3:5 [FURB156]: Replace `0123456789` with `string.digits` test/data/err_156.py:4:5 [FURB156]: Replace `01234567` with `string.octdigits` test/data/err_156.py:5:5 [FURB156]: Replace `0123456789abcdefABCDEF` with `string.hexdigits` test/data/err_156.py:6:5 [FURB156]: Replace `abcdefghijklmnopqrstuvwxyz` with `string.ascii_lowercase` test/data/err_156.py:7:5 [FURB156]: Replace `ABCDEFGHIJKLMNOPQRSTUVWXYZ` with `string.ascii_uppercase` test/data/err_156.py:8:5 [FURB156]: Replace `abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ` with `string.ascii_letters` test/data/err_156.py:9:5 [FURB156]: Replace `!"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}~` with `string.punctuation` test/data/err_156.py:10:5 [FURB156]: Replace ` \t\n\r\v\f` with `string.whitespace` test/data/err_156.py:12:5 [FURB156]: Replace `1234567890` with `string.digits` test/data/err_156.py:13:5 [FURB156]: Replace `12345670` with `string.octdigits` refurb-1.27.0/test/data/err_157.py000066400000000000000000000010741454672660200165320ustar00rootroot00000000000000import decimal from decimal import Decimal # these should match _ = Decimal("0") _ = Decimal("+0") _ = Decimal("-0") _ = Decimal("-1234") _ = Decimal("1234") _ = Decimal("01234") _ = Decimal("\r\n\r 1234") _ = Decimal(float("Infinity")) _ = Decimal(float("-Infinity")) _ = Decimal(float("inf")) _ = Decimal(float("-inf")) _ = Decimal(float("-INF")) _ = Decimal(float("NaN")) _ = Decimal(float("nan")) _ = decimal.Decimal("0") _ = decimal.Decimal(float("nan")) # these should not _ = Decimal("3.14") _ = Decimal("10e3") _ = Decimal(float("3.14")) _ = Decimal("0x123") refurb-1.27.0/test/data/err_157.txt000066400000000000000000000026431454672660200167240ustar00rootroot00000000000000test/data/err_157.py:6:5 [FURB157]: Replace `Decimal("0")` with `Decimal(0)` test/data/err_157.py:7:5 [FURB157]: Replace `Decimal("+0")` with `Decimal(0)` test/data/err_157.py:8:5 [FURB157]: Replace `Decimal("-0")` with `Decimal(-0)` test/data/err_157.py:9:5 [FURB157]: Replace `Decimal("-1234")` with `Decimal(-1234)` test/data/err_157.py:10:5 [FURB157]: Replace `Decimal("1234")` with `Decimal(1234)` test/data/err_157.py:11:5 [FURB157]: Replace `Decimal("01234")` with `Decimal(1234)` test/data/err_157.py:12:5 [FURB157]: Replace `Decimal("\r\n\r 1234")` with `Decimal(1234)` test/data/err_157.py:13:5 [FURB157]: Replace `Decimal(float("Infinity"))` with `Decimal("Infinity")` test/data/err_157.py:14:5 [FURB157]: Replace `Decimal(float("-Infinity"))` with `Decimal("-Infinity")` test/data/err_157.py:15:5 [FURB157]: Replace `Decimal(float("inf"))` with `Decimal("inf")` test/data/err_157.py:16:5 [FURB157]: Replace `Decimal(float("-inf"))` with `Decimal("-inf")` test/data/err_157.py:17:5 [FURB157]: Replace `Decimal(float("-INF"))` with `Decimal("-INF")` test/data/err_157.py:18:5 [FURB157]: Replace `Decimal(float("NaN"))` with `Decimal("NaN")` test/data/err_157.py:19:5 [FURB157]: Replace `Decimal(float("nan"))` with `Decimal("nan")` test/data/err_157.py:20:5 [FURB157]: Replace `decimal.Decimal("0")` with `decimal.Decimal(0)` test/data/err_157.py:21:5 [FURB157]: Replace `decimal.Decimal(float("nan"))` with `decimal.Decimal("nan")` refurb-1.27.0/test/data/err_158.py000066400000000000000000000010631454672660200165310ustar00rootroot00000000000000# these should match def f(x): match x: case bool() as a: pass case bytearray() as b: pass case bytes() as c: pass case dict() as d: pass case float() as e: pass case frozenset() as f: pass case int() as g: pass case list() as h: pass case set() as i: pass case str() as j: pass case tuple() as k: pass # these should not match 1: case int(x): pass case float(x) as y: pass case str(key=value) as y: pass # type: ignore case 1: pass case x: pass refurb-1.27.0/test/data/err_158.txt000066400000000000000000000015101454672660200167150ustar00rootroot00000000000000test/data/err_158.py:5:14 [FURB158]: Replace `bool() as x` with `bool(x)` test/data/err_158.py:6:14 [FURB158]: Replace `bytearray() as x` with `bytearray(x)` test/data/err_158.py:7:14 [FURB158]: Replace `bytes() as x` with `bytes(x)` test/data/err_158.py:8:14 [FURB158]: Replace `dict() as x` with `dict(x)` test/data/err_158.py:9:14 [FURB158]: Replace `float() as x` with `float(x)` test/data/err_158.py:10:14 [FURB158]: Replace `frozenset() as x` with `frozenset(x)` test/data/err_158.py:11:14 [FURB158]: Replace `int() as x` with `int(x)` test/data/err_158.py:12:14 [FURB158]: Replace `list() as x` with `list(x)` test/data/err_158.py:13:14 [FURB158]: Replace `set() as x` with `set(x)` test/data/err_158.py:14:14 [FURB158]: Replace `str() as x` with `str(x)` test/data/err_158.py:15:14 [FURB158]: Replace `tuple() as x` with `tuple(x)` refurb-1.27.0/test/data/err_159.py000066400000000000000000000015351454672660200165360ustar00rootroot00000000000000# these should match _ = "abc".lstrip().rstrip() _ = "abc".rstrip().lstrip() _ = "abc".strip().rstrip() _ = "abc".lstrip().strip() _ = "abc".lstrip().lstrip() _ = "abc".rstrip().rstrip() _ = "abc".strip().strip() _ = "abc".lstrip("x").rstrip("x") _ = "abc".strip("x").rstrip("x") _ = "abc".lstrip("x").lstrip("y") _ = "abc".lstrip("x").lstrip("xy") _ = "abc".lstrip("x").lstrip("x") _ = "abc".strip("x").strip("y") s = "hello world" _ = s.lstrip().rstrip() # these (maybe) should match _ = "abc".lstrip("x").rstrip("xy") # these should not _ = "abc".lstrip().upper() _ = "abc".upper().lstrip() _ = "abc".lstrip("x").rstrip("y") _ = "abc".strip("x").lstrip("y") _ = "abc".strip("x").lstrip() _ = "abc".strip().lstrip("x") class C: def lstrip(self) -> str: return "" def rstrip(self) -> str: return "" _ = C().lstrip().rstrip() refurb-1.27.0/test/data/err_159.txt000066400000000000000000000023111454672660200167160ustar00rootroot00000000000000test/data/err_159.py:3:5 [FURB159]: Replace `x.lstrip().rstrip()` with `x.strip()` test/data/err_159.py:4:5 [FURB159]: Replace `x.rstrip().lstrip()` with `x.strip()` test/data/err_159.py:5:5 [FURB159]: Replace `x.strip().rstrip()` with `x.strip()` test/data/err_159.py:6:5 [FURB159]: Replace `x.lstrip().strip()` with `x.strip()` test/data/err_159.py:7:5 [FURB159]: Replace `x.lstrip().lstrip()` with `x.lstrip()` test/data/err_159.py:8:5 [FURB159]: Replace `x.rstrip().rstrip()` with `x.rstrip()` test/data/err_159.py:9:5 [FURB159]: Replace `x.strip().strip()` with `x.strip()` test/data/err_159.py:11:5 [FURB159]: Replace `x.lstrip('x').rstrip('x')` with `x.strip('x')` test/data/err_159.py:12:5 [FURB159]: Replace `x.strip('x').rstrip('x')` with `x.strip('x')` test/data/err_159.py:14:5 [FURB159]: Replace `x.lstrip('x').lstrip('y')` with `x.lstrip('xy')` test/data/err_159.py:15:5 [FURB159]: Replace `x.lstrip('x').lstrip('xy')` with `x.lstrip('xy')` test/data/err_159.py:16:5 [FURB159]: Replace `x.lstrip('x').lstrip('x')` with `x.lstrip('x')` test/data/err_159.py:17:5 [FURB159]: Replace `x.strip('x').strip('y')` with `x.strip('xy')` test/data/err_159.py:20:5 [FURB159]: Replace `x.lstrip().rstrip()` with `x.strip()` refurb-1.27.0/test/data/err_160.py000066400000000000000000000001241454672660200165170ustar00rootroot00000000000000x = y = 1 # these should match x = x # these should not x = y x = x + 1 x += x refurb-1.27.0/test/data/err_160.txt000066400000000000000000000001261454672660200167100ustar00rootroot00000000000000test/data/err_160.py:5:1 [FURB160]: Remove redundant assignment of variable to itself refurb-1.27.0/test/data/err_163.py000066400000000000000000000005131454672660200165240ustar00rootroot00000000000000import math from math import e, log # these should match _ = math.log(64, 2) _ = math.log(64, 2.0) _ = math.log(100, 10) _ = math.log(100, 10.0) _ = math.log(100, math.e) _ = math.log(100, e) _ = math.log(1 + 2, 2) _ = log(100, 10) # these should not _ = math.log(10, 3) _ = math.log(64, 1 + 1) two = 2 _ = math.log(10, two) refurb-1.27.0/test/data/err_163.txt000066400000000000000000000012361454672660200167160ustar00rootroot00000000000000test/data/err_163.py:6:5 [FURB163]: Replace `math.log(x, 2)` with `math.log2(x)` test/data/err_163.py:7:5 [FURB163]: Replace `math.log(x, 2.0)` with `math.log2(x)` test/data/err_163.py:8:5 [FURB163]: Replace `math.log(x, 10)` with `math.log10(x)` test/data/err_163.py:9:5 [FURB163]: Replace `math.log(x, 10.0)` with `math.log10(x)` test/data/err_163.py:10:5 [FURB163]: Replace `math.log(x, math.e)` with `math.log(x)` test/data/err_163.py:11:5 [FURB163]: Replace `math.log(x, math.e)` with `math.log(x)` test/data/err_163.py:12:5 [FURB163]: Replace `math.log(x, 2)` with `math.log2(x)` test/data/err_163.py:13:5 [FURB163]: Replace `math.log(x, 10)` with `math.log10(x)` refurb-1.27.0/test/data/err_164.py000066400000000000000000000007511454672660200165310ustar00rootroot00000000000000import decimal import fractions from decimal import Decimal from fractions import Fraction # these should match _ = Decimal.from_float(123) _ = Fraction.from_float(123) _ = Fraction.from_decimal(Decimal(123)) _ = decimal.Decimal.from_float(123) _ = fractions.Fraction.from_float(123) # these should not _ = Decimal(123) _ = Fraction(123) _ = Decimal.from_float(123, 456) # type: ignore _ = Fraction.from_float(123, 456) # type: ignore _ = Decimal.from_decimal(123) # type: ignore refurb-1.27.0/test/data/err_164.txt000066400000000000000000000010031454672660200167070ustar00rootroot00000000000000test/data/err_164.py:8:5 [FURB164]: Replace `Decimal.from_float(123)` with `Decimal(123)` test/data/err_164.py:9:5 [FURB164]: Replace `Fraction.from_float(123)` with `Fraction(123)` test/data/err_164.py:10:5 [FURB164]: Replace `Fraction.from_decimal(Decimal(123))` with `Fraction(Decimal(123))` test/data/err_164.py:11:5 [FURB164]: Replace `decimal.Decimal.from_float(123)` with `decimal.Decimal(123)` test/data/err_164.py:12:5 [FURB164]: Replace `fractions.Fraction.from_float(123)` with `fractions.Fraction(123)` refurb-1.27.0/test/data/err_165.py000066400000000000000000000006471454672660200165360ustar00rootroot00000000000000from __future__ import annotations from pathlib import Path # these should match _ = Path().cwd() _ = Path("filename").cwd() class C: @staticmethod def s(*args: str) -> None: pass @classmethod def c(cls, *args: str) -> C: return cls() def f(self) -> None: return C().s() C().c() C().s("hello", "world") C().c("hello", "world") # these should not _ = Path.cwd() C().f() refurb-1.27.0/test/data/err_165.txt000066400000000000000000000006711454672660200167220ustar00rootroot00000000000000test/data/err_165.py:7:5 [FURB165]: Replace `Path().cwd()` with `Path.cwd()` test/data/err_165.py:8:5 [FURB165]: Replace `Path(...).cwd()` with `Path.cwd()` test/data/err_165.py:22:1 [FURB165]: Replace `C().s()` with `C.s()` test/data/err_165.py:23:1 [FURB165]: Replace `C().c()` with `C.c()` test/data/err_165.py:24:1 [FURB165]: Replace `C().s(...)` with `C.s(...)` test/data/err_165.py:25:1 [FURB165]: Replace `C().c(...)` with `C.c(...)` refurb-1.27.0/test/data/err_166.py000066400000000000000000000005631454672660200165340ustar00rootroot00000000000000# these should match _ = int("0b1010"[2:], 2) _ = int("0o777"[2:], 8) _ = int("0xFFFF"[2:], 16) b = "0b11" _ = int(b[2:], 2) _ = int("0xFFFF"[2:], base=16) # these should not _ = int("0b1100", 0) _ = int("123", 3) _ = int("123", 10) # noqa: FURB120 _ = int("0b1010"[3:], 2) _ = int("0b1010"[:2], 2) _ = int("12345"[:2]) _ = int("12345"[:2], xyz=1) # type: ignore refurb-1.27.0/test/data/err_166.txt000066400000000000000000000006161454672660200167220ustar00rootroot00000000000000test/data/err_166.py:3:5 [FURB166]: Replace `int(x[2:], 2)` with `int(x, 0)` test/data/err_166.py:4:5 [FURB166]: Replace `int(x[2:], 8)` with `int(x, 0)` test/data/err_166.py:5:5 [FURB166]: Replace `int(x[2:], 16)` with `int(x, 0)` test/data/err_166.py:8:5 [FURB166]: Replace `int(x[2:], 2)` with `int(x, 0)` test/data/err_166.py:10:5 [FURB166]: Replace `int(x[2:], base=16)` with `int(x, base=0)` refurb-1.27.0/test/data/err_167.py000066400000000000000000000003231454672660200165270ustar00rootroot00000000000000import re from re import I # these should match _ = re.A _ = re.I _ = re.L _ = re.M _ = re.S _ = re.T _ = re.U _ = re.X _ = I # these should not _ = re.compile("^abc$") class C: A: int = 123 _ = C.A refurb-1.27.0/test/data/err_167.txt000066400000000000000000000011671454672660200167250ustar00rootroot00000000000000test/data/err_167.py:6:5 [FURB167]: Replace `re.A` with `re.ASCII` test/data/err_167.py:7:5 [FURB167]: Replace `re.I` with `re.IGNORECASE` test/data/err_167.py:8:5 [FURB167]: Replace `re.L` with `re.LOCALE` test/data/err_167.py:9:5 [FURB167]: Replace `re.M` with `re.MULTILINE` test/data/err_167.py:10:5 [FURB167]: Replace `re.S` with `re.DOTALL` test/data/err_167.py:11:5 [FURB167]: Replace `re.T` with `re.TEMPLATE` test/data/err_167.py:12:5 [FURB167]: Replace `re.U` with `re.UNICODE` test/data/err_167.py:13:5 [FURB167]: Replace `re.X` with `re.VERBOSE` test/data/err_167.py:15:5 [FURB167]: Replace `re.I` with `re.IGNORECASE` refurb-1.27.0/test/data/err_168.py000066400000000000000000000007011454672660200165300ustar00rootroot00000000000000# these should match _ = isinstance(123, type(None)) _ = isinstance(123, (int, type(None))) _ = isinstance(123, (type(None), int)) _ = isinstance(123, int | type(None)) _ = isinstance(123, int | float | type(None)) _ = isinstance(123, type(None) | int) _ = isinstance(123, type(None) | int | float) # these should not _ = isinstance(123, int) _ = isinstance(123, type(123)) _ = isinstance(123, (int, type(123))) _ = isinstance(123, int | float) refurb-1.27.0/test/data/err_168.txt000066400000000000000000000014311454672660200167200ustar00rootroot00000000000000test/data/err_168.py:3:5 [FURB168]: Replace `isinstance(x, type(None))` with `x is None` test/data/err_168.py:4:5 [FURB168]: Replace `isinstance(x, (..., type(None)))` with `x is None or isinstance(x, ...)` test/data/err_168.py:5:5 [FURB168]: Replace `isinstance(x, (type(None), ...))` with `x is None or isinstance(x, ...)` test/data/err_168.py:6:5 [FURB168]: Replace `isinstance(x, ... | type(None))` with `x is None or isinstance(x, ...)` test/data/err_168.py:7:5 [FURB168]: Replace `isinstance(x, ... | type(None))` with `x is None or isinstance(x, ...)` test/data/err_168.py:8:5 [FURB168]: Replace `isinstance(x, type(None) | ...)` with `x is None or isinstance(x, ...)` test/data/err_168.py:9:5 [FURB168]: Replace `isinstance(x, type(None) | ...)` with `x is None or isinstance(x, ...)` refurb-1.27.0/test/data/err_169.py000066400000000000000000000003461454672660200165360ustar00rootroot00000000000000# these should match _ = type(123) is type(None) _ = type(123) == type(None) _ = type(123) is not type(None) _ = type(123) != type(None) # these should not _ = type(123) is type(456) _ = type(123) is int _ = int is type(None) refurb-1.27.0/test/data/err_169.txt000066400000000000000000000005401454672660200167210ustar00rootroot00000000000000test/data/err_169.py:3:5 [FURB169]: Replace `type(x) is type(None)` with `x is None` test/data/err_169.py:4:5 [FURB169]: Replace `type(x) == type(None)` with `x is None` test/data/err_169.py:5:5 [FURB169]: Replace `type(x) is not type(None)` with `x is not None` test/data/err_169.py:6:5 [FURB169]: Replace `type(x) != type(None)` with `x is not None` refurb-1.27.0/test/data/err_170.py000066400000000000000000000027601454672660200165300ustar00rootroot00000000000000import re from re import search PATTERN = re.compile("hello( world)?") # these should match _ = re.search(PATTERN, "hello world") _ = re.match(PATTERN, "hello world") _ = re.fullmatch(PATTERN, "hello world") _ = re.split(PATTERN, "hello world") _ = re.split(PATTERN, "hello world", 1) _ = re.split(PATTERN, "hello world", maxsplit=1) _ = re.findall(PATTERN, "hello world") _ = re.finditer(PATTERN, "hello world") _ = re.sub(PATTERN, "hello world", "goodbye world") _ = re.sub(PATTERN, "hello world", "goodbye world", 1) _ = re.sub(PATTERN, "hello world", "goodbye world", count=1) _ = re.subn(PATTERN, "hello world", "goodbye world") _ = re.subn(PATTERN, "hello world", "goodbye world", count=1) _ = search(PATTERN, "hello world") # these should not _ = re.search(PATTERN, "hello world", re.IGNORECASE) _ = re.match(PATTERN, "hello world", re.IGNORECASE) _ = re.fullmatch(PATTERN, "hello world", re.IGNORECASE) _ = re.split(PATTERN, "hello world", flags=re.IGNORECASE) _ = re.split(PATTERN, "hello world", 1, flags=re.IGNORECASE) _ = re.findall(PATTERN, "hello world", re.IGNORECASE) _ = re.finditer(PATTERN, "hello world", re.IGNORECASE) _ = re.sub(PATTERN, "hello world", "goodbye world", flags=re.IGNORECASE) _ = re.sub(PATTERN, "hello world", "goodbye world", 1, re.IGNORECASE) _ = re.subn(PATTERN, "hello world", "goodbye world", flags=re.IGNORECASE) _ = re.subn(PATTERN, "hello world", "goodbye world", 1, flags=re.IGNORECASE) _ = PATTERN.search("hello world") _ = re.search("hello world", "hello world") refurb-1.27.0/test/data/err_170.txt000066400000000000000000000024521454672660200167150ustar00rootroot00000000000000test/data/err_170.py:8:5 [FURB170]: Replace `re.search(x, ...)` with `x.search(...)` test/data/err_170.py:9:5 [FURB170]: Replace `re.match(x, ...)` with `x.match(...)` test/data/err_170.py:10:5 [FURB170]: Replace `re.fullmatch(x, ...)` with `x.fullmatch(...)` test/data/err_170.py:11:5 [FURB170]: Replace `re.split(x, ...)` with `x.split(...)` test/data/err_170.py:12:5 [FURB170]: Replace `re.split(x, ..., ...)` with `x.split(..., ...)` test/data/err_170.py:13:5 [FURB170]: Replace `re.split(x, ..., maxsplit=...)` with `x.split(..., maxsplit=...)` test/data/err_170.py:14:5 [FURB170]: Replace `re.findall(x, ...)` with `x.findall(...)` test/data/err_170.py:15:5 [FURB170]: Replace `re.finditer(x, ...)` with `x.finditer(...)` test/data/err_170.py:16:5 [FURB170]: Replace `re.sub(x, ..., ...)` with `x.sub(..., ...)` test/data/err_170.py:17:5 [FURB170]: Replace `re.sub(x, ..., ..., ...)` with `x.sub(..., ..., ...)` test/data/err_170.py:18:5 [FURB170]: Replace `re.sub(x, ..., ..., count=...)` with `x.sub(..., ..., count=...)` test/data/err_170.py:19:5 [FURB170]: Replace `re.subn(x, ..., ...)` with `x.subn(..., ...)` test/data/err_170.py:20:5 [FURB170]: Replace `re.subn(x, ..., ..., count=...)` with `x.subn(..., ..., count=...)` test/data/err_170.py:22:5 [FURB170]: Replace `re.search(x, ...)` with `x.search(...)` refurb-1.27.0/test/data/err_171.py000066400000000000000000000002751454672660200165300ustar00rootroot00000000000000# these should match _ = 1 in (1,) _ = 1 in [1] # noqa: FURB109 _ = 1 not in (1,) _ = 1 not in [1] # noqa: FURB109 # these should not _ = 1 in (1, 2) _ = 1 in [1, 2] # noqa: FURB109 refurb-1.27.0/test/data/err_171.txt000066400000000000000000000004361454672660200167160ustar00rootroot00000000000000test/data/err_171.py:3:5 [FURB171]: Replace `x in (y,)` with `x == y` test/data/err_171.py:4:5 [FURB171]: Replace `x in [y]` with `x == y` test/data/err_171.py:5:5 [FURB171]: Replace `x not in (y,)` with `x != y` test/data/err_171.py:6:5 [FURB171]: Replace `x not in [y]` with `x != y` refurb-1.27.0/test/data/err_172.py000066400000000000000000000005531454672660200165300ustar00rootroot00000000000000from pathlib import Path # these should match _ = Path("file.txt").name.endswith(".txt") _ = Path("file.ABC").name.endswith(".ABC") # these should not _ = Path("file.txt.gz").name.endswith(".txt.gz") _ = Path("file").name.endswith("file") _ = Path("file").name.endswith("") _ = Path("file").suffix.endswith(".txt") _ = Path("file").name.startswith("file") refurb-1.27.0/test/data/err_172.txt000066400000000000000000000003001454672660200167050ustar00rootroot00000000000000test/data/err_172.py:5:5 [FURB172]: Replace `x.name.endswith(".txt")` with `x.suffix == ".txt"` test/data/err_172.py:6:5 [FURB172]: Replace `x.name.endswith(".ABC")` with `x.suffix == ".ABC"` refurb-1.27.0/test/data/err_173.py000066400000000000000000000022641454672660200165320ustar00rootroot00000000000000x = {"a": 1} y = {"b": 2} z = {"c": 3} # these should match _ = {**x, **y} _ = {**x, "b": 2} _ = {**x, "b": 2, "c": 3} _ = {"a": 1, **x} _ = {"a": 1, "b": 2, **x} _ = {"a": 1, **x, **y} _ = {**x, **y, "c": 3} _ = {**x, **y, **z} _ = {**x, **y, **z, **x} _ = {**x, **y, **z, **x, **x} _ = {**x, **y, **z, **x, "a": 1} from collections import ChainMap, Counter, OrderedDict, defaultdict, UserDict chainmap = ChainMap() _ = {"k": "v", **chainmap} counter = Counter() _ = {"k": "v", **counter} ordereddict = OrderedDict() _ = {"k": "v", **ordereddict} dd = defaultdict() _ = {"k": "v", **dd} userdict = UserDict() _ = {"k": "v", **userdict} _ = dict(**x) _ = dict(x, **y) _ = dict(**x, **y) _ = dict(x, a=1) _ = dict(**x, a=1, b=2) _ = dict(**x, **y, a=1, b=2) # these should not _ = {} _ = {**x} _ = {**x, "a": 1, **y} _ = {"a": 1} _ = {"a": 1, "b": 2} _ = {"a": 1, **x, "b": 2} class C: from typing import Any def keys(self): return [] def __getitem__(self, key: str) -> Any: pass c = C() _ = {"k": "v", **c} # TODO: support more expr types _ = {"k": "v", **{}} _ = dict(x) # noqa: FURB123 _ = dict(*({},)) _ = dict() # noqa: FURB112 _ = dict(a=1, b=2) refurb-1.27.0/test/data/err_173.txt000066400000000000000000000035111454672660200167150ustar00rootroot00000000000000test/data/err_173.py:7:5 [FURB173]: Replace `{**x, **y}` with `x | y` test/data/err_173.py:8:5 [FURB173]: Replace `{**x, ...}` with `x | {...}` test/data/err_173.py:9:5 [FURB173]: Replace `{**x, ..., ...}` with `x | {...} | {...}` test/data/err_173.py:10:5 [FURB173]: Replace `{..., **x}` with `{...} | x` test/data/err_173.py:11:5 [FURB173]: Replace `{..., ..., **x}` with `{...} | {...} | x` test/data/err_173.py:12:5 [FURB173]: Replace `{..., **x, **y}` with `{...} | x | y` test/data/err_173.py:13:5 [FURB173]: Replace `{**x, **y, ...}` with `x | y | {...}` test/data/err_173.py:14:5 [FURB173]: Replace `{**x, **y, **z}` with `x | y | z` test/data/err_173.py:15:5 [FURB173]: Replace `{**x, **y, **z, **x}` with `x | y | z | x` test/data/err_173.py:16:5 [FURB173]: Replace `{**x, **y, **z, **x, **x}` with `x | y | z | x | x` test/data/err_173.py:17:5 [FURB173]: Replace `{**x, **y, **z, **x, ...}` with `x | y | z | x | {...}` test/data/err_173.py:22:5 [FURB173]: Replace `{..., **chainmap}` with `{...} | chainmap` test/data/err_173.py:25:5 [FURB173]: Replace `{..., **counter}` with `{...} | counter` test/data/err_173.py:28:5 [FURB173]: Replace `{..., **ordereddict}` with `{...} | ordereddict` test/data/err_173.py:31:5 [FURB173]: Replace `{..., **dd}` with `{...} | dd` test/data/err_173.py:34:5 [FURB173]: Replace `{..., **userdict}` with `{...} | userdict` test/data/err_173.py:37:5 [FURB173]: Replace `dict(**x)` with `{**x}` test/data/err_173.py:38:5 [FURB173]: Replace `dict(x, **y)` with `x | y` test/data/err_173.py:39:5 [FURB173]: Replace `dict(**x, **y)` with `x | y` test/data/err_173.py:40:5 [FURB173]: Replace `dict(x, a=1)` with `x | {"a": 1}` test/data/err_173.py:41:5 [FURB173]: Replace `dict(**x, a=1, b=2)` with `x | {"a": 1, "b": 2}` test/data/err_173.py:42:5 [FURB173]: Replace `dict(**x, **y, a=1, b=2)` with `x | y | {"a": 1, "b": 2}` refurb-1.27.0/test/data/err_174.py000066400000000000000000000007041454672660200165300ustar00rootroot00000000000000import secrets from secrets import token_bytes, token_hex # these should match token_bytes(32).hex() token_bytes(None).hex() # noqa: FURB120 token_bytes().hex() secrets.token_bytes().hex() token_bytes()[:8] token_hex()[:8] token_bytes(None)[:8] # noqa: FURB120 token_hex(None)[:8] # noqa: FURB120 secrets.token_bytes()[:8] # these should not token_hex()[:5] bytes().hex() # noqa: FURB112 n = 32 token_bytes(n).hex() token_bytes().hex("_") refurb-1.27.0/test/data/err_174.txt000066400000000000000000000014571454672660200167250ustar00rootroot00000000000000test/data/err_174.py:6:1 [FURB174]: Replace `token_bytes(32).hex()` with `token_hex(32)` test/data/err_174.py:7:1 [FURB174]: Replace `token_bytes(None).hex()` with `token_hex()` test/data/err_174.py:8:1 [FURB174]: Replace `token_bytes().hex()` with `token_hex()` test/data/err_174.py:9:1 [FURB174]: Replace `secrets.token_bytes().hex()` with `secrets.token_hex()` test/data/err_174.py:11:1 [FURB174]: Replace `token_bytes()[:8]` with `token_bytes(8)` test/data/err_174.py:12:1 [FURB174]: Replace `token_hex()[:8]` with `token_hex(4)` test/data/err_174.py:13:1 [FURB174]: Replace `token_bytes(None)[:8]` with `token_bytes(8)` test/data/err_174.py:14:1 [FURB174]: Replace `token_hex(None)[:8]` with `token_hex(4)` test/data/err_174.py:15:1 [FURB174]: Replace `secrets.token_bytes()[:8]` with `secrets.token_bytes(8)` refurb-1.27.0/test/data/err_175.py000066400000000000000000000005221454672660200165270ustar00rootroot00000000000000from fastapi import FastAPI, Query app = FastAPI() @app.get("/") def index( # These should match a: str = Query(), b: str = Query(...), c: str | None = Query(None), d: str = Query(default=""), e = Query(), # These should not f: str = Query(title=""), g: str = Query("", title="") ) -> None: pass refurb-1.27.0/test/data/err_175.txt000066400000000000000000000006071454672660200167220ustar00rootroot00000000000000test/data/err_175.py:8:5 [FURB175]: Replace `a: T = Query()` with `a: T = x` test/data/err_175.py:9:5 [FURB175]: Replace `b: T = Query(...)` with `b: T` test/data/err_175.py:10:5 [FURB175]: Replace `c: T = Query(x)` with `c: T = x` test/data/err_175.py:11:5 [FURB175]: Replace `d: T = Query(default=x)` with `d: T = x` test/data/err_175.py:12:5 [FURB175]: Replace `e = Query()` with `e = x` refurb-1.27.0/test/data/err_176.py000066400000000000000000000005271454672660200165350ustar00rootroot00000000000000import datetime as dt from datetime import datetime, timezone # Should warn: start_date = datetime.utcnow() old_date = datetime.utcfromtimestamp(1) start_date = dt.datetime.utcnow() old_date = dt.datetime.utcfromtimestamp(1) # Should not warn: start_date = datetime.now(tz=timezone.utc) old_date = datetime.fromtimestamp(1, tz=timezone.utc) refurb-1.27.0/test/data/err_176.txt000066400000000000000000000006101454672660200167150ustar00rootroot00000000000000test/data/err_176.py:5:14 [FURB176]: Replace `utcnow()` with `now(tz=timezone.utc)` test/data/err_176.py:6:12 [FURB176]: Replace `utcfromtimestamp(...)` with `fromtimestamp(..., tz=timezone.utc)` test/data/err_176.py:7:14 [FURB176]: Replace `utcnow()` with `now(tz=timezone.utc)` test/data/err_176.py:8:12 [FURB176]: Replace `utcfromtimestamp(...)` with `fromtimestamp(..., tz=timezone.utc)` refurb-1.27.0/test/data/err_177.py000066400000000000000000000005301454672660200165300ustar00rootroot00000000000000import pathlib from pathlib import Path # these should match _ = Path().resolve() _ = Path("").resolve() # noqa: FURB153 _ = Path(".").resolve() # noqa: FURB153 _ = pathlib.Path().resolve() # these should not _ = Path("some_file").resolve() _ = Path().resolve(True) _ = Path().resolve(False) # noqa: FURB120 p = Path() _ = p.resolve() refurb-1.27.0/test/data/err_177.txt000066400000000000000000000005111454672660200167160ustar00rootroot00000000000000test/data/err_177.py:6:5 [FURB177]: Replace `Path().resolve()` with `Path.cwd()` test/data/err_177.py:7:5 [FURB177]: Replace `Path("").resolve()` with `Path.cwd()` test/data/err_177.py:8:5 [FURB177]: Replace `Path(".").resolve()` with `Path.cwd()` test/data/err_177.py:9:5 [FURB177]: Replace `Path().resolve()` with `Path.cwd()` refurb-1.27.0/test/data/err_178.py000066400000000000000000000013311454672660200165310ustar00rootroot00000000000000import shlex from shlex import quote from shlex import quote as shlex_quote args = ["a", "b", "c d"] # these should match _ = " ".join(shlex.quote(arg) for arg in args) _ = " ".join([shlex.quote(arg) for arg in args]) _ = " ".join(shlex.quote(arg) for arg in ("hello", "world")) _ = " ".join(quote(arg) for arg in args) _ = " ".join(shlex_quote(arg) for arg in args) _ = " ".join(shlex.quote(arg + "") for arg in args) _ = " ".join(shlex.quote(arg) for arg in args if arg) _ = " ".join(shlex.quote(arg + "") for arg in args if arg) # these should not _ = " ".join(str(arg) for arg in args) # noqa: FURB123 _ = " ".join(shlex.quote(arg, ...) for arg in args) # type: ignore _ = ";".join(shlex.quote(arg) for arg in args) refurb-1.27.0/test/data/err_178.txt000066400000000000000000000015661454672660200167320ustar00rootroot00000000000000test/data/err_178.py:9:5 [FURB178]: Replace `" ".join(shlex.quote(x) for x in y)` with `shlex.join(y)` test/data/err_178.py:10:5 [FURB178]: Replace `" ".join(shlex.quote(x) for x in y)` with `shlex.join(y)` test/data/err_178.py:11:5 [FURB178]: Replace `" ".join(shlex.quote(x) for x in y)` with `shlex.join(y)` test/data/err_178.py:12:5 [FURB178]: Replace `" ".join(quote(x) for x in y)` with `join(y)` test/data/err_178.py:13:5 [FURB178]: Replace `" ".join(shlex_quote(x) for x in y)` with `join(y)` test/data/err_178.py:15:5 [FURB178]: Replace `" ".join(shlex.quote(...) for x in y)` with `shlex.join(... for x in y)` test/data/err_178.py:16:5 [FURB178]: Replace `" ".join(shlex.quote(...) for x in y if ...)` with `shlex.join(... for x in y if ...)` test/data/err_178.py:17:5 [FURB178]: Replace `" ".join(shlex.quote(...) for x in y if ...)` with `shlex.join(... for x in y if ...)` refurb-1.27.0/test/data/err_179.py000066400000000000000000000045471454672660200165460ustar00rootroot00000000000000from functools import reduce from operator import add, concat, iadd from itertools import chain import functools import itertools import operator rows = [[1, 2, 3], [4, 5, 6], [7, 8, 9]] def f(): return rows # these should match def flatten_via_generator(rows): return (col for row in rows for col in row) def flatten_via_list_comp(rows): return [col for row in rows for col in row] def flatten_via_set_comp(rows): return {col for row in rows for col in row} def flatten_with_function_source(): return (col for row in f() for col in row) def flatten_via_sum(rows): return sum(rows, []) def flatten_via_chain_splat(rows): return chain(*rows) def flatten_via_chain_splat_2(rows): return itertools.chain(*rows) def flatten_via_reduce_add(rows): return reduce(add, rows) def flatten_via_reduce_add_with_default(rows): return reduce(add, rows, []) def flatten_via_reduce_concat(rows): return reduce(concat, rows) def flatten_via_reduce_concat_with_default(rows): return reduce(concat, rows, []) def flatten_via_reduce_full_namespace(rows): return functools.reduce(operator.add, rows) # these should not def flatten_via_generator_modified(rows): return (col + 1 for row in rows for col in row) def flatten_via_generator_modified_2(rows): return (col for [row] in rows for col in row) def flatten_via_generator_modified_3(rows): return (col for row in rows for [col] in row) def flatten_via_generator_with_if(rows): return (col for row in rows for col in row if col) def flatten_via_generator_with_if_2(rows): return (col for row in rows if row for col in row) def flatten_via_dict_comp(rows): return {col: "" for row in rows for col in row} async def flatten_async_generator(rows): return (col async for row in rows for col in row) async def flatten_async_generator_2(rows): return (col for row in rows async for col in row) async def flatten_async_generator_3(rows): return (col async for row in rows async for col in row) def flatten_via_sum_with_default(rows): return sum(rows, [1]) def flatten_via_chain_without_splat(rows): return chain(rows) def flatten_via_chain_from_iterable(rows): return chain.from_iterable(rows) def flatten_via_reduce_iadd(rows): return reduce(iadd, rows, []) def flatten_via_reduce_non_empty_default(rows): return reduce(add, rows, [1, 2, 3]) refurb-1.27.0/test/data/err_179.txt000066400000000000000000000024061454672660200167250ustar00rootroot00000000000000test/data/err_179.py:17:12 [FURB179]: Replace `... for ... in x for ... in ...` with `chain.from_iterable(x)` test/data/err_179.py:20:12 [FURB179]: Replace `[... for ... in x for ... in ...]` with `list(chain.from_iterable(x))` test/data/err_179.py:23:12 [FURB179]: Replace `{... for ... in x for ... in ...}` with `set(chain.from_iterable(x))` test/data/err_179.py:26:12 [FURB179]: Replace `... for ... in x for ... in ...` with `chain.from_iterable(x)` test/data/err_179.py:29:12 [FURB179]: Replace `sum(rows, [])` with `chain.from_iterable(rows)` test/data/err_179.py:32:12 [FURB179]: Replace `chain(*rows)` with `chain.from_iterable(rows)` test/data/err_179.py:35:12 [FURB179]: Replace `itertools.chain(*rows)` with `itertools.chain.from_iterable(rows)` test/data/err_179.py:38:12 [FURB179]: Replace `reduce(add, rows)` with `chain.from_iterable(rows)` test/data/err_179.py:41:12 [FURB179]: Replace `reduce(add, rows, [])` with `chain.from_iterable(rows)` test/data/err_179.py:44:12 [FURB179]: Replace `reduce(concat, rows)` with `chain.from_iterable(rows)` test/data/err_179.py:47:12 [FURB179]: Replace `reduce(concat, rows, [])` with `chain.from_iterable(rows)` test/data/err_179.py:50:12 [FURB179]: Replace `functools.reduce(operator.add, rows)` with `chain.from_iterable(rows)` refurb-1.27.0/test/data/err_180.py000066400000000000000000000006341454672660200165270ustar00rootroot00000000000000import abc from abc import ABCMeta, ABC, abstractmethod class Dummy: pass # these will match class Animal(metaclass=ABCMeta): @abstractmethod def speak(self) -> None: ... class Computer(metaclass=abc.ABCMeta): @abstractmethod def compute(self) -> None: ... # these will not class Vehicle(ABC): @abstractmethod def move(self) -> None: ... class Human(Animal): name: str refurb-1.27.0/test/data/err_180.txt000066400000000000000000000002421454672660200167110ustar00rootroot00000000000000test/data/err_180.py:10:14 [FURB180]: Replace `metaclass=ABCMeta` with `ABC` test/data/err_180.py:15:16 [FURB180]: Replace `metaclass=abc.ABCMeta` with `abc.ABC` refurb-1.27.0/test/data/err_181.py000066400000000000000000000017301454672660200165260ustar00rootroot00000000000000import hashlib from hashlib import ( blake2b, blake2s, md5, sha1, sha3_224, sha3_256, sha3_384, sha3_512, sha224, ) from hashlib import sha256 from hashlib import sha256 as hash_algo from hashlib import sha384, sha512, shake_128, shake_256 # these will match blake2b().digest().hex() blake2s().digest().hex() md5().digest().hex() sha1().digest().hex() sha224().digest().hex() sha256().digest().hex() sha384().digest().hex() sha3_224().digest().hex() sha3_256().digest().hex() sha3_384().digest().hex() sha3_512().digest().hex() sha512().digest().hex() shake_128().digest(10).hex() shake_256().digest(10).hex() hashlib.sha256().digest().hex() sha256(b"text").digest().hex() hash_algo().digest().hex() h = sha256() h.digest().hex() # these will not sha256().digest() sha256().digest().hex("_") sha256().digest().hex(bytes_per_sep=4) sha256().hexdigest() class Hash: def digest(self) -> bytes: return b"" Hash().digest().hex() refurb-1.27.0/test/data/err_181.txt000066400000000000000000000034521454672660200167200ustar00rootroot00000000000000test/data/err_181.py:19:1 [FURB181]: Replace `blake2b().digest().hex()` with `blake2b().hexdigest()` test/data/err_181.py:20:1 [FURB181]: Replace `blake2s().digest().hex()` with `blake2s().hexdigest()` test/data/err_181.py:21:1 [FURB181]: Replace `md5().digest().hex()` with `md5().hexdigest()` test/data/err_181.py:22:1 [FURB181]: Replace `sha1().digest().hex()` with `sha1().hexdigest()` test/data/err_181.py:23:1 [FURB181]: Replace `sha224().digest().hex()` with `sha224().hexdigest()` test/data/err_181.py:24:1 [FURB181]: Replace `sha256().digest().hex()` with `sha256().hexdigest()` test/data/err_181.py:25:1 [FURB181]: Replace `sha384().digest().hex()` with `sha384().hexdigest()` test/data/err_181.py:26:1 [FURB181]: Replace `sha3_224().digest().hex()` with `sha3_224().hexdigest()` test/data/err_181.py:27:1 [FURB181]: Replace `sha3_256().digest().hex()` with `sha3_256().hexdigest()` test/data/err_181.py:28:1 [FURB181]: Replace `sha3_384().digest().hex()` with `sha3_384().hexdigest()` test/data/err_181.py:29:1 [FURB181]: Replace `sha3_512().digest().hex()` with `sha3_512().hexdigest()` test/data/err_181.py:30:1 [FURB181]: Replace `sha512().digest().hex()` with `sha512().hexdigest()` test/data/err_181.py:31:1 [FURB181]: Replace `shake_128().digest(10).hex()` with `shake_128().hexdigest(10)` test/data/err_181.py:32:1 [FURB181]: Replace `shake_256().digest(10).hex()` with `shake_256().hexdigest(10)` test/data/err_181.py:34:1 [FURB181]: Replace `hashlib.sha256().digest().hex()` with `hashlib.sha256().hexdigest()` test/data/err_181.py:36:1 [FURB181]: Replace `sha256(b'text').digest().hex()` with `sha256(b'text').hexdigest()` test/data/err_181.py:38:1 [FURB181]: Replace `hash_algo().digest().hex()` with `hash_algo().hexdigest()` test/data/err_181.py:41:1 [FURB181]: Replace `h.digest().hex()` with `h.hexdigest()` refurb-1.27.0/test/data/err_182.py000066400000000000000000000021521454672660200165260ustar00rootroot00000000000000import hashlib from hashlib import ( blake2b, blake2s, md5, sha1, sha3_224, sha3_256, sha3_384, sha3_512, sha224, ) from hashlib import sha256 from hashlib import sha256 as hash_algo from hashlib import sha384, sha512, shake_128, shake_256 # these will match h1 = blake2b() h1.update(b"data") h2 = blake2s() h2.update(b"data") h3 = md5() h3.update(b"data") h4 = sha1() h4.update(b"data") h5 = sha224() h5.update(b"data") h6 = sha256() h6.update(b"data") h7 = sha384() h7.update(b"data") h8 = sha3_224() h8.update(b"data") h9 = sha3_256() h9.update(b"data") h10 = sha3_384() h10.update(b"data") h11 = sha3_512() h11.update(b"data") h12 = sha512() h12.update(b"data") h13 = shake_128() h13.update(b"data") h14 = shake_256() h14.update(b"data") h15 = hashlib.sha256() h15.update(b"data") h16 = hash_algo() h16.update(b"data") # these will not h17 = sha256() h17.digest() h18 = sha256(b"data") h18.update(b"more data") h18.digest() h19 = sha256() pass h19.digest() class Hash: def update(self, data: bytes) -> None: return None h20 = Hash() h20.update(b"data") refurb-1.27.0/test/data/err_182.txt000066400000000000000000000034211454672660200167150ustar00rootroot00000000000000test/data/err_182.py:19:1 [FURB182]: Replace `h1 = blake2b(); h1.update(b'data')` with `h1 = blake2b(b'data')` test/data/err_182.py:22:1 [FURB182]: Replace `h2 = blake2s(); h2.update(b'data')` with `h2 = blake2s(b'data')` test/data/err_182.py:25:1 [FURB182]: Replace `h3 = md5(); h3.update(b'data')` with `h3 = md5(b'data')` test/data/err_182.py:28:1 [FURB182]: Replace `h4 = sha1(); h4.update(b'data')` with `h4 = sha1(b'data')` test/data/err_182.py:31:1 [FURB182]: Replace `h5 = sha224(); h5.update(b'data')` with `h5 = sha224(b'data')` test/data/err_182.py:34:1 [FURB182]: Replace `h6 = sha256(); h6.update(b'data')` with `h6 = sha256(b'data')` test/data/err_182.py:37:1 [FURB182]: Replace `h7 = sha384(); h7.update(b'data')` with `h7 = sha384(b'data')` test/data/err_182.py:40:1 [FURB182]: Replace `h8 = sha3_224(); h8.update(b'data')` with `h8 = sha3_224(b'data')` test/data/err_182.py:43:1 [FURB182]: Replace `h9 = sha3_256(); h9.update(b'data')` with `h9 = sha3_256(b'data')` test/data/err_182.py:46:1 [FURB182]: Replace `h10 = sha3_384(); h10.update(b'data')` with `h10 = sha3_384(b'data')` test/data/err_182.py:49:1 [FURB182]: Replace `h11 = sha3_512(); h11.update(b'data')` with `h11 = sha3_512(b'data')` test/data/err_182.py:52:1 [FURB182]: Replace `h12 = sha512(); h12.update(b'data')` with `h12 = sha512(b'data')` test/data/err_182.py:55:1 [FURB182]: Replace `h13 = shake_128(); h13.update(b'data')` with `h13 = shake_128(b'data')` test/data/err_182.py:58:1 [FURB182]: Replace `h14 = shake_256(); h14.update(b'data')` with `h14 = shake_256(b'data')` test/data/err_182.py:61:1 [FURB182]: Replace `h15 = hashlib.sha256(); h15.update(b'data')` with `h15 = hashlib.sha256(b'data')` test/data/err_182.py:64:1 [FURB182]: Replace `h16 = hash_algo(); h16.update(b'data')` with `h16 = hash_algo(b'data')` refurb-1.27.0/test/data/err_183.py000066400000000000000000000001551454672660200165300ustar00rootroot00000000000000x = " " # these should match f"{x}" f"{123}" # these should not f"hello{x}world" f"{x} {x}" f"{x:{x}}" refurb-1.27.0/test/data/err_183.txt000066400000000000000000000002121454672660200167110ustar00rootroot00000000000000test/data/err_183.py:5:1 [FURB183]: Replace `f"{x}"` with `str(x)` test/data/err_183.py:6:1 [FURB183]: Replace `f"{123}"` with `str(123)` refurb-1.27.0/test/data/err_184.py000066400000000000000000000065601454672660200165370ustar00rootroot00000000000000class torch: @staticmethod def ones(*args): return torch @staticmethod def long(): return torch @staticmethod def to(device: str): return torch.Tensor() class Tensor: pass def transform(x): return x class spark: class read: @staticmethod def parquet(file_name: str): return spark.DataFrame() class functions: @staticmethod def lit(constant): return constant @staticmethod def col(col_name): return col_name class DataFrame: @staticmethod def withColumnRenamed(col_in, col_out): return spark.DataFrame() @staticmethod def withColumn(col_in, col_out): return spark.DataFrame() @staticmethod def select(*args): return spark.DataFrame() class F: @staticmethod def lit(value): return value # these will match def get_tensors(device: str) -> torch.Tensor: a = torch.ones(2, 1) a = a.long() a = a.to(device) return a def process(file_name: str): common_columns = ["col1_renamed", "col2_renamed", "custom_col"] df = spark.read.parquet(file_name) df = df \ .withColumnRenamed('col1', 'col1_renamed') \ .withColumnRenamed('col2', 'col2_renamed') df = df \ .select(common_columns) \ .withColumn('service_type', spark.functions.lit('green')) return df def projection(df_in: spark.DataFrame) -> spark.DataFrame: df = ( df_in.select(["col1", "col2"]) .withColumnRenamed("col1", "col1a") ) return df.withColumn("col2a", spark.functions.col("col2").cast("date")) def assign_multiple(df): df = df.select("column") result_df = df.select("another_column") final_df = result_df.withColumn("column2", F.lit("abc")) return final_df # not yet supported def assign_alternating(df, df2): df = df.select("column") df2 = df2.select("another_column") df = df.withColumn("column2", F.lit("abc")) return df, df2 # these will not def ignored(x): _ = x.op1() _ = _.op2() return _ def _(x): y = x.m() return y.operation(*[v for v in y]) def assign_multiple_referenced(df, df2): df = df.select("column") result_df = df.select("another_column") return df, result_df def invalid(df_in: spark.DataFrame, alternative_df: spark.DataFrame) -> spark.DataFrame: df = ( df_in.select(["col1", "col2"]) .withColumnRenamed("col1", "col1a") ) return alternative_df.withColumn("col2a", spark.functions.col("col2").cast("date")) def no_match(): y = 10 y = transform(y) return y def f(x): if x: name = "alice" stripped = name.strip() print(stripped) else: name = "bob" print(name) def g(x): try: name = "alice" stripped = name.strip() print(stripped) except ValueError: name = "bob" print(name) def h(x): for _ in (1, 2, 3): name = "alice" stripped = name.strip() print(stripped) else: name = "bob" print(name) def assign_multiple_try(df): try: df = df.select("column") result_df = df.select("another_column") final_df = result_df.withColumn("column2", F.lit("abc")) return final_df except ValueError: return None refurb-1.27.0/test/data/err_184.txt000066400000000000000000000010201454672660200167100ustar00rootroot00000000000000test/data/err_184.py:59:5 [FURB184]: Assignment statement should be chained test/data/err_184.py:60:5 [FURB184]: Assignment statement should be chained test/data/err_184.py:67:5 [FURB184]: Assignment statement should be chained test/data/err_184.py:70:5 [FURB184]: Assignment statement should be chained test/data/err_184.py:81:5 [FURB184]: Return statement should be chained test/data/err_184.py:86:5 [FURB184]: Assignment statement should be chained test/data/err_184.py:87:5 [FURB184]: Assignment statement should be chained refurb-1.27.0/test/data/err_185.py000066400000000000000000000003761454672660200165370ustar00rootroot00000000000000x = {} y = set() # these should match _ = x.copy() | {} _ = {} | x.copy() _ = y.copy() | set() _ = set() | y.copy() _ = x.copy() | {} | x.copy() class C: def copy(self) -> dict: return {} c = C() # these should not _ = c.copy() | {} refurb-1.27.0/test/data/err_185.txt000066400000000000000000000006051454672660200167210ustar00rootroot00000000000000test/data/err_185.py:5:5 [FURB185]: Replace `x.copy()` with `x` test/data/err_185.py:6:10 [FURB185]: Replace `x.copy()` with `x` test/data/err_185.py:8:5 [FURB185]: Replace `y.copy()` with `y` test/data/err_185.py:9:13 [FURB185]: Replace `y.copy()` with `y` test/data/err_185.py:11:5 [FURB185]: Replace `x.copy()` with `x` test/data/err_185.py:11:21 [FURB185]: Replace `x.copy()` with `x` refurb-1.27.0/test/data/err_186.py000066400000000000000000000006061454672660200165340ustar00rootroot00000000000000l = [] # these should match l = sorted(l) l = sorted(l, key=lambda x: x > 0) l = sorted(l, reverse=True) l = sorted(l, key=lambda x: x > 0, reverse=True) # these should not l2 = sorted(l) l2 = sorted(l, key=lambda x: x > 0) l2 = sorted(l, reverse=True) d = {} # dont warn since d is a dict and does not have a .sort() method d = sorted(d) l = sorted(l, lambda x: x) # type: ignore refurb-1.27.0/test/data/err_186.txt000066400000000000000000000006661454672660200167310ustar00rootroot00000000000000test/data/err_186.py:5:1 [FURB186]: Replace `l = sorted(l)` with `l.sort()` test/data/err_186.py:6:1 [FURB186]: Replace `l = sorted(l, key=lambda x: x > 0)` with `l.sort(key=lambda x: x > 0)` test/data/err_186.py:7:1 [FURB186]: Replace `l = sorted(l, reverse=True)` with `l.sort(reverse=True)` test/data/err_186.py:8:1 [FURB186]: Replace `l = sorted(l, key=lambda x: x > 0, reverse=True)` with `l.sort(key=lambda x: x > 0, reverse=True)` refurb-1.27.0/test/data/inline_comments.py000066400000000000000000000011331454672660200205250ustar00rootroot00000000000000# these should be ignored x = int(0) # noqa: FURB123 x = int(0) # noqa x = int(0) # noqa # line below contains trailing whitespace! x = int(0) # noqa x = int(0) # existing comment # noqa x = int(0) # noqa: FURB123,RUF100 x = int(0) # noqa: FURB123, RUF100 x = int(0) # noqa: FURB123, RUF100 x = int(0) # noqa: FURB123 RUF100 x = int(0) # noqa: FURB123 and RUF100 x = int(0), int(0) # noqa: FURB123 # these should not x = int(0) x = int(0) # some comment x = int(0) # noqa: FURB999 x = int(0) # noqa: FURB1234 x = int(0) # noqa: 123 x = str("# noqa: FURB123 ") x = str('# noqa: FURB123 ') refurb-1.27.0/test/data/inline_comments.txt000066400000000000000000000007611454672660200207220ustar00rootroot00000000000000test/data/inline_comments.py:18:5 [FURB123]: Replace `int(x)` with `x` test/data/inline_comments.py:19:5 [FURB123]: Replace `int(x)` with `x` test/data/inline_comments.py:20:5 [FURB123]: Replace `int(x)` with `x` test/data/inline_comments.py:21:5 [FURB123]: Replace `int(x)` with `x` test/data/inline_comments.py:22:5 [FURB123]: Replace `int(x)` with `x` test/data/inline_comments.py:23:5 [FURB123]: Replace `str(x)` with `x` test/data/inline_comments.py:24:5 [FURB123]: Replace `str(x)` with `x` refurb-1.27.0/test/data/pathlib.py000066400000000000000000000006041454672660200167670ustar00rootroot00000000000000from pathlib import Path # test for additional instances where path objects should be checked folder = Path("folder") with open(folder / "file.txt") as f: pass with open(folder / "another_folder" / "file.txt") as f: pass # these should not match with open(folder + "file.txt") as f: # type: ignore pass with open("folder" / "file.txt") as f: # type: ignore pass refurb-1.27.0/test/data/pathlib.txt000066400000000000000000000002151454672660200171540ustar00rootroot00000000000000test/data/pathlib.py:7:6 [FURB117]: Replace `open(x)` with `x.open()` test/data/pathlib.py:10:6 [FURB117]: Replace `open(x)` with `x.open()` refurb-1.27.0/test/data_3.10/000077500000000000000000000000001454672660200154335ustar00rootroot00000000000000refurb-1.27.0/test/data_3.10/err_161.py000066400000000000000000000007141454672660200171660ustar00rootroot00000000000000# these should match _ = bin(0b1111).count("1") _ = bin(0b0011 & 0b11).count("1") _ = bin(1 < 2).count("1") _ = bin(0b1111)[2:].count("1") # noqa: FURB116 x = 0b1111 _ = bin(x).count("1") _ = bin(int("123")).count("1") _ = bin([1][0]).count("1") # these should not _ = "hello".count("1") _ = bin(0b1111).endswith("1") _ = bin(1, 2).count("1") # type: ignore _ = bin(0b1111)[3:].count("1") _ = bin(0b1111)[2:1].count("1") _ = bin(0b1111).count("1", 123) refurb-1.27.0/test/data_3.10/err_161.txt000066400000000000000000000012001454672660200173440ustar00rootroot00000000000000test/data_3.10/err_161.py:3:5 [FURB161]: Replace `bin(x).count("1")` with `x.bit_count()` test/data_3.10/err_161.py:4:5 [FURB161]: Replace `bin(x).count("1")` with `(x).bit_count()` test/data_3.10/err_161.py:5:5 [FURB161]: Replace `bin(x).count("1")` with `(x).bit_count()` test/data_3.10/err_161.py:6:5 [FURB161]: Replace `bin(x)[2:].count("1")` with `x.bit_count()` test/data_3.10/err_161.py:9:5 [FURB161]: Replace `bin(x).count("1")` with `x.bit_count()` test/data_3.10/err_161.py:10:5 [FURB161]: Replace `bin(x).count("1")` with `x.bit_count()` test/data_3.10/err_161.py:11:5 [FURB161]: Replace `bin(x).count("1")` with `x.bit_count()` refurb-1.27.0/test/data_3.11/000077500000000000000000000000001454672660200154345ustar00rootroot00000000000000refurb-1.27.0/test/data_3.11/err_162.py000066400000000000000000000016631454672660200171740ustar00rootroot00000000000000from datetime import datetime # these should match datetime.fromisoformat("".replace("Z", "+00:00")) datetime.fromisoformat("".replace("Z", "-00:00")) datetime.fromisoformat("".replace("Z", "+0000")) datetime.fromisoformat("".replace("Z", "-0000")) datetime.fromisoformat("".replace("Z", "+00")) datetime.fromisoformat("".replace("Z", "-00")) x = "" datetime.fromisoformat(x.replace("Z", "+00:00")) datetime.fromisoformat(""[:-1] + "+00:00") datetime.fromisoformat("".strip("Z") + "+00:00") datetime.fromisoformat("".rstrip("Z") + "+00:00") # these should not datetime.fromisoformat("".replace("XYZ", "+00:00")) datetime.fromisoformat("".replace("Z", "+10:00")) datetime.fromisoformat(""[:1] + "+00:00") datetime.fromisoformat(""[1:1] + "+00:00") datetime.fromisoformat(""[:-1:1] + "+00:00") class C: def replace(self, this: str, that: str) -> str: return this + that c = C() datetime.fromisoformat(c.replace("Z", "+00:00")) refurb-1.27.0/test/data_3.11/err_162.txt000066400000000000000000000021631454672660200173570ustar00rootroot00000000000000test/data_3.11/err_162.py:5:1 [FURB162]: Replace `fromisoformat(x.replace("Z", "+00:00"))` with `fromisoformat(x)` test/data_3.11/err_162.py:6:1 [FURB162]: Replace `fromisoformat(x.replace("Z", "-00:00"))` with `fromisoformat(x)` test/data_3.11/err_162.py:7:1 [FURB162]: Replace `fromisoformat(x.replace("Z", "+0000"))` with `fromisoformat(x)` test/data_3.11/err_162.py:8:1 [FURB162]: Replace `fromisoformat(x.replace("Z", "-0000"))` with `fromisoformat(x)` test/data_3.11/err_162.py:9:1 [FURB162]: Replace `fromisoformat(x.replace("Z", "+00"))` with `fromisoformat(x)` test/data_3.11/err_162.py:10:1 [FURB162]: Replace `fromisoformat(x.replace("Z", "-00"))` with `fromisoformat(x)` test/data_3.11/err_162.py:13:1 [FURB162]: Replace `fromisoformat(x.replace("Z", "+00:00"))` with `fromisoformat(x)` test/data_3.11/err_162.py:15:1 [FURB162]: Replace `fromisoformat(x[:-1] + "+00:00")` with `fromisoformat(x)` test/data_3.11/err_162.py:16:1 [FURB162]: Replace `fromisoformat(x.strip("Z") + "+00:00")` with `fromisoformat(x)` test/data_3.11/err_162.py:17:1 [FURB162]: Replace `fromisoformat(x.rstrip("Z") + "+00:00")` with `fromisoformat(x)` refurb-1.27.0/test/data_3.9/000077500000000000000000000000001454672660200153635ustar00rootroot00000000000000refurb-1.27.0/test/data_3.9/err_121.py000066400000000000000000000002451454672660200171110ustar00rootroot00000000000000num = 123 # Test error message for Python <= 3.9, since the type union form is only # supported in Python 3.10+ _ = isinstance(num, float) or isinstance(num, int) refurb-1.27.0/test/data_3.9/err_121.txt000066400000000000000000000001651454672660200173010ustar00rootroot00000000000000test/data_3.9/err_121.py:6:21 [FURB121]: Replace `isinstance(x, y) or isinstance(x, z)` with `isinstance(x, (y, z))` refurb-1.27.0/test/e2e/000077500000000000000000000000001454672660200145345ustar00rootroot00000000000000refurb-1.27.0/test/e2e/custom_check.py000066400000000000000000000000251454672660200175520ustar00rootroot00000000000000print("hello world") refurb-1.27.0/test/e2e/dummy.py000066400000000000000000000001531454672660200162400ustar00rootroot00000000000000""" This is a dummy file just to make sure that the refurb command is installed and running correctly. """ refurb-1.27.0/test/e2e/empty_package/000077500000000000000000000000001454672660200173455ustar00rootroot00000000000000refurb-1.27.0/test/e2e/empty_package/.gitkeep000066400000000000000000000000001454672660200207640ustar00rootroot00000000000000refurb-1.27.0/test/e2e/gbk.py000066400000000000000000000000401454672660200156430ustar00rootroot00000000000000print("") print("一些中文") refurb-1.27.0/test/e2e/stub_pkg/000077500000000000000000000000001454672660200163525ustar00rootroot00000000000000refurb-1.27.0/test/e2e/stub_pkg/file.py000066400000000000000000000001341454672660200176410ustar00rootroot00000000000000class C: x: int def __init__(self) -> None: self.x = int(0) # noqa: UP018 refurb-1.27.0/test/e2e/stub_pkg/file.pyi000066400000000000000000000000711454672660200200120ustar00rootroot00000000000000class C: x: int def __init__(self) -> None: ... refurb-1.27.0/test/invalid_checks/000077500000000000000000000000001454672660200170275ustar00rootroot00000000000000refurb-1.27.0/test/invalid_checks/invalid_check.py000066400000000000000000000003521454672660200221640ustar00rootroot00000000000000from dataclasses import dataclass from refurb.error import Error @dataclass class ErrorInfo(Error): prefix = "XYZ" code = 104 msg: str = "Your message here" def check(node: int, errors: list[Error]) -> None: pass refurb-1.27.0/test/mypy_visitor.py000066400000000000000000000145141454672660200172150ustar00rootroot00000000000000""" This module provides a mapping between a method name in a Visitor or Mypy's ASTs and the type of the Node it is meant to visit. This is an enabler to ensuring the correctness of the generated Mypy AST visitor Refurb uses to run its checks. This information is surprisingly hard to obtain programmatically. The approach here is to explore all the methods of an existing Visitor class in Mypy: mypy.traverser.TraverserVisitor and obtain the type annotation for their first (non-self) parameter. This is further complicated by the fact that Mypy loads by default as compiled code, and typing information for methods if thus not available. Here we use a trick found here on Stack Overflow https://stackoverflow.com/a/68685189/ to create a context manager that temporarily forces a preference for pure python modules when importing. So roughly, we do this: 1. Import the mypy things we need 2. Capture the globals (so that we can resolve the strigified type annotations to the correct types) 3. Clear the mypy imported modules 4. Import them again with their pure python versions 5. Inspect the Visitor to get the type names, but resolve them using the captured globals (from the native versions) 6. Restore the native mypy implementations """ import inspect import sys import typing from collections.abc import Callable, Iterator from contextlib import contextmanager from dataclasses import dataclass from importlib.abc import PathEntryFinder from importlib.machinery import FileFinder from types import FunctionType from typing import Any import mypy.nodes import mypy.traverser VisitorNodeTypeMap = dict[str, type[mypy.nodes.Node]] Namespace = dict[str, Any] # type: ignore @contextmanager def prefer_pure_python_imports() -> Iterator[None]: """ During the scope of this context manager, all imports will be done using pure python versions when available. Credit to this answer on SO: https://stackoverflow.com/a/68685189/ """ @dataclass class PreferPureLoaderHook: orig_hook: Callable[[str], PathEntryFinder] def __call__(self, path: str) -> PathEntryFinder: finder = self.orig_hook(path) if isinstance(finder, FileFinder): # Move pure python file loaders to the front finder._loaders.sort( # type: ignore key=lambda pair: 0 if pair[0] in {".py", ".pyc"} else 1 ) return finder sys.path_hooks = [PreferPureLoaderHook(h) for h in sys.path_hooks] sys.path_importer_cache.clear() yield # Restore the previous behaviour original_hooks = [] for hook in sys.path_hooks: assert isinstance(hook, PreferPureLoaderHook) original_hooks.append(hook.orig_hook) sys.path_hooks = original_hooks sys.path_importer_cache.clear() @contextmanager def pure_python_mypy() -> Iterator[None]: """ Inside this context, all mypy related imports are done with the pure python versions. Any existing mypy module that was imported before needs to be reimported before use within the context. Upon exiting, the previous implementations are restored. """ def loaded_mypy_modules() -> Iterator[str]: """Covenient block to get names of imported mypy modules""" for mod_name in sys.modules: if mod_name == "mypy" or mod_name.startswith("mypy."): yield mod_name # First, backup all imported mypy modules and remove them from sys.modules, # so they will not be found in resolution saved_mypy = {} for mod_name in list(loaded_mypy_modules()): saved_mypy[mod_name] = sys.modules.pop(mod_name) with prefer_pure_python_imports(): # After the modules are clean, ensure the newly imported mypy modules # are their pure python versions. # - Pure python: methods are FunctionType # - Native: methods are MethodDescriptorType from mypy.traverser import TraverserVisitor # noqa: PLC0415 assert isinstance(typing.cast(FunctionType, TraverserVisitor.visit_var), FunctionType) # Give back control yield # We're back and this is where we do cleanup. We'll remove all imported # mypy modules (pure python) and restore the previously backed-up ones # (allegedly native implementations) for mod_name in list(loaded_mypy_modules()): del sys.modules[mod_name] for mod_name, module in saved_mypy.items(): sys.modules[mod_name] = module def _get_class_globals(target_class: type, localns: Namespace) -> Namespace: """ Get the globals namespace for the full class hierarchy that starts in target_class. This follows the recommendation of PEP-563 to resolve stringified type annotations at runtime. """ all_globals = localns.copy() for base in inspect.getmro(target_class): all_globals.update(vars(sys.modules[base.__module__])) return all_globals def _make_mappings(globalns: Namespace) -> VisitorNodeTypeMap: """ Generate a mapping between the name of a visitor method in TraverserVisitor and the type of its first (non-self) parameter. """ visitor_method_map = {} from mypy.traverser import TraverserVisitor # noqa: PLC0415 methods = inspect.getmembers( TraverserVisitor, lambda o: inspect.isfunction(o) and o.__name__.startswith("visit_"), ) for method_name, method in methods: method_params = list(inspect.signature(method).parameters.values()) param_name = method_params[1].name method_types = typing.get_type_hints(method, globalns=globalns) visitor_method_map[method_name] = method_types[param_name] return visitor_method_map # Capture the global namespace of the hierarchy of TraverserVisitor before we # replace it with a short-lived pure-python version inside the context manager # below. _globals = _get_class_globals(mypy.traverser.TraverserVisitor, locals()) def get_mypy_visitor_mapping() -> VisitorNodeTypeMap: """ Provide the visitor method name to node type mapping as it comes from Mypy. Resolve the mappings using the pure-python version of mypy (necessary to obtain method signature type info) but then ensure the types are resolved to their native counterparts (by passing the previously captured global namespace) """ with pure_python_mypy(): return _make_mappings(globalns=_globals) refurb-1.27.0/test/test_arg_parsing.py000066400000000000000000000423121454672660200177700ustar00rootroot00000000000000import os from pathlib import Path from unittest.mock import patch import pytest from refurb.error import ErrorCategory, ErrorCode from refurb.settings import Settings from refurb.settings import parse_command_line_args as parse_args from refurb.settings import parse_config_file, parse_error_id def test_parse_explain(): assert parse_args(["--explain", "123"]) == Settings(explain=ErrorCode(123)) def test_parse_explain_missing_option() -> None: msg = 'refurb: missing argument after "--explain"' with pytest.raises(ValueError, match=msg): parse_args(["--explain"]) def test_parse_explain_furb_prefix() -> None: assert parse_args(["--explain", "FURB123"]) == Settings(explain=ErrorCode(123)) def test_require_numbers_as_explain_id() -> None: with pytest.raises(ValueError, match='refurb: "abc" must be in form FURB123 or 123'): parse_args(["--explain", "abc"]) def test_parse_files() -> None: assert parse_args(["a", "b", "c"]) == Settings(files=["a", "b", "c"]) def test_check_for_unsupported_flags() -> None: with pytest.raises(ValueError, match='refurb: unsupported option "-x"'): parse_args(["-x"]) def test_parse_help_args() -> None: assert parse_args([]) == Settings(help=True) assert parse_args(["--help"]) == Settings(help=True) assert parse_args(["-h"]) == Settings(help=True) def test_parse_version_args() -> None: assert parse_args(["--version"]) == Settings(version=True) def test_parse_ignore() -> None: got = parse_args(["--ignore", "FURB123", "--ignore", "321"]) expected = Settings(ignore={ErrorCode(123), ErrorCode(321)}) assert got == expected def test_parse_ignore_category() -> None: got = parse_args(["--ignore", "#category"]) expected = Settings(ignore={ErrorCategory("category")}) assert got == expected def test_parse_ignore_check_missing_arg() -> None: with pytest.raises(ValueError, match='refurb: missing argument after "--ignore"'): parse_args(["--ignore"]) def test_parse_enable() -> None: got = parse_args(["--enable", "FURB123", "--enable", "321"]) expected = Settings(enable={ErrorCode(123), ErrorCode(321)}) assert got == expected def test_parse_enable_category() -> None: got = parse_args(["--enable", "#category"]) expected = Settings(enable={ErrorCategory("category")}) assert got == expected def test_parse_enable_check_missing_arg() -> None: with pytest.raises(ValueError, match='refurb: missing argument after "--enable"'): parse_args(["--enable"]) def test_debug_parsing() -> None: assert parse_args(["--debug", "file"]) == Settings(files=["file"], debug=True) def test_quiet_flag_parsing() -> None: assert parse_args(["--quiet", "file"]) == Settings(files=["file"], quiet=True) def test_generate_subcommand() -> None: assert parse_args(["gen"]) == Settings(generate=True) def test_load_flag() -> None: assert parse_args(["--load", "some_module"]) == Settings(load=["some_module"]) def test_parse_load_flag_missing_arg() -> None: with pytest.raises(ValueError, match='refurb: missing argument after "--load"'): parse_args(["--load"]) def test_parse_config_file_flag_missing_arg() -> None: with pytest.raises(ValueError, match='refurb: missing argument after "--config-file"'): parse_args(["--config-file"]) def test_config_file_flag() -> None: assert parse_args(["--config-file", "some_file"]) == Settings( config_file="some_file", ) def test_parse_config_file() -> None: contents = """\ [tool.refurb] load = ["some", "folders"] ignore = [100, "FURB101"] enable = ["FURB111", "FURB222"] format = "github" sort_by = "error" color = false """ config = parse_config_file(contents) assert config == Settings( load=["some", "folders"], ignore={ErrorCode(100), ErrorCode(101)}, enable={ErrorCode(111), ErrorCode(222)}, format="github", sort_by="error", color=False, ) def test_merge_command_line_args_and_config_file() -> None: contents = """\ [tool.refurb] load = ["some", "folders"] ignore = [100, "FURB101"] """ command_line_args = parse_args(["some_file.py"]) config_file = parse_config_file(contents) merged = Settings.merge(config_file, command_line_args) assert merged == Settings( files=["some_file.py"], load=["some", "folders"], ignore={ErrorCode(100), ErrorCode(101)}, ) def test_command_line_args_merge_config_file() -> None: contents = """\ [tool.refurb] load = ["some", "folders"] ignore = [100, "FURB101"] enable = ["FURB111", "FURB222"] quiet = true format = "github" sort_by = "error" python_version = "3.7" mypy_args = ["some", "args"] """ command_line_args = parse_args(["--load", "x", "--ignore", "123", "--enable", "FURB200"]) config_file = parse_config_file(contents) merged = Settings.merge(config_file, command_line_args) assert merged == Settings( load=["some", "folders", "x"], ignore={ErrorCode(100), ErrorCode(101), ErrorCode(123)}, enable={ErrorCode(111), ErrorCode(222), ErrorCode(200)}, quiet=True, format="github", sort_by="error", python_version=(3, 7), mypy_args=["some", "args"], ) def test_config_missing_ignore_option_is_allowed() -> None: contents = """\ [tool.refurb] load = ["x"] """ assert parse_config_file(contents) == Settings(load=["x"]) def test_config_missing_load_option_is_allowed() -> None: contents = """\ [tool.refurb] ignore = [123] """ assert parse_config_file(contents) == Settings(ignore={ErrorCode(123)}) def test_parse_error_codes() -> None: tests = { "FURB123": ErrorCode(123), "123": ErrorCode(123), "ABC100": ErrorCode(prefix="ABC", id=100), "ABCDE100": ValueError, "ABC1234": ValueError, "AB123": ValueError, "invalid": ValueError, "12": ValueError, "-123": ValueError, } for input, output in tests.items(): if output is ValueError: msg = "must be in form FURB123 or 123" with pytest.raises(ValueError, match=msg): parse_error_id(input) else: assert parse_error_id(input) == output def test_disable_error() -> None: settings = parse_args(["--disable", "FURB100"]) assert settings == Settings(disable={ErrorCode(100)}) def test_disable_error_category() -> None: settings = parse_args(["--disable", "#category"]) assert settings == Settings(disable={ErrorCategory("category")}) def test_disable_existing_enabled_error() -> None: settings = parse_args(["--enable", "FURB100", "--disable", "FURB100"]) assert settings == Settings(disable={ErrorCode(100)}) def test_enable_existing_disabled_error() -> None: settings = parse_args(["--disable", "FURB100", "--enable", "FURB100"]) assert settings == Settings(enable={ErrorCode(100)}) def test_parse_disable_check_missing_arg() -> None: with pytest.raises(ValueError, match='refurb: missing argument after "--disable"'): parse_args(["--disable"]) def test_disable_in_config_file() -> None: contents = """\ [tool.refurb] disable = ["FURB111", "FURB222"] """ config_file = parse_config_file(contents) assert config_file == Settings(disable={ErrorCode(111), ErrorCode(222)}) def test_disable_overrides_enable_in_config_file() -> None: contents = """\ [tool.refurb] enable = ["FURB111", "FURB222"] disable = ["FURB111", "FURB333", "FURB444"] """ config_file = parse_config_file(contents) assert config_file == Settings( enable={ErrorCode(222)}, disable={ErrorCode(111), ErrorCode(333), ErrorCode(444)}, ) def test_disable_cli_arg_overrides_config_file() -> None: contents = """\ [tool.refurb] enable = ["FURB111", "FURB222", "FURB333"] disable = ["FURB111", "FURB444"] """ config_file = parse_config_file(contents) command_line_args = parse_args(["--disable", "FURB333"]) merged = Settings.merge(config_file, command_line_args) assert merged == Settings( enable={ErrorCode(222)}, disable={ErrorCode(111), ErrorCode(333), ErrorCode(444)}, ) def test_disable_all_flag_parsing() -> None: assert parse_args(["--disable-all", "file"]) == Settings(files=["file"], disable_all=True) def test_disable_all_flag_disables_existing_enables() -> None: settings = parse_args(["--enable", "FURB123", "--disable-all", "--enable", "FURB456"]) assert settings == Settings(disable_all=True, enable={ErrorCode(456)}) def test_disable_all_in_config_file() -> None: contents = """\ [tool.refurb] disable_all = true enable = ["FURB123"] """ config_file = parse_config_file(contents) assert config_file == Settings( disable_all=True, enable={ErrorCode(123)}, ) def test_disable_all_command_line_override() -> None: contents = """\ [tool.refurb] disable_all = false enable = ["FURB123"] """ config_file = parse_config_file(contents) command_line_args = parse_args(["--disable-all", "--enable", "FURB456"]) merged = Settings.merge(config_file, command_line_args) assert merged == Settings( disable_all=True, enable={ErrorCode(456)}, ) def test_parse_python_version_flag() -> None: settings = parse_args(["--python-version", "3.9"]) assert settings.python_version == (3, 9) def test_parse_invalid_python_version_flag_will_fail() -> None: versions = ["3.10.8", "x.y", "-3.-8"] for version in versions: with pytest.raises(ValueError, match="version must be in form `x.y`"): parse_args(["--python-version", version]) def test_parse_python_version_flag_in_config_file() -> None: contents = """\ [tool.refurb] python_version = "3.5" """ config_file = parse_config_file(contents) assert config_file.python_version == (3, 5) def test_enable_all_flag() -> None: assert parse_args(["--enable-all"]) == Settings(enable_all=True) def test_enable_all_will_clear_any_previously_disabled_checks() -> None: settings = parse_args(["--disable", "FURB100", "--enable-all"]) assert settings == Settings(enable_all=True) def test_enable_all_in_config_file() -> None: config = """\ [tool.refurb] enable_all = true """ assert parse_config_file(config).enable_all def test_enable_all_and_disable_all_are_mutually_exclusive() -> None: with pytest.raises(ValueError, match="can't be used at the same time"): Settings(enable_all=True, disable_all=True) def test_merging_enable_all_field() -> None: config = """\ [tool.refurb] enable = ["FURB100", "FURB101", "FURB102"] disable = ["FURB100", "FURB103"] """ config_file = parse_config_file(config) command_line_args = parse_args(["--enable-all", "--disable", "FURB105"]) merged_settings = Settings.merge(config_file, command_line_args) assert merged_settings == Settings(enable_all=True, disable={ErrorCode(105)}) def test_parse_config_file_categories() -> None: config = """\ [tool.refurb] enable = ["#category-a"] disable = ["#category-b"] ignore = ["#category-c"] """ config_file = parse_config_file(config) assert config_file == Settings( enable={ErrorCategory("category-a")}, disable={ErrorCategory("category-b")}, ignore={ErrorCategory("category-c")}, ) def test_parse_mypy_extra_args() -> None: settings = parse_args(["--", "mypy", "args", "here"]) assert settings == Settings(mypy_args=["mypy", "args", "here"]) def test_parse_mypy_extra_args_in_config() -> None: config = """\ [tool.refurb] mypy_args = ["some", "args"] """ config_file = parse_config_file(config) assert config_file == Settings(mypy_args=["some", "args"]) def test_cli_args_override_mypy_args_in_config_file() -> None: config = """\ [tool.refurb] mypy_args = ["some", "args"] """ config_file = parse_config_file(config) cli_args = parse_args(["--", "new", "args"]) merged = Settings.merge(config_file, cli_args) assert merged == Settings(mypy_args=["new", "args"]) def test_flags_which_support_comma_separated_cli_args() -> None: settings = parse_args( [ "--enable", "100,101", "--disable", "102,103", "--ignore", "104,105", ] ) assert settings == Settings( enable={ErrorCode(100), ErrorCode(101)}, disable={ErrorCode(102), ErrorCode(103)}, ignore={ErrorCode(104), ErrorCode(105)}, ) def test_parse_amend_file_paths() -> None: config = """\ [tool.refurb] ignore = ["FURB100"] [[tool.refurb.amend]] path = "some/file/path" ignore = ["FURB101", "FURB102"] [[tool.refurb.amend]] path = "some/other/path" ignore = [102, 103] """ config_file = parse_config_file(config) assert config_file == Settings( ignore={ ErrorCode(100), ErrorCode(101, path=Path("some/file/path")), ErrorCode(102, path=Path("some/file/path")), ErrorCode(102, path=Path("some/other/path")), ErrorCode(103, path=Path("some/other/path")), } ) def test_invalid_amend_field_fails() -> None: config = """\ [tool.refurb] amend = "oops" """ msg = r'"amend" field\(s\) must be a TOML table' with pytest.raises(ValueError, match=msg): parse_config_file(config) def test_extra_fields_in_amend_table_fails() -> None: config = """\ [[tool.refurb.amend]] path = "some/folder" ignore = ["FURB123"] extra = "data" """ msg = 'only "path" and "ignore" fields are supported' with pytest.raises(ValueError, match=msg): parse_config_file(config) def test_missing_or_malformed_fields_in_amend_table_fails() -> None: msg = '"path" or "ignore" fields are missing or malformed' config = """\ [[tool.refurb.amend]] ignore = ["FURB123"] """ with pytest.raises(ValueError, match=msg): parse_config_file(config) config = """\ [[tool.refurb.amend]] path = "some/folder" """ with pytest.raises(ValueError, match=msg): parse_config_file(config) config = """\ [[tool.refurb.amend]] path = true ignore = false """ with pytest.raises(ValueError, match=msg): parse_config_file(config) def test_extra_fields_config_file_fails() -> None: config = """\ [tool.refurb] unknown = "" fields = "" """ msg = r"refurb: unknown field\(s\): unknown, fields" with pytest.raises(ValueError, match=msg): parse_config_file(config) def test_incorrectly_typed_args_raises_error() -> None: tests = { "ignore = false": "must be a list", "enable = false": "must be a list", "disable = false": "must be a list", "load = false": "must be a list", "mypy_args = false": "must be a list", "quiet = []": "must be a bool", "disable_all = []": "must be a bool", "enable_all = []": "must be a bool", "python_version = false": "must be a string", } for test, expected in tests.items(): config = f"[tool.refurb]\n{test}" with pytest.raises(ValueError, match=expected): parse_config_file(config) def test_parse_empty_config_file() -> None: assert parse_config_file("") == Settings() def test_parse_format_flag() -> None: assert parse_args(["--format", "github"]) == Settings(format="github") def test_check_format_must_be_valid() -> None: msg = 'refurb: "oops" is not a valid format' with pytest.raises(ValueError, match=msg): parse_args(["--format", "oops"]) def test_parse_sort_by_flag() -> None: assert parse_args(["--sort", "error"]) == Settings(sort_by="error") def test_check_sort_by_field_must_be_valid() -> None: msg = 'refurb: cannot sort by "oops"' with pytest.raises(ValueError, match=msg): parse_args(["--sort", "oops"]) def test_disallow_empty_string_in_cli() -> None: tests = [ [""], ["file.py", ""], ] for test in tests: msg = "refurb: argument cannot be empty" with pytest.raises(ValueError, match=msg): parse_args(test) def test_ignored_flags_cause_error() -> None: tests = [ ["--help", "file.py"], ["--version", "file.py"], ["-h", "file.py"], ["file.py", "--help"], ["--version", "file.py"], ["file.py", "-h"], ] for test in tests: msg = f"refurb: unexpected value before/after `{test[0]}`" with pytest.raises(ValueError, match=msg): parse_args(test) def test_generate_subcommand_is_ignored_if_other_files_are_passed() -> None: assert parse_args(["gen", "something"]) == Settings(files=["gen", "something"]) def test_parse_verbose_flag() -> None: assert parse_args(["--verbose"]) == Settings(verbose=True) assert parse_args(["-v"]) == Settings(verbose=True) def test_parse_timing_stats_flag() -> None: assert parse_args(["--timing-stats", "file"]) == Settings(timing_stats=Path("file")) def test_parse_timing_stats_flag_without_arg_is_an_error() -> None: with pytest.raises(ValueError, match='refurb: missing argument after "--timing-stats"'): parse_args(["--timing-stats"]) def test_parse_no_color_flag() -> None: assert parse_args(["--no-color"]) == Settings(color=False) def test_no_color_env_var_disables_color() -> None: with patch.dict(os.environ, {"NO_COLOR": "1"}): settings = Settings() assert not settings.color refurb-1.27.0/test/test_check_formatting.py000066400000000000000000000046461454672660200210130ustar00rootroot00000000000000import re from functools import cache from pathlib import Path import refurb from refurb.error import Error from refurb.loader import get_error_class, get_modules def assert_category_exists(error: type[Error]) -> None: assert error.categories or not error.enabled, "categories field is missing" def assert_categories_are_sorted(error: type[Error]) -> None: error_msg = "categories are not sorted" assert tuple(sorted(error.categories)) == error.categories, error_msg def assert_categories_are_valid(error: type[Error], categories: list[str]) -> None: # By "valid" I mean that they are well-defined (in the documentation), and # are sorted. Basically, parse the documentation file for the categories, # which includes a list of all categories, and make sure each check only # uses categories defined in that list. This prevents typos from causing # a check to have an incorrect category. for category in error.categories: assert category in categories, f'category "{category}" is invalid' def assert_name_field_in_valid_format(name: str) -> None: error_name_format = "^[a-z]+(-[a-z]+){1,}$" error_msg = f'name must be in format "{error_name_format}"' assert re.match(error_name_format, name), error_msg def assert_name_is_unique(name: str, names: set[str]) -> None: assert name not in names, f'name "{name}" is already being used' names.add(name) @cache def get_categories_from_docs() -> list[str]: category_docs = Path(refurb.__file__).parent.parent / "docs/categories.md" with category_docs.open() as f: categories = [] for line in f: if line.startswith("## "): categories.extend([cat.strip().strip("`") for cat in line[3:].split(", ")]) return categories def test_checks_are_formatted_properly() -> None: error_names: set[str] = set() for module in get_modules([]): error = get_error_class(module) if not error: continue try: assert_category_exists(error) assert_categories_are_sorted(error) assert_categories_are_valid(error, get_categories_from_docs()) assert error.name, "name field missing for class" assert_name_field_in_valid_format(error.name) assert_name_is_unique(error.name, error_names) except AssertionError as ex: raise ValueError(f"{module.__file__}: {ex}") from ex refurb-1.27.0/test/test_checks.py000066400000000000000000000156131454672660200167400ustar00rootroot00000000000000from pathlib import Path from refurb.error import Error, ErrorCategory, ErrorCode from refurb.main import run_refurb from refurb.settings import Settings, parse_command_line_args def get_test_data_path() -> Path: data_path = Path(__file__).parent / "data" assert data_path.exists() assert data_path.is_dir() return data_path.relative_to(Path.cwd()) TEST_DATA_PATH = get_test_data_path() def test_checks() -> None: run_checks_in_folder(TEST_DATA_PATH) def test_fatal_mypy_error_is_bubbled_up() -> None: errors = run_refurb(Settings(files=["something"])) assert errors == ["refurb: can't read file 'something': No such file or directory"] def test_mypy_error_is_bubbled_up() -> None: errors = run_refurb(Settings(files=["some_file.py"])) assert errors == ["refurb: can't read file 'some_file.py': No such file or directory"] def test_ignore_check_is_respected() -> None: test_file = str(TEST_DATA_PATH / "err_100.py") errors = run_refurb(Settings(files=[test_file], ignore={ErrorCode(100), ErrorCode(123)})) assert len(errors) == 0 def test_ignore_custom_check_is_respected() -> None: args = [ "test/e2e/custom_check.py", "--load", "test.custom_checks.disallow_call", ] ignore_args = [*args, "--ignore", "XYZ100"] errors_normal = run_refurb(parse_command_line_args(args)) errors_while_ignoring = run_refurb(parse_command_line_args(ignore_args)) assert errors_normal assert not errors_while_ignoring def test_system_exit_is_caught() -> None: test_pkg = "test/e2e/empty_package" errors = run_refurb(Settings(files=[test_pkg])) assert errors == ["refurb: There are no .py[i] files in directory 'test/e2e/empty_package'"] DISABLED_CHECK = "test.custom_checks.disabled_check" def test_disabled_check_is_not_ran_by_default() -> None: errors = run_refurb(Settings(files=["test/e2e/dummy.py"], load=[DISABLED_CHECK])) assert not errors def test_disabled_check_ran_if_explicitly_enabled() -> None: errors = run_refurb( Settings( files=["test/e2e/dummy.py"], load=[DISABLED_CHECK], enable={ErrorCode(prefix="XYZ", id=101)}, ) ) expected = "test/e2e/dummy.py:1:1 [XYZ101]: This message is disabled by default" assert len(errors) == 1 assert str(errors[0]) == expected def test_disabled_check_ran_if_enable_all_is_set() -> None: errors = run_refurb( Settings( files=["test/e2e/dummy.py"], load=[DISABLED_CHECK], enable_all=True, ) ) expected = "test/e2e/dummy.py:1:1 [XYZ101]: This message is disabled by default" assert len(errors) == 1 assert str(errors[0]) == expected def test_disable_all_will_only_load_explicitly_enabled_checks() -> None: errors = run_refurb( Settings( files=["test/data/"], disable_all=True, enable={ErrorCode(100)}, ) ) assert all(isinstance(error, Error) and error.code == 100 for error in errors) def test_disable_will_actually_disable_check_loading() -> None: errors = run_refurb( Settings( files=["test/data/err_123.py"], disable={ErrorCode(123)}, ) ) assert not errors def test_load_will_only_load_each_modules_once() -> None: errors_normal = run_refurb( Settings( files=["test/e2e/custom_check.py"], load=["test.custom_checks"], ) ) duplicated_load_errors = run_refurb( Settings( files=["test/e2e/custom_check.py"], load=["test.custom_checks", "test.custom_checks"], ) ) assert len(errors_normal) == len(duplicated_load_errors) def test_load_builtin_checks_again_does_nothing() -> None: errors_normal = run_refurb(Settings(files=["test/data/err_100.py"])) duplicated_load_errors = run_refurb( Settings( files=["test/data/err_100.py"], load=["refurb"], ) ) assert len(errors_normal) == len(duplicated_load_errors) def test_injection_of_settings_into_checks() -> None: errors = run_refurb( Settings( files=["test/e2e/dummy.py"], load=["test.custom_checks.settings"], ) ) msg = "test/e2e/dummy.py:1:1 [XYZ103]: Files being checked: ['test/e2e/dummy.py']" assert len(errors) == 1 assert str(errors[0]) == msg def test_explicitly_disabled_check_is_ignored_when_enable_all_is_set() -> None: errors = run_refurb( Settings( files=["test/data/err_123.py"], enable_all=True, disable={ErrorCode(123)}, ) ) assert not errors def test_explicitly_enabled_check_from_disabled_category_is_ran() -> None: errors = run_refurb( Settings( files=["test/data/err_123.py"], disable={ErrorCategory("readability")}, enable={ErrorCode(123)}, ) ) assert errors def test_explicitly_enabled_category_still_runs() -> None: errors = run_refurb( Settings( files=["test/data/err_123.py"], disable_all=True, enable={ErrorCategory("readability")}, ) ) assert errors def test_error_not_ignored_if_path_doesnt_apply() -> None: errors = run_refurb( Settings( files=["test/data/err_123.py"], ignore={ErrorCode(123, path=Path("some_other_file.py"))}, ) ) assert errors def test_error_not_ignored_if_error_code_doesnt_apply() -> None: errors = run_refurb( Settings( files=["test/data/err_123.py"], ignore={ErrorCode(456, path=Path("test/data/err_123.py"))}, ) ) assert errors def test_error_ignored_if_path_applies() -> None: errors = run_refurb( Settings( files=["test/data/err_123.py"], ignore={ErrorCode(123, path=Path("test/data/err_123.py"))}, ) ) assert not errors def test_error_ignored_if_category_matches() -> None: error = ErrorCategory("readability", path=Path("test/data/err_123.py")) errors = run_refurb(Settings(files=["test/data/err_123.py"], ignore={error})) assert not errors def test_checks_with_python_version_dependant_error_msgs() -> None: run_checks_in_folder(Path("test/data_3.9"), version=(3, 9)) run_checks_in_folder(Path("test/data_3.10"), version=(3, 10)) run_checks_in_folder(Path("test/data_3.11"), version=(3, 11)) def run_checks_in_folder(folder: Path, *, version: tuple[int, int] | None = None) -> None: settings = Settings(files=[str(folder)], enable_all=True) if version: settings.python_version = version errors = run_refurb(settings) got = "\n".join([str(error) for error in errors]) files = sorted(folder.glob("*.txt"), key=lambda p: p.name) expected = "\n".join(txt for file in files if (txt := file.read_text()[:-1])) assert got == expected refurb-1.27.0/test/test_explain.py000066400000000000000000000020741454672660200171350ustar00rootroot00000000000000from refurb.checks.pathlib.with_suffix import ErrorInfo as furb100 from refurb.error import ErrorCode from refurb.explain import explain from refurb.settings import Settings def test_get_check_explanation_by_id() -> None: explanation = explain(Settings(explain=ErrorCode(100))) assert "error" not in explanation.lower() assert furb100.name assert furb100.name in explanation assert "FURB100" in explanation assert all(cat in explanation for cat in furb100.categories) def test_verbose_check_includes_filepath() -> None: explanation = explain(Settings(explain=ErrorCode(100), verbose=True)) assert "Filename: " in explanation def test_error_if_check_doesnt_exist() -> None: msg = explain(Settings(explain=ErrorCode(999))) assert msg == 'refurb: Error code "FURB999" not found' def test_check_with_no_docstring_gives_error() -> None: msg = explain( Settings( explain=ErrorCode(102, "XYZ"), load=["test.custom_checks"], ) ) assert msg == 'refurb: Explanation for "XYZ102" not found' refurb-1.27.0/test/test_gen.py000066400000000000000000000013461454672660200162470ustar00rootroot00000000000000from pathlib import Path from unittest.mock import patch from refurb.gen import folders_needing_init_file def test_folder_not_in_cwd_is_ignored(): with patch("pathlib.Path.cwd", lambda: Path("/some/random/path")): assert folders_needing_init_file(Path("./some/path")) == [] def test_relative_path_works(): assert folders_needing_init_file(Path("./a/b/c")) == [ Path.cwd() / "a" / "b" / "c", Path.cwd() / "a" / "b", Path.cwd() / "a", ] def test_absolute_path_works(): assert folders_needing_init_file(Path.cwd() / "a" / "b" / "c" / "d") == [ Path.cwd() / "a" / "b" / "c" / "d", Path.cwd() / "a" / "b" / "c", Path.cwd() / "a" / "b", Path.cwd() / "a", ] refurb-1.27.0/test/test_github_annotations.py000066400000000000000000000014631454672660200213750ustar00rootroot00000000000000from dataclasses import dataclass from pathlib import Path from refurb.error import Error from refurb.main import format_as_github_annotation def test_string_error_messages_are_translated_as_is() -> None: msg = format_as_github_annotation("testing") assert msg == "::error title=Refurb Error::testing" def test_error_is_converted_correctly() -> None: @dataclass class CustomError(Error): prefix = "ABC" code = 123 msg: str = "This is a test" absolute_path = Path("filename.py").resolve() error = CustomError(line=1, column=2, filename=str(absolute_path)) # column is 3 due to mypy node columns starting at 0 expected = "::error line=1,col=3,title=Refurb ABC123,file=filename.py::This is a test" assert format_as_github_annotation(error) == expected refurb-1.27.0/test/test_loader.py000066400000000000000000000032601454672660200167410ustar00rootroot00000000000000import pytest from mypy.nodes import CallExpr from refurb.error import Error from refurb.loader import extract_function_types, is_valid_error_class def test_check_must_be_callable() -> None: with pytest.raises(TypeError, match="Check function must be callable"): list(extract_function_types(1)) def test_check_must_have_valid_number_of_args() -> None: def check() -> None: pass with pytest.raises(TypeError, match="Check function must take 2-3 parameters"): list(extract_function_types(check)) def test_invalid_type_union_nodes_are_ignored() -> None: def check(node: CallExpr | int, errors: list[Error]) -> None: pass with pytest.raises(TypeError, match='"int" is not a valid Mypy node type'): list(extract_function_types(check)) def test_invalid_node_types_are_ignored() -> None: def check(node: int, errors: list[Error]) -> None: pass with pytest.raises(TypeError, match='"int" is not a valid Mypy node type'): list(extract_function_types(check)) def test_invalid_error_types_are_ignored() -> None: def check(node: CallExpr, errors: list[int]) -> None: pass with pytest.raises(TypeError, match=r'"error" param must be of type list\[Error\]'): list(extract_function_types(check)) def test_check_with_optional_settings_param() -> None: def check(node: CallExpr, errors: list[Error], settings: int) -> None: pass with pytest.raises(TypeError, match='"settings: int" is not a valid service'): list(extract_function_types(check)) def test_error_info_class_must_be_valid() -> None: class ErrorInfo: pass assert not is_valid_error_class(ErrorInfo) refurb-1.27.0/test/test_main.py000066400000000000000000000231061454672660200164200ustar00rootroot00000000000000import json import os from dataclasses import dataclass from functools import partial from importlib import metadata from locale import LC_ALL, setlocale from pathlib import Path from tempfile import NamedTemporaryFile from unittest.mock import patch import pytest from refurb.error import Error from refurb.main import main, run_refurb, sort_errors from refurb.settings import Settings, load_settings, parse_command_line_args def test_invalid_args_returns_error_code(): assert main(["--invalid"]) == 1 def test_explain_returns_success_code(): assert main(["--explain", "100"]) == 0 def test_run_refurb_no_errors_returns_success_code(): assert main(["test/e2e/dummy.py"]) == 0 def test_run_refurb_with_errors_returns_error_code(): assert main(["non_existent_file.py"]) == 1 def test_errors_are_sorted(): @dataclass class Error100(Error): code = 100 @dataclass class Error101(Error): code = 101 @dataclass class CustomError100(Error): prefix = "ABC" code = 100 errors: list[Error | str] = [ Error100(filename="0_first", line=10, column=5, msg=""), Error101(filename="1_last", line=1, column=5, msg=""), Error100(filename="0_first", line=2, column=7, msg=""), Error100(filename="1_last", line=1, column=10, msg=""), Error101(filename="0_first", line=10, column=5, msg=""), Error100(filename="1_last", line=10, column=5, msg=""), Error101(filename="0_first", line=1, column=5, msg=""), Error100(filename="1_last", line=2, column=7, msg=""), Error100(filename="0_first", line=1, column=10, msg=""), Error101(filename="1_last", line=10, column=5, msg=""), CustomError100(filename="1_last", line=10, column=5, msg=""), "some other error", ] settings = Settings(sort_by="filename") sorted_errors = sorted(errors, key=lambda e: sort_errors(e, settings)) assert sorted_errors == [ "some other error", Error101(filename="0_first", line=1, column=5, msg=""), Error100(filename="0_first", line=1, column=10, msg=""), Error100(filename="0_first", line=2, column=7, msg=""), Error100(filename="0_first", line=10, column=5, msg=""), Error101(filename="0_first", line=10, column=5, msg=""), Error101(filename="1_last", line=1, column=5, msg=""), Error100(filename="1_last", line=1, column=10, msg=""), Error100(filename="1_last", line=2, column=7, msg=""), CustomError100(filename="1_last", line=10, column=5, msg=""), Error100(filename="1_last", line=10, column=5, msg=""), Error101(filename="1_last", line=10, column=5, msg=""), ] settings.sort_by = "error" sorted_errors = sorted(errors, key=partial(sort_errors, settings=settings)) assert sorted_errors == [ "some other error", CustomError100(filename="1_last", line=10, column=5, msg=""), Error100(filename="0_first", line=1, column=10, msg=""), Error100(filename="0_first", line=2, column=7, msg=""), Error100(filename="0_first", line=10, column=5, msg=""), Error100(filename="1_last", line=1, column=10, msg=""), Error100(filename="1_last", line=2, column=7, msg=""), Error100(filename="1_last", line=10, column=5, msg=""), Error101(filename="0_first", line=1, column=5, msg=""), Error101(filename="0_first", line=10, column=5, msg=""), Error101(filename="1_last", line=1, column=5, msg=""), Error101(filename="1_last", line=10, column=5, msg=""), ] def test_debug_flag(): settings = Settings(files=["test/e2e/dummy.py"], debug=True) output = run_refurb(settings) assert output == [ """\ MypyFile:1( test/e2e/dummy.py ExpressionStmt:1( StrExpr(\\u000aThis is a dummy file just to make sure that the refurb command is installed\\u000aand running correctly.\\u000a)))""" ] def test_generate_subcommand(): with patch("refurb.main.generate") as p: main(["gen"]) p.assert_called_once() def test_help_flag_calls_print(): for args in (["--help"], ["-h"], []): with patch("builtins.print") as p: main(args) # type: ignore p.assert_called_once() assert "usage" in p.call_args[0][0] def test_version_flag_calls_version_func(): with patch("refurb.main.version") as p: main(["--version"]) p.assert_called_once() def test_explain_flag_mentioned_if_error_exists(): with patch("builtins.print") as p: main(["test/data/err_100.py"]) p.assert_called_once() assert "Run `refurb --explain ERR`" in p.call_args[0][0] def test_explain_flag_not_mentioned_when_quiet_flag_is_enabled(): with patch("builtins.print") as p: main(["test/data/err_100.py", "--quiet"]) p.assert_called_once() assert "Run `refurb --explain ERR`" not in p.call_args[0][0] def test_no_blank_line_printed_if_there_are_no_errors(): with patch("builtins.print") as p: main(["test/e2e/dummy.py"]) assert p.call_count == 0 def test_invalid_checks_returns_nice_message() -> None: with patch("builtins.print") as p: args = [ "test/e2e/dummy.py", "--load", "test.invalid_checks.invalid_check", ] main(args) expected = 'test/invalid_checks/invalid_check.py:13: "int" is not a valid Mypy node type' assert expected in str(p.call_args[0][0]) @pytest.mark.skipif(not os.getenv("CI"), reason="Locale installation required") def test_utf8_is_used_to_load_files_when_error_occurs() -> None: """ See issue https://github.com/dosisod/refurb/issues/37. This check will set the zh_CN.GBK locale, run a particular file, and if all goes well, no exception will be thrown. This test is only ran when the CI environment variable is set, which is set by GitHub Actions. """ setlocale(LC_ALL, "zh_CN.GBK") try: main(["test/e2e/gbk.py"]) except UnicodeDecodeError: setlocale(LC_ALL, "") raise setlocale(LC_ALL, "") def test_load_custom_config_file(): args = [ "test/data/err_101.py", "--quiet", "--config-file", "test/config/config.toml", ] errors = run_refurb(load_settings(args)) assert not errors def test_amended_ignores_are_relative_to_config_file(): os.chdir("test") args = [ "data/err_123.py", "--config-file", "config/amend_config.toml", ] errors = run_refurb(load_settings(args)) os.chdir("..") assert not errors def test_raise_error_if_config_file_is_invalid(): tests = { ".": "is a directory", "file_not_found": "was not found", } for config_file, expected in tests.items(): with pytest.raises(ValueError, match=expected): load_settings(["--config-file", config_file]) def test_mypy_args_are_forwarded() -> None: errors = run_refurb(Settings(mypy_args=["--version"])) assert len(errors) == 1 assert isinstance(errors[0], str) assert errors[0].startswith(f"mypy {metadata.version('mypy')}") def test_stub_files_dont_hide_errors() -> None: errors = run_refurb(parse_command_line_args(["test/e2e/stub_pkg"])) assert len(errors) == 1 assert "FURB123" in str(errors[0]) def test_verbose_flag_prints_all_enabled_checks() -> None: with patch("builtins.print") as p: main(["test/data/err_100.py", "--verbose", "--enable-all"]) stdout = "\n".join(args[0][0] for args in p.call_args_list) # Current number of checks at time of writing. This number doesn't need to # be kept updated, it is only set to a known value to verify that it is # doing what it should. current_check_count = 76 for error_id in range(100, 100 + current_check_count): assert f"FURB{error_id}" in stdout def test_verbose_flag_prints_message_when_all_checks_disabled() -> None: with patch("builtins.print") as p: main(["test/data/err_100.py", "--verbose", "--disable-all"]) stdout = "\n".join(args[0][0] for args in p.call_args_list) assert "FURB100" not in stdout assert "No checks enabled" in stdout def test_timing_stats_outputs_stats_file() -> None: with NamedTemporaryFile(mode="r", encoding="utf8") as tmp: main(["test/e2e/dummy.py", "--timing-stats", tmp.name]) stats_file = Path(tmp.name) assert stats_file.exists() data = json.loads(stats_file.read_text()) match data: case { "mypy_total_time_spent_in_ms": int(_), "mypy_time_spent_parsing_modules_in_ms": dict(mypy_timing), "refurb_time_spent_checking_file_in_ms": dict(refurb_timing), }: msg = "All values must be ints" assert all(isinstance(v, int) for v in mypy_timing.values()), msg assert all(isinstance(v, int) for v in refurb_timing.values()), msg return pytest.fail("Data is not in proper format") def test_color_is_enabled_by_default(): with patch("builtins.print") as p: main(["test/data/err_123.py"]) p.assert_called_once() assert "\x1b" in p.call_args[0][0] def test_no_color_printed_when_disabled(): with patch("builtins.print") as p: main(["test/data/err_123.py", "--no-color"]) p.assert_called_once() assert "\x1b" not in p.call_args[0][0] def test_error_github_actions_formatting(): with patch("builtins.print") as p: main(["test/data/err_123.py", "--format", "github"]) p.assert_called_once() assert "::error" in p.call_args[0][0] refurb-1.27.0/test/test_visitor.py000066400000000000000000000051651454672660200172000ustar00rootroot00000000000000import itertools import typing from collections.abc import Iterable import pytest from mypy.nodes import Node from refurb.settings import Settings from refurb.types import Checks from refurb.visitor import METHOD_NODE_MAPPINGS, RefurbVisitor from refurb.visitor.mapping import VisitorNodeTypeMap from .mypy_visitor import get_mypy_visitor_mapping @pytest.fixture() def dummy_visitor() -> RefurbVisitor: """ This fixture provides a RefurbVisitor instance with a visit method for each possible node, but no checks to run. This forces method generation but calling the methods does nothing. """ checks = Checks(list, {ty: [] for ty in METHOD_NODE_MAPPINGS.values()}) return RefurbVisitor(checks, Settings()) def get_visit_methods( visitor: RefurbVisitor, ) -> Iterable[tuple[str, type[Node]]]: """ Find visitor methods in the instance (those that have been generated in __init__) and in the class' __dict__ (the ones that are overridden directly in the class). Not using inspect.getmembers because that goes too deep into the parents and that would deafeat the purpose of this, which is testing that the methods are defined in the RefurbVisitor. """ method_sources = itertools.chain( [ (method_name, getattr(visitor, method_name)) for method_name in dir(visitor) if hasattr(visitor, method_name) ], visitor.__class__.__dict__.items(), ) for method_name, method in method_sources: if callable(method) and method_name.startswith("visit_"): yield method_name, method def test_visitor_generation(dummy_visitor: RefurbVisitor) -> None: """ Ensure the visitor creates all expected methods with the right types (The ones listed in refurb.visitor.METHOD_NODE_MAPPINGS). """ visitor_mappings: VisitorNodeTypeMap = {} for method_name, method in get_visit_methods(dummy_visitor): method_types = typing.get_type_hints(method) assert "o" in method_types, f"No 'o' parameter in method {method_name}" node_type = method_types["o"] visitor_mappings[method_name] = node_type assert visitor_mappings == METHOD_NODE_MAPPINGS def test_mypy_consistence() -> None: """ Ensure the visitor method name to node type mappings used in refurb are in sync with the ones of mypy. This is meant as a failsafe, especially when the mypy dependency is upgraded. If this fails, review the mappings in refurb.visitor.METHOD_NODE_MAPPINGS. """ mypy_visitor_mapping = get_mypy_visitor_mapping() assert mypy_visitor_mapping == METHOD_NODE_MAPPINGS