Skip to content

Commit

Permalink
feat: #256 Automatically pick up config options from setup.cfg if it …
Browse files Browse the repository at this point in the history
…is present in the project root (#279)
  • Loading branch information
hakancelikdev committed Feb 3, 2023
1 parent 264eb3e commit 5fd5e49
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 119 deletions.
9 changes: 9 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.

## [Unreleased] - YYYY-MM-DD

## [x.y.z] - YYYY-MM-DD

### Added

- Automatically pick up config options from setup.cfg if it is present in the project
root else check and if it exists use pyproject.toml. #256

If you want you can disable this feature by passing `--disable-auto-discovery-config`

## [0.13.0] - 2023-02-01

### Changed
Expand Down
72 changes: 41 additions & 31 deletions docs/tutorial/command-line-options.md
Original file line number Diff line number Diff line change
@@ -1,37 +1,36 @@
You can list many options by running unimport --help

```bash
usage: unimport [-h] [--color {auto,always,never}] [--check] [-c PATH]
[--include include] [--exclude exclude] [--gitignore]
[--ignore-init] [--include-star-import] [-d] [-r | -p] [-v]
[sources [sources ...]]

A linter, formatter for finding and removing unused import statements.

positional arguments:
sources Files and folders to find the unused imports.

optional arguments:
-h, --help show this help message and exit
--color {auto,always,never}
Select whether to use color in the output. Defaults to
`auto`.
--check Prints which file the unused imports are in.
-c PATH, --config PATH
Read configuration from PATH.
--include include File include pattern.
--exclude exclude File exclude pattern.
--gitignore Exclude .gitignore patterns. if present.
--ignore-init Ignore the __init__.py file.
--include-star-import
Include star imports during scanning and refactor.
-d, --diff Prints a diff of all the changes unimport would make
to a file.
-r, --remove Remove unused imports automatically.
-p, --permission Refactor permission after see diff.
-v, --version Prints version of unimport

Get rid of all unused imports 🥳
usage: unimport [-h] [--color {auto,always,never}] [--check] [-c PATH] [--disable-auto-discovery-config] [--include include] [--exclude exclude] [--gitignore] [--ignore-init]
[--include-star-import] [-d] [-r | -p] [-v]
[sources ...]

A linter, formatter for finding and removing unused import statements.

positional arguments:
sources Files and folders to find the unused imports.

options:
-h, --help show this help message and exit
--color {auto,always,never}
Select whether to use color in the output. Defaults to `auto`.
--check Prints which file the unused imports are in.
-c PATH, --config PATH
Read configuration from PATH.
--disable-auto-discovery-config
Automatically pick up config options from setup.cfg if it is present in the project root else check and if it exists use pyproject.toml.
--include include File include pattern.
--exclude exclude File exclude pattern.
--gitignore Exclude .gitignore patterns. if present.
--ignore-init Ignore the __init__.py file.
--include-star-import
Include star imports during scanning and refactor.
-d, --diff Prints a diff of all the changes unimport would make to a file.
-r, --remove Remove unused imports automatically.
-p, --permission Refactor permission after see diff.
-v, --version Prints version of unimport

Get rid of all unused imports 🥳
```
---
Expand Down Expand Up @@ -75,6 +74,17 @@ Read configuration from PATH
---
## Disable auto discovery config
> (optional: default `False`)
Automatically pick up config options from setup.cfg if it is present in the project root
else check and if it exists use pyproject.toml.
**Usage**
- `$ unimport --disable-auto-discovery-config`
## Include
> (optional: default '\\.(py)$') file include pattern
Expand Down
8 changes: 6 additions & 2 deletions docs/tutorial/configurations.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
It's possible to configure **unimport** from `pyproject.toml` or `setup.cfg` files if
you have.

**When reading your configurations, it gives priority to the configurations you enter
from the console.**
Automatically pick up config options from setup.cfg if it is present in the project root
else check and if it exists use pyproject.toml.

If you want you can disable this feature by passing `--disable-auto-discovery-config` or
you can pass the path to the configuration file by passing
`--config path/to/pyproject.toml`.

For example:

Expand Down
19 changes: 17 additions & 2 deletions src/unimport/commands/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"add_sources_option",
"add_check_option",
"add_config_option",
"add_disable_auto_discovery_config_option",
"add_include_option",
"add_exclude_option",
"add_gitignore_option",
Expand Down Expand Up @@ -44,14 +45,28 @@ def add_config_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"-c",
"--config",
default=".",
default=None,
help="Read configuration from PATH.",
metavar="PATH",
action="store",
type=Path,
)


def add_disable_auto_discovery_config_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--disable-auto-discovery-config",
default=Config.disable_auto_discovery_config,
help=(
"""
Automatically pick up config options from setup.cfg if it is present in the project
root else check and if it exists use pyproject.toml.
"""
),
action="store_true",
)


def add_include_option(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--include",
Expand Down Expand Up @@ -150,6 +165,6 @@ def add_color_option(parser: argparse.ArgumentParser) -> None:
"--color",
default=Config.color,
type=str,
metavar="{" + ",".join(Config._get_color_choices()) + "}",
metavar="{" + ",".join(Config.get_color_choices()) + "}",
help="Select whether to use color in the output. Defaults to `%(default)s`.",
)
1 change: 1 addition & 0 deletions src/unimport/commands/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ def generate_parser() -> argparse.ArgumentParser:
options.add_sources_option(parser)
options.add_check_option(parser)
options.add_config_option(parser)
options.add_disable_auto_discovery_config_option(parser)
options.add_include_option(parser)
options.add_exclude_option(parser)
options.add_gitignore_option(parser)
Expand Down
56 changes: 37 additions & 19 deletions src/unimport/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@
__all__ = ("Config", "ParseConfig")


CONFIG_FILES: Dict[str, str] = {
"setup.cfg": "unimport",
"pyproject.toml": "tool.unimport",
}


@dataclasses.dataclass
class Config:
default_sources: ClassVar[List[Path]] = [Path(".")] # Not init attribute
Expand All @@ -31,6 +37,7 @@ class Config:
use_color: bool = dataclasses.field(init=False) # Not init attribute

sources: Optional[List[Path]] = None
disable_auto_discovery_config: bool = False
include: str = C.INCLUDE_REGEX_PATTERN
exclude: str = C.EXCLUDE_REGEX_PATTERN
gitignore: bool = False
Expand All @@ -57,7 +64,7 @@ def __post_init__(self):

self.diff = self.diff or self.permission
self.remove = self.remove or not any((self.diff, self.check))
self.use_color: bool = self._use_color(self.color)
self.use_color: bool = self.is_use_color(self.color)

if self.gitignore:
self.gitignore_patterns = utils.get_exclude_list_from_gitignore()
Expand All @@ -75,15 +82,15 @@ def get_paths(self) -> Iterator[Path]:
)

@classmethod
def _get_color_choices(cls) -> Tuple[str]:
def get_color_choices(cls) -> Tuple[str]:
return getattr(
Config.__annotations__["color"],
"__args__" if C.PY37_PLUS else "__values__",
)

@classmethod
def _use_color(cls, color: str) -> bool:
if color not in cls._get_color_choices():
def is_use_color(cls, color: str) -> bool:
if color not in cls.get_color_choices():
raise ValueError(color)

return color == "always" or (color == "auto" and sys.stderr.isatty() and TERMINAL_SUPPORT_COLOR)
Expand Down Expand Up @@ -113,28 +120,28 @@ def build(

@dataclasses.dataclass
class ParseConfig:
CONFIG_FILES: ClassVar[Dict[str, str]] = {
"setup.cfg": "unimport",
"pyproject.toml": "tool.unimport",
}

config_file: Path

def __post_init__(self):
self.section: str = self.CONFIG_FILES[self.config_file.name]
if not self.config_file.exists():
raise FileNotFoundError(f"Config file not found: {self.config_file}")

self.config_section: Optional[str] = CONFIG_FILES.get(self.config_file.name, None)
if self.config_section is None:
raise ValueError(f"Unsupported config file: {self.config_file}")

def parse(self) -> Dict[str, Any]:
return getattr(self, f"parse_{self.config_file.suffix.strip('.')}")()

def parse_cfg(self) -> Dict[str, Any]:
parser = configparser.ConfigParser(allow_no_value=True)
parser.read(self.config_file)
if parser.has_section(self.section):
if parser.has_section(self.config_section):

def get_config_as_list(name: str) -> List[str]:
return literal_eval(
parser.get(
self.section,
self.config_section,
name,
fallback=getattr(Config, name),
)
Expand All @@ -154,10 +161,10 @@ def get_config_as_list(name: str) -> List[str]:
"ignore_init": bool,
"color": str,
}
for key, value in parser[self.section].items():
for key, value in parser[self.config_section].items():
key_type = config_annotations_mapping[key]
if key_type == bool:
cfg_context[key] = parser.getboolean(self.section, key)
cfg_context[key] = parser.getboolean(self.config_section, key)
elif key_type == str:
cfg_context[key] = value # type: ignore
elif key_type == List[Path]:
Expand All @@ -168,16 +175,27 @@ def get_config_as_list(name: str) -> List[str]:

def parse_toml(self) -> Dict[str, Any]:
parsed_toml = toml.loads(self.config_file.read_text())
toml_context: Dict[str, Any] = parsed_toml.get("tool", {}).get("unimport", {})
toml_context: Dict[str, Any] = dict(
functools.reduce(lambda x, y: x.get(y, {}), self.config_section.split("."), parsed_toml) # type: ignore[attr-defined]
)
if toml_context:
sources = toml_context.get("sources", Config.default_sources)
toml_context["sources"] = [Path(path) for path in sources]
return toml_context

@classmethod
def parse_args(cls, args: argparse.Namespace) -> Config:
if args.config and args.config.name in cls.CONFIG_FILES:
def parse_args(cls, args: argparse.Namespace) -> "Config":
config_context: Optional[Dict[str, Any]] = None

if args.config is not None:
config_context = cls(args.config).parse()
return Config.build(args=args.__dict__, config_context=config_context)
elif args.config is None and args.disable_auto_discovery_config is False:
for path in CONFIG_FILES.keys():
config_context = cls(Path(path)).parse()
if config_context:
break

if not config_context:
config_context = None

return Config.build(args=vars(args))
return Config.build(args=vars(args), config_context=config_context)
2 changes: 1 addition & 1 deletion tests/commands/test_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_add_config_option(parser: argparse.ArgumentParser):

options.add_config_option(parser)

assert vars(parser.parse_args([])) == dict(config=Path("."))
assert vars(parser.parse_args([])) == dict(config=None)
assert vars(parser.parse_args(["-c", "config.toml"])) == dict(config=Path("config.toml"))
assert vars(parser.parse_args(["--config", "config.toml"])) == dict(config=Path("config.toml"))

Expand Down
55 changes: 3 additions & 52 deletions tests/commands/test_parser.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import argparse
import contextlib
import io
import textwrap
from pathlib import Path

import pytest

from unimport.constants import PY39_PLUS


@pytest.fixture(scope="module")
def parser() -> argparse.ArgumentParser:
Expand All @@ -30,11 +25,12 @@ def test_generate_parser_argument_parser(parser: argparse.ArgumentParser):


def test_generate_parser_empty_parse_args(parser: argparse.ArgumentParser):
assert vars(parser.parse_args(["--color", "never"])) == dict(
assert vars(parser.parse_args(["--disable-auto-discovery-config", "--color", "never"])) == dict(
check=False,
color="never",
config=Path("."),
config=None,
diff=False,
disable_auto_discovery_config=True,
exclude="^$",
gitignore=False,
ignore_init=False,
Expand All @@ -44,48 +40,3 @@ def test_generate_parser_empty_parse_args(parser: argparse.ArgumentParser):
remove=False,
sources=[Path(".")],
)


@pytest.mark.skipif(PY39_PLUS, reason="This test should work on versions 3.8 and lower.")
def test_generate_parser_print_help(parser: argparse.ArgumentParser):
# NOTE: If this test changes, be sure to update this page https://unimport.hakancelik.dev/#command-line-options

with contextlib.redirect_stdout(io.StringIO()) as f:
parser.print_help()
help_message = f.getvalue()

assert help_message == textwrap.dedent(
"""\
usage: unimport [-h] [--color {auto,always,never}] [--check] [-c PATH]
[--include include] [--exclude exclude] [--gitignore]
[--ignore-init] [--include-star-import] [-d] [-r | -p] [-v]
[sources [sources ...]]
A linter, formatter for finding and removing unused import statements.
positional arguments:
sources Files and folders to find the unused imports.
optional arguments:
-h, --help show this help message and exit
--color {auto,always,never}
Select whether to use color in the output. Defaults to
`auto`.
--check Prints which file the unused imports are in.
-c PATH, --config PATH
Read configuration from PATH.
--include include File include pattern.
--exclude exclude File exclude pattern.
--gitignore Exclude .gitignore patterns. if present.
--ignore-init Ignore the __init__.py file.
--include-star-import
Include star imports during scanning and refactor.
-d, --diff Prints a diff of all the changes unimport would make
to a file.
-r, --remove Remove unused imports automatically.
-p, --permission Refactor permission after see diff.
-v, --version Prints version of unimport
Get rid of all unused imports 🥳
"""
)
Loading

0 comments on commit 5fd5e49

Please sign in to comment.