Repository: beeware/toga

Issue:
Your fist toga app crashes on Linux Mint19
## Expected Behavior
Launching the app displays the app window


## Current Behavior
<!--- If you're describing a bug, what currently happens? -->
(venv) shay@shayscan:~/projects/toga$ python3 -m helloworld
Traceback (most recent call last):
  File "/usr/lib/python3.6/runpy.py", line 193, in _run_module_as_main
    "__main__", mod_spec)
  File "/usr/lib/python3.6/runpy.py", line 85, in _run_code
    exec(code, run_globals)
  File "/home/shay/projects/toga/helloworld.py", line 25, in <module>
    main().main_loop()
  File "/home/shay/projects/toga/helloworld.py", line 21, in main
    return toga.App('First App', 'org.beeware.helloworld', startup=build)
  File "/home/shay/projects/toga/venv/lib/python3.6/site-packages/toga/app.py", line 185, in __init__
    self.icon = 'resources/{app_name}'.format(app_name=self.app_name)
  File "/home/shay/projects/toga/venv/lib/python3.6/site-packages/toga/app.py", line 318, in icon
    self._icon.bind(self.factory)
  File "/home/shay/projects/toga/venv/lib/python3.6/site-packages/toga/icons.py", line 41, in bind
    resource_path = factory.paths.app
  File "/home/shay/projects/toga/venv/lib/python3.6/site-packages/toga_gtk/paths.py", line 10, in app
    return Path(sys.modules[App.app.module_name].__file__).parent
KeyError: ''

## Steps to reproduce
<!--- Provide a set of steps describing how to reproduce this bug. If you have a live example, provide the link below -->
1. Create env as per tutorial https://toga.readthedocs.io/en/latest/tutorial/tutorial-0.html

2. Run with python3 -m helloworld


## Your Environment
<!--- Provide details on your current environment you found the bug in -->

* Python Version (list the specific version number)
Python 3.6.9
* Operating System and Version (select from the following and list the specific version number; if your OS is not listed, list that as well)

  - [ ] macOS - version:
  - [x] Linux - Mint:   - 19:
  - [ ] Windows - version:
  - [ ] Other - name:  - version:

* Toga Version (list the specific version number or git hash)

* Toga Target (the type of app you are trying to generate)

  - [ ] android
  - [ ] cocoa
  - [ ] django
  - [x] gtk
  - [ ] iOS
  - [ ] tvOS
  - [ ] watchOS
  - [ ] winforms
  - [ ] win32
  - [ ] Other (please specify)



Discussion:
I got this to run OK by adding a check for self._app_name being an empty string in toga/app.py, 
While the trace indicated a Path problem I found the app.py checks for self._app_name being None at line 102. But it has already made it a str by assigning it a value. As a result the 'toga' is never added and hence the App.app.module_name is empty.

       # Keep an accessible copy of the app instance
        App.app = self
        # We need a module name to load app metadata. If an app_name has been
        # provided, we can set the app name now, and derive the module name
        # from there.
        if app_name:
            self._app_name = app_name
        else:
            self._app_name = sys.modules['__main__'].__package__
            # During tests, and when running from a prompt, there won't be
            # a __main__ module. Fall back to a module that we know *does*
            # exist.
            if self._app_name is None or self._app_name == '': # my change
                self._app_name = 'toga'

Thanks for the report! Looks like some recent changes have clashed with the instructions in the tutorial.

However, there's definitely a bug here. Your patch is heading in the right direction, but there are a couple of edge cases you haven't considered (and in looking closer, there's a couple of *other* edge cases that the existing code doesn't catch, either). I'll add a fix for this shortly.

The good news is that there's an immediate workaround. The tutorial will start without any changes if you use `python helloworld.py` (i.e., *don't* use the `-m` flag). It will also work if you put the tutorial code in a folder with a `__main__.py` file; see [the version of tutorial 0 in the examples folder](https://github.com/beeware/toga/tree/master/examples/tutorial0) for an example of how this works.


Resolving patch:
diff --git a/src/core/toga/app.py b/src/core/toga/app.py
--- a/src/core/toga/app.py
+++ b/src/core/toga/app.py
@@ -100,23 +100,26 @@ def __init__(
         if app_name:
             self._app_name = app_name
         else:
-            self._app_name = sys.modules['__main__'].__package__
+            # If the code is contained in appname.py, and you start the app
+            # using `python -m appname`, the main module package will report
+            # as ''. Set the initial app name as None.
+            # If the code is contained in appname.py, and you start the app
+            # using `python appname.py`, the main module will report as None.
+            # If the code is contained in a folder, and you start the app
+            # using `python -m appname`, the main module will report as the
+            # name of the folder.
+            main_module_pkg = sys.modules['__main__'].__package__
+            if main_module_pkg == '':
+                self._app_name = None
+            else:
+                self._app_name = main_module_pkg
+
             # During tests, and when running from a prompt, there won't be
             # a __main__ module.
 
             # Try deconstructing the app name from the app ID
             if self._app_name is None and app_id:
-                try:
-                    self._app_name = app_id.split('.')[-1]
-                    # Make sure that the module name actually exists.
-                    sys.modules[self.module_name]
-                except KeyError:
-                    # Well that didn't work...
-                    self._app_name = None
-
-            # Fall back to a module that we *know* exists
-            if self._app_name is None:
-                self._app_name = 'toga'
+                self._app_name = app_id.split('.')[-1]
 
         # Load the app metdata (if it is available)
         # Apps packaged with Briefcase will have this metadata.
@@ -127,11 +130,23 @@ def __init__(
 
         # Now that we have metadata, we can fix the app name (in the case
         # where the app name and the module name differ - e.g., an app name
-        # of `hello-world` will have a module name of `hello_world`).
-        # We use the PEP566 key "Name", rather than "App-Name".
-        if app_name is None and self.metadata['Name'] is not None:
+        # of ``hello-world`` will have a module name of ``hello_world``).
+        # We use the PEP566-compliant key ``Name```, rather than the internally
+        # consistent key ``App-Name```.
+        if self.metadata['Name'] is not None:
             self._app_name = self.metadata['Name']
 
+        # Whatever app name has been given, speculatively attempt to import
+        # the app module. Single-file apps won't have an app folder; apps with
+        # misleading or misconfigured app names haven't given us enough
+        # metadata to determine the app folder. In those cases, fall back to
+        # an app name that *will* exist (``toga```)
+        try:
+            sys.modules[self.module_name]
+        except KeyError:
+            # Well that didn't work...
+            self._app_name = 'toga'
+
         # If a name has been provided, use it; otherwise, look to
         # the module metadata. However, a name *must* be provided.
         if formal_name:


Test patch: