Contributing¶
Thanks for your interest in contributing to pymdm! This guide covers local setup, the conventions the project follows, and how to land a change.
Local setup¶
pymdm uses uv for environment and dependency management. From a fresh clone:
make install-dev # uv sync --extra dev
make pre-commit-install # one-time per clone
make help lists every Make target with a short description.
Running tests, lint, and format¶
make test # full unit test suite
make test-cov # tests with terminal coverage report
make test-cov-html # tests with HTML coverage report (coverage/htmlcov/index.html)
make lint # ruff format --check + ruff check
make format # ruff format + ruff check --fix
The pre-commit hooks run a subset of these on every commit. If a hook fails, fix the underlying issue, re-stage, and commit again — don’t pass --no-verify.
Branch naming¶
feat/<short-description>for new featuresfix/<short-description>for bug fixesdocs/<short-description>for documentation-only changeschore/<short-description>for tooling, CI, dependencies
Keep descriptions kebab-case and concise. Example: feat/intune-graph-helpers.
Conventional commits¶
The project uses Conventional Commits so the changelog and version bumps stay reproducible.
<type>(<scope>): <subject>
<body>
<footer>
<type>:feat,fix,docs,refactor,test,chore,ci,build<scope>: optional, e.g.darwin,win32,mdm,logger,webhookAppend
!after the type/scope to mark a breaking change:feat(darwin)!: ...For breaking changes also include a
BREAKING CHANGE:footer with migration notes.
Example:
feat(darwin)!: rework DarwinDefaults to instance-based with user-context support
DarwinDefaults is now constructed with an optional CommandRunner; read/
write/delete accept as_user=True to dispatch through run_as_user.
BREAKING CHANGE: DarwinDefaults static methods are removed. Callers must
now instantiate (DarwinDefaults() preserves prior root-context behavior).
Architecture orientation¶
pymdm has two orthogonal Protocol-based abstraction layers:
Platform layer (
src/pymdm/platforms/) — OS-specific operations (Darwin, Win32)MDM provider layer (
src/pymdm/mdm/) — Jamf vs Intune script parameter conventions
Public-facing classes (SystemInfo, ParamParser) are thin facades over these layers. Preserve the facade pattern when refactoring — many consumers depend on the static-method API.
Detection factories (get_platform(), get_command_support(), get_provider()) read environment variables (PYMDM_PLATFORM, PYMDM_MDM_PROVIDER) before falling back to sys.platform. Tests that flip platforms mid-run must call clear_platform_cache().
For deeper agent-ready guidance, see CLAUDE.md and .cursor/rules/.
Adding a new platform or MDM provider¶
Implement the relevant Protocol (
PlatformInfo+PlatformCommandSupportfor OS,MdmParamProviderfor MDM).Add detection arms to
_detection.py(platforms) or_base.py::get_provider(MDM).Mirror the existing test files (
test_platforms_<name>.pyortest_mdm_<name>.py).Update
README.mdinstall paths if the new platform changes the dependency story.Update
CHANGELOG.mdunder[Unreleased].
Tests¶
pytestonly (nopytest-asyncio, nopytest-mock).Mock at the module-local subprocess reference:
pymdm.platforms.darwin.subprocess.run, NOT globalsubprocess.run.Shared fixtures (
temp_dir,temp_log_file,mock_logger) live intests/conftest.py.Tests must be deterministic — no live network/subprocess calls, no timing-dependent assertions.
Coverage lives at 90% currently. Don’t ship a PR that drops coverage; the gate is configured in
pyproject.toml(currently commented out but enforced via review).
Filing issues¶
Bug reports: use the bug report form.
Feature requests: use the feature request form.
Documentation issues: file as a feature request with the Documentation scope.
Style for examples¶
When writing example code in docstrings, READMEs, or tests, use jappleseed as the placeholder username — never anyone’s real name.
Releasing¶
Releases are workflow-driven via build-release.yml:
Bump
__version__insrc/pymdm/__init__.py.Move
[Unreleased]to a new dated heading inCHANGELOG.md.Open a PR with these changes; merge to
main.Trigger the Build, Release and Publish workflow manually (
workflow_dispatch).The action validates the version isn’t already tagged, builds sdist + wheel, publishes to PyPI, and drafts a GitHub release.