Skip to content

Commit b580da9

Browse files
authored
Merge pull request #100 from homebysix/dev
1.22.0 merge to main
2 parents 8422e56 + ce830ce commit b580da9

File tree

6 files changed

+192
-8
lines changed

6 files changed

+192
-8
lines changed

.pre-commit-hooks.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,8 @@
128128
- id: format-xml-plist
129129
name: Auto-format plist [XML]
130130
description: Auto-format a Property List (plist) as XML.
131-
entry: plutil -convert xml1
132-
language: system
131+
entry: format-xml-plist
132+
language: python
133133
files: '\.(mobileconfig|pkginfo|plist|recipe)$'
134134
types: [text]
135135

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ All notable changes to this project will be documented in this file. This projec
1414

1515
Nothing yet.
1616

17+
## [1.22.0] - 2025-11-25
18+
19+
### Changed
20+
21+
- `format-xml-plist` hook now uses Python's plistlib instead of macOS's plutil. This enables compatibility with a wider selection of CI/CD runners, including pre-commit.ci.
22+
1723
## [1.21.1] - 2025-10-26
1824

1925
### Fixed
@@ -441,7 +447,8 @@ Nothing yet.
441447

442448
- Initial release
443449

444-
[Unreleased]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.21.0...HEAD
450+
[Unreleased]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.22.0...HEAD
451+
[1.22.0]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.21.0...v1.22.0
445452
[1.21.0]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.20.0...v1.21.0
446453
[1.20.0]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.19.0...v1.20.0
447454
[1.19.0]: https://github.com/homebysix/pre-commit-macadmin/compare/v1.18.0...v1.19.0

README.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ For any hook in this repo you wish to use, add the following to your pre-commit
1515

1616
```yaml
1717
- repo: https://github.com/homebysix/pre-commit-macadmin
18-
rev: v1.21.1
18+
rev: v1.22.0
1919
hooks:
2020
- id: check-plists
2121
# - id: ...
@@ -147,7 +147,7 @@ When combining arguments that take lists (for example: `--required-keys`, `--cat
147147

148148
```yaml
149149
- repo: https://github.com/homebysix/pre-commit-macadmin
150-
rev: v1.21.1
150+
rev: v1.22.0
151151
hooks:
152152
- id: check-munki-pkgsinfo
153153
args: ['--catalogs', 'testing', 'stable', '--']
@@ -157,7 +157,7 @@ But if you also use the `--categories` argument, you would move the trailing `--
157157

158158
```yaml
159159
- repo: https://github.com/homebysix/pre-commit-macadmin
160-
rev: v1.21.1
160+
rev: v1.22.0
161161
hooks:
162162
- id: check-munki-pkgsinfo
163163
args: ['--catalogs', 'testing', 'stable', '--categories', 'Design', 'Engineering', 'Web Browsers', '--']
@@ -169,7 +169,7 @@ If it looks better to your eye, feel free to use a multi-line list for long argu
169169

170170
```yaml
171171
- repo: https://github.com/homebysix/pre-commit-macadmin
172-
rev: v1.21.1
172+
rev: v1.22.0
173173
hooks:
174174
- id: check-munki-pkgsinfo
175175
args: [
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/python
2+
"""This hook auto-formats Property List (plist) files as XML."""
3+
4+
import argparse
5+
import plistlib
6+
from typing import List, Optional
7+
from xml.parsers.expat import ExpatError
8+
9+
10+
def build_argument_parser() -> argparse.ArgumentParser:
11+
"""Build and return the argument parser."""
12+
13+
parser = argparse.ArgumentParser(
14+
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
15+
)
16+
parser.add_argument("filenames", nargs="*", help="Filenames to format.")
17+
return parser
18+
19+
20+
def main(argv: Optional[List[str]] = None) -> int:
21+
"""Main process."""
22+
23+
# Parse command line arguments.
24+
argparser = build_argument_parser()
25+
args = argparser.parse_args(argv)
26+
27+
retval = 0
28+
for filename in args.filenames:
29+
try:
30+
# Read the plist file
31+
with open(filename, "rb") as openfile:
32+
plist_data = plistlib.load(openfile)
33+
34+
# Write it back in XML format
35+
with open(filename, "wb") as openfile:
36+
plistlib.dump(
37+
plist_data, openfile, fmt=plistlib.FMT_XML, sort_keys=True
38+
)
39+
except (ExpatError, ValueError) as err:
40+
print(f"{filename}: plist formatting error: {err}")
41+
retval = 1
42+
except Exception as err:
43+
print(f"{filename}: unexpected error: {err}")
44+
retval = 1
45+
46+
return retval
47+
48+
49+
if __name__ == "__main__":
50+
exit(main())

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
name="pre-commit-macadmin",
77
description="Pre-commit hooks for Mac admins, client engineers, and IT consultants.",
88
url="https://github.com/homebysix/pre-commit-macadmin",
9-
version="1.21.1",
9+
version="1.22.0",
1010
author="Elliot Jordan",
1111
author_email="[email protected]",
1212
packages=["pre_commit_macadmin_hooks"],
@@ -28,6 +28,7 @@
2828
"check-preference-manifests = pre_commit_macadmin_hooks.check_preference_manifests:main",
2929
"forbid-autopkg-overrides = pre_commit_macadmin_hooks.forbid_autopkg_overrides:main",
3030
"forbid-autopkg-trust-info = pre_commit_macadmin_hooks.forbid_autopkg_trust_info:main",
31+
"format-xml-plist = pre_commit_macadmin_hooks.format_xml_plist:main",
3132
"munki-makecatalogs = pre_commit_macadmin_hooks.munki_makecatalogs:main",
3233
]
3334
},

tests/test_format_xml_plist.py

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/python
2+
"""Tests for the format_xml_plist hook."""
3+
4+
import plistlib
5+
import tempfile
6+
import unittest
7+
from pathlib import Path
8+
9+
from pre_commit_macadmin_hooks import format_xml_plist
10+
11+
12+
class TestFormatXMLPlist(unittest.TestCase):
13+
"""Tests for the format_xml_plist hook."""
14+
15+
def test_format_valid_xml_plist(self):
16+
"""Test formatting a valid XML plist."""
17+
test_data = {"key1": "value1", "key2": 123, "key3": ["a", "b", "c"]}
18+
19+
with tempfile.NamedTemporaryFile(
20+
mode="wb", suffix=".plist", delete=False
21+
) as tmp:
22+
# Write plist in XML format
23+
plistlib.dump(test_data, tmp, fmt=plistlib.FMT_XML)
24+
tmp_path = tmp.name
25+
26+
try:
27+
# Run the formatter
28+
result = format_xml_plist.main([tmp_path])
29+
30+
# Should succeed
31+
self.assertEqual(result, 0)
32+
33+
# Verify the file can still be read and contains the same data
34+
with open(tmp_path, "rb") as f:
35+
formatted_data = plistlib.load(f)
36+
self.assertEqual(formatted_data, test_data)
37+
finally:
38+
Path(tmp_path).unlink()
39+
40+
def test_format_binary_plist(self):
41+
"""Test formatting a binary plist converts it to XML."""
42+
test_data = {"key1": "value1", "key2": 456}
43+
44+
with tempfile.NamedTemporaryFile(
45+
mode="wb", suffix=".plist", delete=False
46+
) as tmp:
47+
# Write plist in binary format
48+
plistlib.dump(test_data, tmp, fmt=plistlib.FMT_BINARY)
49+
tmp_path = tmp.name
50+
51+
try:
52+
# Run the formatter
53+
result = format_xml_plist.main([tmp_path])
54+
55+
# Should succeed
56+
self.assertEqual(result, 0)
57+
58+
# Verify the file is now XML format
59+
with open(tmp_path, "rb") as f:
60+
content = f.read()
61+
# XML plists start with <?xml
62+
self.assertTrue(content.startswith(b"<?xml"))
63+
64+
# Verify data integrity
65+
with open(tmp_path, "rb") as f:
66+
formatted_data = plistlib.load(f)
67+
self.assertEqual(formatted_data, test_data)
68+
finally:
69+
Path(tmp_path).unlink()
70+
71+
def test_format_invalid_plist(self):
72+
"""Test that invalid plists return an error."""
73+
with tempfile.NamedTemporaryFile(
74+
mode="w", suffix=".plist", delete=False
75+
) as tmp:
76+
tmp.write("This is not a valid plist")
77+
tmp_path = tmp.name
78+
79+
try:
80+
# Run the formatter
81+
result = format_xml_plist.main([tmp_path])
82+
83+
# Should fail
84+
self.assertEqual(result, 1)
85+
finally:
86+
Path(tmp_path).unlink()
87+
88+
def test_format_multiple_files(self):
89+
"""Test formatting multiple plist files."""
90+
test_data_1 = {"file": "first"}
91+
test_data_2 = {"file": "second"}
92+
93+
with tempfile.NamedTemporaryFile(
94+
mode="wb", suffix=".plist", delete=False
95+
) as tmp1:
96+
plistlib.dump(test_data_1, tmp1, fmt=plistlib.FMT_XML)
97+
tmp1_path = tmp1.name
98+
99+
with tempfile.NamedTemporaryFile(
100+
mode="wb", suffix=".plist", delete=False
101+
) as tmp2:
102+
plistlib.dump(test_data_2, tmp2, fmt=plistlib.FMT_BINARY)
103+
tmp2_path = tmp2.name
104+
105+
try:
106+
# Run the formatter on both files
107+
result = format_xml_plist.main([tmp1_path, tmp2_path])
108+
109+
# Should succeed
110+
self.assertEqual(result, 0)
111+
112+
# Verify both files
113+
with open(tmp1_path, "rb") as f:
114+
data1 = plistlib.load(f)
115+
with open(tmp2_path, "rb") as f:
116+
data2 = plistlib.load(f)
117+
118+
self.assertEqual(data1, test_data_1)
119+
self.assertEqual(data2, test_data_2)
120+
finally:
121+
Path(tmp1_path).unlink()
122+
Path(tmp2_path).unlink()
123+
124+
125+
if __name__ == "__main__":
126+
unittest.main()

0 commit comments

Comments
 (0)