Skip to content

Releases: dosisod/refurb

Version 1.11.1

07 Feb 04:54
Compare
Choose a tag to compare

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

06 Feb 05:14
2949f72
Compare
Choose a tag to compare

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

Full Changelog: v1.10.0...v1.11.0

Version 1.10.0

11 Jan 04:26
8d3e76a
Compare
Choose a tag to compare

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

Full Changelog: v1.9.1...v1.10.0

Version 1.9.1

25 Dec 23:31
6977854
Compare
Choose a tag to compare

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

19 Dec 04:21
2e90c3c
Compare
Choose a tag to compare

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

New Contributors

Full Changelog: v1.8.0...v1.9.0

Version 1.8.0

27 Nov 05:32
5749446
Compare
Choose a tag to compare

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

14 Nov 03:31
06879c2
Compare
Choose a tag to compare

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

Full Changelog: v1.6.0...v1.7.0

Version 1.6.0

11 Nov 19:55
74bec8d
Compare
Choose a tag to compare

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 and Error 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

05 Nov 00:00
369f32e
Compare
Choose a tag to compare

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

New Contributors

Full Changelog: v1.4.0...v1.5.0

Version 1.4.0

17 Oct 03:55
a4d13c7
Compare
Choose a tag to compare

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