xml-fmt 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,3 @@
1
+ .tox/
2
+ .*_cache
3
+ __pycache__
@@ -0,0 +1,18 @@
1
+ Permission is hereby granted, free of charge, to any person obtaining a
2
+ copy of this software and associated documentation files (the
3
+ "Software"), to deal in the Software without restriction, including
4
+ without limitation the rights to use, copy, modify, merge, publish,
5
+ distribute, sublicense, and/or sell copies of the Software, and to
6
+ permit persons to whom the Software is furnished to do so, subject to
7
+ the following conditions:
8
+
9
+ The above copyright notice and this permission notice shall be included
10
+ in all copies or substantial portions of the Software.
11
+
12
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
13
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
14
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
15
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
16
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
17
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
18
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
xml_fmt-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,60 @@
1
+ Metadata-Version: 2.4
2
+ Name: xml-fmt
3
+ Version: 1.0.0
4
+ Summary: Run pre-commit with uv
5
+ Project-URL: Bug Tracker, https://github.com/tox-dev/xml-fmt/issues
6
+ Project-URL: Changelog, https://github.com/tox-dev/xml-fmt/releases
7
+ Project-URL: Documentation, https://github.com/tox-dev/xml-fmt/
8
+ Project-URL: Source Code, https://github.com/tox-dev/xml-fmt
9
+ Author-email: Bernat Gabor <gaborjbernat@gmail.com>
10
+ License: Permission is hereby granted, free of charge, to any person obtaining a
11
+ copy of this software and associated documentation files (the
12
+ "Software"), to deal in the Software without restriction, including
13
+ without limitation the rights to use, copy, modify, merge, publish,
14
+ distribute, sublicense, and/or sell copies of the Software, and to
15
+ permit persons to whom the Software is furnished to do so, subject to
16
+ the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included
19
+ in all copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
22
+ OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+ License-File: LICENSE.txt
29
+ Keywords: XML,format
30
+ Classifier: License :: OSI Approved :: MIT License
31
+ Classifier: Operating System :: OS Independent
32
+ Classifier: Programming Language :: Python
33
+ Classifier: Programming Language :: Python :: 3 :: Only
34
+ Classifier: Programming Language :: Python :: 3.9
35
+ Classifier: Programming Language :: Python :: 3.10
36
+ Classifier: Programming Language :: Python :: 3.11
37
+ Classifier: Programming Language :: Python :: 3.12
38
+ Classifier: Programming Language :: Python :: 3.13
39
+ Requires-Python: >=3.9
40
+ Description-Content-Type: text/markdown
41
+
42
+ # xml-fmt
43
+
44
+ [![PyPI](https://img.shields.io/pypi/v/xml-fmt?style=flat-square)](https://pypi.org/project/xml-fmt)
45
+ [![PyPI - Implementation](https://img.shields.io/pypi/implementation/xml-fmt?style=flat-square)](https://pypi.org/project/xml-fmt)
46
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/xml-fmt?style=flat-square)](https://pypi.org/project/xml-fmt)
47
+ [![Downloads](https://static.pepy.tech/badge/xml-fmt/month)](https://pepy.tech/project/xml-fmt)
48
+ [![PyPI - License](https://img.shields.io/pypi/l/xml-fmt?style=flat-square)](https://opensource.org/licenses/MIT)
49
+ [![check](https://github.com/tox-dev/xml-fmt/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/xml-fmt/actions/workflows/check.yaml)
50
+
51
+ ### Using `xml-fmt` with pre-commit
52
+
53
+ Add it to your `.pre-commit-config.yaml`:
54
+
55
+ ```yaml
56
+ - repo: https://github.com/tox-dev/xml-fmt
57
+ rev: "" # Use the sha / tag you want to point at
58
+ hooks:
59
+ - id: pyproject-fmt
60
+ ```
@@ -0,0 +1,19 @@
1
+ # xml-fmt
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/xml-fmt?style=flat-square)](https://pypi.org/project/xml-fmt)
4
+ [![PyPI - Implementation](https://img.shields.io/pypi/implementation/xml-fmt?style=flat-square)](https://pypi.org/project/xml-fmt)
5
+ [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/xml-fmt?style=flat-square)](https://pypi.org/project/xml-fmt)
6
+ [![Downloads](https://static.pepy.tech/badge/xml-fmt/month)](https://pepy.tech/project/xml-fmt)
7
+ [![PyPI - License](https://img.shields.io/pypi/l/xml-fmt?style=flat-square)](https://opensource.org/licenses/MIT)
8
+ [![check](https://github.com/tox-dev/xml-fmt/actions/workflows/check.yaml/badge.svg)](https://github.com/tox-dev/xml-fmt/actions/workflows/check.yaml)
9
+
10
+ ### Using `xml-fmt` with pre-commit
11
+
12
+ Add it to your `.pre-commit-config.yaml`:
13
+
14
+ ```yaml
15
+ - repo: https://github.com/tox-dev/xml-fmt
16
+ rev: "" # Use the sha / tag you want to point at
17
+ hooks:
18
+ - id: pyproject-fmt
19
+ ```
@@ -0,0 +1,148 @@
1
+ [build-system]
2
+ build-backend = "hatchling.build"
3
+ requires = [
4
+ "hatch-vcs>=0.4",
5
+ "hatchling>=1.27",
6
+ ]
7
+
8
+ [project]
9
+ name = "xml-fmt"
10
+ description = "Run pre-commit with uv"
11
+ readme = "README.md"
12
+ keywords = [
13
+ "format",
14
+ "XML",
15
+ ]
16
+ license.file = "LICENSE.txt"
17
+ authors = [
18
+ { name = "Bernat Gabor", email = "gaborjbernat@gmail.com" },
19
+ ]
20
+ requires-python = ">=3.9"
21
+ classifiers = [
22
+ "License :: OSI Approved :: MIT License",
23
+ "Operating System :: OS Independent",
24
+ "Programming Language :: Python",
25
+ "Programming Language :: Python :: 3 :: Only",
26
+ "Programming Language :: Python :: 3.9",
27
+ "Programming Language :: Python :: 3.10",
28
+ "Programming Language :: Python :: 3.11",
29
+ "Programming Language :: Python :: 3.12",
30
+ "Programming Language :: Python :: 3.13",
31
+ ]
32
+ dynamic = [
33
+ "version",
34
+ ]
35
+ urls."Bug Tracker" = "https://github.com/tox-dev/xml-fmt/issues"
36
+ urls."Changelog" = "https://github.com/tox-dev/xml-fmt/releases"
37
+ urls.Documentation = "https://github.com/tox-dev/xml-fmt/"
38
+ urls."Source Code" = "https://github.com/tox-dev/xml-fmt"
39
+ scripts.xml-fmt = "xml_fmt.__main__:run"
40
+
41
+ [dependency-groups]
42
+ dev = [
43
+ { include-group = "test" },
44
+ { include-group = "type" },
45
+ ]
46
+ test = [
47
+ "covdefaults>=2.3",
48
+ "pytest>=8.3.4",
49
+ "pytest-cov>=6",
50
+ "pytest-mock>=3.14",
51
+ ]
52
+ type = [
53
+ "mypy==1.14.1",
54
+ { include-group = "test" },
55
+ ]
56
+ fix = [
57
+ "pre-commit-uv>=4.1.4",
58
+ ]
59
+ pkg-meta = [
60
+ "check-wheel-contents>=0.6.1",
61
+ "twine>=6.1",
62
+ "uv>=0.5.26",
63
+ ]
64
+
65
+ [tool.hatch]
66
+ build.dev-mode-dirs = [
67
+ "src",
68
+ ]
69
+ build.targets.sdist.include = [
70
+ "/src",
71
+ "/tests",
72
+ "tox.ini",
73
+ ]
74
+ build.targets.wheel.only-include = [ "src" ]
75
+ build.targets.wheel.sources = [ "src" ]
76
+ version.source = "vcs"
77
+
78
+ [tool.ruff]
79
+ target-version = "py39"
80
+ line-length = 120
81
+ format.preview = true
82
+ format.docstring-code-line-length = 100
83
+ format.docstring-code-format = true
84
+ lint.select = [
85
+ "ALL",
86
+ ]
87
+ lint.ignore = [
88
+ "COM812", # Conflict with formatter
89
+ "CPY", # No copyright statements
90
+ "D203", # `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible.
91
+ "D212", # `multi-line-summary-first-line` (D212) and `multi-line-summary-second-line` (D213) are incompatible.
92
+ "DOC501", # not working with Sphinx
93
+ "ISC001", # Conflict with formatter
94
+ "S104", # Possible binding to all interfaces
95
+ ]
96
+ lint.per-file-ignores."tests/**/*.py" = [
97
+ "D", # don't care about documentation in tests
98
+ "FBT", # don't care about booleans as positional arguments in tests
99
+ "INP001", # no implicit namespace
100
+ "PLC2701", # private import
101
+ "PLR0913", # any number of arguments in tests
102
+ "PLR0917", # any number of arguments in tests
103
+ "PLR2004", # Magic value used in comparison, consider replacing with a constant variable.
104
+ "S", # `subprocess` call: check for execution of untrusted input
105
+ "S101", # asserts allowed in tests.
106
+ ]
107
+ lint.isort = { known-first-party = [
108
+ "xml_fmt",
109
+ ], required-imports = [
110
+ "from __future__ import annotations",
111
+ ] }
112
+ lint.preview = true
113
+
114
+ [tool.codespell]
115
+ builtin = "clear,usage,en-GB_to_en-US"
116
+ count = true
117
+
118
+ [tool.pyproject-fmt]
119
+ max_supported_python = "3.13"
120
+
121
+ [tool.pytest.ini_options]
122
+ filterwarnings = [ "error" ]
123
+ verbosity_assertions = 2
124
+ testpaths = [
125
+ "tests",
126
+ ]
127
+
128
+ [tool.coverage]
129
+ html.show_contexts = true
130
+ html.skip_covered = false
131
+ paths.source = [
132
+ "src",
133
+ ".tox/*/.venv/lib/*/site-packages",
134
+ ".tox\\*\\.venv\\Lib\\site-packages",
135
+ ".tox/*/lib/*/site-packages",
136
+ ".tox\\*\\Lib\\site-packages",
137
+ "**/src",
138
+ "**\\src",
139
+ ]
140
+ report.fail_under = 63
141
+ run.parallel = true
142
+ run.plugins = [
143
+ "covdefaults",
144
+ ]
145
+
146
+ [tool.mypy]
147
+ show_error_codes = true
148
+ strict = true
@@ -0,0 +1 @@
1
+ """XML formatter."""
@@ -0,0 +1,160 @@
1
+ """Format XMLs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from argparse import ArgumentDefaultsHelpFormatter, ArgumentParser, ArgumentTypeError, Namespace
8
+ from difflib import unified_diff
9
+ from importlib.metadata import version
10
+ from pathlib import Path
11
+ from typing import TYPE_CHECKING
12
+ from xml.etree.ElementTree import XML, indent, tostring # noqa: S405
13
+
14
+ if TYPE_CHECKING:
15
+ from collections.abc import Iterable, Sequence
16
+
17
+
18
+ class Options(Namespace):
19
+ """Options for pyproject-fmt tool."""
20
+
21
+ inputs: list[Path | None]
22
+ stdout: bool
23
+ check: bool
24
+ no_print_diff: bool
25
+
26
+ indent: str
27
+ expand_empty_elements: bool
28
+
29
+
30
+ def run(args: Sequence[str] | None = None) -> int:
31
+ """
32
+ Run the formatter.
33
+
34
+ :param args: command line arguments, by default use sys.argv[1:]
35
+ :return: exit code - 0 means already formatted correctly, otherwise 1
36
+ """ # noqa: DOC201
37
+ parser = _build_cli()
38
+ opts = Options()
39
+ parser.parse_args(args=args, namespace=opts)
40
+ results = [_handle_one(filename, opts) for filename in opts.inputs]
41
+ return 1 if any(results) else 0 # exit with non success on change
42
+
43
+
44
+ class _Formatter(ArgumentDefaultsHelpFormatter):
45
+ def __init__(self, prog: str) -> None:
46
+ super().__init__(prog, max_help_position=29, width=240)
47
+
48
+
49
+ def _build_cli() -> ArgumentParser:
50
+ parser = ArgumentParser(formatter_class=_Formatter, prog="xml-fmt")
51
+ parser.add_argument(
52
+ "-V",
53
+ "--version",
54
+ action="version",
55
+ help="version of the project",
56
+ version=f"%(prog)s ({version('xml-fmt')})",
57
+ )
58
+
59
+ mode_group = parser.add_argument_group("run mode")
60
+ mode = mode_group.add_mutually_exclusive_group()
61
+ msg = "print the formatted XML to the stdout, implied if reading from stdin"
62
+ mode.add_argument("-s", "--stdout", action="store_true", help=msg)
63
+ msg = "check and fail if any input would be formatted, printing any diffs"
64
+ mode.add_argument("--check", action="store_true", help=msg)
65
+ mode_group.add_argument(
66
+ "-n",
67
+ "--no-print-diff",
68
+ action="store_true",
69
+ help="controls if to print diff - when running in check mode",
70
+ )
71
+
72
+ format_group = parser.add_argument_group("formatting behavior")
73
+ format_group.add_argument("--indent", default=" ", help="indentation characters to use - two space by default")
74
+ format_group.add_argument(
75
+ "--expand-empty-elements",
76
+ action="store_true",
77
+ help="controls if empty XML elements should be collapsed into a self closing element or expanded",
78
+ )
79
+
80
+ msg = "XML (XSD) file(s) to format, use '-' to read from stdin"
81
+ parser.add_argument("inputs", nargs="+", type=_path_creator, help=msg)
82
+ return parser
83
+
84
+
85
+ def _path_creator(argument: str) -> Path | None:
86
+ if argument == "-":
87
+ return None # stdin, no further validation needed
88
+ path = Path(argument).absolute()
89
+ if not path.exists():
90
+ msg = "path does not exist"
91
+ raise ArgumentTypeError(msg)
92
+ if not path.is_file():
93
+ msg = "path is not a file"
94
+ raise ArgumentTypeError(msg)
95
+ if not os.access(path, os.R_OK):
96
+ msg = "cannot read path"
97
+ raise ArgumentTypeError(msg)
98
+ if not os.access(path, os.W_OK):
99
+ msg = "cannot write path"
100
+ raise ArgumentTypeError(msg)
101
+ return path
102
+
103
+
104
+ def _handle_one(filename: Path | None, opts: Options) -> bool:
105
+ before = sys.stdin.read() if filename is None else filename.read_text(encoding="utf-8")
106
+ formatted = _format(before, opts)
107
+
108
+ changed = before != formatted
109
+ if filename is None or opts.stdout: # when reading from stdin or writing to stdout, print new format
110
+ print(formatted, end="") # noqa: T201
111
+ return changed
112
+
113
+ if before != formatted and not opts.check:
114
+ filename.write_text(formatted, encoding="utf-8")
115
+ if opts.no_print_diff:
116
+ return changed
117
+ try:
118
+ name = str(filename.relative_to(Path.cwd()))
119
+ except ValueError:
120
+ name = str(filename)
121
+ diff: Iterable[str] = []
122
+ if changed:
123
+ diff = unified_diff(before.splitlines(), formatted.splitlines(), fromfile=name, tofile=name)
124
+
125
+ if diff:
126
+ diff = _color_diff(diff)
127
+ print("\n".join(diff)) # print diff on change # noqa: T201
128
+ else:
129
+ print(f"no change for {name}") # noqa: T201
130
+ return changed
131
+
132
+
133
+ def _format(raw: str, opts: Options) -> str:
134
+ element = XML(raw)
135
+ indent(element, opts.indent)
136
+ return tostring(
137
+ element,
138
+ encoding="unicode",
139
+ xml_declaration=True,
140
+ short_empty_elements=not opts.expand_empty_elements,
141
+ )
142
+
143
+
144
+ GREEN = "\u001b[32m"
145
+ RED = "\u001b[31m"
146
+ RESET = "\u001b[0m"
147
+
148
+
149
+ def _color_diff(diff: Iterable[str]) -> Iterable[str]:
150
+ for line in diff:
151
+ if line.startswith("+"):
152
+ yield f"{GREEN}{line}{RESET}"
153
+ elif line.startswith("-"):
154
+ yield f"{RED}{line}{RESET}"
155
+ else:
156
+ yield line
157
+
158
+
159
+ if __name__ == "__main__":
160
+ raise SystemExit(run(sys.argv[1:]))
File without changes
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ from io import StringIO
5
+ from typing import TYPE_CHECKING
6
+
7
+ import pytest
8
+
9
+ from xml_fmt.__main__ import run
10
+
11
+ if TYPE_CHECKING:
12
+ from pathlib import Path
13
+
14
+ from pytest_mock import MockerFixture
15
+
16
+
17
+ @pytest.fixture
18
+ def min_xml_str() -> str:
19
+ return "<?xml version='1.0' encoding='utf-8'?>\n<root />"
20
+
21
+
22
+ @pytest.fixture
23
+ def min_xml(tmp_path: Path, min_xml_str: str) -> Path:
24
+ path = tmp_path / "name.xml"
25
+ path.write_text(min_xml_str)
26
+ return path
27
+
28
+
29
+ def test_xml_help(capsys: pytest.CaptureFixture[str]) -> None:
30
+ with pytest.raises(SystemExit) as exc:
31
+ run(["--help"])
32
+
33
+ assert exc.value.code == 0
34
+
35
+ out, err = capsys.readouterr()
36
+ assert not err
37
+ assert "XML (XSD)" in out
38
+
39
+
40
+ def test_xml_format_no_print_diff(capsys: pytest.CaptureFixture[str], tmp_path: Path, min_xml_str: str) -> None:
41
+ xml = tmp_path / "name.xml"
42
+ xml.write_text("<?xml version='1.0' encoding='utf-8'?>\n<root></root>\n")
43
+
44
+ exit_code = run([str(xml), "--no-print-diff"])
45
+
46
+ assert xml.read_text() == min_xml_str
47
+ assert exit_code == 1
48
+
49
+ out, err = capsys.readouterr()
50
+ assert not err
51
+ assert out.splitlines() == []
52
+
53
+
54
+ def test_xml_format_already_good(
55
+ capsys: pytest.CaptureFixture[str], min_xml: Path, monkeypatch: pytest.MonkeyPatch, min_xml_str: str
56
+ ) -> None:
57
+ monkeypatch.setenv("NO_FMT", "1")
58
+
59
+ exit_code = run([str(min_xml)])
60
+ assert exit_code == 0
61
+
62
+ assert min_xml.read_text() == min_xml_str
63
+
64
+ out, err = capsys.readouterr()
65
+ assert not err
66
+ assert out.splitlines() == [f"no change for {min_xml}"]
67
+
68
+
69
+ def test_xml_stdin(capsys: pytest.CaptureFixture[str], mocker: MockerFixture, min_xml_str: str) -> None:
70
+ mocker.patch("sys.stdin", StringIO("<?xml version='1.0' encoding='utf-8'?>\n<root></root>\n"))
71
+
72
+ exit_code = run(["-"])
73
+ assert exit_code == 1
74
+
75
+ out, err = capsys.readouterr()
76
+ assert not err
77
+ assert out == min_xml_str
78
+
79
+
80
+ def test_xml_path_missing(capsys: pytest.CaptureFixture[str], tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
81
+ monkeypatch.chdir(tmp_path)
82
+
83
+ with pytest.raises(SystemExit):
84
+ run(["demo.xml"])
85
+
86
+ out, err = capsys.readouterr()
87
+ assert "\nxml-fmt: error: argument inputs: path does not exist\n" in err
88
+ assert not out
89
+
90
+
91
+ def test_xml_path_is_folder(capsys: pytest.CaptureFixture[str], tmp_path: Path) -> None:
92
+ path = tmp_path / "demo.xml"
93
+ os.mkfifo(path)
94
+
95
+ with pytest.raises(SystemExit):
96
+ run([str(path)])
97
+
98
+ out, err = capsys.readouterr()
99
+ assert "\nxml-fmt: error: argument inputs: path is not a file\n" in err
100
+ assert not out
101
+
102
+
103
+ def test_xml_path_no_read(capsys: pytest.CaptureFixture[str], min_xml: Path) -> None:
104
+ start = min_xml.stat().st_mode
105
+ min_xml.chmod(0o000)
106
+
107
+ try:
108
+ with pytest.raises(SystemExit):
109
+ run([str(min_xml)])
110
+ finally:
111
+ min_xml.chmod(start)
112
+
113
+ out, err = capsys.readouterr()
114
+ assert "\nxml-fmt: error: argument inputs: cannot read path\n" in err
115
+ assert not out
116
+
117
+
118
+ def test_xml_path_no_write(capsys: pytest.CaptureFixture[str], min_xml: Path) -> None:
119
+ start = min_xml.stat().st_mode
120
+ min_xml.chmod(0o400)
121
+
122
+ try:
123
+ with pytest.raises(SystemExit):
124
+ run([str(min_xml)])
125
+ finally:
126
+ min_xml.chmod(start)
127
+
128
+ out, err = capsys.readouterr()
129
+ assert "\nxml-fmt: error: argument inputs: cannot write path\n" in err
130
+ assert not out