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
on:

View file

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

View file

@ -8,12 +8,14 @@ import os
import pathlib
import re
import textwrap
from pathlib import Path
import click
import git
import requests
import frappe
from frappe.utils.change_log import get_app_branch
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,
"type": bool,
},
"branch_name": {"prompt": "Branch Name", "default": get_app_branch("frappe")},
}
for property, config in new_app_config.items():
@ -116,6 +119,13 @@ def get_license_text(license_name: str) -> str:
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):
frappe.create_folder(
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:
f.write(frappe.as_unicode(pyproject_template.format(**hooks)))
with open(os.path.join(dest, hooks.app_name, "README.md"), "w") as f:
f.write(
frappe.as_unicode(
f"## {hooks.app_title}\n\n{hooks.app_description}\n\n#### License\n\n{hooks.app_license}"
)
)
with open(os.path.join(dest, hooks.app_name, ".pre-commit-config.yaml"), "w") as f:
f.write(frappe.as_unicode(precommit_template.format(**hooks)))
license_body = get_license_text(license_name=hooks.app_license)
with open(os.path.join(dest, hooks.app_name, "license.txt"), "w") as f:
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)
copy_from_frappe(".editorconfig", app_directory)
copy_from_frappe(".eslintrc", app_directory)
if hooks.create_github_workflow:
_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:
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)))
# 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.index.commit("feat: Initialize App")
@ -191,6 +207,10 @@ def _create_github_workflow_files(dest, hooks):
with open(ci_workflow, "w") as f:
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(
'''
@ -322,6 +342,41 @@ build-backend = "flit_core.buildapi"
# These dependencies are only installed when developer mode is enabled
[tool.bench.dev-dependencies]
# 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}"
@ -672,3 +727,181 @@ patches_template = """[pre_model_sync]
[post_model_sync]
# 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.
"""