diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebf9365..f577110 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,8 +47,8 @@ jobs: - name: Test with tox run: tox - verify-projects: - name: Verify project + verify-create: + name: Verify app creation needs: unit-tests uses: beeware/.github/.github/workflows/app-create-verify.yml@main with: @@ -60,8 +60,8 @@ jobs: framework: [ "toga", "pyside6", "pygame", "console" ] runner-os: [ "macos-15", "ubuntu-latest", "windows-latest" ] - verify-apps: - name: Build app + verify-build: + name: Verify app build needs: unit-tests uses: beeware/.github/.github/workflows/app-build-verify.yml@main with: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 57c2ffc..f4d84d2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,32 +9,22 @@ repos: - id: check-docstring-first - id: end-of-file-fixer - id: trailing-whitespace - - repo: https://github.com/PyCQA/isort - rev: 9.0.0b1 + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.7 hooks: - - id: isort - additional_dependencies: [toml] - - repo: https://github.com/asottile/pyupgrade - rev: v3.21.2 + - id: docformatter + args: [--in-place, --black] + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.22 hooks: - - id: pyupgrade - args: [--py39-plus] - # Docformatter 1.7.5 isn't compatible with Pre-commit 4.0 - # - repo: https://github.com/PyCQA/docformatter - # rev: v1.7.5 - # hooks: - # - id: docformatter - # args: [--in-place, --black] - - repo: https://github.com/psf/black-pre-commit-mirror - rev: 26.5.1 + - id: ruff-format + - id: ruff-check + args: [ --fix ] + - repo: https://github.com/rvben/rumdl-pre-commit + rev: v0.2.37 hooks: - - id: black - language_version: python3 - - repo: https://github.com/PyCQA/flake8 - rev: 7.3.0 - hooks: - - id: flake8 - args: [--max-line-length=119] + - id: rumdl + - id: rumdl-fmt - repo: https://github.com/codespell-project/codespell rev: v2.4.3 hooks: diff --git a/README.md b/README.md index 3a780a6..5832c17 100644 --- a/README.md +++ b/README.md @@ -1,33 +1,29 @@ # Briefcase Bootstrap template -A template for starting a Python app that will be deployed using -Briefcase. +A template for starting a Python app that will be deployed using Briefcase. ## Using this template -In normal usage, you won't need to reference this template at all - it -is used automatically by Briefcase when you run `briefcase new`. +In normal usage, you won't need to reference this template at all - it is used automatically by Briefcase when you run `briefcase new`. -If you are developing a modification to this template and want to test -it, you can tell Briefcase to use your own template by passing in the -`-t` option: +If you are developing a modification to this template and want to test it, you can tell Briefcase to use your own template by passing in the `-t` option: ```sh briefcase new -t ``` -Alternatively, if you want to test this template *without* using -Briefcase, you can use -[cookiecutter](http://github.com/cookiecutter/cookiecutter) directly. +Alternatively, if you want to test this template *without* using Briefcase, you can use [cookiecutter](http://github.com/cookiecutter/cookiecutter) directly. + +1. Install [cookiecutter](http://github.com/cookiecutter/cookiecutter): -1. Install [cookiecutter](http://github.com/cookiecutter/cookiecutter): ```sh pip install cookiecutter ``` -2. Run `cookiecutter` on this template: +2. Run `cookiecutter` on this template: + ```sh cookiecutter https://github.com/beeware/briefcase-template ``` -3. Add your code to the project. +3. Add your code to the project. diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 3f8f622..6754db3 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -7,7 +7,7 @@ app_name = "{{ cookiecutter.app_name }}" if not re.match(PEP508_NAME_RE, app_name): - print("ERROR: `%s` is not a valid Python package name!" % app_name) + print(f"ERROR: `{app_name}` is not a valid Python package name!") # exits with status 1 to indicate failure sys.exit(1) diff --git a/pyproject.toml b/pyproject.toml index 1e567b5..438eafc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,8 @@ +[project] +name = "briefcase-template" +version = "1.0.0" +requires_python = ">=3.10" + [dependency-groups] briefcase = [ "briefcase @ git+https://github.com/beeware/briefcase", # provides cookiecutter @@ -6,15 +11,18 @@ pre-commit = [ "pre-commit == 4.6.0", ] tox = [ - "tox == 4.56.4", + "tox-uv == 1.35.2", ] verify = [ "toml == 0.10.2", + "ruff == 0.16.0", + # Legacy; needed until we clean up .github "flake8 == 7.3.0", ] create-verify = [ - "black == 26.5.1", {include-group = "verify"}, + # Legacy; needed until we clean up .github + "black == 26.5.1", ] test = [ "pytest == 9.1.1", @@ -27,3 +35,58 @@ dev = [ {include-group = "tox"}, {include-group = "test"}, ] + + +[tool.ruff.lint] +# In addition to the default rules, these additional rules will be used: +extend-select = [ + "E", # pycodestyle + "W", # pycodestyle + "F", # pyflakes + "UP", # pyupgrade + "B", # flake8-bugbear + "YTT", # flake8-2020 + "ASYNC", # flake8-async + "C4", # flake8-comprehensions + "I", # isort + "RUF", # ruff-specific rules + "PT", # flake8-pytest-style + "FURB", # refurb + "ISC", # flake8-implicit-str-concat + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "SIM", # flake8-simplify +] + +ignore = [ + "SIM105", # use `contextlib.suppress` (too opinionated) + "SIM108", # replace if-else with ternary operator (too opinionated) +] + +[tool.rumdl] +flavor = "mkdocs" +include = ["**/*.md"] +exclude = ["CODE_OF_CONDUCT.md"] +respect-gitignore = true +force-exclude = true + +# Disable rules: +# MD013: Line length - flagging on mkdocstrings content +# MD014: Forces commands in codeblocks to show output +# MD042: Empty link - buggy, still flagging on valid MkDocs formatting +# MD052: Reference link not found - buggy, flagging on valis MkDocs links +disable = ["MD014", "MD042", "MD052"] + +[tool.rumdl.MD013] # Line length +line_length = 999999 # Force reflow to paragraph content to remove linebreaks +reflow = true +reflow_mode = "normalize" + +[tool.rumdl.MD026] # Remove punctuation at the end of headings +punctuation = ",;:" + +[tool.rumdl.MD033] # No inline HTML +allowed_elements = ["nospell",] # pyspelling inline disable tag + +[tool.rumdl.MD046] +style = "fenced" diff --git a/tests/test_app_template.py b/tests/test_app_template.py index 35968f7..035ae8d 100644 --- a/tests/test_app_template.py +++ b/tests/test_app_template.py @@ -1,18 +1,17 @@ -import os import py_compile +import subprocess from pathlib import Path import pytest import toml from cookiecutter import main -from flake8.api import legacy as flake8 BASIC_APP_CONTEXT = { "formal_name": "Hello World", "app_name": "{{ cookiecutter.formal_name|lower|replace(' ', '') }}", "class_name": ( - "{{ cookiecutter.formal_name.title()" - ".replace(' ','').replace('-','').replace('!','').replace('.','').replace(',','') }}" + "{{ cookiecutter.formal_name.title().replace(' ','').replace('-','')" + ".replace('!','').replace('.','').replace(',','') }}" ), "module_name": "{{ cookiecutter.app_name|lower|replace('-', '_') }}", "project_name": "Project Awesome", @@ -36,12 +35,11 @@ def main(): - print(f"hello world - it's {datetime.now()}") + print(f"hello world - it's {datetime.utc()}") """ APP_START_SOURCE = """\ -import app - +from .app import app if __name__ == "__main__": app() @@ -80,29 +78,27 @@ def main(): pytest.param( { **BASIC_APP_CONTEXT, - **dict( - test_framework="unittest", - app_source=APP_SOURCE, - app_start_source=APP_START_SOURCE, - pyproject_table_macOS=SIMPLE_TABLE_CONTENT.format("macOS"), - pyproject_table_linux=SIMPLE_TABLE_CONTENT.format("linux"), - pyproject_table_linux_system_debian=SIMPLE_TABLE_CONTENT.format("deb"), - pyproject_table_linux_system_rhel=SIMPLE_TABLE_CONTENT.format("rhel"), - pyproject_table_linux_system_suse=SIMPLE_TABLE_CONTENT.format("suse"), - pyproject_table_linux_system_arch=SIMPLE_TABLE_CONTENT.format("arch"), - pyproject_table_linux_appimage=SIMPLE_TABLE_CONTENT.format("appimage"), - pyproject_table_linux_flatpak=SIMPLE_TABLE_CONTENT.format("flatpak"), - pyproject_table_windows=SIMPLE_TABLE_CONTENT.format("windows"), - pyproject_table_iOS=SIMPLE_TABLE_CONTENT.format("iOS"), - pyproject_table_android=SIMPLE_TABLE_CONTENT.format("android"), - pyproject_table_web=SIMPLE_TABLE_CONTENT.format("web"), - briefcase_version="v0.3.16-2", - template_source="https://example.com/beeware/briefcase-template", - template_branch="my-branch", - ), + "test_framework": "unittest", + "app_source": APP_SOURCE, + "app_start_source": APP_START_SOURCE, + "pyproject_table_macOS": SIMPLE_TABLE_CONTENT.format("macOS"), + "pyproject_table_linux": SIMPLE_TABLE_CONTENT.format("linux"), + "pyproject_table_linux_system_debian": SIMPLE_TABLE_CONTENT.format("deb"), + "pyproject_table_linux_system_rhel": SIMPLE_TABLE_CONTENT.format("rhel"), + "pyproject_table_linux_system_suse": SIMPLE_TABLE_CONTENT.format("suse"), + "pyproject_table_linux_system_arch": SIMPLE_TABLE_CONTENT.format("arch"), + "pyproject_table_linux_appimage": SIMPLE_TABLE_CONTENT.format("appimage"), + "pyproject_table_linux_flatpak": SIMPLE_TABLE_CONTENT.format("flatpak"), + "pyproject_table_windows": SIMPLE_TABLE_CONTENT.format("windows"), + "pyproject_table_iOS": SIMPLE_TABLE_CONTENT.format("iOS"), + "pyproject_table_android": SIMPLE_TABLE_CONTENT.format("android"), + "pyproject_table_web": SIMPLE_TABLE_CONTENT.format("web"), + "briefcase_version": "v0.X.Y-2", + "template_source": "foo/bar", + "template_branch": "my-branch", }, '''\ -# This project was generated with v0.3.16-2 using template: https://example.com/beeware/briefcase-template @ my-branch +# This project was generated with v0.X.Y-2 using template: foo/bar @ my-branch [tool.briefcase] project_name = "Project Awesome" bundle = "com.example" @@ -187,34 +183,33 @@ def main(): "web==1.1.0", ] -''', # noqa: E501 +''', id="normal-context", ), pytest.param( { **BASIC_APP_CONTEXT, - **dict( - app_source=APP_SOURCE, - app_start_source=APP_START_SOURCE, - pyproject_table_briefcase_extra_content=""" + "app_source": APP_SOURCE, + "app_start_source": APP_START_SOURCE, + "pyproject_table_briefcase_extra_content": """ field = "pyproject_table_briefcase_extra_content" answer = 42 """, - pyproject_table_briefcase_app_extra_content=""" + "pyproject_table_briefcase_app_extra_content": """ other_resources = [ "dir", "otherdir", "pyproject_table_briefcase_app_extra_content", ] """, - pyproject_table_macOS=SIMPLE_TABLE_CONTENT.format("macOS"), - pyproject_table_linux=SIMPLE_TABLE_CONTENT.format("linux"), - pyproject_table_linux_appimage=SIMPLE_TABLE_CONTENT.format("appimage"), - pyproject_table_linux_flatpak=SIMPLE_TABLE_CONTENT.format("flatpak"), - pyproject_table_windows=SIMPLE_TABLE_CONTENT.format("windows"), - pyproject_table_iOS=SIMPLE_TABLE_CONTENT.format("iOS"), - pyproject_table_android=SIMPLE_TABLE_CONTENT.format("android"), - pyproject_extra_content="""\ + "pyproject_table_macOS": SIMPLE_TABLE_CONTENT.format("macOS"), + "pyproject_table_linux": SIMPLE_TABLE_CONTENT.format("linux"), + "pyproject_table_linux_appimage": SIMPLE_TABLE_CONTENT.format("appimage"), + "pyproject_table_linux_flatpak": SIMPLE_TABLE_CONTENT.format("flatpak"), + "pyproject_table_windows": SIMPLE_TABLE_CONTENT.format("windows"), + "pyproject_table_iOS": SIMPLE_TABLE_CONTENT.format("iOS"), + "pyproject_table_android": SIMPLE_TABLE_CONTENT.format("android"), + "pyproject_extra_content": """\ [tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_one] field = "pyproject_extra_content" @@ -226,13 +221,12 @@ def main(): "value", ] """, - briefcase_version="v0.3.16-3", - template_source="https://example.com/beeware/briefcase-template", - template_branch="my-branch", - ), + "briefcase_version": "v0.X.Y-3", + "template_source": "foo/bar", + "template_branch": "my-branch", }, '''\ -# This project was generated with v0.3.16-3 using template: https://example.com/beeware/briefcase-template @ my-branch +# This project was generated with v0.X.Y-3 using template: foo/bar @ my-branch [tool.briefcase] project_name = "Project Awesome" bundle = "com.example" @@ -310,20 +304,21 @@ def main(): "value", "value", ] -''', # noqa: E501 +''', id="normal-context-with-extra-content", ), pytest.param( { **BASIC_APP_CONTEXT, - **dict( - app_source=APP_SOURCE, - app_start_source=APP_START_SOURCE, - pyproject_table_briefcase_extra_content='field = "pyproject_table_briefcase_extra_content"\n', - pyproject_table_briefcase_app_extra_content=""" + "app_source": APP_SOURCE, + "app_start_source": APP_START_SOURCE, + "pyproject_table_briefcase_extra_content": ( + 'field = "pyproject_table_briefcase_extra_content"\n' + ), + "pyproject_table_briefcase_app_extra_content": """ other_resources = ["dir", "pyproject_table_briefcase_app_extra_content"] """, - pyproject_extra_content="""\ + "pyproject_extra_content": """\ [tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_one] field = "pyproject_extra_content_one" @@ -331,13 +326,12 @@ def main(): field = "pyproject_extra_content_two" """, - briefcase_version="v0.3.16-3", - template_source="https://example.com/beeware/briefcase-template", - template_branch="my-branch", - ), + "briefcase_version": "v0.X.Y-3", + "template_source": "foo/bar", + "template_branch": "my-branch", }, '''\ -# This project was generated with v0.3.16-3 using template: https://example.com/beeware/briefcase-template @ my-branch +# This project was generated with v0.X.Y-3 using template: foo/bar @ my-branch [tool.briefcase] project_name = "Project Awesome" bundle = "com.example" @@ -369,7 +363,7 @@ def main(): [tool.briefcase.helloworld.my_custom_format_two] field = "pyproject_extra_content_two" -''', # noqa: E501 +''', id="only-extra-content", ), ] @@ -387,19 +381,7 @@ def app_directory(tmp_path, context): return tmp_path -def _all_filenames(directory): - """Return list of filenames in a directory, excluding __pycache__ files.""" - filenames = [] - for root, _, files in os.walk(str(directory)): - for f in files: - full_filename = Path(root) / f - if "__pycache__" not in full_filename.parts: - filenames.append(full_filename) - filenames.sort() - return filenames - - -@pytest.mark.parametrize("context, expected_toml", TEST_CASES) +@pytest.mark.parametrize(("context", "expected_toml"), TEST_CASES) def test_parse_pyproject_toml(app_directory, context, expected_toml): """Test for errors in parsing the generated pyproject.toml file.""" pyproject_toml = app_directory / "helloworld" / "pyproject.toml" @@ -408,18 +390,22 @@ def test_parse_pyproject_toml(app_directory, context, expected_toml): assert expected_toml == pyproject_toml.read_text() -@pytest.mark.parametrize("context, expected_toml", TEST_CASES) -def test_flake8_app(app_directory, context, expected_toml): - """Check there are no flake8 errors in any of the generated python files.""" - files = [f for f in _all_filenames(app_directory) if f.suffix == ".py"] - style_guide = flake8.get_style_guide() - report = style_guide.check_files(list(map(str, files))) - assert report.get_statistics("E") == [], "Flake8 found violations" +@pytest.mark.parametrize(("context", "expected_toml"), TEST_CASES) +def test_ruff(app_directory, context, expected_toml): + """Check there are no ruff errors in any of the generated python files.""" + try: + subprocess.run(["ruff", "check", app_directory], check=True) + except subprocess.CalledProcessError: + pytest.fail("Ruff found style violations") + + try: + subprocess.run(["ruff", "format", app_directory], check=True) + except subprocess.CalledProcessError: + pytest.fail("Ruff found format violations") -@pytest.mark.parametrize("context, expected_toml", TEST_CASES) +@pytest.mark.parametrize(("context", "expected_toml"), TEST_CASES) def test_files_compile(app_directory, context, expected_toml): - files = [f for f in _all_filenames(app_directory) if f.suffix == ".py"] - for filename in files: + for filename in app_directory.glob("**/*.py"): # If there is a compilation error, pytest is triggered py_compile.compile(str(filename)) diff --git a/{{ cookiecutter.app_name }}/{{ cookiecutter.test_source_dir }}/test_app.py b/{{ cookiecutter.app_name }}/{{ cookiecutter.test_source_dir }}/test_app.py index 4d955da..f93f302 100644 --- a/{{ cookiecutter.app_name }}/{{ cookiecutter.test_source_dir }}/test_app.py +++ b/{{ cookiecutter.app_name }}/{{ cookiecutter.test_source_dir }}/test_app.py @@ -4,6 +4,7 @@ def test_first(): """An initial test for the app.""" assert 1 + 1 == 2 {% elif cookiecutter.test_framework == "unittest" %} +# ruff: noqa: PT009 import unittest @@ -11,4 +12,4 @@ class {{ cookiecutter.class_name }}Tests(unittest.TestCase): def test_first(self): """An initial test for the app.""" self.assertEqual(1 + 1, 2) -{% endif %} \ No newline at end of file +{% endif %} diff --git a/{{ cookiecutter.app_name }}/{{ cookiecutter.test_source_dir }}/{{ cookiecutter.module_name }}.py b/{{ cookiecutter.app_name }}/{{ cookiecutter.test_source_dir }}/{{ cookiecutter.module_name }}.py index 92ae800..c36804e 100644 --- a/{{ cookiecutter.app_name }}/{{ cookiecutter.test_source_dir }}/{{ cookiecutter.module_name }}.py +++ b/{{ cookiecutter.app_name }}/{{ cookiecutter.test_source_dir }}/{{ cookiecutter.module_name }}.py @@ -26,7 +26,8 @@ def run_tests(): # Overwrite the cache directory to somewhere writable "-o", f"cache_dir={tempfile.gettempdir()}/.pytest_cache", - ] + args + *args, + ] ) {%- elif cookiecutter.test_framework == "unittest" -%} import os