-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #18 from ukgovdatascience/refactor_hooks
Refactor cookiecutter hooks and tests
- Loading branch information
Showing
43 changed files
with
667 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,3 +2,6 @@ | |
source = | ||
./hooks | ||
./{{ cookiecutter.repo_name }}/src | ||
[report] | ||
exclude_lines = | ||
if __name__ == .__main__.: |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,7 +5,7 @@ | |
"repository_hosting_platform": ["GitHub", "GitLab"], | ||
"organisation_name": "Your GitHub/GitLab organisation name", | ||
"contact_email": "[email protected]", | ||
"departmental_aqa_framework": ["GDS"], | ||
"departmental_framework": ["GDS", "N/A"], | ||
|
||
"project_name": "Your new project name", | ||
"repo_name": "{{ cookiecutter.project_name.lower().replace(' ', '-') }}", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2 changes: 2 additions & 0 deletions
2
...s/{{ cookiecutter.repo_name }}/.govcookiecutter/department_frameworks/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
```{include} ../../../../../{{ cookiecutter.repo_name }}/.govcookiecutter/department_frameworks/README.md | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
# `hooks` API reference | ||
|
||
This page gives an overview of all public `hooks` objects, functions and methods. All classes and functions exposed | ||
in `hooks.*` namespace are public. | ||
|
||
```{toctree} | ||
:maxdepth: 2 | ||
./pre_gen_project.md | ||
./post_gen_project.md | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Post-cookiecutter generation hooks | ||
|
||
These `hooks` package functions are executed after the project structure has been generated. | ||
|
||
<!-- Functions should be referenced in the `hooks.__init__.py` --> | ||
```{eval-rst} | ||
.. currentmodule:: hooks | ||
``` | ||
|
||
## HM Government department framework functions | ||
|
||
```{eval-rst} | ||
.. autosummary:: | ||
:toctree: api/ | ||
remove_folder | ||
set_aqa_framework | ||
set_request_template | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# Pre-cookiecutter generation hooks | ||
|
||
These `hooks` package functions are executed before the project structure has been generated. | ||
|
||
<!-- Functions should be referenced in the `hooks.__init__.py` --> | ||
```{eval-rst} | ||
.. currentmodule:: hooks | ||
``` | ||
|
||
## Cookiecutter input validation | ||
|
||
```{eval-rst} | ||
.. autosummary:: | ||
:toctree: api/ | ||
check_valid_email_address_format | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
from hooks.post_gen_project import remove_folder, set_aqa_framework, set_request_template | ||
from hooks.pre_gen_project import check_valid_email_address_format | ||
__all__ = ("check_valid_email_address_format", "remove_folder", "set_aqa_framework", "set_request_template", ) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,83 +1,101 @@ | ||
from pathlib import Path | ||
from shutil import rmtree | ||
import os | ||
from typing import Union | ||
|
||
# Define the folder path to the 'docs/aqa_frameworks' folder, and the future `docs/aqa` folder | ||
PATH_DOCS_AQA_FRAMEWORKS = os.path.join("docs", "aqa_frameworks") | ||
PATH_DOCS_AQA = os.path.join("docs", "aqa") | ||
|
||
# Define the folder path to the `docs/pull_merge_request_templates` folder | ||
PATH_PR_MR_DEPT_TEMPLATES = os.path.join("docs", "pull_merge_request_templates") | ||
def remove_folder(folder: Union[Path, str]) -> None: | ||
"""Remove a folder. | ||
# Define the folder path where pull/merge requests should exist as values in a dictionary, where the keys are the | ||
# remote hosts | ||
PATH_PR_MR_TEMPLATE = { | ||
"GitHub": [os.path.join(".github"), "pull_request_template.md"], | ||
"GitLab": [os.path.join(".gitlab", "merge_request_templates"), "{{ cookiecutter.project_name }}.md"] | ||
} | ||
Args: | ||
folder: A folder path to be removed. | ||
Returns: | ||
None - removes the `folder` folder. | ||
""" | ||
rmtree(folder) | ||
|
||
|
||
def select_dept_aqa_framework(user_option: str, default_option: str = "GDS") -> None: | ||
"""Create analytical quality assurance (AQA) documents for a specific HM Government department. | ||
def set_aqa_framework(dir_department_framework_aqa: Union[Path, str], | ||
dir_cookiecutter_docs_aqa: Union[Path, str]) -> None: | ||
"""Set a specific HM Government department analytical quality assurance (AQA) framework. | ||
Args: | ||
user_option (str): User option that defines a HM Government departmental AQA framework to use. | ||
default_option (str): Default: GDS. Default option if user_option is not an existing framework. | ||
dir_department_framework_aqa: A folder path that contains a specific HM Government department AQA framework. | ||
dir_cookiecutter_docs_aqa: A folder path within the outputted project structure, where the contents of | ||
`dir_department_framework_aqa` will reside. | ||
Returns: | ||
A new folder in docs/aqa containing the selected HM Government departmental AQA framework. | ||
The department-specific AQA framework in the outputted project structure's `dir_cookiecutter_docs_aqa` folder. | ||
""" | ||
|
||
# Get all the directories in `PATH_DOCS_AQA_FRAMEWORKS` | ||
all_folders = [d for d in os.listdir(PATH_DOCS_AQA_FRAMEWORKS) if os.path.isdir(d)] | ||
# Remove the default `docs/aqa` folder, and its contents. Then recursively create a new folder path to here | ||
remove_folder(dir_cookiecutter_docs_aqa) | ||
|
||
# Select the correct folder; use `default_option` if `user_option` is not a valid sub-folder in `all_folders` | ||
selected_folder = user_option if user_option in all_folders else default_option | ||
# Copy the relevant HM Government departmental AQA framework to the `docs/aqa` folder | ||
_ = Path(dir_department_framework_aqa).rename(dir_cookiecutter_docs_aqa) | ||
|
||
# Copy the relevant HM Government departmental AQA framework to the `docs/aqa` folder, using `default_option` if | ||
# `user_option` is not a valid sub-folder in `PATH_DOCS_AQA_FRAMEWORKS` | ||
os.rename(os.path.join(PATH_DOCS_AQA_FRAMEWORKS, selected_folder), PATH_DOCS_AQA) | ||
|
||
# Remove all framework folders now we have the correct AQA framework | ||
rmtree(PATH_DOCS_AQA_FRAMEWORKS) | ||
def set_request_template(path_department_framework_request_template: Union[Path, str], | ||
dir_govcookiecutter: Union[Path, str], repository_hosting_platform: str) -> None: | ||
"""Set a pull or merge request template in the outputted project structure for a specific HM Government department. | ||
|
||
def select_pull_merge_request_template(user_option: str, repo_host: str, default_option: str = "GDS") -> None: | ||
"""Select a pull/merge request template depending on HM Government department, and repository remote host. | ||
A pull request template is created if the user chooses GitHub as their repository hosting platform. A merge request | ||
template is created if they choose GitLab instead. | ||
Args: | ||
user_option (str): User option that defines a HM Government departmental pull/merge request template to use. | ||
repo_host (str): Repository hosting name. | ||
default_option (str): Default: GDS. Default option if user_option is not an existing pull/merge request | ||
template. | ||
path_department_framework_request_template: A file path to the specific HM Government department pull or merge | ||
request template. | ||
dir_govcookiecutter: A folder path to the outputted `govcookiecutter` template. | ||
repository_hosting_platform: The repository hosting platform. Must be one of "GitHub" or "GitLab" (case | ||
insensitive). | ||
Returns: | ||
A pull/merge request template in the correct location for the selected repo_host. | ||
A department-specific pull or merge request template in the correct location, depending on their choice of | ||
GitHub or GitLab as the repository hosting platform. If neither GitHub or GitLab are chosen, a `ValueError` is | ||
raised. | ||
""" | ||
|
||
# Get all Markdown files in `PATH_PR_MR_DEPT_TEMPLATES` | ||
md_files = [os.path.splitext(f)[0] for f in os.listdir(PATH_PR_MR_DEPT_TEMPLATES) if f.endswith(".md")] | ||
# Define the file path where the request template will be moved, which is dependent on the repository hosting | ||
# platform. If the `dir_request_template` is not initially one of `.github` or `.gitlab`, raise a `ValueError` | ||
if repository_hosting_platform.lower() == "github": | ||
path_request_template = Path(dir_govcookiecutter).joinpath(f".{repository_hosting_platform.lower()}", | ||
"pull_request_template.md") | ||
elif repository_hosting_platform.lower() == "gitlab": | ||
path_request_template = Path(dir_govcookiecutter).joinpath(f".{repository_hosting_platform.lower()}", | ||
"merge_request_templates", | ||
"{{ cookiecutter.project_name }}.md") | ||
else: | ||
raise ValueError("`repository_hosting_platform` must be one of `GitHub` or `GitLab`: " | ||
f"{repository_hosting_platform}") | ||
|
||
# Recursively create all the directories to the parent directory of `path_request_template` if they do not already | ||
# exist | ||
if not path_request_template.parent.is_dir(): | ||
path_request_template.parent.mkdir(parents=True, exist_ok=True) | ||
|
||
# Determine the selected file; fallback to `default_option` if `user_option` is not in `md_files` | ||
selected_md_file = f"{user_option if user_option in md_files else default_option}.md" | ||
# Move the `path_department_framework_request_template` to `path_request_template` | ||
_ = Path(path_department_framework_request_template).rename(path_request_template) | ||
|
||
# Create a directory for the new location | ||
if not os.path.isdir(PATH_PR_MR_TEMPLATE[repo_host][0]): | ||
os.makedirs(PATH_PR_MR_TEMPLATE[repo_host][0], exist_ok=True) | ||
|
||
# Move the `selected_md_file` to the correct location | ||
os.rename(os.path.join(PATH_PR_MR_DEPT_TEMPLATES, selected_md_file), os.path.join(*PATH_PR_MR_TEMPLATE[repo_host])) | ||
if __name__ == "__main__": | ||
|
||
# Remove all pull/merge request templates now that we have the correct one | ||
rmtree(PATH_PR_MR_DEPT_TEMPLATES) | ||
# Define the folder path to `.govcookiecutter` | ||
DIR_GOVCOOKIECUTTER = Path(".govcookiecutter") | ||
|
||
# Check `{{ cookiecutter.departmental_framework }}` is not `N/A` | ||
if "{{ cookiecutter.departmental_framework }}" != "N/A": | ||
|
||
if __name__ == "__main__": | ||
# Define the folder path to the specific department framework of interest in the `department_frameworks` folder | ||
DIR_DEPARTMENT_FRAMEWORKS = DIR_GOVCOOKIECUTTER.joinpath("department_frameworks", | ||
"{{ cookiecutter.departmental_framework }}") | ||
|
||
# Select the appropriate AQA framework | ||
select_dept_aqa_framework("{{ cookiecutter.departmental_aqa_framework }}") | ||
# Transfer the `aqa` folder, and the pull/merge request templates to the correct folder paths | ||
set_aqa_framework(DIR_DEPARTMENT_FRAMEWORKS.joinpath("aqa"), Path("docs").joinpath("aqa")) | ||
set_request_template(DIR_DEPARTMENT_FRAMEWORKS.joinpath("request_template.md"), Path.cwd(), | ||
"{{ cookiecutter.repository_hosting_platform }}") | ||
|
||
# Select the appropriate pull/merge request template | ||
select_pull_merge_request_template("{{ cookiecutter.departmental_aqa_framework }}", | ||
"{{ cookiecutter.repository_hosting_platform }}") | ||
# Remove `DIR_GOVCOOKIECUTTER` | ||
remove_folder(DIR_GOVCOOKIECUTTER) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import re | ||
|
||
# Regular expression to check for a valid email address — based on the HTML5 standard | ||
# (https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address). This is more restrictive than the RFC | ||
# standard; see the comments in this SO answer for further information: https://stackoverflow.com/a/201378 | ||
REGEX_EMAIL_ADDRESS = r"^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" \ | ||
r"(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" | ||
|
||
|
||
def check_valid_email_address_format(email: str) -> None: | ||
"""Check that an email address is of a valid format using regular expressions. | ||
Uses the HTML5 standard for email address format. The regular expression pattern is available `here`_. | ||
Args: | ||
email: An email address to validate. | ||
Returns: | ||
None - raises an `AssertionError` if `email` is not a valid email address format. | ||
.. _here: | ||
https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address | ||
""" | ||
assert bool(re.fullmatch(REGEX_EMAIL_ADDRESS, email)), f"Invalid email address supplied: {email}" | ||
|
||
|
||
if __name__ == "__main__": | ||
|
||
# Check the format of the contact email address supplied is a valid one | ||
check_valid_email_address_format("{{ cookiecutter.contact_email }}") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,4 +4,6 @@ detect-secrets==1.0.3 | |
myst-parser | ||
pre-commit | ||
pytest | ||
pytest-mock | ||
pytest-xdist | ||
Sphinx |
Oops, something went wrong.