pax_global_header 0000666 0000000 0000000 00000000064 14546726602 0014526 g ustar 00root root 0000000 0000000 52 comment=a295cee6d188f5797aefe5d7cf77a353ed48ea93
refurb-1.27.0/ 0000775 0000000 0000000 00000000000 14546726602 0013102 5 ustar 00root root 0000000 0000000 refurb-1.27.0/.github/ 0000775 0000000 0000000 00000000000 14546726602 0014442 5 ustar 00root root 0000000 0000000 refurb-1.27.0/.github/ISSUE_TEMPLATE/ 0000775 0000000 0000000 00000000000 14546726602 0016625 5 ustar 00root root 0000000 0000000 refurb-1.27.0/.github/ISSUE_TEMPLATE/bug-report.yml 0000664 0000000 0000000 00000004427 14546726602 0021445 0 ustar 00root root 0000000 0000000 name: "🐞 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.yml 0000664 0000000 0000000 00000000033 14546726602 0020611 0 ustar 00root root 0000000 0000000 blank_issues_enabled: true
refurb-1.27.0/.github/ISSUE_TEMPLATE/enhancement.yml 0000664 0000000 0000000 00000001362 14546726602 0021637 0 ustar 00root root 0000000 0000000 name: "🚀 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.yml 0000664 0000000 0000000 00000000314 14546726602 0017270 0 ustar 00root root 0000000 0000000 version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
refurb-1.27.0/.github/workflows/ 0000775 0000000 0000000 00000000000 14546726602 0016477 5 ustar 00root root 0000000 0000000 refurb-1.27.0/.github/workflows/ci.yml 0000664 0000000 0000000 00000002122 14546726602 0017612 0 ustar 00root root 0000000 0000000 name: 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.yml 0000664 0000000 0000000 00000000634 14546726602 0020521 0 ustar 00root root 0000000 0000000 name: 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/.gitignore 0000664 0000000 0000000 00000000132 14546726602 0015066 0 ustar 00root root 0000000 0000000 .venv
__pycache__
.mypy_cache
dist
.coverage
# Used for local plugin development:
tmp.py
refurb-1.27.0/.pre-commit-hooks.yaml 0000664 0000000 0000000 00000000277 14546726602 0017247 0 ustar 00root root 0000000 0000000 - 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/LICENSE 0000664 0000000 0000000 00000104515 14546726602 0014115 0 ustar 00root root 0000000 0000000 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/Makefile 0000664 0000000 0000000 00000001367 14546726602 0014551 0 ustar 00root root 0000000 0000000 .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.md 0000664 0000000 0000000 00000035656 14546726602 0014400 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000415 14546726602 0017142 0 ustar 00root root 0000000 0000000 black==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/ 0000775 0000000 0000000 00000000000 14546726602 0014032 5 ustar 00root root 0000000 0000000 refurb-1.27.0/docs/adding-new-checks.md 0000664 0000000 0000000 00000027541 14546726602 0017640 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000011320 14546726602 0016476 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000006144 14546726602 0016521 0 ustar 00root root 0000000 0000000 # 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.md 0000664 0000000 0000000 00000114154 14546726602 0015622 0 ustar 00root root 0000000 0000000
# 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/ 0000775 0000000 0000000 00000000000 14546726602 0015462 5 ustar 00root root 0000000 0000000 refurb-1.27.0/docs/configs/README.md 0000664 0000000 0000000 00000000326 14546726602 0016742 0 ustar 00root root 0000000 0000000 # 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.toml 0000664 0000000 0000000 00000000507 14546726602 0020005 0 ustar 00root root 0000000 0000000 # 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.toml 0000664 0000000 0000000 00000001655 14546726602 0020324 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000001714 14546726602 0016500 0 ustar 00root root 0000000 0000000 import 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.toml 0000664 0000000 0000000 00000005166 14546726602 0016026 0 ustar 00root root 0000000 0000000 [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/ 0000775 0000000 0000000 00000000000 14546726602 0014367 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0016466 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/__main__.py 0000664 0000000 0000000 00000000222 14546726602 0016455 0 ustar 00root root 0000000 0000000 import 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/ 0000775 0000000 0000000 00000000000 14546726602 0015627 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0017726 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/builtin/ 0000775 0000000 0000000 00000000000 14546726602 0017275 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/builtin/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021374 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/builtin/list_extend.py 0000664 0000000 0000000 00000003472 14546726602 0022177 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002267 14546726602 0021116 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003755 14546726602 0024210 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004216 14546726602 0024042 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002135 14546726602 0023037 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004636 14546726602 0024574 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004013 14546726602 0023033 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003365 14546726602 0022503 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001153 14546726602 0021003 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003205 14546726602 0022133 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004263 14546726602 0024441 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003147 14546726602 0025537 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004067 14546726602 0022520 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003666 14546726602 0023361 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003304 14546726602 0024074 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003712 14546726602 0021313 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004012 14546726602 0022031 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000023076 14546726602 0017501 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0020002 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/contextlib/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0022101 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/contextlib/with_suppress.py 0000664 0000000 0000000 00000003617 14546726602 0023302 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017423 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/datetime/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021522 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/datetime/simplify_fromisoformat.py 0000664 0000000 0000000 00000006137 14546726602 0024607 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002721 14546726602 0024160 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017225 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/decimal/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021324 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/decimal/simplify_ctor.py 0000664 0000000 0000000 00000003736 14546726602 0022473 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0016576 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/flow/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0020675 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/flow/no_trailing_continue.py 0000664 0000000 0000000 00000004274 14546726602 0023370 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003636 14546726602 0023064 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004015 14546726602 0022163 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004227 14546726602 0022410 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017454 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/function/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021553 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/function/use_implicit_default.py 0000664 0000000 0000000 00000013111 14546726602 0024215 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017643 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/functools/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021742 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/functools/use_cache.py 0000664 0000000 0000000 00000003056 14546726602 0022140 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017241 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/hashlib/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021340 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/hashlib/simplify_ctor.py 0000664 0000000 0000000 00000004217 14546726602 0022502 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003677 14546726602 0022470 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017416 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/iterable/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021515 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/iterable/implicit_readlines.py 0000664 0000000 0000000 00000002671 14546726602 0023636 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003032 14546726602 0021605 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002016 14546726602 0023450 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017653 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/itertools/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021752 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/itertools/use_chain_from_iterable.py 0000664 0000000 0000000 00000007621 14546726602 0025063 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000005611 14546726602 0022553 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017241 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/logical/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021340 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/logical/use_equal_chain.py 0000664 0000000 0000000 00000002477 14546726602 0022752 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002567 14546726602 0021107 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001427 14546726602 0021113 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0016560 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/math/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0020657 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/math/simplify_log.py 0000664 0000000 0000000 00000002367 14546726602 0021637 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001641 14546726602 0022024 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017252 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/pathlib/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021351 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/pathlib/cwd.py 0000664 0000000 0000000 00000001310 14546726602 0020374 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002113 14546726602 0021140 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003426 14546726602 0021303 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003334 14546726602 0021241 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003252 14546726602 0020734 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002067 14546726602 0022641 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004577 14546726602 0021274 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002675 14546726602 0020577 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004234 14546726602 0021606 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002750 14546726602 0022513 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003072 14546726602 0020750 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002220 14546726602 0021120 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003164 14546726602 0022010 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000001026 14546726602 0020600 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002316 14546726602 0022165 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002747 14546726602 0022034 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0021156 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/pattern_matching/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0023255 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/pattern_matching/simplify_as_builtin.py 0000664 0000000 0000000 00000002660 14546726602 0025601 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0020120 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/readability/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0022217 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/readability/fluid_interface.py 0000664 0000000 0000000 00000011601 14546726602 0023614 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002232 14546726602 0022132 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002622 14546726602 0024354 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001254 14546726602 0023322 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002735 14546726602 0023325 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003404 14546726602 0024323 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000012106 14546726602 0022603 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000005044 14546726602 0023315 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001777 14546726602 0024532 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003177 14546726602 0024656 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000005207 14546726602 0024543 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002623 14546726602 0024150 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000005675 14546726602 0024234 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000011467 14546726602 0023512 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000005130 14546726602 0023300 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002055 14546726602 0023004 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000006757 14546726602 0023403 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003657 14546726602 0022350 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004064 14546726602 0023175 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003072 14546726602 0023533 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0016741 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/regex/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021040 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/regex/use_long_flag.py 0000664 0000000 0000000 00000002404 14546726602 0022117 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000005017 14546726602 0023207 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017277 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/secrets/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021376 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/secrets/simplify_token_function.py 0000664 0000000 0000000 00000005413 14546726602 0024615 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0016752 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/shlex/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021051 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/shlex/use_join.py 0000664 0000000 0000000 00000004004 14546726602 0021135 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0017135 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/string/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021234 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/string/charsets.py 0000664 0000000 0000000 00000004225 14546726602 0021326 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000007155 14546726602 0021650 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002555 14546726602 0022542 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003742 14546726602 0023610 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000005276 14546726602 0022576 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000004400 14546726602 0021721 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003046 14546726602 0022710 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0020160 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/third_party/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0022257 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/third_party/fastapi/ 0000775 0000000 0000000 00000000000 14546726602 0021607 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/third_party/fastapi/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0023706 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/checks/third_party/fastapi/simplify_query.py 0000664 0000000 0000000 00000004105 14546726602 0025242 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003210 14546726602 0016066 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000002252 14546726602 0016402 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000007164 14546726602 0015522 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000012312 14546726602 0016206 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000024531 14546726602 0015672 0 ustar 00root root 0000000 0000000 import 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.typed 0000664 0000000 0000000 00000000000 14546726602 0016054 0 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/settings.py 0000664 0000000 0000000 00000025632 14546726602 0016611 0 ustar 00root root 0000000 0000000 from __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.py 0000664 0000000 0000000 00000000467 14546726602 0016114 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0016066 5 ustar 00root root 0000000 0000000 refurb-1.27.0/refurb/visitor/__init__.py 0000664 0000000 0000000 00000000276 14546726602 0020204 0 ustar 00root root 0000000 0000000 from .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.py 0000664 0000000 0000000 00000010234 14546726602 0020073 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000066153 14546726602 0020470 0 ustar 00root root 0000000 0000000 # 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.py 0000664 0000000 0000000 00000003607 14546726602 0020145 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0014061 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0016160 0 ustar 00root root 0000000 0000000 refurb-1.27.0/test/config/ 0000775 0000000 0000000 00000000000 14546726602 0015326 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/config/amend_config.toml 0000664 0000000 0000000 00000000074 14546726602 0020635 0 ustar 00root root 0000000 0000000 [[tool.refurb.amend]]
path = "../data"
ignore = ["FURB123"]
refurb-1.27.0/test/config/config.toml 0000664 0000000 0000000 00000000035 14546726602 0017466 0 ustar 00root root 0000000 0000000 [tool.refurb]
ignore = [101]
refurb-1.27.0/test/conftest.py 0000664 0000000 0000000 00000000707 14546726602 0016264 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0016713 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/custom_checks/__init__.py 0000664 0000000 0000000 00000000000 14546726602 0021012 0 ustar 00root root 0000000 0000000 refurb-1.27.0/test/custom_checks/disabled_check.py 0000664 0000000 0000000 00000000542 14546726602 0022172 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000000731 14546726602 0022077 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000000571 14546726602 0021760 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001077 14546726602 0021132 0 ustar 00root root 0000000 0000000 from 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/ 0000775 0000000 0000000 00000000000 14546726602 0014772 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/data/bug_cast.py 0000664 0000000 0000000 00000000310 14546726602 0017125 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000000100 14546726602 0017311 0 ustar 00root root 0000000 0000000 test/data/bug_cast.py:7:11 [FURB123]: Replace `int(x)` with `x`
refurb-1.27.0/test/data/bug_equivalent_nodes.py 0000664 0000000 0000000 00000004352 14546726602 0021552 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000002041 14546726602 0021732 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000117130 14546726602 0021426 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000000 14546726602 0021600 0 ustar 00root root 0000000 0000000 refurb-1.27.0/test/data/bug_type_reassignment.py 0000664 0000000 0000000 00000001001 14546726602 0021731 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000115 14546726602 0022125 0 ustar 00root root 0000000 0000000 test/data/bug_type_reassignment.py:18:6 [FURB123]: Replace `str(x)` with `x`
refurb-1.27.0/test/data/err_100.py 0000664 0000000 0000000 00000000410 14546726602 0016507 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000000430 14546726602 0016700 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001707 14546726602 0016522 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001451 14546726602 0016705 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001374 14546726602 0016523 0 ustar 00root root 0000000 0000000 name = "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.txt 0000664 0000000 0000000 00000001056 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000452 14546726602 0016520 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000335 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000117 14546726602 0016517 0 ustar 00root root 0000000 0000000 import os
from os import getcwd
a = getcwd()
b = os.getcwd()
c = os.getcwdb()
refurb-1.27.0/test/data/err_104.txt 0000664 0000000 0000000 00000000345 14546726602 0016711 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000146 14546726602 0016522 0 ustar 00root root 0000000 0000000 # this will match
print("")
# these will not
print()
print("abc")
print("", "")
print("", end="")
refurb-1.27.0/test/data/err_105.txt 0000664 0000000 0000000 00000000107 14546726602 0016706 0 ustar 00root root 0000000 0000000 test/data/err_105.py:3:1 [FURB105]: Replace `print("")` with `print()`
refurb-1.27.0/test/data/err_106.py 0000664 0000000 0000000 00000001314 14546726602 0016521 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001751 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001165 14546726602 0016526 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001220 14546726602 0016705 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000700 14546726602 0016521 0 ustar 00root root 0000000 0000000 x = 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.txt 0000664 0000000 0000000 00000001354 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000574 14546726602 0016533 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001073 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000333 14546726602 0016514 0 ustar 00root root 0000000 0000000 x = 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.txt 0000664 0000000 0000000 00000000454 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000623 14546726602 0016517 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001015 14546726602 0016702 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000405 14546726602 0016516 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001101 14546726602 0016677 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001226 14546726602 0016521 0 ustar 00root root 0000000 0000000 nums = []
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.txt 0000664 0000000 0000000 00000001127 14546726602 0016710 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000237 14546726602 0016523 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000216 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000003442 14546726602 0016525 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000005074 14546726602 0016717 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000210 14546726602 0016514 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000347 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000664 14546726602 0016532 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000001030 14546726602 0016705 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001242 14546726602 0016524 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000004537 14546726602 0016725 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000533 14546726602 0016527 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001051 14546726602 0016712 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002620 14546726602 0016516 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000006003 14546726602 0016704 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000642 14546726602 0016521 0 ustar 00root root 0000000 0000000 num = 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.txt 0000664 0000000 0000000 00000000520 14546726602 0016703 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000730 14546726602 0016520 0 ustar 00root root 0000000 0000000 lines = ["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.txt 0000664 0000000 0000000 00000000500 14546726602 0016702 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001141 14546726602 0016516 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000002120 14546726602 0016703 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000727 14546726602 0016530 0 ustar 00root root 0000000 0000000 x = 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.txt 0000664 0000000 0000000 00000001627 14546726602 0016717 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000003143 14546726602 0016524 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001062 14546726602 0016711 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002517 14546726602 0016531 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000500 14546726602 0016706 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001004 14546726602 0016520 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000000277 14546726602 0016722 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001210 14546726602 0016520 0 ustar 00root root 0000000 0000000 # `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.txt 0000664 0000000 0000000 00000001002 14546726602 0016706 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001426 14546726602 0016532 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000541 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000537 14546726602 0016524 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000335 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000234 14546726602 0016517 0 ustar 00root root 0000000 0000000 names = {"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.txt 0000664 0000000 0000000 00000000110 14546726602 0016677 0 ustar 00root root 0000000 0000000 test/data/err_131.py:6:1 [FURB131]: Replace `del x[:]` with `x.clear()`
refurb-1.27.0/test/data/err_132.py 0000664 0000000 0000000 00000000641 14546726602 0016522 0 ustar 00root root 0000000 0000000 s = 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.txt 0000664 0000000 0000000 00000000131 14546726602 0016703 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002146 14546726602 0016525 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000704 14546726602 0016712 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000630 14546726602 0016522 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000000277 14546726602 0016720 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002277 14546726602 0016534 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000002013 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000412 14546726602 0016522 0 ustar 00root root 0000000 0000000 x = 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.txt 0000664 0000000 0000000 00000001220 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001335 14546726602 0016530 0 ustar 00root root 0000000 0000000 nums = [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.txt 0000664 0000000 0000000 00000001131 14546726602 0016711 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002354 14546726602 0016533 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000433 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001045 14546726602 0016530 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001553 14546726602 0016723 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000710 14546726602 0016516 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000425 14546726602 0016710 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000374 14546726602 0016525 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000000526 14546726602 0016713 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000551 14546726602 0016523 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000441 14546726602 0016710 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001150 14546726602 0016520 0 ustar 00root root 0000000 0000000 s = 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.txt 0000664 0000000 0000000 00000001217 14546726602 0016713 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000464 14546726602 0016530 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000000752 14546726602 0016717 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000536 14546726602 0016531 0 ustar 00root root 0000000 0000000 nums = [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.txt 0000664 0000000 0000000 00000000311 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002025 14546726602 0016525 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000004160 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001471 14546726602 0016532 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000003302 14546726602 0016714 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002030 14546726602 0016523 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000002141 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000645 14546726602 0016536 0 ustar 00root root 0000000 0000000 b = 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.txt 0000664 0000000 0000000 00000001471 14546726602 0016723 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001152 14546726602 0016520 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000002612 14546726602 0016711 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000610 14546726602 0016517 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000000545 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000326 14546726602 0016524 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001342 14546726602 0016712 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000511 14546726602 0016521 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000001144 14546726602 0016713 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001604 14546726602 0016526 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001735 14546726602 0016722 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001205 14546726602 0016524 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000003135 14546726602 0016717 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000576 14546726602 0016537 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001645 14546726602 0016724 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001074 14546726602 0016532 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000002643 14546726602 0016724 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001063 14546726602 0016531 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001510 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001535 14546726602 0016536 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000002311 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000124 14546726602 0016517 0 ustar 00root root 0000000 0000000 x = 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.txt 0000664 0000000 0000000 00000000126 14546726602 0016710 0 ustar 00root root 0000000 0000000 test/data/err_160.py:5:1 [FURB160]: Remove redundant assignment of variable to itself
refurb-1.27.0/test/data/err_163.py 0000664 0000000 0000000 00000000513 14546726602 0016524 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000001236 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000751 14546726602 0016531 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000001003 14546726602 0016707 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000647 14546726602 0016536 0 ustar 00root root 0000000 0000000 from __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.txt 0000664 0000000 0000000 00000000671 14546726602 0016722 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000563 14546726602 0016534 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000616 14546726602 0016722 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000323 14546726602 0016527 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000001167 14546726602 0016725 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000701 14546726602 0016530 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001431 14546726602 0016720 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000346 14546726602 0016536 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000540 14546726602 0016721 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002760 14546726602 0016530 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000002452 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000275 14546726602 0016530 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000436 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000553 14546726602 0016530 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000000300 14546726602 0016705 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002264 14546726602 0016532 0 ustar 00root root 0000000 0000000 x = {"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.txt 0000664 0000000 0000000 00000003511 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000704 14546726602 0016530 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000001457 14546726602 0016725 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000522 14546726602 0016527 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000000607 14546726602 0016722 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000527 14546726602 0016535 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000000610 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000530 14546726602 0016530 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000000511 14546726602 0016716 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001331 14546726602 0016531 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000001566 14546726602 0016732 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000004547 14546726602 0016546 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000002406 14546726602 0016725 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000634 14546726602 0016527 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000000242 14546726602 0016711 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001730 14546726602 0016526 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000003452 14546726602 0016720 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000002152 14546726602 0016526 0 ustar 00root root 0000000 0000000 import 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.txt 0000664 0000000 0000000 00000003421 14546726602 0016715 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000155 14546726602 0016530 0 ustar 00root root 0000000 0000000 x = " "
# 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.txt 0000664 0000000 0000000 00000000212 14546726602 0016711 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000006560 14546726602 0016537 0 ustar 00root root 0000000 0000000 class 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.txt 0000664 0000000 0000000 00000001020 14546726602 0016710 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000376 14546726602 0016537 0 ustar 00root root 0000000 0000000 x = {}
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.txt 0000664 0000000 0000000 00000000605 14546726602 0016721 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000606 14546726602 0016534 0 ustar 00root root 0000000 0000000 l = []
# 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.txt 0000664 0000000 0000000 00000000666 14546726602 0016731 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000001133 14546726602 0020525 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000000761 14546726602 0020722 0 ustar 00root root 0000000 0000000 test/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.py 0000664 0000000 0000000 00000000604 14546726602 0016767 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000000215 14546726602 0017154 0 ustar 00root root 0000000 0000000 test/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/ 0000775 0000000 0000000 00000000000 14546726602 0015433 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/data_3.10/err_161.py 0000664 0000000 0000000 00000000714 14546726602 0017166 0 ustar 00root root 0000000 0000000 # 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.txt 0000664 0000000 0000000 00000001200 14546726602 0017344 0 ustar 00root root 0000000 0000000 test/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/ 0000775 0000000 0000000 00000000000 14546726602 0015434 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/data_3.11/err_162.py 0000664 0000000 0000000 00000001663 14546726602 0017174 0 ustar 00root root 0000000 0000000 from 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.txt 0000664 0000000 0000000 00000002163 14546726602 0017357 0 ustar 00root root 0000000 0000000 test/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/ 0000775 0000000 0000000 00000000000 14546726602 0015363 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/data_3.9/err_121.py 0000664 0000000 0000000 00000000245 14546726602 0017111 0 ustar 00root root 0000000 0000000 num = 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.txt 0000664 0000000 0000000 00000000165 14546726602 0017301 0 ustar 00root root 0000000 0000000 test/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/ 0000775 0000000 0000000 00000000000 14546726602 0014534 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/e2e/custom_check.py 0000664 0000000 0000000 00000000025 14546726602 0017552 0 ustar 00root root 0000000 0000000 print("hello world")
refurb-1.27.0/test/e2e/dummy.py 0000664 0000000 0000000 00000000153 14546726602 0016240 0 ustar 00root root 0000000 0000000 """
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/ 0000775 0000000 0000000 00000000000 14546726602 0017345 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/e2e/empty_package/.gitkeep 0000664 0000000 0000000 00000000000 14546726602 0020764 0 ustar 00root root 0000000 0000000 refurb-1.27.0/test/e2e/gbk.py 0000664 0000000 0000000 00000000040 14546726602 0015643 0 ustar 00root root 0000000 0000000 print("")
print("一些中文")
refurb-1.27.0/test/e2e/stub_pkg/ 0000775 0000000 0000000 00000000000 14546726602 0016352 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/e2e/stub_pkg/file.py 0000664 0000000 0000000 00000000134 14546726602 0017641 0 ustar 00root root 0000000 0000000 class C:
x: int
def __init__(self) -> None:
self.x = int(0) # noqa: UP018
refurb-1.27.0/test/e2e/stub_pkg/file.pyi 0000664 0000000 0000000 00000000071 14546726602 0020012 0 ustar 00root root 0000000 0000000 class C:
x: int
def __init__(self) -> None: ...
refurb-1.27.0/test/invalid_checks/ 0000775 0000000 0000000 00000000000 14546726602 0017027 5 ustar 00root root 0000000 0000000 refurb-1.27.0/test/invalid_checks/invalid_check.py 0000664 0000000 0000000 00000000352 14546726602 0022164 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000014514 14546726602 0017215 0 ustar 00root root 0000000 0000000 """
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.py 0000664 0000000 0000000 00000042312 14546726602 0017770 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000004646 14546726602 0021013 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000015613 14546726602 0016740 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000002074 14546726602 0017135 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001346 14546726602 0016247 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000001463 14546726602 0021375 0 ustar 00root root 0000000 0000000 from 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.py 0000664 0000000 0000000 00000003260 14546726602 0016741 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000023106 14546726602 0016420 0 ustar 00root root 0000000 0000000 import 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.py 0000664 0000000 0000000 00000005165 14546726602 0017200 0 ustar 00root root 0000000 0000000 import 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