diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..6c895f2 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,31 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install package + run: | + python -m pip install --upgrade pip + pip install -e . + + - name: Run tests + run: python -m unittest discover -s tests -v diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a809f7..cbd1ce4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -19,7 +19,7 @@ repos: rev: v3.21.2 hooks: - id: pyupgrade - args: ['--py36-plus'] + args: ['--py310-plus'] - repo: https://github.com/pycqa/flake8 rev: 7.3.0 hooks: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..458486c --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,41 @@ +# Contributing + +Thanks for considering a contribution! + +## Setup + +```sh +python3 -m venv .venv +.venv/bin/pip install -e . +.venv/bin/pip install pre-commit +.venv/bin/pre-commit install +``` + +## Running tests + +```sh +.venv/bin/python -m unittest discover -vs tests +``` + +Tests also run in CI on Python 3.10–3.14. New hooks should ship with tests +in `tests/`. + +## Linting and formatting + +`pre-commit` runs `black`, `isort`, `flake8`, and `pyupgrade`. Either let +the installed hook handle it on commit, or run it manually: + +```sh +.venv/bin/pre-commit run --all-files +``` + +pre-commit.ci also runs these hooks on every PR. + +## Adding a new hook + +A new hook needs: + +- An entry in `.pre-commit-hooks.yaml` +- An entry point in `setup.py` +- A usage example in `README.md` +- Tests in `tests/` diff --git a/pre_commit_macadmin_hooks/check_autopkg_recipe_list.py b/pre_commit_macadmin_hooks/check_autopkg_recipe_list.py index bcc5217..33dac68 100755 --- a/pre_commit_macadmin_hooks/check_autopkg_recipe_list.py +++ b/pre_commit_macadmin_hooks/check_autopkg_recipe_list.py @@ -8,7 +8,6 @@ import argparse import json import plistlib -from typing import List, Optional from xml.parsers.expat import ExpatError import ruamel.yaml @@ -26,7 +25,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_autopkg_recipes.py b/pre_commit_macadmin_hooks/check_autopkg_recipes.py index 89dc51d..6b1c891 100755 --- a/pre_commit_macadmin_hooks/check_autopkg_recipes.py +++ b/pre_commit_macadmin_hooks/check_autopkg_recipes.py @@ -6,7 +6,7 @@ import os import sys from contextlib import contextmanager -from typing import Any, Dict, List +from typing import Any from packaging.version import Version @@ -88,7 +88,7 @@ def build_argument_parser() -> argparse.ArgumentParser: def validate_recipe_prefix( - recipe: Dict[str, Any], filename: str, prefix: List[str] + recipe: dict[str, Any], filename: str, prefix: list[str] ) -> bool: """Verify that the recipe identifier starts with the expected prefix.""" diff --git a/pre_commit_macadmin_hooks/check_git_config_email.py b/pre_commit_macadmin_hooks/check_git_config_email.py index fd55a26..2339332 100644 --- a/pre_commit_macadmin_hooks/check_git_config_email.py +++ b/pre_commit_macadmin_hooks/check_git_config_email.py @@ -3,7 +3,6 @@ import argparse import subprocess -from typing import List, Optional def build_argument_parser() -> argparse.ArgumentParser: @@ -20,7 +19,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_jamf_extension_attributes.py b/pre_commit_macadmin_hooks/check_jamf_extension_attributes.py index 112577d..bd38d04 100644 --- a/pre_commit_macadmin_hooks/check_jamf_extension_attributes.py +++ b/pre_commit_macadmin_hooks/check_jamf_extension_attributes.py @@ -3,7 +3,6 @@ import argparse import re -from typing import List, Optional from pre_commit_macadmin_hooks.util import validate_shebangs @@ -24,7 +23,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_jamf_json_manifests.py b/pre_commit_macadmin_hooks/check_jamf_json_manifests.py index 8f346a9..4face82 100755 --- a/pre_commit_macadmin_hooks/check_jamf_json_manifests.py +++ b/pre_commit_macadmin_hooks/check_jamf_json_manifests.py @@ -8,7 +8,7 @@ import argparse import json from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple +from typing import Any from pre_commit_macadmin_hooks.util import validate_required_keys @@ -45,7 +45,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def validate_key_types(name: str, manifest: Dict[str, Any], filename: str) -> bool: +def validate_key_types(name: str, manifest: dict[str, Any], filename: str) -> bool: """Validation of manifest key types.""" # Manifest keys and their known types. Omitted keys are left unvalidated. @@ -80,8 +80,8 @@ def validate_key_types(name: str, manifest: Dict[str, Any], filename: str) -> bo def validate_type( - name: str, property: Dict[str, Any], filename: str -) -> Tuple[bool, Optional[str]]: # noqa: A002 + name: str, property: dict[str, Any], filename: str +) -> tuple[bool, str | None]: # noqa: A002 """Ensure property type keu is present and among expected values.""" passed = True type_found = None @@ -102,7 +102,7 @@ def validate_type( def validate_list_item_types( - name: str, manifest: Dict[str, Any], filename: str + name: str, manifest: dict[str, Any], filename: str ) -> bool: """Validation of list member items.""" @@ -131,7 +131,7 @@ def validate_list_item_types( def validate_default( - name: str, prop: Dict[str, Any], type_found: Optional[str], filename: str + name: str, prop: dict[str, Any], type_found: str | None, filename: str ) -> bool: """Ensure that default values have the expected type.""" passed = True @@ -151,7 +151,7 @@ def validate_default( return passed -def validate_urls(name: str, prop: Dict[str, Any], filename: str) -> bool: +def validate_urls(name: str, prop: dict[str, Any], filename: str) -> bool: """Ensure that URL values are actual URLs.""" passed = True @@ -167,7 +167,7 @@ def validate_urls(name: str, prop: Dict[str, Any], filename: str) -> bool: return passed -def validate_properties(properties: Dict[str, Any], filename: str) -> bool: +def validate_properties(properties: dict[str, Any], filename: str) -> bool: """Given a list of properties, run validation on their contents.""" passed = True @@ -206,7 +206,7 @@ def validate_properties(properties: Dict[str, Any], filename: str) -> bool: return passed -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_jamf_profiles.py b/pre_commit_macadmin_hooks/check_jamf_profiles.py index 692fdb1..c03e03d 100644 --- a/pre_commit_macadmin_hooks/check_jamf_profiles.py +++ b/pre_commit_macadmin_hooks/check_jamf_profiles.py @@ -3,7 +3,6 @@ import argparse import plistlib -from typing import List, Optional from xml.parsers.expat import ExpatError @@ -17,7 +16,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_jamf_scripts.py b/pre_commit_macadmin_hooks/check_jamf_scripts.py index e08c4b2..5c3c18d 100644 --- a/pre_commit_macadmin_hooks/check_jamf_scripts.py +++ b/pre_commit_macadmin_hooks/check_jamf_scripts.py @@ -2,7 +2,6 @@ """Check Jamf scripts for common issues.""" import argparse -from typing import List, Optional from pre_commit_macadmin_hooks.util import validate_shebangs @@ -23,7 +22,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_munki_pkgsinfo.py b/pre_commit_macadmin_hooks/check_munki_pkgsinfo.py index aaa1a22..b1989d8 100755 --- a/pre_commit_macadmin_hooks/check_munki_pkgsinfo.py +++ b/pre_commit_macadmin_hooks/check_munki_pkgsinfo.py @@ -5,7 +5,6 @@ import os import plistlib from pathlib import Path -from typing import List, Optional from xml.parsers.expat import ExpatError from pre_commit_macadmin_hooks.util import ( @@ -89,7 +88,7 @@ def _check_case_sensitive_path(path: str) -> bool: p = p.parent -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Typical extensions for installer packages. diff --git a/pre_commit_macadmin_hooks/check_munkiadmin_scripts.py b/pre_commit_macadmin_hooks/check_munkiadmin_scripts.py index 988529e..a730dc0 100755 --- a/pre_commit_macadmin_hooks/check_munkiadmin_scripts.py +++ b/pre_commit_macadmin_hooks/check_munkiadmin_scripts.py @@ -3,7 +3,6 @@ import argparse import os -from typing import List, Optional from pre_commit_macadmin_hooks.util import validate_shebangs @@ -24,7 +23,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_munkipkg_buildinfo.py b/pre_commit_macadmin_hooks/check_munkipkg_buildinfo.py index 1c0dfb8..118271c 100755 --- a/pre_commit_macadmin_hooks/check_munkipkg_buildinfo.py +++ b/pre_commit_macadmin_hooks/check_munkipkg_buildinfo.py @@ -4,7 +4,7 @@ import argparse import json import plistlib -from typing import Any, Dict, List, Optional +from typing import Any from xml.parsers.expat import ExpatError import ruamel.yaml @@ -29,7 +29,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def validate_buildinfo_key_types(buildinfo: Dict[str, Any], filename: str) -> bool: +def validate_buildinfo_key_types(buildinfo: dict[str, Any], filename: str) -> bool: """Ensure build-info files contain the proper types.""" # Pkginfo keys and their known types. Omitted keys are left unvalidated. @@ -62,7 +62,7 @@ def validate_buildinfo_key_types(buildinfo: Dict[str, Any], filename: str) -> bo return passed -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_outset_scripts.py b/pre_commit_macadmin_hooks/check_outset_scripts.py index 074cf10..d945971 100644 --- a/pre_commit_macadmin_hooks/check_outset_scripts.py +++ b/pre_commit_macadmin_hooks/check_outset_scripts.py @@ -3,7 +3,6 @@ import argparse import os -from typing import List, Optional from pre_commit_macadmin_hooks.util import validate_shebangs @@ -24,7 +23,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_plists.py b/pre_commit_macadmin_hooks/check_plists.py index 363332e..eb75578 100755 --- a/pre_commit_macadmin_hooks/check_plists.py +++ b/pre_commit_macadmin_hooks/check_plists.py @@ -3,7 +3,6 @@ import argparse import plistlib -from typing import List, Optional from xml.parsers.expat import ExpatError @@ -17,7 +16,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/check_preference_manifests.py b/pre_commit_macadmin_hooks/check_preference_manifests.py index 4bf72a2..aca9768 100755 --- a/pre_commit_macadmin_hooks/check_preference_manifests.py +++ b/pre_commit_macadmin_hooks/check_preference_manifests.py @@ -9,7 +9,7 @@ import argparse import plistlib from datetime import datetime -from typing import Any, Dict, List, Optional, Tuple +from typing import Any from xml.parsers.expat import ExpatError from pre_commit_macadmin_hooks.util import PLIST_TYPES @@ -40,8 +40,8 @@ def build_argument_parser() -> argparse.ArgumentParser: def validate_required_keys( - input_dict: Dict[str, Any], - required_keys: Tuple[str, ...], + input_dict: dict[str, Any], + required_keys: tuple[str, ...], dict_name: str, filename: str, ) -> bool: @@ -54,7 +54,7 @@ def validate_required_keys( return passed -def validate_manifest_key_types(manifest: Dict[str, Any], filename: str) -> bool: +def validate_manifest_key_types(manifest: dict[str, Any], filename: str) -> bool: """Validation of manifest key types.""" # manifest keys and their known types. Omitted keys are left un-validated. @@ -410,7 +410,7 @@ def validate_subkeys(subkeys, filename): return passed -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/forbid_autopkg_overrides.py b/pre_commit_macadmin_hooks/forbid_autopkg_overrides.py index 3f85856..1ad6bc4 100755 --- a/pre_commit_macadmin_hooks/forbid_autopkg_overrides.py +++ b/pre_commit_macadmin_hooks/forbid_autopkg_overrides.py @@ -2,7 +2,6 @@ """This hook prevents AutoPkg overrides from being added to the repo.""" import argparse -from typing import List, Optional from pre_commit_macadmin_hooks.util import load_autopkg_recipe @@ -17,7 +16,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Overrides should not contain top-level Process arrays. diff --git a/pre_commit_macadmin_hooks/forbid_autopkg_trust_info.py b/pre_commit_macadmin_hooks/forbid_autopkg_trust_info.py index 1333b5e..8e67e86 100644 --- a/pre_commit_macadmin_hooks/forbid_autopkg_trust_info.py +++ b/pre_commit_macadmin_hooks/forbid_autopkg_trust_info.py @@ -3,7 +3,6 @@ repo.""" import argparse -from typing import List, Optional from pre_commit_macadmin_hooks.util import load_autopkg_recipe @@ -18,7 +17,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/format_autopkg_yaml_recipes.py b/pre_commit_macadmin_hooks/format_autopkg_yaml_recipes.py index b93f232..0b931f0 100644 --- a/pre_commit_macadmin_hooks/format_autopkg_yaml_recipes.py +++ b/pre_commit_macadmin_hooks/format_autopkg_yaml_recipes.py @@ -4,7 +4,6 @@ import argparse import io import re -from typing import List, Optional import ruamel.yaml from ruamel.yaml.constructor import DuplicateKeyError @@ -74,7 +73,7 @@ def _reorder_recipe(recipe) -> None: def _insert_section_blank_lines(output: str) -> str: """Ensure a single blank line precedes each top-level recipe section.""" - result: List[str] = [] + result: list[str] = [] for line in output.split("\n"): if not line.startswith(_TOP_LEVEL_TRIGGERS): result.append(line) @@ -127,7 +126,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" argparser = build_argument_parser() args = argparser.parse_args(argv) diff --git a/pre_commit_macadmin_hooks/format_xml_plist.py b/pre_commit_macadmin_hooks/format_xml_plist.py index 28e81f0..66d1fbf 100644 --- a/pre_commit_macadmin_hooks/format_xml_plist.py +++ b/pre_commit_macadmin_hooks/format_xml_plist.py @@ -3,7 +3,6 @@ import argparse import plistlib -from typing import List, Optional from xml.parsers.expat import ExpatError @@ -17,7 +16,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Parse command line arguments. diff --git a/pre_commit_macadmin_hooks/munki_makecatalogs.py b/pre_commit_macadmin_hooks/munki_makecatalogs.py index 3df5ab3..51381ea 100755 --- a/pre_commit_macadmin_hooks/munki_makecatalogs.py +++ b/pre_commit_macadmin_hooks/munki_makecatalogs.py @@ -5,7 +5,6 @@ import argparse import os import subprocess -from typing import List, Optional def build_argument_parser() -> argparse.ArgumentParser: @@ -21,7 +20,7 @@ def build_argument_parser() -> argparse.ArgumentParser: return parser -def main(argv: Optional[List[str]] = None) -> int: +def main(argv: list[str] | None = None) -> int: """Main process.""" # Path to makecatalogs. diff --git a/pre_commit_macadmin_hooks/util.py b/pre_commit_macadmin_hooks/util.py index 3f865f7..026caab 100644 --- a/pre_commit_macadmin_hooks/util.py +++ b/pre_commit_macadmin_hooks/util.py @@ -3,7 +3,7 @@ import json import plistlib from datetime import datetime -from typing import Any, Dict, List, Optional +from typing import Any import ruamel.yaml @@ -38,7 +38,7 @@ ] -def load_autopkg_recipe(path: str) -> Optional[Dict[str, Any]]: +def load_autopkg_recipe(path: str) -> dict[str, Any] | None: """Loads an AutoPkg recipe in plist, yaml, or json format.""" recipe = None @@ -68,7 +68,7 @@ def load_autopkg_recipe(path: str) -> Optional[Dict[str, Any]]: def validate_required_keys( - input_dict: Dict[str, Any], filename: str, required_keys: List[str] + input_dict: dict[str, Any], filename: str, required_keys: list[str] ) -> bool: """Verifies that required_keys are present in dictionary.""" passed = True @@ -79,7 +79,7 @@ def validate_required_keys( return passed -def detect_deprecated_keys(input_dict: Dict[str, Any], filename: str) -> bool: +def detect_deprecated_keys(input_dict: dict[str, Any], filename: str) -> bool: """Verifies that no deprecated keys are present in dictionary.""" # List from: https://github.com/munki/munki/wiki/Supported-Pkginfo-Keys deprecated_keys = ( @@ -95,7 +95,7 @@ def detect_deprecated_keys(input_dict: Dict[str, Any], filename: str) -> bool: return passed -def detect_typoed_keys(input_dict: Dict[str, Any], filename: str) -> bool: +def detect_typoed_keys(input_dict: dict[str, Any], filename: str) -> bool: """Verifies that specific key name typos are not present in dictionary.""" key_corrections = { "appleitem": "apple_item", @@ -143,7 +143,7 @@ def detect_typoed_keys(input_dict: Dict[str, Any], filename: str) -> bool: return passed -def validate_restart_action_key(pkginfo: Dict[str, Any], filename: str) -> bool: +def validate_restart_action_key(pkginfo: dict[str, Any], filename: str) -> bool: """Verifies that the RestartAction key is set correctly.""" passed = True allowed_values = ( @@ -162,7 +162,7 @@ def validate_restart_action_key(pkginfo: Dict[str, Any], filename: str) -> bool: return passed -def validate_uninstall_method(pkginfo: Dict[str, Any], filename: str) -> bool: +def validate_uninstall_method(pkginfo: dict[str, Any], filename: str) -> bool: """Verifies that uninstall_method and uninstall_script is used appropriately.""" passed = True uninst_method = pkginfo.get("uninstall_method") @@ -182,7 +182,7 @@ def validate_uninstall_method(pkginfo: Dict[str, Any], filename: str) -> bool: def validate_supported_architectures( - pkginfo: Dict[str, Any], filename: str, recipe_mode: bool = False + pkginfo: dict[str, Any], filename: str, recipe_mode: bool = False ) -> bool: """Verifies that supported_architectures values are valid. @@ -204,7 +204,7 @@ def validate_supported_architectures( return passed -def validate_pkginfo_key_types(pkginfo: Dict[str, Any], filename: str) -> bool: +def validate_pkginfo_key_types(pkginfo: dict[str, Any], filename: str) -> bool: """Validation of pkginfo key types. Used for AutoPkg- and Munki-related hooks. @@ -288,7 +288,7 @@ def validate_pkginfo_key_types(pkginfo: Dict[str, Any], filename: str) -> bool: def validate_shebangs( - script_content: str, filename: str, addl_shebangs: Optional[List[str]] = None + script_content: str, filename: str, addl_shebangs: list[str] | None = None ) -> bool: """Verifies that scripts begin with a valid shebang.""" if addl_shebangs is None: