Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ repos:
- flake8-builtins
- flake8-comprehensions
- repo: https://github.com/hukkin/mdformat
rev: 0.7.21
rev: 1.0.0
hooks:
- id: mdformat
additional_dependencies:
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ repos:
- id: mdformat
name: mdformat
entry: mdformat
files: "tests/pre-commit-test.md"
files: ".*/pre-commit-test.md"
types: [markdown]
language: system
104 changes: 68 additions & 36 deletions mdformat_pyproject/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

import markdown_it
import mdformat
from mdformat._cli import print_paragraphs

TYPE_CHECKING = False
if TYPE_CHECKING:
Expand All @@ -23,63 +24,94 @@
import tomli as tomllib


@cache
def _find_pyproject_toml_path(search_path: Path) -> Path | None:
"""Find the pyproject.toml file that applies to the search path.

The search is done ascending through the folders tree until a pyproject.toml
file is found in the same folder. If the root '/' is reached, None is returned.
"""
if search_path.is_file():
search_path = search_path.parent

for parent in (search_path, *search_path.parents):
candidate = parent / "pyproject.toml"
if candidate.is_file():
return candidate

return None


@cache
def _parse_pyproject(pyproject_path: Path) -> _ConfigOptions | None:
"""Extract and validate the mdformat options from the pyproject.toml file.
"""Extract and validate the mdformat options from the ``pyproject.toml`` file.

The options are searched inside a ``[tool.mdformat]`` section within the TOML file,
and they are validated using the default functions from ``mdformat._conf``.

The options are searched inside a [tool.mdformat] key within the toml file,
and they are validated using the default functions from `mdformat._conf`.
If no ``[tool.mdformat]`` section is found, ``None`` is returned.
"""
with pyproject_path.open(mode="rb") as pyproject_file:
content = tomllib.load(pyproject_file)
try:
content = tomllib.load(pyproject_file)
except tomllib.TOMLDecodeError as e:
raise mdformat._conf.InvalidConfError(f"Invalid TOML syntax: {e}")

options = content.get("tool", {}).get("mdformat")
if options is not None:
mdformat._conf._validate_keys(options, pyproject_path)
mdformat._conf._validate_values(options, pyproject_path)
if options is None:
return None

mdformat._conf._validate_keys(options, pyproject_path)
mdformat._conf._validate_values(options, pyproject_path)

return options


_orig_read_toml_opts = mdformat._conf.read_toml_opts


@cache
def read_toml_opts(conf_dir: Path) -> tuple[dict, Path | None]:
"""Alternative read_toml_opts that reads from pyproject.toml instead of .mdformat.toml.
def patched_read_toml_opts(search_path: Path) -> tuple[dict, Path | None]:
"""Patched version of ``mdformat._conf.read_toml_opts``.

Search the first ``.mdformat.toml`` or ``pyproject.toml`` file in the provided
``search_path`` folder or its parents.

The search is done ascending through the folders tree until a ``.mdformat.toml`` or a
``pyproject.toml`` file is found. If the root ``/`` is reached, ``None`` is returned.

Notice that if `.mdformat.toml` exists it is ignored.
``.mdformat.toml`` takes precedence over ``pyproject.toml`` if both are present in the
same folder. In that case, a warning is logged to inform the user that the
``pyproject.toml`` file has been ignored.

``pyproject.toml`` files without a ``[tool.mdformat]`` section are ignored.

This behavior mimics the one from Ruff, as described in `issues#17
<https://github.com/csala/mdformat-pyproject/issues/17>`_.
"""
pyproject_path = _find_pyproject_toml_path(conf_dir)
if pyproject_path:
pyproject_opts = _parse_pyproject(pyproject_path)
else:
pyproject_opts = {}
if search_path.is_file():
search_path = search_path.parent

return pyproject_opts, pyproject_path
for parent in (search_path, *search_path.parents):
# Try to find a pyproject.toml file first, with a [tool.mdformat] section.
pyproject_options = None
pyproject_path = parent / "pyproject.toml"
if pyproject_path.is_file():
pyproject_options = _parse_pyproject(pyproject_path)

# Read options from .mdformat.toml if present.
mdformat_path = parent / ".mdformat.toml"
if mdformat_path.is_file():
# If both pyproject.toml and .mdformat.toml are present, the latter takes
# precedence, but we warn the user that pyproject.toml is being ignored.
if pyproject_options is not None:
print_paragraphs(
(
f"Warning: ignoring mdformat options from {pyproject_path} "
f"since {mdformat_path} is present.",
)
)

# Return options from .mdformat.toml using the original function, even if
# they are empty.
return _orig_read_toml_opts(mdformat_path.parent)

# Return options from pyproject.toml if .mdformat.toml is not present.
elif pyproject_options is not None:
return pyproject_options, pyproject_path

# No config file found, return empty options.
return {}, None


def update_mdit(mdit: markdown_it.MarkdownIt) -> None:
"""No-op, since this plugin only monkey patches and does not modify mdit."""
"""No-op, since this plugin only monkey patches and does not modify ``mdit``."""
pass


RENDERERS: dict[str, Render] = {}

# Monkey patch mdformat._conf to use our own read_toml_opts version
mdformat._conf.read_toml_opts = read_toml_opts
mdformat._cli.read_toml_opts = patched_read_toml_opts
File renamed without changes.
16 changes: 16 additions & 0 deletions tests/mdformat_toml/pre-commit-test.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Test file

This repository contains 2 separate configuration files with contradictory
settings:

1. The `tests/mdformat_toml/.mdformat.toml`, which defines a `wrap` length of 80
and sets `number` to `false`, meaning that numbered lists would be written
starting with `1.` on all the rows.
1. The `pyproject.toml`, which should be used after the `mdformat-pyproject`
plugin is installed, and which defines wrap length of 99 and sets `number` to
`true`, meaning that numbered lists would be written starting with
consecutive numbers.

This file is written according to the rules specified in the `.mdformat.toml`
file placed alongside it, and serves as a valid test to see if the plugin is
working as expected and honoring `.mdformat.toml` when found.
5 changes: 2 additions & 3 deletions tests/pre-commit-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

This repository contains 2 separate configuration files with contradictory settings:

1. The `.mdformat.toml`, which should be used by default by `mdformat`, and which defines a `wrap`
length of 80 and sets `number` to `false`, meaning that numbered lists would be written starting
with `1.` on all the rows.
1. The `tests/mdformat_toml/.mdformat.toml`, which defines a `wrap` length of 80 and sets `number`
to `false`, meaning that numbered lists would be written starting with `1.` on all the rows.
2. The `pyproject.toml`, which should be used after the `mdformat-pyproject` plugin is installed,
and which defines wrap length of 99 and sets `number` to `true`, meaning that numbered lists
would be written starting with consecutive numbers.
Expand Down
Loading