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
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ jobs:
- name: Build docker
run: docker build . -t header-validator:py3.8.13-alpine
- name: Run action
run: docker run --workdir /github/workspace -v "/home/runner/work/validate-python-headers/validate-python-headers":"/github/workspace" header-validator:py3.8.13-alpine Apache-2.0 'François-Guillaume Fernandez' 2022 src/ __init__.py .github/
run: docker run --workdir /github/workspace -v "/home/runner/work/validate-python-headers/validate-python-headers":"/github/workspace" header-validator:py3.8.13-alpine 'François-Guillaume Fernandez' 2022 Apache-2.0 src/ __init__.py .github/ ''
19 changes: 16 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,31 @@ repos:
rev: v4.3.0
hooks:
- id: check-yaml
exclude: .conda
- id: check-toml
- id: check-added-large-files
- id: end-of-file-fixer
- id: trailing-whitespace
- id: check-ast
- id: debug-statements
- id: check-json
- id: check-merge-conflict
- id: no-commit-to-branch
- id: debug-statements
language_version: python3
- repo: https://github.com/psf/black
rev: 22.3.0
hooks:
- id: black
- id: black
- repo: https://github.com/pycqa/isort
rev: 5.10.1
hooks:
- id: isort
- id: isort
- repo: https://github.com/PyCQA/flake8
rev: 4.0.1
hooks:
- id: flake8
- repo: https://github.com/PyCQA/autoflake
rev: v1.7.7
hooks:
- id: autoflake
- id: autoflake
33 changes: 2 additions & 31 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,39 +85,10 @@ To run all quality checks together
make quality
```

##### Lint verification

To ensure that your incoming PR complies with the lint settings, you need to install [flake8](https://flake8.pycqa.org/en/latest/) and run the following command from the repository's root folder:

```shell
flake8 ./
```
This will read the `.flake8` setting file and let you know whether your commits need some adjustments.

##### Import order

In order to ensure there is a common import order convention, run [isort](https://github.com/PyCQA/isort) as follows:

```shell
isort .
```
This will reorder the imports of your local files.

##### Annotation typing

Additionally, to catch type-related issues and have a cleaner codebase, annotation typing are expected. After installing [mypy](https://github.com/python/mypy), you can run the verifications as follows:

```shell
mypy
```
The `pyproject.toml` file will be read to check your typing.

##### Code formatting

Finally, code formatting is a good practice for shareable projects. After installing [black](https://github.com/psf/black), you can run the verifications as follows:
The previous command won't modify anything in your codebase. Some fixes (import ordering and code formatting) can be done automatically using the following command:

```shell
black .
make style
```

### Submit your modifications
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@ build:
# Run tests for the library
test:
docker build . -t header-validator:py3.8.13-alpine
docker run --workdir /github/workspace -v src:/github/workspace/src header-validator:py3.8.13-alpine Apache-2.0 'François-Guillaume Fernandez' 2022 src/ __init__.py .github/
docker run --workdir /github/workspace -v src:/github/workspace/src header-validator:py3.8.13-alpine 'François-Guillaume Fernandez' 2022 Apache-2.0 src/ __init__.py .github/ ''
28 changes: 23 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,6 @@ This action checks the copyright and license notices in the headers of your Pyth

## Inputs

### `license`

**Required** Identifier of the license for your project (cf. [SPDX identifiers](https://spdx.org/licenses/)).

### `owner`

**Required** The copyright owner.
Expand All @@ -33,6 +29,10 @@ This action checks the copyright and license notices in the headers of your Pyth

**Required** The starting year of your project.

### `license`

Identifier of the license for your project (cf. [SPDX identifiers](https://spdx.org/licenses/)). Default `null`.

### `folders`

The folders to inspect, separated by a comma. Default `"."`.
Expand All @@ -45,11 +45,17 @@ The files to ignore, separated by a comma. Default `"__init__.py"`.

The folders to ignore, separated by a comma. Default `".github/"`.

### `license-notice`

The path to a license notice text. If license is `null`, the header will be expected to have this text as a license notice. Default `null`.

## Outputs

The list of files with header issues.

## Example usage
## Example usages

Using an Open-source license:

```
uses: frgfm/validate-python-headers@main
Expand All @@ -61,6 +67,18 @@ with:
ignore-folders: '.github/'
```

On closed source code:

```
uses: frgfm/validate-python-headers@main
with:
license-notice: '.github/license-notice.txt'
owner: 'François-Guillaume Fernandez'
starting-year: 2022
ignore-files: 'version.py,__init__.py'
ignore-folders: '.github/'
```


## Contributing

Expand Down
16 changes: 11 additions & 5 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ branding:
color: blue

inputs:
license:
# https://spdx.org/licenses/
description: 'License of your project'
required: true
owner:
description: 'Name of the copyright owner'
required: true
starting-year:
description: 'Starting year of your project'
required: true
license:
# https://spdx.org/licenses/
description: 'License of your project'
required: false
default: null
folders:
description: 'Folders to inspect'
required: false
Expand All @@ -28,6 +29,10 @@ inputs:
description: 'Folders to ignore'
required: false
default: '.github/'
license-notice:
description: 'Custom license notice to be used if license is null'
required: false
default: null

outputs:
issues: # id of output
Expand All @@ -37,9 +42,10 @@ runs:
using: 'docker'
image: 'Dockerfile'
args:
- ${{ inputs.license }}
- ${{ inputs.owner }}
- ${{ inputs.starting-year }}
- ${{ inputs.license }}
- ${{ inputs.folders }}
- ${{ inputs.ignore-files }}
- ${{ inputs.ignore-folders }}
- ${{ inputs.license-notice }}
2 changes: 1 addition & 1 deletion entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/bin/sh -l
set -eax

python /validate_headers.py "${1}" "${2}" "${3}" --folders "${4}" --ignore-files "${5}" --ignore-folders "${6}"
python /validate_headers.py "${1}" "${2}" --license "${3}" --folders "${4}" --ignore-files "${5}" --ignore-folders "${6}" --license-notice "${7}"
58 changes: 37 additions & 21 deletions src/validate_headers.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (C) 2022, François-Guillaume Fernandez.
# Copyright (C) 2022-2023, François-Guillaume Fernandez.

# This program is licensed under the Apache License 2.0.
# See LICENSE or go to <https://www.apache.org/licenses/LICENSE-2.0> for full license details.
Expand All @@ -7,7 +7,7 @@
import json
from datetime import datetime
from pathlib import Path
from typing import Dict, List
from typing import Dict, List, Union

SHEBANG = ["#!usr/bin/python\n"]
BLANK_LINE = "\n"
Expand All @@ -19,32 +19,46 @@
}


def get_header_options(license_id: str, owner: str, starting_year: int) -> List[List[str]]:
def get_header_options(
owner: str, starting_year: int, license_id: Union[str, None], license_notice: Union[str, None]
) -> List[List[str]]:

# Year check
current_year = datetime.now().year
assert starting_year <= current_year, f"Invalid first copyright year: {starting_year}"

# License check
license_info = LICENSES.get(license_id)
assert isinstance(license_info, dict), f"Invalid license identifier: {license_id}"
if starting_year > current_year:
raise ValueError(f"Invalid first copyright year: {starting_year}")

# Owner check
assert len(owner) > 0, "Please specify the copyright owner"
if len(owner) == 0:
raise ValueError("Please specify the copyright owner")

# License file check
assert Path("LICENSE").is_file(), "Unable to locate local copy of license text."
# License check
license_notices = []
if isinstance(license_id, str):
license_info = LICENSES.get(license_id)
if not isinstance(license_info, dict):
raise KeyError(f"Invalid license identifier: {license_id}")
# License file check
if not Path("LICENSE").is_file():
raise FileNotFoundError("Unable to locate local copy of license text.")
license_notices = [
[
f"# This program is licensed under the {license_info['name']}.\n",
f"# See LICENSE or go to <{url}> for full license details.\n",
]
for url in license_info["urls"]
]
elif isinstance(license_notice, str):
if not Path(license_notice).is_file():
raise FileNotFoundError("Unable to locate the text of the license notice.")
with open(license_notice, "r") as f:
license_notices = [f.readlines()]
else:
raise ValueError("One of the following args needs to be specified: 'license_id', 'license_notice'")

# Header build
year_options = [f"{current_year}"] + [f"{year}-{current_year}" for year in range(starting_year, current_year)]
copyright_notices = [[f"# Copyright (C) {year_str}, {owner}.\n"] for year_str in year_options]
license_notices = [
[
f"# This program is licensed under the {license_info['name']}.\n",
f"# See LICENSE or go to <{url}> for full license details.\n",
]
for url in license_info["urls"]
]

return [
SHEBANG + [BLANK_LINE] + copyright_notice + [BLANK_LINE] + license_notice
Expand All @@ -60,7 +74,7 @@ def get_header_options(license_id: str, owner: str, starting_year: int) -> List[
def main(args):

# Check args & define all header options
header_options = get_header_options(args.license, args.owner, args.year)
header_options = get_header_options(args.owner, args.year, args.license, args.license_notice)

ignored_files = args.ignore_files.split(",")
ignored_folders = [Path(folder) for folder in args.ignore_folders.split(",")]
Expand All @@ -71,7 +85,8 @@ def main(args):
# For every python file in the repository
for folder in folders:
folder_path = Path(folder)
assert folder_path.is_dir(), f"Invalid folder path: {folder}"
if not folder_path.is_dir():
raise FileNotFoundError(f"Invalid folder path: {folder}")
for source_path in folder_path.rglob("**/*.py"):
if source_path.name in ignored_files or any(folder in source_path.parents for folder in ignored_folders):
continue
Expand Down Expand Up @@ -103,12 +118,13 @@ def parse_args():
description="Header validator for your Python files", formatter_class=argparse.ArgumentDefaultsHelpFormatter
)

parser.add_argument("license", type=str, help="identifier of the license being used")
parser.add_argument("owner", type=str, help="name of the copyright owner")
parser.add_argument("year", type=int, help="first copyright year of the project")
parser.add_argument("--license", type=str, default=None, help="identifier of the license being used")
parser.add_argument("--folders", type=str, default=".", help="folders to inspect")
parser.add_argument("--ignore-files", type=str, default="", help="files to ignore")
parser.add_argument("--ignore-folders", type=str, default="", help="folders to ignore")
parser.add_argument("--license-notice", type=str, default=None, help="path to custom license notice")
args = parser.parse_args()

return args
Expand Down