Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/.ruff.toml
/*.egg-info/
__pycache__/
/.pytest_tmp*/
1 change: 1 addition & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ repos:
- id: polymath-toml
- id: polymath-json
- id: polymath-copyright
args: [--license, proprietary, --copyright-org, 'Polymath Robotics, Inc.']
27 changes: 14 additions & 13 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,77 +11,77 @@
name: Polymath Code Standard [general]
language: python
description: File hygiene checks that run on all staged files.
entry: polymath_code_standard --group general
entry: polymath_code_standard general
types: [file]

- <<: *python-hook
id: polymath-python
name: Polymath Code Standard [python]
description: Python checks — ruff format, ruff lint, AST validation.
entry: polymath_code_standard --group python
entry: polymath_code_standard python
types: [python]

- <<: *python-hook
id: polymath-cpp
name: Polymath Code Standard [cpp]
description: C/C++ checks — clang-format and cpplint.
entry: polymath_code_standard --group cpp
entry: polymath_code_standard cpp
types_or: [c, c++]

- <<: *python-hook
id: polymath-shell
name: Polymath Code Standard [shell]
description: Shell checks — shellcheck. Detects scripts by shebang, not just extension.
entry: polymath_code_standard --group shell
entry: polymath_code_standard shell
types: [shell]

- <<: *python-hook
id: polymath-cmake
name: Polymath Code Standard [cmake]
description: CMake checks — cmakelint.
entry: polymath_code_standard --group cmake
entry: polymath_code_standard cmake
types: [cmake]

- <<: *python-hook
id: polymath-docker
name: Polymath Code Standard [docker]
description: Dockerfile checks — hadolint.
entry: polymath_code_standard --group docker
entry: polymath_code_standard docker
types: [dockerfile]

- <<: *python-hook
id: polymath-markdown
name: Polymath Code Standard [markdown]
description: Markdown checks — pymarkdown.
entry: polymath_code_standard --group markdown
entry: polymath_code_standard markdown
types: [markdown]

- <<: *python-hook
id: polymath-xml
name: Polymath Code Standard [xml]
description: XML checks — check-xml and ament_xmllint.
entry: polymath_code_standard --group xml
entry: polymath_code_standard xml
types: [xml]

- <<: *python-hook
id: polymath-yaml
name: Polymath Code Standard [yaml]
description: YAML checks — check-yaml and yamllint.
entry: polymath_code_standard --group yaml
entry: polymath_code_standard yaml
types: [yaml]

- <<: *python-hook
id: polymath-toml
name: Polymath Code Standard [toml]
description: TOML validation.
entry: polymath_code_standard --group toml
entry: polymath_code_standard toml
types: [toml]

- <<: *python-hook
id: polymath-json
name: Polymath Code Standard [json]
description: JSON/JSON5 validation.
entry: polymath_code_standard --group json
entry: polymath_code_standard json
types_or: [json, json5]
exclude: .*\.geojson$

Expand All @@ -91,12 +91,13 @@
description: >
Insert copyright headers. Handles all supported file types:
Python/CMake/Shell (# style) and C/C++ (// style).
entry: polymath_code_standard --group copyright
Requires args: [--license, <SPDX_ID or 'proprietary'>, --copyright-org, <ORG>]
entry: polymath_code_standard copyright
types_or: [python, cmake, shell, c, c++]

- <<: *python-hook
id: polymath-ansible
name: Polymath Code Standard [ansible]
description: Run ansible-lint with Polymath settings
entry: polymath_code_standard --group ansible
entry: polymath_code_standard ansible
types: [file]
91 changes: 91 additions & 0 deletions polymath_code_standard/checker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Copyright (c) 2026-present Polymath Robotics, Inc. All rights reserved
# Proprietary. Any unauthorized copying, distribution, or modification of this software is strictly prohibited.
import argparse
import functools
import importlib.resources
import os
import subprocess
import sys
from abc import ABC, abstractmethod
from dataclasses import dataclass
from pathlib import Path

from identify.identify import tags_from_path

# Path to resource files which are config inputs to the various hooks
CONFIG_DIR = importlib.resources.files('polymath_code_standard') / 'config'


@functools.cache
def _file_tags(path: str) -> frozenset[str]:
return frozenset(tags_from_path(path))


def filter_files(files: list[str], types: frozenset[str]) -> list[str]:
"""Return files that have at least one of the given identify type tags."""
return [f for f in files if _file_tags(f) & types]


@dataclass
class Result:
"""Outcome of a single invocation of a tool."""

name: str
passed: bool
skipped: bool = False
output: str = ''
cmd: list[str] | None = None

def print(self) -> None:
if self.output and not self.passed:
for line in self.output.splitlines():
print(f' [{self.name}] {line}')


def tool(name: str) -> str:
"""Return the absolute path to a console script in this venv."""
return str(Path(sys.executable).parent / name)


def run(name: str, cmd: list[str], files: list[str] | None = None, env: dict | None = None) -> Result:
"""Run a check as a subprocess.

files=[] → skipped (no applicable files for this type)
files=None → run with no extra arguments
env → merged on top of os.environ when provided
"""
if files is not None and not files:
return Result(name=name, passed=True, skipped=True)
full_cmd = cmd + (files or [])
merged_env = {**os.environ, **env} if env else None
proc = subprocess.run(full_cmd, capture_output=True, text=True, env=merged_env)
output = (proc.stdout + proc.stderr).strip()
return Result(name=name, passed=proc.returncode == 0, output=output, cmd=full_cmd)


class CheckerGroup(ABC):
@property
@abstractmethod
def name(self) -> str: ...

def register_args(self, subparser: argparse.ArgumentParser) -> None:
"""Register subparser arguments. Override to add group-specific options."""
subparser.add_argument('files', nargs='*', help='Staged files passed by pre-commit')

@abstractmethod
def run(self, args: argparse.Namespace) -> list[Result]: ...

@classmethod
def _check(
cls, tool_name: str, args: list[str], files: list[str] | None, name: str = None, env: dict | None = None
) -> Result:
return run(name or tool_name, [tool(tool_name)] + args, files, env=env)


_GROUPS: list[CheckerGroup] = []


def check_group(cls: type[CheckerGroup]) -> type[CheckerGroup]:
"""Decorator that registers a CheckerGroup with the global group list."""
_GROUPS.append(cls())
return cls
2 changes: 2 additions & 0 deletions polymath_code_standard/checkers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Copyright (c) 2026-present Polymath Robotics, Inc. All rights reserved
# Proprietary. Any unauthorized copying, distribution, or modification of this software is strictly prohibited.
21 changes: 21 additions & 0 deletions polymath_code_standard/checkers/ansible.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright (c) 2026-present Polymath Robotics, Inc. All rights reserved
# Proprietary. Any unauthorized copying, distribution, or modification of this software is strictly prohibited.
import argparse

from polymath_code_standard.checker import CONFIG_DIR, CheckerGroup, Result, check_group


@check_group
class AnsibleGroup(CheckerGroup):
name = 'ansible'

def run(self, args: argparse.Namespace) -> list[Result]:
return [
self._check(
'python3',
['-m', 'ansiblelint', '-v', '--force-color', '-c', CONFIG_DIR / 'ansible-lint.yml'],
args.files,
name='ansible-lint',
env={'ANSIBLE_COLLECTIONS_PATH': 'ansible/collections'},
)
]
13 changes: 13 additions & 0 deletions polymath_code_standard/checkers/cmake.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Copyright (c) 2026-present Polymath Robotics, Inc. All rights reserved
# Proprietary. Any unauthorized copying, distribution, or modification of this software is strictly prohibited.
import argparse

from polymath_code_standard.checker import CheckerGroup, Result, check_group


@check_group
class CMakeGroup(CheckerGroup):
name = 'cmake'

def run(self, args: argparse.Namespace) -> list[Result]:
return [self._check('cmakelint', ['--linelength=140'], args.files)]
138 changes: 138 additions & 0 deletions polymath_code_standard/checkers/copyright.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
# Copyright (c) 2026-present Polymath Robotics, Inc. All rights reserved
# Proprietary. Any unauthorized copying, distribution, or modification of this software is strictly prohibited.
import argparse
import datetime
import os
import re
import tempfile
from pathlib import Path

from polymath_code_standard.checker import CheckerGroup, Result, check_group, filter_files
from polymath_code_standard.licenses import PROPRIETARY, get_license_full_text, get_license_header


@check_group
class CopyrightGroup(CheckerGroup):
name = 'copyright'

def register_args(self, subparser: argparse.ArgumentParser) -> None:
super().register_args(subparser)
subparser.add_argument(
'--license',
dest='license_id',
required=True,
metavar='SPDX_ID',
help="SPDX license ID (e.g. MIT, Apache-2.0) or 'proprietary'",
)
subparser.add_argument(
'--copyright-org',
required=True,
metavar='ORG',
help='Organization name for the copyright line',
)
subparser.add_argument(
'--copyright-year',
default=str(datetime.date.today().year),
metavar='YEAR',
help='Copyright start year (default: current year)',
)
subparser.add_argument(
'--reuse-style',
action='store_true',
help='Force REUSE-style 2-line copyright headers, even when a standard block is available',
)
subparser.add_argument(
'--relicense',
action='store_true',
help='Strip any existing leading comment block before inserting the new header (for relicensing)',
)

def run(self, args: argparse.Namespace) -> list[Result]:
header_text = get_license_header(
args.license_id, args.copyright_year, args.copyright_org, reuse_style_header=args.reuse_style
)
py_cmake_shell = filter_files(args.files, frozenset({'python', 'cmake', 'shell'}))
cpp = filter_files(args.files, frozenset({'c', 'c++'}))

if args.relicense:
for f in py_cmake_shell:
self._strip_leading_comment_block(f, '#')
for f in cpp:
self._strip_leading_comment_block(f, '//')

fd, license_filepath = tempfile.mkstemp(suffix='.txt', prefix='polymath_license_')
try:
os.write(fd, header_text.encode('utf-8'))
os.close(fd)
results = [
self._check(
'polymath_copyright_header',
[
'--license-filepath',
license_filepath,
'--comment-style',
'#',
'--allow-past-years',
'--no-extra-eol',
],
py_cmake_shell,
name='copyright (py/cmake/shell)',
),
self._check(
'polymath_copyright_header',
['--license-filepath', license_filepath, '--comment-style', '//', '--allow-past-years'],
cpp,
name='copyright (cpp)',
),
]
finally:
try:
os.unlink(license_filepath)
except OSError:
pass

results.append(self._check_license_file(args.license_id, args.copyright_year, args.copyright_org))
return results

@staticmethod
def _strip_leading_comment_block(filepath: str, comment_prefix: str) -> None:
"""Remove the leading comment block from a file, preserving shebang/coding lines.

Strips all contiguous comment lines (matching comment_prefix) starting after
any shebang or encoding declaration, plus one following blank line. Used so
that a subsequent insert_license run can write a fresh header in their place.
"""
path = Path(filepath)
lines = path.read_text(encoding='utf-8', errors='replace').splitlines(keepends=True)
idx = 0
if lines and lines[0].startswith('#!'):
idx = 1
if idx < len(lines) and re.match(r'#\s*-\*-\s*coding', lines[idx]):
idx += 1
block_start = idx
while idx < len(lines) and lines[idx].rstrip('\r\n').lstrip().startswith(comment_prefix):
idx += 1
if idx < len(lines) and not lines[idx].strip():
idx += 1
if idx > block_start:
path.write_text(''.join(lines[:block_start] + lines[idx:]), encoding='utf-8')

@staticmethod
def _check_license_file(license_id: str, year: str, org: str) -> Result:
if license_id == PROPRIETARY:
return Result(name='LICENSE file', passed=True, skipped=True)
license_file = Path.cwd() / 'LICENSE'
try:
expected = get_license_full_text(license_id, year, org)
except Exception as exc:
return Result(name='LICENSE file', passed=False, output=str(exc))
current = license_file.read_text(encoding='utf-8') if license_file.exists() else None
if current == expected:
return Result(name='LICENSE file', passed=True)
license_file.write_text(expected, encoding='utf-8')
action = 'updated' if current is not None else 'created'
return Result(
name='LICENSE file',
passed=False,
output=f'LICENSE file {action} — please re-stage and recommit',
)
Loading
Loading