diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 732b2c5..f6221cf 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: + name: Unit tests + needs: pre-commit 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 -r requirements.txt + - 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..4639004 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,41 @@ +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..baf2d4b 100644 --- a/tests/test_app_template.py +++ b/tests/test_app_template.py @@ -1,70 +1,466 @@ -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 = [ + "{}==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=""" + "pyproject_requires" +""", + pyproject_test_requires=""" + "pyproject_test_requires" +""", + 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", + ), + }, + '''\ +# 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 = [ + "pyproject_requires" +] +test_requires = [ + "pyproject_test_requires" +] + +[tool.briefcase.app.helloworld.macOS] +requires = [ + "macOS==1.1.0", +] + +[tool.briefcase.app.helloworld.linux] +requires = [ + "linux==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.debian] +requires = [ + "deb==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.rhel] +requires = [ + "rhel==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.suse] +requires = [ + "suse==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.system.arch] +requires = [ + "arch==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.appimage] +requires = [ + "appimage==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.flatpak] +requires = [ + "flatpak==1.1.0", +] + +[tool.briefcase.app.helloworld.windows] +requires = [ + "windows==1.1.0", +] + +# Mobile deployments +[tool.briefcase.app.helloworld.iOS] +requires = [ + "iOS==1.1.0", +] + +[tool.briefcase.app.helloworld.android] +requires = [ + "android==1.1.0", +] + +# Web deployments +[tool.briefcase.app.helloworld.web] +requires = [ + "web==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 = "pyproject_table_briefcase_additional" +answer = 42 +""", + pyproject_table_briefcase_app_additional=""" + +other_resources = [ + "dir", + "otherdir", + "pyproject_table_briefcase_app_additional", +]""", + pyproject_requires=""" + "pyproject_requires" +""", + pyproject_test_requires=""" + "pyproject_test_requires" +""", + 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" + +nested_table = { "answer" = 42, "field" = "asdf" } + +[tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_two] +list = [ + "value", + "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 = "pyproject_table_briefcase_additional" +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 = [ + "pyproject_requires" +] +test_requires = [ + "pyproject_test_requires" +] + +other_resources = [ + "dir", + "otherdir", + "pyproject_table_briefcase_app_additional", +] + +[tool.briefcase.app.helloworld.macOS] +requires = [ + "macOS==1.1.0", +] + +[tool.briefcase.app.helloworld.linux] +requires = [ + "linux==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.appimage] +requires = [ + "appimage==1.1.0", +] + +[tool.briefcase.app.helloworld.linux.flatpak] +requires = [ + "flatpak==1.1.0", +] + +[tool.briefcase.app.helloworld.windows] +requires = [ + "windows==1.1.0", +] + +# Mobile deployments +[tool.briefcase.app.helloworld.iOS] +requires = [ + "iOS==1.1.0", +] + +[tool.briefcase.app.helloworld.android] +requires = [ + "android==1.1.0", +] + +[tool.briefcase.helloworld.my_custom_format_one] +field = "pyproject_extra_content" + +nested_table = { "answer" = 42, "field" = "asdf" } + +[tool.briefcase.helloworld.my_custom_format_two] +list = [ + "value", + "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 = "pyproject_table_briefcase_additional"', + pyproject_table_briefcase_app_additional=""" +other_resources = ["dir", "pyproject_table_briefcase_app_additional"] +""", + pyproject_requires=""" + "pyproject_requires" +""", + pyproject_test_requires=""" + "pyproject_test_requires" +""", + pyproject_extra_content=""" +[tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_one] +field = "pyproject_extra_content_one" + +[tool.briefcase.{{ cookiecutter.app_name|escape_non_ascii }}.my_custom_format_two] +field = "pyproject_extra_content_two" +""", + 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 = "pyproject_table_briefcase_additional" + +[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 = [ + "pyproject_requires" +] +test_requires = [ + "pyproject_test_requires" +] +other_resources = ["dir", "pyproject_table_briefcase_app_additional"] + +[tool.briefcase.helloworld.my_custom_format_one] +field = "pyproject_extra_content_one" + +[tool.briefcase.helloworld.my_custom_format_two] +field = "pyproject_extra_content_two" +''', + id="only extra content", + ), ] @pytest.fixture -def app_directory(tmpdir_factory, args): +def app_directory(tmp_path, 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() main.cookiecutter( - str(root_dir), no_input=True, output_dir=str(output_dir), + str(Path(__file__).parent.parent.resolve()), + no_input=True, + output_dir=str(tmp_path), + extra_context=context, ) - return output_dir + return tmp_path 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 - if "__pycache__" not in full_filename: + full_filename = Path(root) / f + if "__pycache__" not in full_filename.parts: 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 + with open(pyproject_toml) as toml_file: + assert expected_toml == toml_file.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""" - files = [f for f in _all_filenames(app_directory) if f.endswith(".py")] +@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(files) + report = style_guide.check_files(list(map(str, files))) assert report.get_statistics("E") == [], "Flake8 found violations" -@pytest.mark.parametrize('args', TEST_CASES) -def test_files_compile(app_directory, args): - files = [f for f in _all_filenames(app_directory) if f.endswith(".py")] +@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: # If there is a compilation error, pytest is triggered - py_compile.compile(filename) + py_compile.compile(str(filename)) diff --git a/tox.ini b/tox.ini index 8eacbe7..1f66a79 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 }}/LICENSE b/{{ cookiecutter.app_name }}/LICENSE index ecd85b9..e994ffd 100644 --- a/{{ cookiecutter.app_name }}/LICENSE +++ b/{{ cookiecutter.app_name }}/LICENSE @@ -1,11 +1,23 @@ {% if cookiecutter.license == 'MIT license' %} Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.author }} -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. {% elif cookiecutter.license == 'BSD license' %} Copyright (c) {% now 'local', '%Y' %}, {{ cookiecutter.author }} All rights reserved. diff --git a/{{ cookiecutter.app_name }}/pyproject.toml b/{{ cookiecutter.app_name }}/pyproject.toml index 47fdcf1..15ba598 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_iOS %} [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_iOS }} +{% 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..4d955da 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 diff --git a/{{ cookiecutter.app_name }}/tests/{{ cookiecutter.module_name }}.py b/{{ cookiecutter.app_name }}/tests/{{ cookiecutter.module_name }}.py index 8939c77..92ae800 100644 --- a/{{ cookiecutter.app_name }}/tests/{{ cookiecutter.module_name }}.py +++ b/{{ cookiecutter.app_name }}/tests/{{ cookiecutter.module_name }}.py @@ -42,7 +42,7 @@ def run_tests(): runner = unittest.TextTestRunner(verbosity=2) result = runner.run(suite) returncode = int(not result.wasSuccessful()) -{%- endif %} +{%- endif +%} print(f">>>>>>>>>> EXIT {returncode} <<<<<<<<<<")