File: no_join.py

package info (click to toggle)
python-refurb 1.27.0-1
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 1,700 kB
  • sloc: python: 9,468; makefile: 40; sh: 6
file content (85 lines) | stat: -rw-r--r-- 2,431 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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}`")
            )