Skip to content

Commit

Permalink
feat: add type hinting
Browse files Browse the repository at this point in the history
  • Loading branch information
fredrikaverpil committed Dec 26, 2024
1 parent 9a546d0 commit e46f6e2
Show file tree
Hide file tree
Showing 13 changed files with 277 additions and 192 deletions.
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,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",
]

[tool.hatch.version]
Expand Down Expand Up @@ -107,6 +108,3 @@ strict = true
[[tool.mypy.overrides]]
module = ["dotty_dict.*", "pip_requirements_parser.*", "tomli.*"]
ignore_missing_imports = true

[tool.pytest.ini_options]
addopts = "--ignore=tests/manual"
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
73 changes: 41 additions & 32 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 Literal, Optional, Union

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

Expand All @@ -25,15 +26,16 @@ 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"])
exclude_deps: List[str] = field(default_factory=list)
paths: list[str] = field(default_factory=lambda: ["src"])
sections: list[str] = field(default_factory=lambda: ["project.dependencies"])
exclude_deps: list[str] = field(default_factory=list)
deps_file: str = "pyproject.toml"
venvs: List[str] = field(
venvs: list[str] = field(
default_factory=lambda: [os.environ.get("VIRTUAL_ENV", ".venv")]
)
features: List[str] = field(default_factory=list)
features: list[str] = field(default_factory=list)


class Features(Enum):
Expand All @@ -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
11 changes: 5 additions & 6 deletions src/creosote/formatters.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import sys
from typing import List

from loguru import logger

Expand All @@ -8,33 +7,33 @@ 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,
format="<level>{message}</level>",
)


def print_results(unused_dependency_names: List[str], format_: str) -> None:
def print_results(unused_dependency_names: list[str], format_: str) -> None:
if unused_dependency_names:
if format_ == "porcelain":
print("\n".join(unused_dependency_names))
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! ✨")
12 changes: 6 additions & 6 deletions src/creosote/models.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import dataclasses
from typing import List, Optional
from typing import Optional


@dataclasses.dataclass
class ImportInfo:
module: List[str]
name: List[str]
module: list[str]
name: list[str]
alias: Optional[str] = None


@dataclasses.dataclass
class DependencyInfo:
name: str # as defined in the dependencies specification file
top_level_import_names: Optional[List[str]] = None
record_import_names: Optional[List[str]] = None
top_level_import_names: Optional[list[str]] = None
record_import_names: Optional[list[str]] = None
canonicalized_dep_name: Optional[str] = None
associated_imports: List[ImportInfo] = dataclasses.field(default_factory=list)
associated_imports: list[ImportInfo] = dataclasses.field(default_factory=list)
Loading

0 comments on commit e46f6e2

Please sign in to comment.