Skip to content

Commit bd7f79e

Browse files
authored
feat: add --stdin-filename so per-file-ignores works for stdin input
1 parent 64afafa commit bd7f79e

10 files changed

Lines changed: 162 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
[Semantic Versioning](https://semver.org/)
44

5+
## [Unreleased]
6+
7+
### Feature
8+
9+
- New `--stdin-filename` option tells djLint the real path of content piped in on stdin (`djlint -`). Stdin previously always carried the name `-`, which no realistic `per-file-ignores` pattern matches, so per-file ignores were silently dead for piped input; the given name is now what `per-file-ignores` matches against and what linter messages report. Editor integrations that lint the open buffer through stdin get the same per-file ignores as a run over the file on disk. Path separators are normalized the same way they are for files on disk, so a Windows-style path matches a pattern written with `/`.
10+
511
## [1.42.3] - 2026-07-23
612

713
### Fix

docs/src/_data/configuration.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,5 +670,18 @@
670670
},
671671
{ "name": "cli", "value": "--format-attribute-js-json-min-props 3" }
672672
]
673+
},
674+
{
675+
"name": "stdin_filename",
676+
"tags": ["formatter", "linter"],
677+
"description": {
678+
"en": "Filename to use for per-file-ignores and messages when reading from stdin. Defaults to \"-\".",
679+
"ru": "Имя файла, используемое для per-file-ignores и сообщений при чтении из stdin. По умолчанию \"-\".",
680+
"fr": "Nom de fichier à utiliser pour per-file-ignores et les messages lors de la lecture depuis stdin. La valeur par défaut est \"-\".",
681+
"zh": "从标准输入(stdin)读取时,用于 per-file-ignores 及消息中的文件名。默认值为 \"-\""
682+
},
683+
"usage": [
684+
{ "name": "cli", "value": "--stdin-filename templates/index.html" }
685+
]
673686
}
674687
]

docs/src/_includes/cli.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ Options:
1111
-i, --ignore TEXT Codes to ignore. ex: "H014,H017"
1212
--reformat Reformat the file(s).
1313
--check Check formatting on the file(s).
14+
--stdin-filename TEXT Filename to use for per-file-ignores and
15+
messages when reading from stdin. [default:
16+
-]
1417
--indent INTEGER Indent spacing. [default: 4]
1518
--quiet Do not print diff when reformatting.
1619
--profile TEXT Enable defaults by template language. ops:

docs/src/docs/getting-started.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,9 @@ Output -
8383
```html
8484
<div></div>
8585
```
86+
87+
When reading from stdin, djLint has no real path to match against `per-file-ignores` or to show in messages, so it uses `-` by default. Pass `--stdin-filename` to tell djLint the real path of the piped content, for example when an editor integration pipes a file's contents in on save.
88+
89+
```bash
90+
echo "<div></div>" | djlint - --stdin-filename templates/index.html
91+
```

src/djlint/__init__.py

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@
5959
)
6060
@click.option("--reformat", is_flag=True, help="Reformat the file(s).")
6161
@click.option("--check", is_flag=True, help="Check formatting on the file(s).")
62+
@click.option(
63+
"--stdin-filename",
64+
type=str,
65+
default=None,
66+
help=(
67+
"Filename to use for per-file-ignores and messages when reading"
68+
" from stdin. [default: -]"
69+
),
70+
show_default=False,
71+
)
6272
@click.option(
6373
"--indent",
6474
type=int,
@@ -294,6 +304,7 @@ def main(
294304
reformat: bool,
295305
indent: int | None,
296306
check: bool,
307+
stdin_filename: str | None,
297308
quiet: bool,
298309
profile: str | None,
299310
require_pragma: bool,
@@ -361,6 +372,7 @@ def main(
361372
lint=lint or not (reformat or check),
362373
reformat=reformat,
363374
check=check,
375+
stdin_filename=stdin_filename,
364376
use_gitignore=use_gitignore,
365377
warn=warn,
366378
preserve_leading_space=preserve_leading_space,
@@ -531,18 +543,22 @@ def process_stdin(
531543
output: ProcessResult = {}
532544
html = stdin_text
533545
formatted_code = None
546+
stdin_filename = config.stdin_filename or "-"
534547

535548
if config.reformat or config.check:
536549
from djlint.reformat import reformat_string # noqa: PLC0415
537550

538551
output["format_message"], formatted_code = reformat_string(
539-
config, stdin_text, "-"
552+
config, stdin_text, stdin_filename
540553
)
541554
html = formatted_code
542555

543556
if config.lint:
544557
from djlint.lint import linter # noqa: PLC0415
545558

546-
output["lint_message"] = linter(config, html, "-", "-")
559+
# as lint_file() does, match per_file_ignores against a posix path
560+
output["lint_message"] = linter(
561+
config, html, stdin_filename, Path(stdin_filename).as_posix()
562+
)
547563

548564
return output, formatted_code

src/djlint/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1120,6 +1120,7 @@ class Config:
11201120
"start_template_tags",
11211121
"statistics",
11221122
"stdin",
1123+
"stdin_filename",
11231124
"tag_indent",
11241125
"tag_unindent",
11251126
"tag_unindent_line",
@@ -1185,6 +1186,7 @@ def __init__(
11851186
max_blank_lines: int | None = None,
11861187
github_output: bool = False,
11871188
stdin: bool | None = None,
1189+
stdin_filename: str | None = None,
11881190
) -> None:
11891191
self.project_root = find_project_root(
11901192
Path.cwd() if src == "-" else Path(src).resolve()
@@ -1216,6 +1218,7 @@ def setting_int(key: str, default: int) -> int:
12161218
self.warn = warn
12171219
self.github_output = github_output
12181220
self.statistics = statistics
1221+
self.stdin_filename = stdin_filename
12191222

12201223
# simple options; the command line takes precedence over the config
12211224
self.extension = str(

tests/test_config/test_stdin_filename/__init__.py

Whitespace-only changes.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[tool.djlint.per-file-ignores]
2+
"myfile.html" = "H025"
3+
"templates/index.html" = "H025"
4+
"^-$" = "H020"
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
"""Djlint tests specific to --stdin-filename.
2+
3+
run::
4+
5+
pytest tests/test_config/test_stdin_filename/test_config.py --cov=src/djlint \
6+
--cov-branch --cov-report xml:coverage.xml --cov-report term-missing
7+
8+
for a single test, run::
9+
10+
pytest tests/test_config/test_stdin_filename/test_config.py::test_stdin_filename_matches_per_file_ignores \
11+
--cov=src/djlint --cov-branch --cov-report xml:coverage.xml --cov-report term-missing
12+
13+
"""
14+
15+
from __future__ import annotations
16+
17+
from pathlib import Path
18+
from typing import TYPE_CHECKING
19+
20+
from djlint import main as djlint
21+
22+
if TYPE_CHECKING:
23+
from click.testing import CliRunner
24+
25+
# the pyproject.toml in this directory has:
26+
# "myfile.html" = "H025"
27+
# "templates/index.html" = "H025"
28+
# "^-$" = "H020"
29+
# and the html below triggers both H025 and H020 when neither is ignored.
30+
_HTML = "<div>\n <div></div>"
31+
_CONFIG = "tests/test_config/test_stdin_filename/pyproject.toml"
32+
33+
34+
def test_stdin_filename_matches_per_file_ignores(runner: CliRunner) -> None:
35+
"""A --stdin-filename matching a per-file-ignores pattern suppresses it."""
36+
result = runner.invoke(
37+
djlint,
38+
("-", "--stdin-filename", "myfile.html", "--configuration", _CONFIG),
39+
input=_HTML,
40+
)
41+
assert "H025" not in result.output
42+
assert "H020" in result.output
43+
44+
45+
def test_stdin_filename_not_matching_per_file_ignores(
46+
runner: CliRunner,
47+
) -> None:
48+
"""A --stdin-filename that matches no pattern reports every rule."""
49+
result = runner.invoke(
50+
djlint,
51+
("-", "--stdin-filename", "other.html", "--configuration", _CONFIG),
52+
input=_HTML,
53+
)
54+
assert "H025" in result.output
55+
assert "H020" in result.output
56+
57+
58+
def test_stdin_filename_default_unchanged(runner: CliRunner) -> None:
59+
"""Without --stdin-filename, per-file-ignores still match against "-"."""
60+
result = runner.invoke(
61+
djlint, ("-", "--configuration", _CONFIG), input=_HTML
62+
)
63+
assert "H020" not in result.output
64+
assert "H025" in result.output
65+
66+
67+
def test_stdin_filename_uses_native_separators(runner: CliRunner) -> None:
68+
"""A native-separator path matches a pattern written with "/"."""
69+
result = runner.invoke(
70+
djlint,
71+
(
72+
"-",
73+
"--stdin-filename",
74+
str(Path("templates", "index.html")),
75+
"--configuration",
76+
_CONFIG,
77+
),
78+
input=_HTML,
79+
)
80+
assert "H025" not in result.output
81+
assert "H020" in result.output
82+
83+
84+
def test_stdin_filename_used_in_lint_message(runner: CliRunner) -> None:
85+
"""The --stdin-filename value is used as the error dict's filename key."""
86+
result = runner.invoke(
87+
djlint,
88+
(
89+
"-",
90+
"--stdin-filename",
91+
"myfile.html",
92+
"--configuration",
93+
_CONFIG,
94+
"--linter-output-format",
95+
"{filename} {code}",
96+
),
97+
input=_HTML,
98+
)
99+
assert "myfile.html H020" in result.output

tests/test_djlint/test_djlint.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,16 @@ def test_stdin(runner: CliRunner) -> None:
148148
assert "No files to check!" in result.output
149149

150150

151+
def test_stdin_filename_option(runner: CliRunner) -> None:
152+
result = runner.invoke(
153+
djlint,
154+
("-", "--stdin-filename", "custom.html"),
155+
input='<div><p id="a"></p></div>',
156+
)
157+
assert result.exit_code == 0
158+
assert "Linted 1 file" in result.output
159+
160+
151161
def test_stdin_reformat_without_temp_file(
152162
runner: CliRunner, monkeypatch: pytest.MonkeyPatch
153163
) -> None:

0 commit comments

Comments
 (0)