Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions docs/conf.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import codecs
import os
import re

from pathlib import Path


def read(*parts):
"""
Build an absolute path from *parts* and and return the contents of the
resulting file. Assume UTF-8 encoding.
"""
here = os.path.abspath(os.path.dirname(__file__))
with codecs.open(os.path.join(here, *parts), "rb", "utf-8") as f:
here = Path(__file__).parent.absolute()
with codecs.open(str(here.joinpath(*parts)), "rb", "utf-8") as f:
Comment on lines +12 to +13

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
here = Path(__file__).parent.absolute()
with codecs.open(str(here.joinpath(*parts)), "rb", "utf-8") as f:
HERE = Path(__file__).parent.absolute()
with HERE.joinpath(*parts).open("r", encoding="utf-8") as f:

No need to convert to a str, almost every open function/method accepts path-like objects. Don't open a file in binary when you apply an encoding.

Other parts use HERE.

return f.read()


Expand Down
9 changes: 5 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
# Copyright 2020 Lynn Root

import codecs
import os
import re

from pathlib import Path

from setuptools import find_packages, setup


HERE = os.path.abspath(os.path.dirname(__file__))
HERE = Path(__file__).parent.absolute()


#####
Expand All @@ -23,7 +24,7 @@ def read(*filenames, **kwargs):
sep = kwargs.get("sep", "\n")
buf = []
for fl in filenames:
with codecs.open(os.path.join(HERE, fl), "rb", encoding) as f:
with codecs.open(HERE / fl, "rb", encoding) as f:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
with codecs.open(HERE / fl, "rb", encoding) as f:
with (HERE / fl).open("r", encoding) as f:

You should open the file in text mode, not binary mode and then apply the encoding. Path objects provide a Path.open() method.

Since when is the abbreviation of filename fl?

buf.append(f.read())
return sep.join(buf)

Expand All @@ -43,7 +44,7 @@ def find_meta(meta):
NAME = "interrogate"
PACKAGE_NAME = "interrogate"
PACKAGES = find_packages(where="src")
META_PATH = os.path.join("src", PACKAGE_NAME, "__init__.py")
META_PATH = Path("src") / PACKAGE_NAME / "__init__.py"

META_FILE = read(META_PATH)
KEYWORDS = ["documentation", "coverage", "quality"]
Expand Down
46 changes: 26 additions & 20 deletions src/interrogate/badge_gen.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
"""
from __future__ import annotations

import os
import sys

from importlib import resources
from typing import Union
from pathlib import Path
from typing import TYPE_CHECKING, Union
from xml.dom import minidom


if TYPE_CHECKING:
from os import PathLike

try:
import cairosvg
except ImportError: # pragma: no cover
Expand Down Expand Up @@ -80,8 +83,8 @@


def save_badge(
badge: str, output: str, output_format: str | None = None
) -> str:
badge: str, output: PathLike[str] | str, output_format: str | None = None
) -> Path:
"""Save badge to the specified path.

.. versionadded:: 1.4.0 new ``output_format`` keyword argument
Expand All @@ -96,10 +99,10 @@ def save_badge(
if output_format is None:
output_format = "svg"

if output_format == "svg":
with open(output, "w") as f:
f.write(badge)
output = Path(output)

if output_format == "svg":
output.write_text(badge)
return output

if cairosvg is None:
Expand All @@ -110,16 +113,14 @@ def save_badge(

# need to write the badge as an svg first in order to convert it to
# another format
tmp_output_file = f"{os.path.splitext(output)[0]}.tmp.svg"
tmp_output_file = Path(f"{output.parent / output.stem}.tmp.svg")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
tmp_output_file = Path(f"{output.parent / output.stem}.tmp.svg")
tmp_output_file = output.with_suffix(".tmp.svg")

Source: https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.with_suffix

try:
with open(tmp_output_file, "w") as f:
f.write(badge)

tmp_output_file.write_text(badge)
cairosvg.svg2png(url=tmp_output_file, write_to=output, scale=2)

finally:
try:
os.remove(tmp_output_file)
tmp_output_file.unlink()
except Exception: # pragma: no cover
pass

Expand Down Expand Up @@ -183,7 +184,9 @@ def get_badge(result: float, color: str, style: str | None = None) -> str:
return tmpl


def should_generate_badge(output: str, color: str, result: float) -> bool:
def should_generate_badge(
output: PathLike[str] | str, color: str, result: float
) -> bool:
"""Detect if existing badge needs updating.

This is to help avoid unnecessary newline updates. See
Expand All @@ -203,14 +206,16 @@ def should_generate_badge(output: str, color: str, result: float) -> bool:
:return: Whether or not the badge SVG file should be generated.
:rtype: bool
"""
if not os.path.exists(output):
output = Path(output)

if not output.exists():
return True

if not output.endswith(".svg"):
if output.suffix != ".svg":
return True

try:
badge = minidom.parse(output)
badge = minidom.parse(str(output))

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
badge = minidom.parse(str(output))
badge = minidom.parse(output)

minidom accepts file-like objects: https://docs.python.org/3/library/xml.dom.minidom.html#xml.dom.minidom.parse

except Exception:
# an exception might happen when a file is not an SVG file but has
# `.svg` extension (perhaps a png image was generated with the wrong
Expand Down Expand Up @@ -260,11 +265,11 @@ def get_color(result: float) -> str:


def create(
output: str,
output: PathLike[str] | str,
result: InterrogateResults,
output_format: str | None = None,
output_style: str | None = None,
) -> str:
) -> Path:
"""Create a status badge.

The badge file will only be written if it doesn't exist, or if the
Expand All @@ -290,9 +295,10 @@ def create(
"""
if output_format is None:
output_format = "svg"
if os.path.isdir(output):
output = Path(output)
if output.is_dir():
filename = DEFAULT_FILENAME + "." + output_format
output = os.path.join(output, filename)
output /= filename

result_perc = result.perc_covered
color = get_color(result_perc)
Expand Down
13 changes: 10 additions & 3 deletions src/interrogate/cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Copyright 2020-2024 Lynn Root
"""CLI entrypoint into `interrogate`."""

import os
import sys

from pathlib import Path
from typing import List, Optional, Pattern, Tuple, Union

import click
Expand Down Expand Up @@ -218,6 +218,13 @@
@click.option(
"-o",
"--output",
type=click.Path(
exists=False,
file_okay=True,
dir_okay=True,
writable=True,
resolve_path=True,
),
default=None,
metavar="FILE",
help="Write output to a given FILE. [default: stdout]",
Expand Down Expand Up @@ -313,7 +320,7 @@
help="Read configuration from `pyproject.toml` or `setup.cfg`.",
)
def main(
paths: Optional[List[str]],
paths: Optional[List[Path]],
verbose: int,
quiet: bool,
fail_under: Union[int, float],
Expand Down Expand Up @@ -389,7 +396,7 @@ def main(
),
)
if not paths:
paths = [os.path.abspath(os.getcwd())]
paths = [Path.cwd()]

# NOTE: this will need to be fixed if we want to start supporting
# --whitelist-regex on filenames. This otherwise assumes you
Expand Down
27 changes: 15 additions & 12 deletions src/interrogate/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,23 @@
from __future__ import annotations

import configparser
import os
import pathlib
import re

from collections.abc import Sequence
from typing import Any
from pathlib import Path
from typing import TYPE_CHECKING, Any

import attr
import click


if TYPE_CHECKING:
from os import PathLike

try:
import tomllib
except ImportError:
import tomli as tomllib # type: ignore
import tomli as tomllib


# TODO: idea: break out InterrogateConfig into two classes: one for
Expand Down Expand Up @@ -86,17 +88,17 @@ def _check_style(self, attribute: str, value: str) -> None:
)


def find_project_root(srcs: Sequence[str]) -> pathlib.Path:
def find_project_root(srcs: Sequence[PathLike[str] | str]) -> Path:
"""Return a directory containing .git, .hg, or pyproject.toml.
That directory can be one of the directories passed in `srcs` or their
common parent.
If no directory in the tree contains a marker that would specify it's the
project root, the root of the file system is returned.
"""
if not srcs:
return pathlib.Path("/").resolve()
return Path("/").resolve()

common_base = min(pathlib.Path(src).resolve() for src in srcs)
common_base = min(Path(src).resolve() for src in srcs)
if common_base.is_dir():
# Append a fake file so `parents` below returns `common_base_dir`, too.
common_base /= "fake-file"
Expand All @@ -114,7 +116,9 @@ def find_project_root(srcs: Sequence[str]) -> pathlib.Path:
return directory


def find_project_config(path_search_start: Sequence[str]) -> str | None:
def find_project_config(
path_search_start: Sequence[PathLike[str] | str],
) -> str | None:
"""Find the absolute filepath to a pyproject.toml if it exists."""
project_root = find_project_root(path_search_start)
pyproject_toml = project_root / "pyproject.toml"
Expand All @@ -125,7 +129,7 @@ def find_project_config(path_search_start: Sequence[str]) -> str | None:
return str(setup_cfg) if setup_cfg.is_file() else None


def parse_pyproject_toml(path_config: str) -> dict[str, Any]:
def parse_pyproject_toml(path_config: PathLike[str] | str) -> dict[str, Any]:
"""Parse ``pyproject.toml`` file and return relevant parts for Interrogate.

:param str path_config: Path to ``pyproject.toml`` file.
Expand All @@ -134,8 +138,7 @@ def parse_pyproject_toml(path_config: str) -> dict[str, Any]:
:raise OSError: an I/O-related error when opening ``pyproject.toml``.
:raise tomllib.TOMLDecodeError: unable to load ``pyproject.toml``.
"""
with open(path_config, "rb") as f:
pyproject_toml = tomllib.load(f)
pyproject_toml = tomllib.loads(Path(path_config).read_text())

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
pyproject_toml = tomllib.loads(Path(path_config).read_text())
pyproject_toml = tomllib.loads(Path(path_config).read_text(encoding="utf-8"))

No encoding is specified.

config = pyproject_toml.get("tool", {}).get("interrogate", {})
return {
k.replace("--", "").replace("-", "_"): v for k, v in config.items()
Expand Down Expand Up @@ -221,7 +224,7 @@ def read_config_file(
if not value:
paths = ctx.params.get("paths")
if not paths:
paths = (os.path.abspath(os.getcwd()),)
paths = (Path.cwd(),)
value = find_project_config(paths)
if value is None:
return None
Expand Down
Loading