refine 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- refine/__init__.py +13 -0
- refine/__main__.py +153 -0
- refine/_version.py +3 -0
- refine/_version.pyi +1 -0
- refine/abc.py +68 -0
- refine/config.py +152 -0
- refine/exc.py +29 -0
- refine/mods/__init__.py +0 -0
- refine/mods/cli/__init__.py +0 -0
- refine/mods/cli/flags.py +142 -0
- refine/mods/sql/.sqlfluff +45 -0
- refine/mods/sql/__init__.py +0 -0
- refine/mods/sql/fmt.py +201 -0
- refine/mods/sql/utils.py +46 -0
- refine/processor.py +341 -0
- refine/registry.py +91 -0
- refine/testing.py +73 -0
- refine/utils.py +38 -0
- refine-0.10.0.dist-info/METADATA +45 -0
- refine-0.10.0.dist-info/RECORD +23 -0
- refine-0.10.0.dist-info/WHEEL +4 -0
- refine-0.10.0.dist-info/entry_points.txt +6 -0
- refine-0.10.0.dist-info/licenses/LICENSE +201 -0
refine/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
|
|
5
|
+
try:
|
|
6
|
+
from refine._version import __version__
|
|
7
|
+
except ImportError: # pragma: no cover
|
|
8
|
+
__version__ = "0.0.0.not-installed"
|
|
9
|
+
from importlib.metadata import PackageNotFoundError
|
|
10
|
+
from importlib.metadata import version
|
|
11
|
+
|
|
12
|
+
with contextlib.suppress(PackageNotFoundError):
|
|
13
|
+
__version__ = version("re-code")
|
refine/__main__.py
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Utility to simplify rewriting python code.
|
|
3
|
+
|
|
4
|
+
It can be used for one-off rewrites, or, to maintain code styling rules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import logging
|
|
11
|
+
import pathlib
|
|
12
|
+
import sys
|
|
13
|
+
from multiprocessing import freeze_support
|
|
14
|
+
from typing import NoReturn
|
|
15
|
+
|
|
16
|
+
from refine import __version__
|
|
17
|
+
from refine.config import Config
|
|
18
|
+
from refine.exc import ReCodeSystemExit
|
|
19
|
+
from refine.processor import Processor
|
|
20
|
+
from refine.registry import Registry
|
|
21
|
+
|
|
22
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stderr, format="%(message)s")
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger(__name__)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def main() -> NoReturn: # noqa: PLR0915
|
|
28
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
29
|
+
parser.add_argument("--version", action="version", version=__version__)
|
|
30
|
+
parser.add_argument("files", metavar="FILE", nargs="*", type=pathlib.Path, help="One or more files to process.")
|
|
31
|
+
parser.add_argument(
|
|
32
|
+
"--config",
|
|
33
|
+
type=pathlib.Path,
|
|
34
|
+
help="Path to config file. Defaults to '.refine.ini' on the current directory.",
|
|
35
|
+
default=".refine.toml",
|
|
36
|
+
)
|
|
37
|
+
parser.add_argument("--quiet", "-q", action="store_true", default=False, help="Quiet down the tool output.")
|
|
38
|
+
parser.add_argument(
|
|
39
|
+
"--fail-fast",
|
|
40
|
+
"--ff",
|
|
41
|
+
action="store_true",
|
|
42
|
+
default=False,
|
|
43
|
+
help="Exit as soon as possible on the first processing error",
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument(
|
|
46
|
+
"--list-codemods",
|
|
47
|
+
"--list",
|
|
48
|
+
action="store_true",
|
|
49
|
+
help="List all available codemods",
|
|
50
|
+
)
|
|
51
|
+
parser.add_argument(
|
|
52
|
+
"--exclude-codemod",
|
|
53
|
+
"--exclude",
|
|
54
|
+
default=[],
|
|
55
|
+
action="append",
|
|
56
|
+
help="Exclude codemods from available codemods.",
|
|
57
|
+
)
|
|
58
|
+
parser.add_argument(
|
|
59
|
+
"--select-codemod",
|
|
60
|
+
"--select",
|
|
61
|
+
default=[],
|
|
62
|
+
action="append",
|
|
63
|
+
help="Explicitly select codemod names from available codemods.",
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--codemods-path",
|
|
67
|
+
type=pathlib.Path,
|
|
68
|
+
action="append",
|
|
69
|
+
default=[],
|
|
70
|
+
dest="codemods_paths",
|
|
71
|
+
help="Path to a codemods directory. Can be passed multiple times.",
|
|
72
|
+
)
|
|
73
|
+
args = parser.parse_args()
|
|
74
|
+
if args.quiet:
|
|
75
|
+
logging.getLogger().setLevel(logging.ERROR)
|
|
76
|
+
if not args.config.is_absolute():
|
|
77
|
+
config_file = pathlib.Path.cwd().joinpath(args.config).resolve()
|
|
78
|
+
else:
|
|
79
|
+
config_file = args.config
|
|
80
|
+
if config_file.exists():
|
|
81
|
+
if config_file.name == "pyproject.toml":
|
|
82
|
+
config = Config.from_pyproject_file(config_file)
|
|
83
|
+
else:
|
|
84
|
+
config = Config.from_default_file(config_file)
|
|
85
|
+
elif pathlib.Path.cwd().joinpath("pyproject.toml").exists():
|
|
86
|
+
config = Config.from_pyproject_file(pathlib.Path.cwd().joinpath("pyproject.toml"))
|
|
87
|
+
else:
|
|
88
|
+
config = Config()
|
|
89
|
+
|
|
90
|
+
if args.list_codemods:
|
|
91
|
+
# Add the additional CLI passed codemod paths
|
|
92
|
+
config.codemod_paths[:] = list(set(config.codemod_paths) | set(args.codemods_paths))
|
|
93
|
+
|
|
94
|
+
if args.select_codemod:
|
|
95
|
+
# Add any additional CLI passed selections
|
|
96
|
+
config.select[:] = list(set(config.select) | set(args.select_codemod))
|
|
97
|
+
|
|
98
|
+
if args.exclude_codemod:
|
|
99
|
+
# Add any additional CLI passed exclusions
|
|
100
|
+
config.exclude[:] = list(set(config.exclude) | set(args.exclude_codemod))
|
|
101
|
+
|
|
102
|
+
if args.fail_fast:
|
|
103
|
+
config = config.model_copy(update={"fail_fast": True}, deep=True)
|
|
104
|
+
|
|
105
|
+
registry = Registry()
|
|
106
|
+
registry.load(config.codemod_paths)
|
|
107
|
+
if args.list_codemods:
|
|
108
|
+
log.info("Available codemods:")
|
|
109
|
+
for codemod in registry.codemods():
|
|
110
|
+
# In case the description is comming from the docstring, we just really want the first line.
|
|
111
|
+
description = codemod.DESCRIPTION.strip().splitlines()[0]
|
|
112
|
+
log.info(" - %s: %s", codemod.NAME, description.strip())
|
|
113
|
+
parser.exit()
|
|
114
|
+
|
|
115
|
+
paths = args.files
|
|
116
|
+
if not paths:
|
|
117
|
+
paths = config.repo_root.glob("**/*.py")
|
|
118
|
+
|
|
119
|
+
files: list[pathlib.Path] = []
|
|
120
|
+
for path in paths:
|
|
121
|
+
if path.is_file():
|
|
122
|
+
files.append(path)
|
|
123
|
+
continue
|
|
124
|
+
for subpath in path.rglob("*.py"):
|
|
125
|
+
files.append(subpath)
|
|
126
|
+
|
|
127
|
+
codemods = list(registry.codemods(select_codemods=config.select, exclude_codemods=config.exclude))
|
|
128
|
+
log.info("Selected codemods:")
|
|
129
|
+
for codemod in codemods:
|
|
130
|
+
log.info(" - %s: %s", codemod.NAME, codemod.DESCRIPTION)
|
|
131
|
+
|
|
132
|
+
processor = Processor(config=config, registry=registry, codemods=codemods)
|
|
133
|
+
try:
|
|
134
|
+
result = processor.process(files)
|
|
135
|
+
if result.failures:
|
|
136
|
+
parser.exit(status=1)
|
|
137
|
+
except ReCodeSystemExit as exc:
|
|
138
|
+
parser.exit(status=exc.code, message=exc.message)
|
|
139
|
+
except SystemExit as exc:
|
|
140
|
+
code: str | int | None = exc.code
|
|
141
|
+
if code is None:
|
|
142
|
+
parser.exit(status=1)
|
|
143
|
+
if isinstance(code, int):
|
|
144
|
+
parser.exit(status=code)
|
|
145
|
+
parser.exit(status=1, message=code)
|
|
146
|
+
except KeyboardInterrupt:
|
|
147
|
+
parser.exit(status=1)
|
|
148
|
+
parser.exit(status=0)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
if __name__ == "__main__":
|
|
152
|
+
freeze_support()
|
|
153
|
+
main()
|
refine/_version.py
ADDED
refine/_version.pyi
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__: str
|
refine/abc.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Abstract base classes for defining codemod types and their configurations.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from abc import ABC
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import ClassVar
|
|
10
|
+
from typing import Generic
|
|
11
|
+
from typing import TypeVar
|
|
12
|
+
|
|
13
|
+
from libcst.codemod import CodemodContext
|
|
14
|
+
from libcst.codemod import VisitorBasedCodemodCommand
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
from pydantic import ConfigDict
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class BaseConfig(BaseModel):
|
|
20
|
+
"""
|
|
21
|
+
Base configuration class for codemoders.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
model_config = ConfigDict(frozen=True)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
CodemodConfigType = TypeVar("CodemodConfigType", bound=BaseConfig)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BaseCodemod(VisitorBasedCodemodCommand, ABC, Generic[CodemodConfigType]):
|
|
31
|
+
"""
|
|
32
|
+
Base class for codemoders.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
NAME: ClassVar[str]
|
|
36
|
+
CONFIG_CLS: ClassVar[type[BaseConfig]]
|
|
37
|
+
PRIORITY: ClassVar[int] = 0
|
|
38
|
+
|
|
39
|
+
def __init__(self, context: CodemodContext, config: CodemodConfigType):
|
|
40
|
+
super().__init__(context)
|
|
41
|
+
self.config = config
|
|
42
|
+
self.__post_codemod_init__()
|
|
43
|
+
|
|
44
|
+
def __post_codemod_init__(self) -> None:
|
|
45
|
+
"""
|
|
46
|
+
This method can implement additional codemod initialization.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
@classmethod
|
|
50
|
+
def get_short_description(cls) -> str:
|
|
51
|
+
"""
|
|
52
|
+
Return a short description of the codemod.
|
|
53
|
+
|
|
54
|
+
This short description is used in the CLI to list available codemods and should be a single line.
|
|
55
|
+
|
|
56
|
+
By default, it returns the first line of the class docstring, override this method to provide a
|
|
57
|
+
custom description.
|
|
58
|
+
"""
|
|
59
|
+
doc = cls.__doc__
|
|
60
|
+
if cls is None:
|
|
61
|
+
error_msg = f"Codemod {cls.__name__} must have a docstring to be used in the CLI."
|
|
62
|
+
raise TypeError(error_msg)
|
|
63
|
+
if TYPE_CHECKING:
|
|
64
|
+
assert doc is not None
|
|
65
|
+
return doc.strip().splitlines()[0].strip()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
CodemodType = TypeVar("CodemodType", bound=BaseCodemod)
|
refine/config.py
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""
|
|
2
|
+
re:Code configuration.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import logging
|
|
8
|
+
import os
|
|
9
|
+
import tomllib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from pydantic import ConfigDict
|
|
15
|
+
from pydantic import Field
|
|
16
|
+
|
|
17
|
+
log = logging.getLogger(__name__)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cpu_count() -> int:
|
|
21
|
+
# os.cpu_count() can return None, let's not.
|
|
22
|
+
return os.cpu_count() or 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ConfigError(ValueError):
|
|
26
|
+
"""
|
|
27
|
+
Config related error.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ConfigLoadError(ConfigError):
|
|
32
|
+
"""
|
|
33
|
+
Exception raised when failing to load the configuration from file.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class InvalidConfigError(ConfigError):
|
|
38
|
+
"""
|
|
39
|
+
Exception raised when the loaded configuration is not valid.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class Config(BaseModel):
|
|
44
|
+
"""
|
|
45
|
+
Main codemod configuration schema.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
model_config = ConfigDict(
|
|
49
|
+
# Don't allow the config object to be mutable.
|
|
50
|
+
# Do note that mutable objects(set, list, dict) of the immutable config class remain mutable.
|
|
51
|
+
frozen=True,
|
|
52
|
+
# Allow extra keys, these will be codemods configs
|
|
53
|
+
extra="allow",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
select: list[str] = Field(
|
|
57
|
+
default_factory=list,
|
|
58
|
+
description="""
|
|
59
|
+
List of codemods to run.
|
|
60
|
+
|
|
61
|
+
When no selection is made, all available codemods are run.
|
|
62
|
+
""",
|
|
63
|
+
)
|
|
64
|
+
exclude: list[str] = Field(
|
|
65
|
+
default_factory=list,
|
|
66
|
+
description="""
|
|
67
|
+
List of codemods to exclude.
|
|
68
|
+
|
|
69
|
+
Only makes sense when `select` is empty and all codemods are run.
|
|
70
|
+
""",
|
|
71
|
+
)
|
|
72
|
+
codemod_paths: list[Path] = Field(
|
|
73
|
+
default_factory=list,
|
|
74
|
+
description="""
|
|
75
|
+
List of additional paths to search for codemods.
|
|
76
|
+
""",
|
|
77
|
+
)
|
|
78
|
+
process_pool_size: int = Field(
|
|
79
|
+
default_factory=_cpu_count,
|
|
80
|
+
description="""
|
|
81
|
+
Number of processes to use for parallel processing.
|
|
82
|
+
Defaults to the number of available CPUs.
|
|
83
|
+
""",
|
|
84
|
+
)
|
|
85
|
+
repo_root: Path = Field(
|
|
86
|
+
default_factory=Path.cwd,
|
|
87
|
+
description="""
|
|
88
|
+
The root directory of the repository.
|
|
89
|
+
Defaults to the current working directory.
|
|
90
|
+
""",
|
|
91
|
+
)
|
|
92
|
+
fail_fast: bool = Field(
|
|
93
|
+
default=False,
|
|
94
|
+
description="""
|
|
95
|
+
Stop processing as soon as possible after the first error.
|
|
96
|
+
""",
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def from_dict(cls, data: dict[str, Any]) -> Config:
|
|
101
|
+
"""
|
|
102
|
+
Load the configuration from a dictionary.
|
|
103
|
+
|
|
104
|
+
Arguments:
|
|
105
|
+
data: The configuration to load.
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Config instance.
|
|
109
|
+
"""
|
|
110
|
+
try:
|
|
111
|
+
return Config(**data)
|
|
112
|
+
except ValueError as exc:
|
|
113
|
+
error = f"Invalid configuration: {exc}"
|
|
114
|
+
raise InvalidConfigError(error) from exc
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def from_default_file(cls, path: Path) -> Config:
|
|
118
|
+
"""
|
|
119
|
+
Load the configuration from a file.
|
|
120
|
+
|
|
121
|
+
Arguments:
|
|
122
|
+
path: The path to the configuration file.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Config instance.
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
129
|
+
except tomllib.TOMLDecodeError as exc:
|
|
130
|
+
error = f"Unable to parse {path}: {exc}"
|
|
131
|
+
raise ConfigLoadError(error) from exc
|
|
132
|
+
else:
|
|
133
|
+
return cls.from_dict(data)
|
|
134
|
+
|
|
135
|
+
@classmethod
|
|
136
|
+
def from_pyproject_file(cls, path: Path) -> Config:
|
|
137
|
+
"""
|
|
138
|
+
Load the configuration from a file.
|
|
139
|
+
|
|
140
|
+
Arguments:
|
|
141
|
+
path: The path to the configuration file.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Config instance.
|
|
145
|
+
"""
|
|
146
|
+
try:
|
|
147
|
+
data = tomllib.loads(path.read_text(encoding="utf-8"))
|
|
148
|
+
except tomllib.TOMLDecodeError as exc:
|
|
149
|
+
error = f"Unable to parse {path}: {exc}"
|
|
150
|
+
raise ConfigLoadError(error) from exc
|
|
151
|
+
else:
|
|
152
|
+
return cls.from_dict(data.get("tool", {}).get("refine", {}))
|
refine/exc.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""
|
|
2
|
+
re:Code related exceptions.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ReCodeError(Exception):
|
|
11
|
+
"""
|
|
12
|
+
re:Code specific exception.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ReCodeSystemExit(SystemExit):
|
|
17
|
+
"""
|
|
18
|
+
re:Code system exit exception that accepts a message argument.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
code: int
|
|
22
|
+
message: str | None
|
|
23
|
+
|
|
24
|
+
def __init__(self, code: int, message: str | None = None):
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
assert code is not None
|
|
27
|
+
assert isinstance(code, int)
|
|
28
|
+
super().__init__(code)
|
|
29
|
+
self.message = message
|
refine/mods/__init__.py
ADDED
|
File without changes
|
|
File without changes
|
refine/mods/cli/flags.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This codemod enforces the use of dashes over underscores in CLI arguments of [ArgumentParser][argparse.ArgumentParser].
|
|
3
|
+
|
|
4
|
+
For example, it will transform this code:
|
|
5
|
+
```python
|
|
6
|
+
parser.add_argument("--a_command")
|
|
7
|
+
```
|
|
8
|
+
into this code:
|
|
9
|
+
```python
|
|
10
|
+
parser.add_argument("--a-command")
|
|
11
|
+
```
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import pathlib
|
|
18
|
+
from ast import literal_eval
|
|
19
|
+
from typing import TYPE_CHECKING
|
|
20
|
+
from typing import cast
|
|
21
|
+
|
|
22
|
+
import libcst as cst
|
|
23
|
+
import libcst.matchers as m
|
|
24
|
+
from libcst.codemod import SkipFile
|
|
25
|
+
|
|
26
|
+
from refine.abc import BaseCodemod
|
|
27
|
+
from refine.abc import BaseConfig
|
|
28
|
+
|
|
29
|
+
log = logging.getLogger(__name__)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CliDashesConfig(BaseConfig):
|
|
33
|
+
django_base_command_package: str = "django.core.management"
|
|
34
|
+
django_base_command_class_name: str = "BaseCommand"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class CliDashes(BaseCodemod):
|
|
38
|
+
"""
|
|
39
|
+
Replace `_` with `-`, ie, `--a-command` instead of `--a_command` in CLI commands.
|
|
40
|
+
|
|
41
|
+
This works if the parser argument is typed and only for ArgumentParser. `def foo(parser: ArgumentParser)`.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
NAME = "cli-dashes-over-underscores"
|
|
45
|
+
CONFIG_CLS = CliDashesConfig
|
|
46
|
+
|
|
47
|
+
def __post_codemod_init__(self) -> None:
|
|
48
|
+
"""
|
|
49
|
+
Additional class setup.
|
|
50
|
+
"""
|
|
51
|
+
self.typed_parameters: dict[str, str] = {}
|
|
52
|
+
self.typed_assignments: dict[str, str] = {}
|
|
53
|
+
|
|
54
|
+
def visit_Module(self, mod: cst.Module) -> bool:
|
|
55
|
+
filename: str | None = self.context.filename
|
|
56
|
+
if TYPE_CHECKING:
|
|
57
|
+
assert filename is not None
|
|
58
|
+
if pathlib.Path(filename).name.startswith("test_"):
|
|
59
|
+
skip_reason = "Not touching test files"
|
|
60
|
+
raise SkipFile(skip_reason)
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
def visit_FunctionDef(self, node: cst.FunctionDef) -> None:
|
|
64
|
+
# Collect type annotations of parameters
|
|
65
|
+
self.typed_parameters = {}
|
|
66
|
+
for param in node.params.params:
|
|
67
|
+
if param.annotation:
|
|
68
|
+
param_name = param.name.value
|
|
69
|
+
annotation = param.annotation.annotation
|
|
70
|
+
if isinstance(annotation, cst.Name) and annotation.value == "ArgumentParser":
|
|
71
|
+
self.typed_parameters[param_name] = annotation.value
|
|
72
|
+
|
|
73
|
+
def leave_FunctionDef(self, original: cst.FunctionDef, updated: cst.FunctionDef) -> cst.FunctionDef:
|
|
74
|
+
# Clear state when leaving the function
|
|
75
|
+
self.typed_parameters = {}
|
|
76
|
+
self.typed_assignments = {}
|
|
77
|
+
return updated
|
|
78
|
+
|
|
79
|
+
def visit_AnnAssign(self, node: cst.AnnAssign) -> None:
|
|
80
|
+
if isinstance(node.target, cst.Name):
|
|
81
|
+
var_name = node.target.value
|
|
82
|
+
annotation = node.annotation.annotation
|
|
83
|
+
if isinstance(annotation, cst.Name) and annotation.value == "ArgumentParser":
|
|
84
|
+
self.typed_assignments[var_name] = annotation.value
|
|
85
|
+
|
|
86
|
+
def visit_Assign(self, node: cst.Assign) -> None:
|
|
87
|
+
# NOTE: we're not taking into account import aliases
|
|
88
|
+
assign_target: cst.AssignTarget
|
|
89
|
+
for assign_target in node.targets:
|
|
90
|
+
if not isinstance(assign_target.target, cst.Name):
|
|
91
|
+
# We only want to handle simple assignments:
|
|
92
|
+
# var_a = value_a
|
|
93
|
+
# We're not handling assignment expansion, ie
|
|
94
|
+
# var_a, var_b = value_a, value_b
|
|
95
|
+
continue
|
|
96
|
+
target: cst.Name = cast("cst.Name", assign_target.target)
|
|
97
|
+
var_name = target.value
|
|
98
|
+
if m.matches(
|
|
99
|
+
node.value,
|
|
100
|
+
m.OneOf(
|
|
101
|
+
# blah = ArgumentParser
|
|
102
|
+
m.Call(func=m.Name("ArgumentParser")),
|
|
103
|
+
# blah = argparse.ArgumentParser
|
|
104
|
+
m.Call(func=m.Attribute(value=m.Name("argparse"), attr=m.Name("ArgumentParser"))),
|
|
105
|
+
),
|
|
106
|
+
):
|
|
107
|
+
self.typed_assignments[var_name] = "ArgumentParser"
|
|
108
|
+
|
|
109
|
+
def leave_Call(self, original: cst.Call, updated: cst.Call) -> cst.Call:
|
|
110
|
+
# Check for `<something>.add_argument(...)` calls
|
|
111
|
+
if m.matches(
|
|
112
|
+
original.func,
|
|
113
|
+
m.Attribute(attr=m.Name("add_argument"), value=m.Name()),
|
|
114
|
+
):
|
|
115
|
+
# The `<something>` in `<something>.add_argument`
|
|
116
|
+
caller_func_attribute = cast("cst.Attribute", original.func)
|
|
117
|
+
caller_func_attribute_name = cast("cst.Name", caller_func_attribute.value)
|
|
118
|
+
caller_name = caller_func_attribute_name.value
|
|
119
|
+
|
|
120
|
+
# Verify if the caller object is typed as ArgumentParser
|
|
121
|
+
if caller_name not in self.typed_parameters and caller_name not in self.typed_assignments:
|
|
122
|
+
return updated
|
|
123
|
+
|
|
124
|
+
args: list[cst.Arg] = []
|
|
125
|
+
for arg in updated.args:
|
|
126
|
+
if not isinstance(arg.value, cst.SimpleString):
|
|
127
|
+
args.append(arg)
|
|
128
|
+
continue
|
|
129
|
+
simple_string = arg.value
|
|
130
|
+
flag = literal_eval(simple_string.value)
|
|
131
|
+
if not flag.startswith("--"):
|
|
132
|
+
args.append(arg)
|
|
133
|
+
continue
|
|
134
|
+
if "_" not in flag:
|
|
135
|
+
args.append(arg)
|
|
136
|
+
continue
|
|
137
|
+
updated_flag_value = f"{simple_string.quote}{flag.replace('_', '-')}{simple_string.quote}"
|
|
138
|
+
updated_simple_string = simple_string.with_changes(value=updated_flag_value)
|
|
139
|
+
updated_arg = arg.with_changes(value=updated_simple_string)
|
|
140
|
+
args.append(updated_arg)
|
|
141
|
+
return updated.with_changes(args=args)
|
|
142
|
+
return updated
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[sqlfluff]
|
|
2
|
+
dialect = mysql
|
|
3
|
+
templater = placeholder
|
|
4
|
+
max_line_length = 120
|
|
5
|
+
processes = -1
|
|
6
|
+
|
|
7
|
+
[sqlfluff:templater:placeholder]
|
|
8
|
+
param_style = pyformat
|
|
9
|
+
|
|
10
|
+
[sqlfluff:indentation]
|
|
11
|
+
allow_implicit_indents = True
|
|
12
|
+
indented_joins = True
|
|
13
|
+
tab_space_size = 4
|
|
14
|
+
indent_unit = space
|
|
15
|
+
|
|
16
|
+
[sqlfluff:rules:aliasing.length]
|
|
17
|
+
min_alias_length = 3
|
|
18
|
+
|
|
19
|
+
# The default configuration for capitalisation rules is "consistent"
|
|
20
|
+
# which will auto-detect the setting from the rest of the file. This
|
|
21
|
+
# is less desirable in a new project and you may find this (slightly
|
|
22
|
+
# more strict) setting more useful.
|
|
23
|
+
# Typically we find users rely on syntax highlighting rather than
|
|
24
|
+
# capitalisation to distinguish between keywords and identifiers.
|
|
25
|
+
# Clearly, if your organisation has already settled on uppercase
|
|
26
|
+
# formatting for any of these syntax elements then set them to "upper".
|
|
27
|
+
# See https://stackoverflow.com/questions/608196/why-should-i-capitalize-my-sql-keywords-is-there-a-good-reason
|
|
28
|
+
[sqlfluff:rules:capitalisation.keywords]
|
|
29
|
+
capitalisation_policy = lower
|
|
30
|
+
[sqlfluff:rules:capitalisation.identifiers]
|
|
31
|
+
extended_capitalisation_policy = lower
|
|
32
|
+
[sqlfluff:rules:capitalisation.functions]
|
|
33
|
+
extended_capitalisation_policy = lower
|
|
34
|
+
[sqlfluff:rules:capitalisation.literals]
|
|
35
|
+
capitalisation_policy = lower
|
|
36
|
+
[sqlfluff:rules:capitalisation.types]
|
|
37
|
+
extended_capitalisation_policy = lower
|
|
38
|
+
|
|
39
|
+
# The default configuration for the not equal convention rule is "consistent"
|
|
40
|
+
# which will auto-detect the setting from the rest of the file. This
|
|
41
|
+
# is less desirable in a new project and you may find this (slightly
|
|
42
|
+
# more strict) setting more useful.
|
|
43
|
+
[sqlfluff:rules:convention.not_equal]
|
|
44
|
+
# Default to preferring the "c_style" (i.e. `!=`)
|
|
45
|
+
preferred_not_equal_style = c_style
|
|
File without changes
|