Merge pull request #23848 from barredterra/copy-config-to-new-app

feat: default configuration for new app
This commit is contained in:
Ankush Menat 2024-02-19 12:19:07 +05:30 committed by GitHub
commit e6120f230a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 270 additions and 54 deletions

View file

@ -1,3 +1,4 @@
# When updating this file, please also update the linter_workflow_template in frappe/utils/boilerplate.py
name: Linters name: Linters
on: on:

View file

@ -33,22 +33,20 @@ class TestBoilerPlate(unittest.TestCase):
"app_publisher": "Test Publisher", "app_publisher": "Test Publisher",
"app_email": "example@example.org", "app_email": "example@example.org",
"app_license": "mit", "app_license": "mit",
"branch_name": "develop",
"create_github_workflow": False, "create_github_workflow": False,
} }
) )
cls.default_user_input = frappe._dict( cls.default_user_input = [
{ "", # title (accept default)
"title": "Test App", "This app's description contains 'single quotes' and \"double quotes\".", #
"description": "This app's description contains 'single quotes' and \"double quotes\".", "Test Publisher", # publisher
"publisher": "Test Publisher", "example@example.org", # email
"email": "example@example.org", "", # license (accept default)
"icon": "", # empty -> default "", # create github workflow (accept default)
"color": "", "develop", # branch name
"app_license": "mit", ]
"github_workflow": "n",
}
)
cls.bench_path = frappe.utils.get_bench_path() cls.bench_path = frappe.utils.get_bench_path()
cls.apps_dir = os.path.join(cls.bench_path, "apps") cls.apps_dir = os.path.join(cls.bench_path, "apps")
@ -86,7 +84,7 @@ class TestBoilerPlate(unittest.TestCase):
@staticmethod @staticmethod
def get_user_input_stream(inputs): def get_user_input_stream(inputs):
user_inputs = [] user_inputs = []
for value in inputs.values(): for value in inputs:
if isinstance(value, list): if isinstance(value, list):
user_inputs.extend(value) user_inputs.extend(value)
else: else:
@ -99,14 +97,13 @@ class TestBoilerPlate(unittest.TestCase):
self.assertDictEqual(hooks, self.default_hooks) self.assertDictEqual(hooks, self.default_hooks)
def test_invalid_inputs(self): def test_invalid_inputs(self):
invalid_inputs = copy.copy(self.default_user_input).update( invalid_inputs = copy.copy(self.default_user_input)
{ invalid_inputs[0] = ["1nvalid Title", "valid title"]
"title": ["1nvalid Title", "valid title"], invalid_inputs[3] = ["notavalidemail", "what@is@this.email", "example@example.org"]
"email": ["notavalidemail", "what@is@this.email", "example@example.org"],
}
)
with patch("sys.stdin", self.get_user_input_stream(invalid_inputs)): with patch("sys.stdin", self.get_user_input_stream(invalid_inputs)):
hooks = _get_user_inputs(self.default_hooks.app_name) hooks = _get_user_inputs(self.default_hooks.app_name)
self.assertEqual(hooks.app_title, "valid title") self.assertEqual(hooks.app_title, "valid title")
self.assertEqual(hooks.app_email, "example@example.org") self.assertEqual(hooks.app_email, "example@example.org")
@ -118,17 +115,9 @@ class TestBoilerPlate(unittest.TestCase):
) )
def test_create_app(self): def test_create_app(self):
app_name = "test_app" app_name = "test_app"
hooks = self.default_hooks.copy()
hooks = frappe._dict( hooks.app_name = app_name
{ del hooks["create_github_workflow"]
"app_name": app_name,
"app_title": "Test App",
"app_description": "This app's description contains 'single quotes' and \"double quotes\".",
"app_publisher": "Test Publisher",
"app_email": "example@example.org",
"app_license": "mit",
}
)
self.create_app(hooks) self.create_app(hooks)
new_app_dir = os.path.join(self.bench_path, self.apps_dir, app_name) new_app_dir = os.path.join(self.bench_path, self.apps_dir, app_name)
@ -152,17 +141,10 @@ class TestBoilerPlate(unittest.TestCase):
) )
def test_create_app_without_git_init(self): def test_create_app_without_git_init(self):
app_name = "test_app_no_git" app_name = "test_app_no_git"
hooks = self.default_hooks.copy()
hooks.app_name = app_name
del hooks["create_github_workflow"]
hooks = frappe._dict(
{
"app_name": app_name,
"app_title": "Test App",
"app_description": "This app's description contains 'single quotes' and \"double quotes\".",
"app_publisher": "Test Publisher",
"app_email": "example@example.org",
"app_license": "mit",
}
)
self.create_app(hooks, no_git=True) self.create_app(hooks, no_git=True)
new_app_dir = os.path.join(self.apps_dir, app_name) new_app_dir = os.path.join(self.apps_dir, app_name)
@ -198,13 +180,13 @@ class TestBoilerPlate(unittest.TestCase):
os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable" os.access(frappe.get_app_path("frappe"), os.W_OK), "Only run if frappe app paths is writable"
) )
def test_new_patch_util(self): def test_new_patch_util(self):
user_inputs = { user_inputs = [
"app_name": "frappe", "frappe", # app name
"doctype": "User", "User", # doctype
"docstring": "Delete all users", "Delete all users", # docstring
"file_name": "", # Accept default "", # file_name: accept default
"patch_folder_confirmation": "Y", "Y", # confirm patch folder
} ]
patches_txt = pathlib.Path(pathlib.Path(frappe.get_app_path("frappe", "patches.txt"))) patches_txt = pathlib.Path(pathlib.Path(frappe.get_app_path("frappe", "patches.txt")))
original_patches = patches_txt.read_text() original_patches = patches_txt.read_text()

View file

@ -8,12 +8,14 @@ import os
import pathlib import pathlib
import re import re
import textwrap import textwrap
from pathlib import Path
import click import click
import git import git
import requests import requests
import frappe import frappe
from frappe.utils.change_log import get_app_branch
APP_TITLE_PATTERN = re.compile(r"^(?![\W])[^\d_\s][\w -]+$", flags=re.UNICODE) APP_TITLE_PATTERN = re.compile(r"^(?![\W])[^\d_\s][\w -]+$", flags=re.UNICODE)
@ -56,6 +58,7 @@ def _get_user_inputs(app_name):
"default": False, "default": False,
"type": bool, "type": bool,
}, },
"branch_name": {"prompt": "Branch Name", "default": get_app_branch("frappe")},
} }
for property, config in new_app_config.items(): for property, config in new_app_config.items():
@ -116,6 +119,13 @@ def get_license_text(license_name: str) -> str:
return license_name return license_name
def copy_from_frappe(rel_path: str, new_app_path: str):
"""Copy files from frappe app to new app."""
src = Path(frappe.get_app_path("frappe", "..")) / rel_path
target = Path(new_app_path) / rel_path
Path(target).write_text(Path(src).read_text())
def _create_app_boilerplate(dest, hooks, no_git=False): def _create_app_boilerplate(dest, hooks, no_git=False):
frappe.create_folder( frappe.create_folder(
os.path.join(dest, hooks.app_name, hooks.app_name, frappe.scrub(hooks.app_title)), os.path.join(dest, hooks.app_name, hooks.app_name, frappe.scrub(hooks.app_title)),
@ -142,12 +152,9 @@ def _create_app_boilerplate(dest, hooks, no_git=False):
with open(os.path.join(dest, hooks.app_name, "pyproject.toml"), "w") as f: with open(os.path.join(dest, hooks.app_name, "pyproject.toml"), "w") as f:
f.write(frappe.as_unicode(pyproject_template.format(**hooks))) f.write(frappe.as_unicode(pyproject_template.format(**hooks)))
with open(os.path.join(dest, hooks.app_name, "README.md"), "w") as f: with open(os.path.join(dest, hooks.app_name, ".pre-commit-config.yaml"), "w") as f:
f.write( f.write(frappe.as_unicode(precommit_template.format(**hooks)))
frappe.as_unicode(
f"## {hooks.app_title}\n\n{hooks.app_description}\n\n#### License\n\n{hooks.app_license}"
)
)
license_body = get_license_text(license_name=hooks.app_license) license_body = get_license_text(license_name=hooks.app_license)
with open(os.path.join(dest, hooks.app_name, "license.txt"), "w") as f: with open(os.path.join(dest, hooks.app_name, "license.txt"), "w") as f:
f.write(frappe.as_unicode(license_body)) f.write(frappe.as_unicode(license_body))
@ -168,15 +175,24 @@ def _create_app_boilerplate(dest, hooks, no_git=False):
app_directory = os.path.join(dest, hooks.app_name) app_directory = os.path.join(dest, hooks.app_name)
copy_from_frappe(".editorconfig", app_directory)
copy_from_frappe(".eslintrc", app_directory)
if hooks.create_github_workflow: if hooks.create_github_workflow:
_create_github_workflow_files(dest, hooks) _create_github_workflow_files(dest, hooks)
hooks.readme_ci_section = readme_ci_section
else:
hooks.readme_ci_section = ""
with open(os.path.join(dest, hooks.app_name, "README.md"), "w") as f:
f.write(frappe.as_unicode(readme_template.format(**hooks)))
if not no_git: if not no_git:
with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f: with open(os.path.join(dest, hooks.app_name, ".gitignore"), "w") as f:
f.write(frappe.as_unicode(gitignore_template.format(app_name=hooks.app_name))) f.write(frappe.as_unicode(gitignore_template.format(app_name=hooks.app_name)))
# initialize git repository # initialize git repository
app_repo = git.Repo.init(app_directory, initial_branch="develop") app_repo = git.Repo.init(app_directory, initial_branch=hooks.branch_name)
app_repo.git.add(A=True) app_repo.git.add(A=True)
app_repo.index.commit("feat: Initialize App") app_repo.index.commit("feat: Initialize App")
@ -191,6 +207,10 @@ def _create_github_workflow_files(dest, hooks):
with open(ci_workflow, "w") as f: with open(ci_workflow, "w") as f:
f.write(github_workflow_template.format(**hooks)) f.write(github_workflow_template.format(**hooks))
linter_workflow = workflows_path / "linter.yml"
with open(linter_workflow, "w") as f:
f.write(linter_workflow_template)
PATCH_TEMPLATE = textwrap.dedent( PATCH_TEMPLATE = textwrap.dedent(
''' '''
@ -322,6 +342,41 @@ build-backend = "flit_core.buildapi"
# These dependencies are only installed when developer mode is enabled # These dependencies are only installed when developer mode is enabled
[tool.bench.dev-dependencies] [tool.bench.dev-dependencies]
# package_name = "~=1.1.0" # package_name = "~=1.1.0"
[tool.ruff]
line-length = 110
target-version = "py310"
[tool.ruff.lint]
select = [
"F",
"E",
"W",
"I",
"UP",
"B",
]
ignore = [
"B017", # assertRaises(Exception) - should be more specific
"B018", # useless expression, not assigned to anything
"B023", # function doesn't bind loop variable - will have last iteration's value
"B904", # raise inside except without from
"E101", # indentation contains mixed spaces and tabs
"E402", # module level import not at top of file
"E501", # line too long
"E741", # ambiguous variable name
"F401", # "unused" imports
"F403", # can't detect undefined names from * import
"F405", # can't detect undefined names from * import
"F722", # syntax error in forward type annotation
"F821", # undefined name
"W191", # indentation contains tabs
]
[tool.ruff.format]
quote-style = "double"
indent-style = "tab"
docstring-code-format = true
""" """
hooks_template = """app_name = "{app_name}" hooks_template = """app_name = "{app_name}"
@ -672,3 +727,181 @@ patches_template = """[pre_model_sync]
[post_model_sync] [post_model_sync]
# Patches added in this section will be executed after doctypes are migrated""" # Patches added in this section will be executed after doctypes are migrated"""
precommit_template = """exclude: 'node_modules|.git'
default_stages: [commit]
fail_fast: false
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.3.0
hooks:
- id: trailing-whitespace
files: "{app_name}.*"
exclude: ".*json$|.*txt$|.*csv|.*md|.*svg"
- id: check-yaml
- id: check-merge-conflict
- id: check-ast
- id: check-json
- id: check-toml
- id: check-yaml
- id: debug-statements
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.2.0
hooks:
- id: ruff
name: "Run ruff linter and apply fixes"
args: ["--fix"]
- id: ruff-format
name: "Format Python code"
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v2.7.1
hooks:
- id: prettier
types_or: [javascript, vue, scss]
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
{app_name}/public/dist/.*|
.*node_modules.*|
.*boilerplate.*|
{app_name}/templates/includes/.*|
{app_name}/public/js/lib/.*
)$
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0
hooks:
- id: eslint
types_or: [javascript]
args: ['--quiet']
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
{app_name}/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
{app_name}/templates/includes/.*|
{app_name}/public/js/lib/.*
)$
ci:
autoupdate_schedule: weekly
skip: []
submodules: false
"""
linter_workflow_template = """
name: Linters
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
linter:
name: 'Frappe Linter'
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: pip
- uses: pre-commit/action@v3.0.0
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- name: Run Semgrep rules
run: |
pip install semgrep
semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
deps-vulnerable-check:
name: 'Vulnerable Dependency Check'
runs-on: ubuntu-latest
steps:
- uses: actions/setup-python@v5
with:
python-version: '3.10'
- uses: actions/checkout@v4
- name: Cache pip
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install and run pip-audit
run: |
pip install pip-audit
cd ${GITHUB_WORKSPACE}
pip-audit --desc on .
"""
readme_template = """### {app_title}
{app_description}
### Installation
You can install this app using the [bench](https://github.com/frappe/bench) CLI:
```bash
cd $PATH_TO_YOUR_BENCH
bench get-app $URL_OF_THIS_REPO --branch {branch_name}
bench install-app {app_name}
```
### Contributing
This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository:
```bash
cd apps/{app_name}
pre-commit install
```
Pre-commit is configured to use the following tools for checking and formatting your code:
- ruff
- eslint
- prettier
- pyupgrade
{readme_ci_section}
### License
{app_license}
"""
readme_ci_section = """
### CI
This app can use GitHub Actions for CI. The following workflows are configured:
- CI: Installs this app and runs unit tests on every push to `develop` branch.
- Linters: Runs [Frappe Semgrep Rules](https://github.com/frappe/semgrep-rules) and [pip-audit](https://pypi.org/project/pip-audit/) on every pull request.
"""