Releases: dosisod/refurb
Version 1.11.1
This minor version fixes a typo in FURB156 (use-string-charsets
) which incorrectly said to use the strings
module instead of the string
module.
What's Changed
New Contributors
Full Changelog: v1.11.0...v1.11.1
v1.11.0
This version includes a couple of new checks, including a new check from first-time contributor @doomedraven!
Add simplify-global-and-nonlocal
check (FURB154)
The global
and nonlocal
statements can take multiple variables, meaning you don't need to have a separate line for each variable:
def f():
nonlocal x
nonlocal y
...
The above code can be written like this instead:
def f():
nonlocal x, y
...
Add use-pathlib-stat
check (FURB155)
This check suggests that you use the pathlib
module for doing stat()
file operations, such as getting the file size, date modified, etc.
For example, the following code:
if os.path.getsize("file.txt"):
pass
Can be written like this:
if Path("file.txt").stat().st_size:
pass
Add use-string-charsets
check (FURB156)
The string
library offers some common charsets such as digits, upper/lower case alpha characters, and more. This improves readability, and cuts down on line length as well.
For example:
digits = "0123456789"
if c in digits:
pass
if c in "0123456789abcdefABCDEF":
pass
The above code can be written like this using the string
module:
if c in string.digits:
pass
if c in string.hexdigits:
pass
Add simplify-decimal-ctor
check (FURB157)
The Decimal()
constructor can take many different types such as str
, int
, and float
. Often times you can simplify the construction of Decimal
objects, especially when using plain literals such as "0"
.
if x == Decimal("0"):
pass
if y == Decimal(float("Infinity")):
pass
Good:
if x == Decimal(0):
pass
if y == Decimal("Infinity"):
pass
What's Changed
- Add more info to the explaination output by @dosisod in #185
- Add "simplify-global-and-nonlocal" check by @dosisod in #186
- Fix FURB123 (no-redundant-cast) suggesting to use
.copy()
for literals by @dosisod in #188 - replace os.path.getsize with Path("file.txt").stat().st_size by @doomedraven in #189
- Add
normalize_os_path
helper function by @dosisod in #193 - Add
use-string-charsets
check by @dosisod in #194 - Add
simplify-decimal-ctor
check by @dosisod in #195 - Bump packages, bump version to 1.11.0 by @dosisod in #196
New Contributors
- @doomedraven made their first contribution in #189
Full Changelog: v1.10.0...v1.11.0
Version 1.10.0
Add ability to ignore checks on a per-file/folder basis
You can now ignore errors for a given path! Just add the following to your pyproject.toml
:
[[tool.refurb.amend]]
path = "your/path/here" # <-- can be a file or folder
ignore = ["FURB123"]
Read the docs for more info on how to use this new feature!
Add use-pathlib-mkdir
check (FURB150)
Use the mkdir
function from the pathlib
library instead of using the mkdir
and makedirs
functions from the os
library.
The following code:
import os
os.mkdir("new_folder")
Can be written like:
from pathlib import Path
Path("new_folder").mkdir()
Add use-pathlib-touch
check (FURB151)
Don't use open(x, "w").close()
if you just want to create an empty file, use the less confusing Path.touch()
method instead.
Note that this check is disabled by default (expand this for a full explanation).
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.
For example, this code:
open("file.txt", "w").close()
Can be written like this:
from pathlib import Path
Path("file.txt").touch()
Add use-math-constant
check (FURB152)
Don't hardcode math constants like pi, tau, or e, use the math.pi
, math.tau
, or math.e
constants respectively instead.
For example, this code:
def area(r: float) -> float:
return 3.1415 * r * r
Should be written like this:
import math
def area(r: float) -> float:
return math.pi * r * r
Add simplify-path-constructor
check (FURB153)
The Path()
constructor defaults to the current directory, so don't pass the current directory ("."
) explicitly.
This:
file = Path(".")
Can be written like this:
file = Path()
Add names to checks
All checks now contain a name
field which makes them easier to refer to. Although the name isn't actually used for anything, you will eventually be able to use the name field as a replacement for FURBxxx
on the CLI, in config files, and in noqa
comments. In addition, the name (along with the categories) will be printed at the top of the "explainer" for the check when using --explain
.
What's Changed
- Add
--namespace-packages
flag to mypy by @dosisod in #170 - Add ability to parse path expressions resulting from
/
operator by @dosisod in #171 - Add "pathlib touch" check by @dosisod in #172
- Add missing categories to checks by @dosisod in #175
- Add "no math constants" check by @dosisod in #176
- Add "no explicit cwd" check by @dosisod in #178
- Add
name
field to checks by @dosisod in #179 - Add ability to ignore errors on a per file/folder basis by @dosisod in #183
- Fix integers not working in amend sections, bump packages by @dosisod in #184
Full Changelog: v1.9.1...v1.10.0
Version 1.9.1
This release contains a couple bug fixes:
Folders without an __init__.py
or py.typed
file no longer cause errors
Mypy requires an __init__.py
file to tell it where the root of the project is, which can cause issues if your project doesn't have any of these files. A flag has been added when calling Mypy which fixes this.
Fix "list extend" (FURB113) false positive
Previously this check did not ensure that the .append()
function was called on a list
, meaning the following code would cause an error:
class Queue:
_queue: list
def __init__(self) -> None:
self._queue = []
def append(self, item) -> None:
self._queue.append(item)
q = Queue()
q.append(1) # Use `x.extend(...)` instead of repeatedly calling `x.append()`
q.append(2)
This has now been fixed.
What's Changed
- Add makefile recepie to update
.txt
file output by @dosisod in #164 - Fix duplicate module error when running by @dosisod in #165
- Fix FURB113 emitting error on any
append()
function call by @dosisod in #166 - Bump packages, make bug fix release by @dosisod in #167
Full Changelog: v1.9.0...v1.9.1
Version 1.9.0
This release includes 10 new checks, general bug fixes, and some quality-of-life improvements as well!
Add "use starmap" check (FURB140)
Often times you want to iterate over some zip()
'd values, unpack them, and pass each of the values to function. The starmap
function gives you a faster and more succinct way to do just that.
The following code:
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)
)
Can be re-written as follows:
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)))
Add "pathlib exists" check (FURB141)
When checking whether a file exists or not, try and use the more modern pathlib
module instead of os.path
.
This:
import os
if os.path.exists("filename"):
pass
Can be written like this:
from pathlib import Path
if Path("filename").exists():
pass
Add "no set op in for loop" check (FURB142)
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.
For example, this code:
sentence = "hello world"
vowels = "aeiou"
letters = set(sentence)
for vowel in vowels:
letters.discard(vowel)
Can be rewritten like so:
sentence = "hello world"
vowels = "aeiou"
letters = set(sentence)
letters.difference_update(vowels)
# or
letters -= set(vowels)
Add "no or default" check (FURB143)
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.
This:
def is_markdown_header(line: str) -> bool:
return (line or "").startswith("#")
Can be written like so:
def is_markdown_header(line: str) -> bool:
return line.startswith("#")
Add "pathlib unlink" check (FURB144)
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.
This:
import os
os.remove("filename")
Can be written as:
from pathlib import Path
Path("filename").unlink()
Add "no slice copy" check (FURB145)
Don't use a slice expression (with no bounds) to make a copy of something, use the more readable .copy()
method instead.
For example, this:
nums = [3.1415, 1234]
copy = nums[:]
Can be rewritten as:
nums = [3.1415, 1234]
copy = nums.copy()
Add "pathlib is file" check (FURB146)
Don't use the os.path.isfile
(or similar) functions, use the more modern pathlib
module instead:
if os.path.isfile("file.txt"):
pass
Can be rewritten as:
if Path("file.txt").is_file():
pass
Add "pathlib join" check (FURB147)
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!")
Add "no ignored enumerate" check (FURB148)
Don't use enumerate()
if you are disregarding either the index or the value:
books = ["Ender's Game", "The Black Swan"]
for index, _ in enumerate(books):
print(index)
for _, book in enumerate(books):
print(book)
This instead should be written as:
books = ["Ender's Game", "The Black Swan"]
for index in range(len(books)):
print(index)
for book in books:
print(book)
Add "no is bool compare" check (FURB149)
Don't use is
or ==
to check if a boolean is True or False, simply use the name itself:
failed = True
if failed is True:
print("You failed")
Should be written as:
failed = True
if failed:
print("You failed")
Allow for comma-separated ignore
, enable
, and disable
CLI flags
Previously, if you wanted to enable/disable/ignore multiple checks at once, you would have to write:
$ refurb src --disable FURB123 --disable FURB124 --disable FURB125
Now you write it like this instead:
$ refurb src --disable FURB123,FURB124,FURB125
What's Changed
- Add "use starmap" check by @dosisod in #137
- Add ability to separate
ignore
,enable
, anddisable
CLI flags with commas by @dosisod in #138 - Add "pathlib exists" check by @dosisod in #139
- Add "no set in for loop" check by @dosisod in #140
- Fix
.pyi
files taking precedence over.py
files by @dosisod in #141 - Add "no or default" check by @dosisod in #142
- Add "pathlib unlink" check by @dosisod in #143
- Add "no slice copy" check by @dosisod in #145
- Add "is file" pathlib check by @dosisod in #147
- Add
noqa
comments to test files by @dosisod in #148 - Add "pathlib join" check by @dosisod in #149
- Colorize CI Output by @dosisod in #150
- Fix FURB135 false positives by @dosisod in #153
- Add "no ignored enumerate" check by @dosisod in #154
- Fix typo in no_unecessary_cast explanation by @jaoxford in #156
- Fix FURB145 emitting error for non-slice index expressions by @dosisod in #157
- Bump packages by @dosisod in #158
- Add
is
operator to FURB124 (use equal chain) by @dosisod in #159 - Add "no is bool compare" check by @dosisod in #160
- Update FURB149 to include
==
and!=
by @dosisod in #161
New Contributors
Full Changelog: v1.8.0...v1.9.0
Version 1.8.0
This release includes some bug fixes, new checks, as well as the ability to enable/disable checks based on category!
Categories
Checks can now have a categories
field where they specify what categories they fit into. On the command line, you can use --enable "#category"
to enable all checks in that category. The same applies for ignore
and disable
. This feature is also available in config files. See the docs for more info.
Add "simplify comprehension" Check (FURB137)
This check recommends that you simplify your usage of comprehensions by:
- Removing nested layers of comprehensions (ie,
list([...])
- Write list/set comprehensions using shorthand notation
Here are the examples from the explainer:
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])
And the suggested improvements:
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)
Add "use comprehensions" check (FURB138)
This check will find instances where you append a bunch of elements to a new list, and suggest that you use a list comprehension instead. This can result in higher performance, and in some cases can greatly improve readability.
Example from the explainer:
nums = [1, 2, 3, 4]
odds = []
for num in nums:
if num % 2:
odds.append(num)
Suggested improvement:
nums = [1, 2, 3, 4]
odds = [num for num in nums if num % 2]
Add "no multi-line lstrip()" Check (FURB139)
This check will find multi-line strings that can be written without the use of the lstrip()
function:
"""
Some docstring
""".lstrip()
Suggested improvement:
"""\
Some docstring
"""
What's Changed
- Add ability to enable/disable errors based on category by @dosisod in #123
- Update error messages for FURB108 and FURB124: by @dosisod in #124
- Add enhancement template by @dosisod in #125
- Add "simplify comprehension" check by @dosisod in #126
- Add "use comprehensions" check by @dosisod in #127
- Bump packages by @dosisod in #128
- Fix set comprehensions being suggested to be removed for list/tuple constructions by @dosisod in #131
- Add ability to detect comparison to empty list/dict in FURB115 by @dosisod in #132
- Add more nodes to the
is_equivalent
function by @dosisod in #134 - Add "no multiline lstrip" check by @dosisod in #135
- Bump packages and version by @dosisod in #136
Full Changelog: v1.7.0...v1.8.0
Version 1.7.0
This version includes a bug fix which has been causing some issues, as well as a new flag and a new check.
The "use min/max" check (FURB136)
This check will detect uses of ternary statements (inline-if expressions) which can be written with the builtin min()
/max()
functions instead. For example:
score1 = 90
score2 = 99
highest_score = score1 if score1 > score2 else score2
Refurb will suggest you write this as:
score1 = 90
score2 = 99
highest_score = max(score1, score2)
Mypy Flags
You can now add Mypy flags directly from the Refurb command line like so:
$ refurb files -- --pdb --show-traceback
The --pdb
and --show-traceback
flags will get forwarded to Mypy. This is primarily for development purposes, but can be useful to normal users as well (especially those who already use Mypy). See the docs for more info.
What's Changed
- Add
format()
function to FURB119 by @dosisod in #110 - Add "use min/max" check by @dosisod in #111
- Fix name of member expressions not being checked by @dosisod in #115
- Add ability to pass flags directly into Mypy by @dosisod in #118
- Bump version, update issue template by @dosisod in #120
Full Changelog: v1.6.0...v1.7.0
Version 1.6.0
This version primarily just fixes bugs, but also includes a new check.
FURB106, the "expandtabs" check, is disabled by default now
Turns out .replace("\t", 8 * " ")
is not the same as .expandtabs()
: the replace
version will replace every tab with exactly 8 spaces, but the expandtabs
version will replace each tab with spaces up to the nearest tab stop. If your tabs are only at the start of a string, this works as expected! But, if they are in the middle of the string, this behavior might not be what you want. For this reason, it has been disabled by default. Read the docs for more information on how to enable checks.
Add the "no ignored dict items" check (FURB135)
The items()
method on dict
objects allow you to iterate over all the key-value pairs in a dictionary. If you only need the key or value, but not both, you shouldn't use items()
, but instead use the specific values()
and keys()
methods. For example, Refurb will suggest that this:
books = {"Frank Herbert": "Dune"}
for author, _ in books.items():
print(author)
for _, book in books.items():
print(book)
Should be changed to this:
books = {"Frank Herbert": "Dune"}
for author in books:
print(author)
for book in books.values():
print(book)
What's Changed
- Add "no ignored dict items" check by @dosisod in #95
- Improve and disable the
expandtabs()
check by @dosisod in #96 - Add better node equivalence checks: by @dosisod in #98
- Bump packages by @dosisod in #105
- Add more strict type checking for
check
functions andError
subclasses by @dosisod in #106 - Update and improve error messages by @dosisod in #107
- Also mention another type checker Pytype. by @yilei in #108
- Add categories to checks by @dosisod in #109
New Contributors
Full Changelog: v1.5.0...v1.6.0
Version 1.5.0
Since the last release, many flags and checks have been added, and of course, lots of bugs have been fixed as well!
FURB120 is now disabled by default
Due to popular demand, FURB120 has been disabled by default. If you wish to continue using it, you will need to enable it.
The --enable-all
and --disable-all
flags
Two new flags have been added, --enable-all
and --disable-all
. As the names imply, they will enable or disable all checks. The "enable all" option is good for new codebases which want to opt-in to all available checks, whereas "disable all" can be good for existing codebases which need to be incrementally cleaned up.
Read the docs for more info.
The --python-version
flag
This flag can be used to specify what version of Python your codebase is using. This allows for Mypy to better type check your code, and also allows for more useful error messages. When specifying this flag, it must be in the form X.Y
, such as 3.9
or 3.10
.
The "no trailing continue" check
Similar to the "no trailing return" check, this check applies to instances where the continue
keyword is not needed:
for num in range(10):
print(num)
continue
Here, the continue
is not needed, because we are already at the end of the for loop.
The "use @cache decorator" check
Python 3.9 introduced the @cache
decorator, which is a shorthand for @lru_cache(maxsize=None)
. Refurb will now suggest that you change this:
from functools import lru_cache
@lru_cache(maxsize=None)
def f(x: int) -> int:
return x + 1
To this:
from functools import cache
@cache
def f(x: int) -> int:
return x + 1
If you are using Python 3.9+.
What's Changed
- Detect
Path
objects being passed directly toopen()
by @dosisod in #75 - Add
--disable-all
flag by @dosisod in #76 - Fix disabled checks still being loaded by @dosisod in #80
- Bump/cleanup packages by @dosisod in #81
- Fix false positive in FURB125 by @dosisod in #83
- add Python 3.11 to test matrix by @jairhenrique in #84
- Add
--python-version
flag by @dosisod in #85 - Fix
--load
being able to load a single check multiple times by @dosisod in #86 - Allow for dependency injecting settings into checks: by @dosisod in #87
- Add "no trailing continue" check by @dosisod in #88
- Disable FURB120 by default by @dosisod in #89
- Add
--enable-all
flag by @dosisod in #91 - Fix
--enable-all
not enabling checks in all situations by @dosisod in #92 - Add "use @cache decorator" check for Python 3.9 and up by @dosisod in #93
- Add comparison to other tools by @dosisod in #94
New Contributors
- @jairhenrique made their first contribution in #84
Full Changelog: v1.4.0...v1.5.0
Version 1.4.0
This version includes a bunch of bug fixes, some new flags, and a new check!
(Small) Breaking Change
Previously, the command line arguments overrode any and all settings in the config file, which probably isn't what most users would expect. Now, the command line arguments will merge with the existing config file settings (if there are any), with the command line arguments taking precedence.
The --config-file
flag
This flag can be used to specify where the config file should be pulled from. The file can be named anything, but it must be in TOML format, and the settings must still be in the [tool.refurb]
section. See #62 for more info.
The --disable
flag
To complement the --enable
flag (added in #41), the --disable
flag has been added. This should give users more control over how checks are loaded. See the README for more info.
The "set discard" check
When removing keys from a set()
, you don't need to check if it exists before removing it:
nums = set((1, 2, 3))
if 1 in nums:
nums.remove(1)
Refurb will suggests you do this instead:
nums = set((1, 2, 3))
nums.discard(1)
What's Changed
- Fix operator precedence not being upheld by @dosisod in #54
- Fix bugs in the "with suppress" check by @dosisod in #55
- Use mypy compatible method for conditional import by @michaeloliverx in #39
- Partially fix variable type related issues by @dosisod in #58
- Add more developer documentation by @dosisod in #61
- Add
--config-file
flag by @dosisod in #62 - Make command line arguments merge with config file instead of overriding it by @dosisod in #63
- Fix "use func name" check allowing non-positional arguments by @dosisod in #65
- Fix CI workflow running twice when pushing PR by @dosisod in #66
- Add bug report issue template by @dosisod in #67
- Show error message when error code doesn't have a docstring by @dosisod in #68
- Improve
refurb gen
functionality: by @dosisod in #69 - Improve "no len compare" check by @dosisod in #71
- Add ability to disable checks by @dosisod in #73
- Test generated visitor methods by @jdevera in #60
- Add "set discard" check by @dosisod in #74
New Contributors
Full Changelog: v1.3.0...v1.4.0