From 83bf8beb4f45ef886921df1dfe5366baba0139e7 Mon Sep 17 00:00:00 2001 From: Russell Martin Date: Sat, 4 Nov 2023 15:47:48 -0400 Subject: [PATCH] Generalize template for GUI framework plugins --- .github/workflows/ci.yml | 31 +- .github/workflows/pre-commit-update.yml | 12 + .pre-commit-config.yaml | 42 ++ CONTRIBUTING.md | 1 - cookiecutter.json | 30 +- hooks/pre_gen_project.py | 10 +- requirements.txt | 7 + tests/test_app_template.py | 473 ++++++++++++++++-- tox.ini | 10 +- {{ cookiecutter.app_name }}/pyproject.toml | 291 ++--------- .../__main__.py | 13 +- .../src/{{ cookiecutter.module_name }}/app.py | 165 +----- {{ cookiecutter.app_name }}/tests/test_app.py | 12 +- 13 files changed, 613 insertions(+), 484 deletions(-) create mode 100644 .github/workflows/pre-commit-update.yml create mode 100644 .pre-commit-config.yaml create mode 100644 requirements.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 732b2c5..94db27c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,21 +10,38 @@ concurrency: group: ${{ github.ref }} cancel-in-progress: true +defaults: + run: + shell: bash + +env: + FORCE_COLOR: "1" + jobs: + pre-commit: + name: Pre-commit checks + uses: beeware/.github/.github/workflows/pre-commit-run.yml@main + with: + pre-commit-source: "-r requirements.txt" + unit-tests: + needs: pre-commit + name: Unit tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout + uses: actions/checkout@v4.1.1 + - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v4.7.1 with: python-version: "3.X" - - name: Install dependencies - run: | - python -m pip install tox + + - name: Install Dependencies + run: python -m pip install tox + - name: Test with tox - run: | - tox + run: tox verify-apps: name: Build App diff --git a/.github/workflows/pre-commit-update.yml b/.github/workflows/pre-commit-update.yml new file mode 100644 index 0000000..2a8038c --- /dev/null +++ b/.github/workflows/pre-commit-update.yml @@ -0,0 +1,12 @@ +name: Update pre-commit + +on: + schedule: + - cron: "0 20 * * SUN" # Sunday @ 2000 UTC + workflow_dispatch: + +jobs: + pre-commit-update: + name: Update pre-commit + uses: beeware/.github/.github/workflows/pre-commit-update.yml@main + secrets: inherit diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b0d099d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,42 @@ +exclude: ^{{ cookiecutter.app_name }}/ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-toml + - id: check-yaml + - id: check-case-conflict + - id: check-docstring-first + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/PyCQA/isort + rev: 5.12.0 + hooks: + - id: isort + additional_dependencies: [toml] + - repo: https://github.com/asottile/pyupgrade + rev: v3.15.0 + hooks: + - id: pyupgrade + args: [--py38-plus] + - repo: https://github.com/PyCQA/docformatter + rev: v1.7.5 + hooks: + - id: docformatter + args: [--in-place, --black] + - repo: https://github.com/psf/black + rev: 23.10.1 + hooks: + - id: black + language_version: python3 + - repo: https://github.com/PyCQA/flake8 + rev: 6.1.0 + hooks: + - id: flake8 + args: [--max-line-length=119] + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + additional_dependencies: + - tomli diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d6aad90..ec4f1c0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,4 +5,3 @@ BeeWare <3's contributions! Please be aware, BeeWare operates under a Code of Conduct. See [CONTRIBUTING to BeeWare](https://beeware.org/contributing) for details. - diff --git a/cookiecutter.json b/cookiecutter.json index e9f2b60..e0e2f24 100644 --- a/cookiecutter.json +++ b/cookiecutter.json @@ -20,22 +20,34 @@ "Proprietary", "Other" ], - "gui_framework": [ - "Toga", - "PySide2", - "PySide6", - "PursuedPyBear", - "Pygame", - "None" - ], "test_framework": [ "pytest", "unittest" ], + "app_source": "", + "app_start_source": "", + "pyproject_table_briefcase_additional": "", + "pyproject_table_briefcase_app_additional": "", + "pyproject_requires": "", + "pyproject_test_requires": "", + "pyproject_table_macOS": "", + "pyproject_table_linux": "", + "pyproject_table_linux_system_debian": "", + "pyproject_table_linux_system_rhel": "", + "pyproject_table_linux_system_suse": "", + "pyproject_table_linux_system_arch": "", + "pyproject_table_linux_appimage": "", + "pyproject_table_linux_flatpak": "", + "pyproject_table_windows": "", + "pyproject_table_iOS": "", + "pyproject_table_android": "", + "pyproject_table_web": "", + "pyproject_extra_content": "", "briefcase_version": "Unknown", "template_source": "Not provided", "template_branch": "Not provided", "_extensions": [ "briefcase.integrations.cookiecutter.TOMLEscape" - ] + ], + "_jinja2_env_vars": {"lstrip_blocks": true, "trim_blocks": true} } diff --git a/hooks/pre_gen_project.py b/hooks/pre_gen_project.py index 277a78c..3f8f622 100644 --- a/hooks/pre_gen_project.py +++ b/hooks/pre_gen_project.py @@ -1,17 +1,13 @@ import re import sys - # The restriction on application naming comes from PEP508 -PEP508_NAME_RE = re.compile( - r'^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$', - re.IGNORECASE -) +PEP508_NAME_RE = re.compile(r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", re.IGNORECASE) -app_name = '{{ cookiecutter.app_name }}' +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("ERROR: `%s` is not a valid Python package name!" % app_name) # exits with status 1 to indicate failure sys.exit(1) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0b607b6 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +briefcase @ git+https://github.com/beeware/briefcase.git +cookiecutter ==2.4.0 +flake8 ==6.1.0 +pre-commit ==3.5.0 +pytest ==7.4.3 +toml ==0.10.2 +tox ==4.11.3 diff --git a/tests/test_app_template.py b/tests/test_app_template.py index 3f2ab76..9b3a4c6 100644 --- a/tests/test_app_template.py +++ b/tests/test_app_template.py @@ -1,34 +1,448 @@ -import pathlib -import shutil - -from cookiecutter import main -from flake8.api import legacy as flake8 +import os import py_compile +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(',','') }}" + ), + "module_name": "{{ cookiecutter.app_name|lower|replace('-', '_') }}", + "project_name": "Project Awesome", + "description": "An app that does lots of stuff", + "author": "Jane Developer", + "author_email": "jane@example.com", + "bundle": "com.example", + "url": "https://example.com", + "license": "BSD license", + "test_framework": "pytest", +} + +SIMPLE_TABLE_CONTENT = """ +requires = [ + "requirement==1.1.0", +] +""" + +APP_SOURCE = """\ +from datetime import datetime + + +def main(): + print(f"hello world - it's {datetime.now()}") +""" + +APP_START_SOURCE = """\ +import app + + +if __name__ == "__main__": + app() +""" TEST_CASES = [ - {}, # use only the default briefcase-template values - # GUI framework options - {"gui_framework": "1"}, # Toga GUI framework - {"gui_framework": "2"}, # PySide2 GUI framework - {"gui_framework": "3"}, # PySide6 GUI framework - {"gui_framework": "4"}, # PursuedPyBear GUI framework - {"gui_framework": "5"}, # "None" for GUI framework - # Test framework options - {"test_framework": "1"}, # pytest test framework - {"test_framework": "2"}, # unittest test framework + pytest.param( + BASIC_APP_CONTEXT, + '''\ +# This project was generated with Unknown using template: Not provided@Not provided +[tool.briefcase] +project_name = "Project Awesome" +bundle = "com.example" +version = "0.0.1" +url = "https://example.com" +license = "BSD license" +author = "Jane Developer" +author_email = "jane@example.com" + +[tool.briefcase.app.helloworld] +formal_name = "Hello World" +description = "An app that does lots of stuff" +long_description = """More details about the app should go here. +""" +icon = "src/helloworld/resources/helloworld" +sources = [ + "src/helloworld", +] +test_sources = [ + "tests", +] + +requires = [ +] +test_requires = [ +] + +''', + id="minimum context", + ), + pytest.param( + { + **BASIC_APP_CONTEXT, + **dict( + test_framework="unittest", + app_source=APP_SOURCE, + app_start_source=APP_START_SOURCE, + pyproject_requires=""" + "toga==0.4.0" +""", + pyproject_test_requires=""" + "pytest" +""", + pyproject_table_macOS=SIMPLE_TABLE_CONTENT, + pyproject_table_linux=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_system_debian=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_system_rhel=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_system_suse=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_system_arch=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_appimage=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_flatpak=SIMPLE_TABLE_CONTENT, + pyproject_table_windows=SIMPLE_TABLE_CONTENT, + pyproject_table_iOS=SIMPLE_TABLE_CONTENT, + pyproject_table_android=SIMPLE_TABLE_CONTENT, + pyproject_table_web=SIMPLE_TABLE_CONTENT, + pyproject_extra_content="", + briefcase_version="v0.3.16-2", + template_source="https://example.com/beeware/briefcase-template", + template_branch="my-branch", + ), + }, + '''\ +# This project was generated with v0.3.16-2 using template: https://example.com/beeware/briefcase-template@my-branch +[tool.briefcase] +project_name = "Project Awesome" +bundle = "com.example" +version = "0.0.1" +url = "https://example.com" +license = "BSD license" +author = "Jane Developer" +author_email = "jane@example.com" + +[tool.briefcase.app.helloworld] +formal_name = "Hello World" +description = "An app that does lots of stuff" +long_description = """More details about the app should go here. +""" +icon = "src/helloworld/resources/helloworld" +sources = [ + "src/helloworld", +] +test_sources = [ + "tests", +] + +requires = [ + "toga==0.4.0" +] +test_requires = [ + "pytest" +] + +[tool.briefcase.app.helloworld.macOS] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.debian] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.rhel] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.suse] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.arch] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.appimage] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.flatpak] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.windows] +requires = [ + "requirement==1.1.0", +] + +# Mobile deployments +[tool.briefcase.app.helloworld.iOS] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.android] +requires = [ + "requirement==1.1.0", +] + +# Web deployments +[tool.briefcase.app.helloworld.web] +requires = [ + "requirement==1.1.0", +] + +''', + id="normal context", + ), + pytest.param( + { + **BASIC_APP_CONTEXT, + **dict( + app_source=APP_SOURCE, + app_start_source=APP_START_SOURCE, + pyproject_table_briefcase_additional=""" +field = "asdf" +answer = 42 +""", + pyproject_table_briefcase_app_additional=""" +other_resources = [ + "dir", + "otherdir", +]""", + pyproject_requires=""" + "toga==0.4.0" +""", + pyproject_test_requires=""" + "pytest" +""", + pyproject_table_macOS=SIMPLE_TABLE_CONTENT, + pyproject_table_linux=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_system_debian=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_system_rhel=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_system_suse=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_system_arch=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_appimage=SIMPLE_TABLE_CONTENT, + pyproject_table_linux_flatpak=SIMPLE_TABLE_CONTENT, + pyproject_table_windows=SIMPLE_TABLE_CONTENT, + pyproject_table_iOS=SIMPLE_TABLE_CONTENT, + pyproject_table_android=SIMPLE_TABLE_CONTENT, + pyproject_table_web=SIMPLE_TABLE_CONTENT, + pyproject_extra_content=""" +[tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_one] +field = "value" + +[tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_two] +field = "value" +""", + briefcase_version="v0.3.16-3", + template_source="https://example.com/beeware/briefcase-template", + template_branch="my-branch", + ), + }, + '''\ +# This project was generated with v0.3.16-3 using template: https://example.com/beeware/briefcase-template@my-branch +[tool.briefcase] +project_name = "Project Awesome" +bundle = "com.example" +version = "0.0.1" +url = "https://example.com" +license = "BSD license" +author = "Jane Developer" +author_email = "jane@example.com" +field = "asdf" +answer = 42 + +[tool.briefcase.app.helloworld] +formal_name = "Hello World" +description = "An app that does lots of stuff" +long_description = """More details about the app should go here. +""" +icon = "src/helloworld/resources/helloworld" +sources = [ + "src/helloworld", +] +test_sources = [ + "tests", +] + +requires = [ + "toga==0.4.0" +] +test_requires = [ + "pytest" +] +other_resources = [ + "dir", + "otherdir", +] + +[tool.briefcase.app.helloworld.macOS] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.debian] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.rhel] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.suse] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.arch] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.appimage] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.flatpak] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.windows] +requires = [ + "requirement==1.1.0", +] + +# Mobile deployments +[tool.briefcase.app.helloworld.iOS] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.app.helloworld.android] +requires = [ + "requirement==1.1.0", +] + +# Web deployments +[tool.briefcase.app.helloworld.web] +requires = [ + "requirement==1.1.0", +] + +[tool.briefcase.helloworld.my_custom_format_one] +field = "value" + +[tool.briefcase.helloworld.my_custom_format_two] +field = "value" +''', + 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_additional='\nfield = "asdf"', + pyproject_table_briefcase_app_additional=""" +other_resources = ["dir", "otherdir"] +""", + pyproject_requires=""" + "toga==0.4.0" +""", + pyproject_test_requires=""" + "pytest" +""", + pyproject_extra_content=""" +[tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_one] +field = "value" + +[tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_two] +field = "value" +""", + briefcase_version="v0.3.16-3", + template_source="https://example.com/beeware/briefcase-template", + template_branch="my-branch", + ), + }, + '''\ +# This project was generated with v0.3.16-3 using template: https://example.com/beeware/briefcase-template@my-branch +[tool.briefcase] +project_name = "Project Awesome" +bundle = "com.example" +version = "0.0.1" +url = "https://example.com" +license = "BSD license" +author = "Jane Developer" +author_email = "jane@example.com" +field = "asdf" + +[tool.briefcase.app.helloworld] +formal_name = "Hello World" +description = "An app that does lots of stuff" +long_description = """More details about the app should go here. +""" +icon = "src/helloworld/resources/helloworld" +sources = [ + "src/helloworld", +] +test_sources = [ + "tests", +] + +requires = [ + "toga==0.4.0" +] +test_requires = [ + "pytest" +] +other_resources = ["dir", "otherdir"] + +[tool.briefcase.helloworld.my_custom_format_one] +field = "value" + +[tool.briefcase.helloworld.my_custom_format_two] +field = "value" +''', + id="only extra content", + ), ] @pytest.fixture -def app_directory(tmpdir_factory, args): +def app_directory(tmpdir_factory, context): """Fixture for a default app.""" - output_dir = tmpdir_factory.mktemp("default-app") - output_dir = pathlib.Path(str(output_dir)).resolve() - root_dir = pathlib.Path(__file__).parent.parent.resolve() + output_dir = Path(tmpdir_factory.mktemp("default-app")).resolve() + root_dir = Path(__file__).parent.parent.resolve() main.cookiecutter( - str(root_dir), no_input=True, output_dir=str(output_dir), + str(root_dir), + no_input=True, + output_dir=str(output_dir), + extra_context=context, ) return output_dir @@ -36,34 +450,35 @@ def app_directory(tmpdir_factory, args): def _all_filenames(directory): """Return list of filenames in a directory, excluding __pycache__ files.""" filenames = [] - for root, _, files in pathlib.os.walk(str(directory)): + for root, _, files in os.walk(str(directory)): for f in files: - full_filename = root+pathlib.os.sep+f + full_filename = root + os.sep + f if "__pycache__" not in full_filename: filenames.append(full_filename) filenames.sort() return filenames -@pytest.mark.parametrize('args', TEST_CASES) -def test_parse_pyproject_toml(app_directory): +@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" assert pyproject_toml.is_file() # check pyproject.toml exists toml.load(pyproject_toml) # any error in parsing will trigger pytest + assert expected_toml == open(pyproject_toml).read() -@pytest.mark.parametrize('args', TEST_CASES) -def test_flake8_app(app_directory, args): - """Check there are no flake8 errors in any of the generated python files""" +@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.endswith(".py")] style_guide = flake8.get_style_guide() report = style_guide.check_files(files) assert report.get_statistics("E") == [], "Flake8 found violations" -@pytest.mark.parametrize('args', TEST_CASES) -def test_files_compile(app_directory, args): +@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.endswith(".py")] for filename in files: # If there is a compilation error, pytest is triggered diff --git a/tox.ini b/tox.ini index 8eacbe7..74fa9ca 100644 --- a/tox.ini +++ b/tox.ini @@ -2,11 +2,5 @@ [testenv] skip_install = True -deps = - git+https://github.com/beeware/briefcase.git - cookiecutter - flake8 - pytest - pytest-tldr - toml -commands = pytest tests +deps = -r{toxinidir}/requirements.txt +commands = python -m pytest {posargs:-vv --color yes} tests/ diff --git a/{{ cookiecutter.app_name }}/pyproject.toml b/{{ cookiecutter.app_name }}/pyproject.toml index 47fdcf1..ebeeaa7 100644 --- a/{{ cookiecutter.app_name }}/pyproject.toml +++ b/{{ cookiecutter.app_name }}/pyproject.toml @@ -7,6 +7,7 @@ url = "{{ cookiecutter.url }}" license = "{{ cookiecutter.license }}" author = "{{ cookiecutter.author }}" author_email = "{{ cookiecutter.author_email }}" +{{- cookiecutter.pyproject_table_briefcase_additional }} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}] formal_name = "{{ cookiecutter.formal_name|escape_toml }}" @@ -22,280 +23,74 @@ test_sources = [ ] requires = [ -{%- if cookiecutter.gui_framework == "PySide2" %} - "pyside2~=5.15", -{%- elif cookiecutter.gui_framework == "PySide6" %} - "PySide6-Essentials~=6.5", - # "PySide6-Addons~=6.5", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} - "ppb~=1.1", -{%- elif cookiecutter.gui_framework == "Pygame" %} - "pygame~=2.2", -{%- endif %} +{{- cookiecutter.pyproject_requires }} ] test_requires = [ -{%- if cookiecutter.test_framework == "pytest" %} - "pytest", -{%- endif %} +{{- cookiecutter.pyproject_test_requires }} ] +{{- cookiecutter.pyproject_table_briefcase_app_additional }} +{% if cookiecutter.pyproject_table_macOS%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.macOS] -universal_build = true -requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - "toga-cocoa~=0.4.0", -{%- endif %} - "std-nslog~=1.0.0" -] +{{- cookiecutter.pyproject_table_macOS }} +{% endif %} +{% if cookiecutter.pyproject_table_linux%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.linux] -requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - "toga-gtk~=0.4.0", -{%- endif %} -] +{{- cookiecutter.pyproject_table_linux }} +{% endif %} +{% if cookiecutter.pyproject_table_linux_system_debian%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.linux.system.debian] -system_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to compile pycairo wheel - "libcairo2-dev", - # Needed to compile PyGObject wheel - "libgirepository1.0-dev", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] - -system_runtime_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to provide GTK and its GI bindings - "gir1.2-gtk-3.0", - "libgirepository-1.0-1", - # Dependencies that GTK looks for at runtime - "libcanberra-gtk3-module", - # Needed to provide WebKit2 at runtime - # "gir1.2-webkit2-4.0", -{%- elif cookiecutter.gui_framework == "PySide2" or cookiecutter.gui_framework == "PySide6" %} - # Derived from https://doc.qt.io/qt-6/linux-requirements.html - "libxrender1", - "libxcb-render0", - "libxcb-render-util0", - "libxcb-shape0", - "libxcb-randr0", - "libxcb-xfixes0", - "libxcb-xkb1", - "libxcb-sync1", - "libxcb-shm0", - "libxcb-icccm4", - "libxcb-keysyms1", - "libxcb-image0", - "libxcb-util1", - "libxkbcommon0", - "libxkbcommon-x11-0", - "libfontconfig1", - "libfreetype6", - "libxext6", - "libx11-6", - "libxcb1", - "libx11-xcb1", - "libsm6", - "libice6", - "libglib2.0-0", - "libgl1", - "libegl1-mesa", - "libdbus-1-3", - "libgssapi-krb5-2", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] +{{- cookiecutter.pyproject_table_linux_system_debian }} +{% endif %} +{% if cookiecutter.pyproject_table_linux_system_rhel%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.linux.system.rhel] -system_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to compile pycairo wheel - "cairo-gobject-devel", - # Needed to compile PyGObject wheel - "gobject-introspection-devel", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] - -system_runtime_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to support Python bindings to GTK - "gobject-introspection", - # Needed to provide GTK - "gtk3", - # Dependencies that GTK looks for at runtime - "libcanberra-gtk3", - # Needed to provide WebKit2 at runtime - # "webkit2gtk3", -{%- elif cookiecutter.gui_framework == "PySide2" %} - "qt5-qtbase-gui", -{%- elif cookiecutter.gui_framework == "PySide6" %} - "qt6-qtbase-gui", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] +{{- cookiecutter.pyproject_table_linux_system_rhel }} +{% endif %} +{% if cookiecutter.pyproject_table_linux_system_suse%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.linux.system.suse] -system_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to compile pycairo wheel - "cairo-devel", - # Needed to compile PyGObject wheel - "gobject-introspection-devel", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] - -system_runtime_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to provide GTK - "gtk3", - # Needed to support Python bindings to GTK - "gobject-introspection", "typelib(Gtk)=3.0", - # Dependencies that GTK looks for at runtime - "libcanberra-gtk3-0", - # Needed to provide WebKit2 at runtime - # "libwebkit2gtk3", - # "typelib(WebKit2)", -{%- elif cookiecutter.gui_framework == "PySide2" %} - "libQt5Gui5", -{%- elif cookiecutter.gui_framework == "PySide6" %} - "libQt6Gui6", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] +{{- cookiecutter.pyproject_table_linux_system_suse }} +{% endif %} +{% if cookiecutter.pyproject_table_linux_system_arch%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.linux.system.arch] -system_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to compile pycairo wheel - "cairo", - # Needed to compile PyGObject wheel - "gobject-introspection", - # Runtime dependencies that need to exist so that the - # Arch package passes final validation. - # Needed to provide GTK - "gtk3", - # Dependencies that GTK looks for at runtime - "libcanberra", - # Needed to provide WebKit2 - # "webkit2gtk", -{%- elif cookiecutter.gui_framework == "PySide6" %} - "qt6-base", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] - -system_runtime_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to provide GTK - "gtk3", - # Needed to provide PyGObject bindings - "gobject-introspection-runtime", - # Dependencies that GTK looks for at runtime - "libcanberra", - # Needed to provide WebKit2 at runtime - # "webkit2gtk", -{%- elif cookiecutter.gui_framework == "PySide6" %} - "qt6-base", -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] +{{- cookiecutter.pyproject_table_linux_system_arch }} +{% endif %} +{% if cookiecutter.pyproject_table_linux_appimage%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.linux.appimage] -{%- if cookiecutter.gui_framework == "PySide6" %} -manylinux = "manylinux_2_28" -{%- else %} -manylinux = "manylinux2014" -{%- endif %} - -system_requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - # Needed to compile pycairo wheel - "cairo-gobject-devel", - # Needed to compile PyGObject wheel - "gobject-introspection-devel", - # Needed to provide GTK - "gtk3-devel", - # Dependencies that GTK looks for at runtime, that need to be - # in the build environment to be picked up by linuxdeploy - "libcanberra-gtk3", - "PackageKit-gtk3-module", - "gvfs-client", -{%- elif cookiecutter.gui_framework == "PySide2" %} -# ?? FIXME -{%- elif cookiecutter.gui_framework == "PySide6" %} -# ?? FIXME -{%- elif cookiecutter.gui_framework == "PursuedPyBear" %} -# ?? FIXME -{%- endif %} -] -linuxdeploy_plugins = [ -{%- if cookiecutter.gui_framework == "Toga" %} - "DEPLOY_GTK_VERSION=3 gtk", -{% endif -%} -] +{{- cookiecutter.pyproject_table_linux_appimage }} +{% endif %} +{% if cookiecutter.pyproject_table_linux_flatpak%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.linux.flatpak] -{%- if cookiecutter.gui_framework == "Toga" %} -flatpak_runtime = "org.gnome.Platform" -flatpak_runtime_version = "44" -flatpak_sdk = "org.gnome.Sdk" -{%- elif cookiecutter.gui_framework in ["PySide2", "PySide6"] %} -flatpak_runtime = "org.kde.Platform" -flatpak_runtime_version = "6.4" -flatpak_sdk = "org.kde.Sdk" -{%- else %} -flatpak_runtime = "org.freedesktop.Platform" -flatpak_runtime_version = "22.08" -flatpak_sdk = "org.freedesktop.Sdk" -{%- endif %} +{{- cookiecutter.pyproject_table_linux_flatpak }} +{% endif %} +{% if cookiecutter.pyproject_table_windows%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.windows] -requires = [ -{%- if cookiecutter.gui_framework == "Toga" %} - "toga-winforms~=0.4.0", -{% endif -%} -] +{{- cookiecutter.pyproject_table_windows }} +{% endif %} +{% if cookiecutter.pyproject_table_macOS or cookiecutter.pyproject_table_android %} # Mobile deployments +{% endif %} +{% if cookiecutter.pyproject_table_macOS%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.iOS] -{%- if cookiecutter.gui_framework == "Toga" %} -requires = [ - "toga-iOS~=0.4.0", - "std-nslog~=1.0.0" -] -{%- else %} -supported = false -{%- endif %} +{{- cookiecutter.pyproject_table_macOS }} +{% endif %} +{% if cookiecutter.pyproject_table_android%} [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.android] -{%- if cookiecutter.gui_framework == "Toga" %} -requires = [ - "toga-android~=0.4.0" -] -{%- else %} -supported = false -{%- endif %} +{{- cookiecutter.pyproject_table_android }} +{% endif %} +{% if cookiecutter.pyproject_table_web %} # Web deployments [tool.briefcase.app.{{ cookiecutter.app_name|escape_non_ascii }}.web] -{%- if cookiecutter.gui_framework == "Toga" %} -requires = [ - "toga-web~=0.4.0", -] -style_framework = "Shoelace v2.3" -{%- else %} -supported = false -{%- endif %} +{{- cookiecutter.pyproject_table_web }} +{% endif %} +{{ cookiecutter.pyproject_extra_content }} diff --git a/{{ cookiecutter.app_name }}/src/{{ cookiecutter.module_name }}/__main__.py b/{{ cookiecutter.app_name }}/src/{{ cookiecutter.module_name }}/__main__.py index a9b8bfe..f5ed960 100644 --- a/{{ cookiecutter.app_name }}/src/{{ cookiecutter.module_name }}/__main__.py +++ b/{{ cookiecutter.app_name }}/src/{{ cookiecutter.module_name }}/__main__.py @@ -1,10 +1,9 @@ +{% if cookiecutter.app_start_source %} +{{ cookiecutter.app_start_source }} +{% else %} from {{ cookiecutter.module_name }}.app import main -if __name__ == '__main__': -{%- if cookiecutter.gui_framework == 'Toga' %} - main().main_loop() -{%- elif cookiecutter.gui_framework in ('PySide2', 'PySide6') %} - main() -{%- else %} + +if __name__ == "__main__": main() -{%- endif %} +{% endif %} diff --git a/{{ cookiecutter.app_name }}/src/{{ cookiecutter.module_name }}/app.py b/{{ cookiecutter.app_name }}/src/{{ cookiecutter.module_name }}/app.py index ab0fa77..b4bcd8c 100644 --- a/{{ cookiecutter.app_name }}/src/{{ cookiecutter.module_name }}/app.py +++ b/{{ cookiecutter.app_name }}/src/{{ cookiecutter.module_name }}/app.py @@ -1,169 +1,12 @@ """ {{ cookiecutter.description|escape_toml }} """ -{% if cookiecutter.gui_framework == 'Toga' -%} -import toga -from toga.style import Pack -from toga.style.pack import COLUMN, ROW +{% if cookiecutter.app_source %} +{{ cookiecutter.app_source }} +{% else %} -class {{ cookiecutter.class_name }}(toga.App): - - def startup(self): - """ - Construct and show the Toga application. - - Usually, you would add your application to a main content box. - We then create a main window (with a name matching the app), and - show the main window. - """ - main_box = toga.Box() - - self.main_window = toga.MainWindow(title=self.formal_name) - self.main_window.content = main_box - self.main_window.show() - - -def main(): - return {{ cookiecutter.class_name }}() -{% elif cookiecutter.gui_framework in ('PySide2', 'PySide6') -%} -import sys - -try: - from importlib import metadata as importlib_metadata -except ImportError: - # Backwards compatibility - importlib.metadata was added in Python 3.8 - import importlib_metadata - -from {{ cookiecutter.gui_framework }} import QtWidgets - - -class {{ cookiecutter.class_name }}(QtWidgets.QMainWindow): - def __init__(self): - super().__init__() - self.init_ui() - - def init_ui(self): - self.setWindowTitle('{{ cookiecutter.app_name }}') - self.show() - - -def main(): - # Linux desktop environments use app's .desktop file to integrate the app - # to their application menus. The .desktop file of this app will include - # StartupWMClass key, set to app's formal name, which helps associate - # app's windows to its menu item. - # - # For association to work any windows of the app must have WMCLASS - # property set to match the value set in app's desktop file. For PySide2 - # this is set with setApplicationName(). - - # Find the name of the module that was used to start the app - app_module = sys.modules['__main__'].__package__ - # Retrieve the app's metadata - metadata = importlib_metadata.metadata(app_module) - - QtWidgets.QApplication.setApplicationName(metadata['Formal-Name']) - - app = QtWidgets.QApplication(sys.argv) - main_window = {{ cookiecutter.class_name }}() - {%- if cookiecutter.gui_framework == 'PySide2' %} - sys.exit(app.exec_()) - {%- else %} - sys.exit(app.exec()) - {%- endif %} -{%- elif cookiecutter.gui_framework == 'PursuedPyBear' %} -import os -import sys - -try: - from importlib import metadata as importlib_metadata -except ImportError: - # Backwards compatibility - importlib.metadata was added in Python 3.8 - import importlib_metadata - -import ppb - - -class {{ cookiecutter.class_name }}(ppb.Scene): - def __init__(self, **props): - super().__init__(**props) - - self.add(ppb.Sprite( - image=ppb.Image('{{ cookiecutter.module_name }}/resources/{{ cookiecutter.app_name }}.png'), - )) - - -def main(): - # Linux desktop environments use app's .desktop file to integrate the app - # to their application menus. The .desktop file of this app will include - # StartupWMClass key, set to app's formal name, which helps associate - # app's windows to its menu item. - # - # For association to work any windows of the app must have WMCLASS - # property set to match the value set in app's desktop file. For PPB this - # is set using environment variable. - - # Find the name of the module that was used to start the app - app_module = sys.modules['__main__'].__package__ - # Retrieve the app's metadata - metadata = importlib_metadata.metadata(app_module) - - os.environ['SDL_VIDEO_X11_WMCLASS'] = metadata['Formal-Name'] - - ppb.run( - starting_scene={{ cookiecutter.class_name }}, - title=metadata['Formal-Name'], - ) -{%- elif cookiecutter.gui_framework == 'Pygame' %} -import pygame -import sys -import os - -try: - from importlib import metadata as importlib_metadata -except ImportError: - # Backwards compatibility - importlib.metadata was added in Python 3.8 - import importlib_metadata - -SCREEN_WIDTH, SCREEN_HEIGHT = 800, 600 -WHITE = (255, 255, 255) - - -def main(): - # Linux desktop environments use app's .desktop file to integrate the app - # to their application menus. The .desktop file of this app will include - # StartupWMClass key, set to app's formal name, which helps associate - # app's windows to its menu item. - # - # For association to work any windows of the app must have WMCLASS - # property set to match the value set in app's desktop file. For PPB this - # is set using environment variable. - - # Find the name of the module that was used to start the app - app_module = sys.modules["__main__"].__package__ - # Retrieve the app's metadata - metadata = importlib_metadata.metadata(app_module) - - os.environ["SDL_VIDEO_X11_WMCLASS"] = metadata["Formal-Name"] - - pygame.init() - pygame.display.set_caption(metadata["Formal-Name"]) - screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) - - running = True - while running: - for event in pygame.event.get(): - if event.type == pygame.QUIT: - running = False - break - - screen.fill(WHITE) - pygame.display.flip() - - pygame.quit() -{% else -%} def main(): # This should start and launch your app! pass -{% endif -%} +{% endif %} diff --git a/{{ cookiecutter.app_name }}/tests/test_app.py b/{{ cookiecutter.app_name }}/tests/test_app.py index 5d7f668..6d435e5 100644 --- a/{{ cookiecutter.app_name }}/tests/test_app.py +++ b/{{ cookiecutter.app_name }}/tests/test_app.py @@ -1,16 +1,14 @@ -{%- if cookiecutter.test_framework == 'pytest' -%} +{% if cookiecutter.test_framework == 'pytest' %} def test_first(): - "An initial test for the app" + """An initial test for the app.""" assert 1 + 1 == 2 - -{%- elif cookiecutter.test_framework == "unittest" -%} +{% elif cookiecutter.test_framework == "unittest" %} import unittest class {{ cookiecutter.class_name }}Tests(unittest.TestCase): def test_first(self): - "An initial test for the app" + """An initial test for the app.""" self.assertEqual(1 + 1, 2) - -{%- endif %} +{% endif %} \ No newline at end of file