Skip to content

Commit

Permalink
feat: tighten up typing
Browse files Browse the repository at this point in the history
  • Loading branch information
fredrikaverpil committed Jan 5, 2025
1 parent 3d1c5d7 commit d5d3507
Show file tree
Hide file tree
Showing 13 changed files with 256 additions and 157 deletions.
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
"nbformat>=5.10.4,<6.0",
"pip-requirements-parser>=32.0.1,<33.1",
"tomli>=2.1.0,<3.0.0 ; python_version < '3.11'",
"typing-extensions>=4.12.2",
]

[project.urls]
Expand Down Expand Up @@ -92,8 +93,13 @@ ignore = []
"S105", # hardcoded password
]

[tool.basedpyright]
pythonVersion = "3.8"
venvPath = ".venv"

[tool.mypy]
python_version = "3.8"
strict = true

[[tool.mypy.overrides]]
module = ["dotty_dict.*", "pip_requirements_parser.*", "tomli.*"]
Expand Down
9 changes: 4 additions & 5 deletions src/creosote/cli.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import sys
from collections.abc import Sequence
from typing import Optional

from loguru import logger

Expand All @@ -7,17 +9,14 @@
from creosote.config import Features, fail_fast, parse_args


def main(args_=None):
args, default_config = parse_args(args_)
def main(args_: Optional[Sequence[str]] = None) -> int:
args = parse_args(args_)
if fail_fast(args):
return 1
formatters.configure_logger(verbose=args.verbose, format_=args.format)

logger.debug(f"Creosote version: {__version__}")
logger.debug(f"Command: creosote {' '.join(sys.argv[1:])}")
logger.debug(
f"Default configuration (may have loaded pyproject.toml): {default_config}"
)
logger.debug(f"Arguments: {args}")

if args.features:
Expand Down
63 changes: 36 additions & 27 deletions src/creosote/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
import os
import sys
import typing
from collections.abc import Sequence
from dataclasses import dataclass, field
from enum import Enum
from pathlib import Path
from typing import List, Literal
from typing import List, Literal, Optional, Union

if sys.version_info >= (3, 11):
import tomllib
import tomllib # pyright: ignore[reportUnreachable]
else:
import tomli as tomllib

Expand All @@ -25,6 +26,7 @@ class Config:
the ``dest`` specified in ``add_argument``.
"""

verbose: bool = False
format: Literal["default", "no-color", "porcelain"] = "default"
paths: List[str] = field(default_factory=lambda: ["src"])
sections: List[str] = field(default_factory=lambda: ["project.dependencies"])
Expand Down Expand Up @@ -53,24 +55,30 @@ class CustomAppendAction(argparse.Action):
value when the argument is specified for the first time.
"""

def __init__(self, option_strings, dest, nargs=None, **kwargs):
def __init__( # type: ignore[no-untyped-def]
self,
option_strings: List[str],
dest: str,
nargs: Optional[List[str]] = None,
**kwargs, # pyright: ignore[reportUnknownParameterType, reportMissingParameterType]
):
"""Initialize the action."""
self.called_times = 0
super().__init__(option_strings, dest, **kwargs)
self.called_times: int = 0
super().__init__(option_strings, dest, **kwargs) # pyright: ignore[reportUnknownArgumentType]

def __call__(self, parser, namespace, values, option_string=None):
def __call__(self, parser, namespace, values, option_string=None): # type: ignore[no-untyped-def] # pyright: ignore[reportMissingParameterType, reportImplicitOverride]
"""When the argument is specified on the commandline."""
current_values = getattr(namespace, self.dest)
current_values = getattr(namespace, self.dest) # pyright: ignore[reportAny]

if self.called_times == 0:
current_values = []

current_values.append(values)
_ = current_values.append(values) # pyright: ignore[reportUnknownMemberType]
setattr(namespace, self.dest, current_values)
self.called_times += 1


def show_migration_message():
def show_migration_message() -> None:
"""Show warning if you are using v2.x args with v3.x code."""

args = sys.argv[1:]
Expand All @@ -79,13 +87,13 @@ def show_migration_message():
if outdated_arg in args:
logger.error(
"Creosote was updated to v3.x with breaking changes. "
"You need to update your CLI arguments. "
"See the migration guide at https://github.com/fredrikaverpil/creosote"
+ "You need to update your CLI arguments. "
+ "See the migration guide at https://github.com/fredrikaverpil/creosote"
)
sys.exit(1)


def parse_args(args):
def parse_args(args: Optional[Sequence[str]]) -> Config:
show_migration_message()

defaults = load_defaults()
Expand All @@ -97,29 +105,30 @@ def parse_args(args):
),
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
parser.add_argument(
_ = parser.add_argument(
"--verbose",
dest="verbose",
action="store_true",
default=defaults.verbose,
help="increase output verbosity",
)
parser.add_argument(
_ = parser.add_argument(
"-f",
"--format",
dest="format",
choices=typing.get_args(Config.__annotations__["format"]),
default=defaults.format,
help="output format",
)
parser.add_argument(
_ = parser.add_argument(
"-V",
"--version",
dest="version",
action="version",
version=__version__,
help="show version and exit",
)
parser.add_argument(
_ = parser.add_argument(
"-p",
"--path",
dest="paths",
Expand All @@ -128,7 +137,7 @@ def parse_args(args):
default=defaults.paths,
help="path(s) to Python source code to scan for imports",
)
parser.add_argument(
_ = parser.add_argument(
"-s",
"--section",
dest="sections",
Expand All @@ -137,23 +146,23 @@ def parse_args(args):
default=defaults.sections,
help="pyproject.toml section(s) to scan for dependencies",
)
parser.add_argument(
_ = parser.add_argument(
"--exclude-dep",
dest="exclude_deps",
metavar="DEPENDENCY",
action="append",
default=defaults.exclude_deps,
help="dependency(ies) to exclude from the scan",
)
parser.add_argument(
_ = parser.add_argument(
"-d",
"--deps-file",
dest="deps_file",
metavar="PATH",
default=defaults.deps_file,
help="path to the pyproject.toml or requirements[.txt|.in] file",
)
parser.add_argument(
_ = parser.add_argument(
"-v",
"--venv",
dest="venvs",
Expand All @@ -162,7 +171,7 @@ def parse_args(args):
default=defaults.venvs,
help="path(s) to the virtual environment (or site-packages)",
)
parser.add_argument(
_ = parser.add_argument(
"--use-feature",
dest="features",
metavar="FEATURE",
Expand All @@ -176,10 +185,10 @@ def parse_args(args):
)

parsed_args = parser.parse_args(args)
return parsed_args, defaults
return Config(**vars(parsed_args)) # pyright: ignore[reportAny]


def load_defaults(src: str = "pyproject.toml") -> Config:
def load_defaults(src: Union[str, Path] = "pyproject.toml") -> Config:
"""Load pyproject.toml defaults from user config.
Expects user configuration at ``[tool.creosote]``.
Expand All @@ -190,13 +199,13 @@ def load_defaults(src: str = "pyproject.toml") -> Config:
project_config = tomllib.load(f)
except FileNotFoundError:
project_config = {}
creosote_config = project_config.get("tool", {}).get("creosote", {})
creosote_config = project_config.get("tool", {}).get("creosote", {}) # pyright: ignore[reportAny]
# Convert all hyphens to underscores
creosote_config = {k.replace("-", "_"): v for k, v in creosote_config.items()}
return Config(**creosote_config)
creosote_config = {k.replace("-", "_"): v for k, v in creosote_config.items()} # pyright: ignore[reportAny]
return Config(**creosote_config) # pyright: ignore[reportAny]


def fail_fast(args: argparse.Namespace) -> bool:
def fail_fast(args: Config) -> bool:
"""Check if we should fail fast."""
if is_missing_file(args.deps_file):
logger.error(f"File not found: {args.deps_file}")
Expand Down
8 changes: 4 additions & 4 deletions src/creosote/formatters.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,18 @@ def configure_logger(verbose: bool, format_: str) -> None:
logger.remove()

if format_ == "porcelain":
logger.add(sys.stderr, level="CRITICAL")
_ = logger.add(sys.stderr, level="CRITICAL")
return
if format_ == "no-color":
logger.add(
_ = logger.add(
sys.stderr,
level="DEBUG" if verbose else "INFO",
colorize=False,
format="<level>{message}</level>",
)
else:
# default
logger.add(
_ = logger.add(
sys.stderr,
level="DEBUG" if verbose else "INFO",
colorize=True,
Expand All @@ -34,7 +34,7 @@ def print_results(unused_dependency_names: List[str], format_: str) -> None:
else:
logger.error(
"Oh no, bloated venv! 🤢 🪣\n"
f"Unused dependencies found: {', '.join(unused_dependency_names)}"
+ f"Unused dependencies found: {', '.join(unused_dependency_names)}"
)
else:
logger.info("No unused dependencies found! ✨")
Loading

0 comments on commit d5d3507

Please sign in to comment.