Skip to content

Commit

Permalink
Revert lazy import of documents and statusicons, and work around …
Browse files Browse the repository at this point in the history
…MicroPython's inability to set custom attributes on functions
  • Loading branch information
mhsmith committed Dec 5, 2024
1 parent 6c55ec3 commit cebc052
Show file tree
Hide file tree
Showing 3 changed files with 36 additions and 57 deletions.
44 changes: 17 additions & 27 deletions core/src/toga/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,20 @@

from toga.command import Command, CommandSet
from toga.constants import WindowState
from toga.documents import Document, DocumentSet
from toga.handlers import simple_handler, wrapped_handler
from toga.icons import Icon
from toga.paths import Paths
from toga.platform import get_platform_factory
from toga.statusicons import StatusIconSet
from toga.window import MainWindow, Window, WindowSet

if TYPE_CHECKING:
from toga.dialogs import Dialog
from toga.documents import Document, DocumentSet
from toga.hardware.camera import Camera
from toga.hardware.location import Location
from toga.icons import IconContentT
from toga.screens import Screen
from toga.statusicons import StatusIconSet
from toga.widgets.base import Widget

# Make sure deprecation warnings are shown by default
Expand Down Expand Up @@ -316,7 +316,10 @@ def __init__(
self.icon = icon

# Set up the document types and collection of documents being managed.
self._document_types = [] if document_types is None else document_types
self._documents = DocumentSet(
self,
types=[] if document_types is None else document_types,
)

# Install the lifecycle handlers. If passed in as an argument, or assigned using
# `app.on_event = my_handler`, the event handler will take the app as the first
Expand All @@ -335,6 +338,7 @@ def __init__(
# We need the command set to exist so that startup et al. can add commands;
# but we don't have an impl yet, so we can't set the on_change handler
self._commands = CommandSet()
self._status_icons = StatusIconSet()

self._startup_method = startup

Expand Down Expand Up @@ -578,22 +582,22 @@ def _create_standard_commands(self):
]:
self.commands.add(Command.standard(self, cmd_id))

if self._document_types:
default_document_type = self._document_types[0]
if self.documents.types:
default_document_type = self.documents.types[0]
command = Command.standard(
self,
Command.NEW,
action=simple_handler(self.documents.new, default_document_type),
)
if command:
if len(self._document_types) == 1:
if len(self.documents.types) == 1:
# There's only 1 document type. The new command can be used as is.
self.commands.add(command)
else:
# There's more than one document type. Create a new command for each
# document type, updating the title of the command to disambiguate,
# and modifying the shortcut, order and ID of the document types 2+
for i, document_class in enumerate(self._document_types):
for i, document_class in enumerate(self.documents.types):
command = Command.standard(
self,
Command.NEW,
Expand Down Expand Up @@ -628,16 +632,16 @@ def _create_initial_windows(self):
"""
# Process command line arguments if the backend doesn't handle them
if not self._impl.HANDLES_COMMAND_LINE:
if self._document_types:
if self.documents.types:
for filename in sys.argv[1:]:
self._open_initial_document(filename)

# Ensure there is at least one window
if self.main_window is None and len(self.windows) == 0:
if self._document_types:
if self.documents.types:
if self._impl.CLOSE_ON_LAST_WINDOW:
# Pass in the first document type as the default
self.documents.new(self._document_types[0])
self.documents.new(self.documents.types[0])
else:
self.loop.run_until_complete(self.documents.request_open())
else:
Expand Down Expand Up @@ -732,14 +736,7 @@ def commands(self) -> CommandSet:
@property
def documents(self) -> DocumentSet:
"""The list of documents associated with this app."""
try:
return self._documents
except AttributeError:
# Initialize on first access.
from .documents import DocumentSet

self._documents = DocumentSet(self, self._document_types)
return self._documents
return self._documents

@property
def location(self) -> Location:
Expand Down Expand Up @@ -773,14 +770,7 @@ def screens(self) -> list[Screen]:
@property
def status_icons(self) -> StatusIconSet:
"""The status icons displayed by the app."""
try:
return self._status_icons
except AttributeError:
# Initialize on first access.
from .statusicons import StatusIconSet

self._status_icons = StatusIconSet()
return self._status_icons
return self._status_icons

@property
def widgets(self) -> WidgetRegistry:
Expand Down Expand Up @@ -1064,6 +1054,6 @@ def document_types(self) -> dict[str, type[Document]]:
)
return {
extension: doc_type
for doc_type in self._document_types
for doc_type in self.documents.types
for extension in doc_type.extensions
}
34 changes: 19 additions & 15 deletions core/src/toga/documents.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@

import toga
from toga import dialogs
from toga.handlers import overridable, overridden
from toga.window import MainWindow, Window

if TYPE_CHECKING:
Expand Down Expand Up @@ -117,13 +116,15 @@ def open(self, path: str | Path):
def save(self, path: str | Path | None = None):
"""Save the document as a file.
If a path is provided, and the :meth:`~toga.Document.write` method has been
overwritten, the path for the document will be updated. Otherwise, the existing
path will be used.
If a path is provided, the path for the document will be updated. Otherwise, the
existing path will be used.
If the :meth:`~toga.Document.write` method has not been implemented, this method
is a no-op.
:param path: If provided, the new file name for the document.
"""
if overridden(self.write):
if self._writable():
if path:
self._path = Path(path).absolute()
# Re-set the title of the document with the new path
Expand All @@ -132,6 +133,10 @@ def save(self, path: str | Path | None = None):
# Clear the modification flag.
self.modified = False

# A document is writable if its class overrides the `write` method.
def _writable(self):
return getattr(type(self), "write") is not Document.write

def show(self) -> None:
"""Show the visual representation for this document."""
self.main_window.show()
Expand Down Expand Up @@ -162,7 +167,6 @@ def read(self) -> None:
:attr:`~toga.Document.path`, and populate the document window.
"""

@overridable
def write(self) -> None:
"""Persist a representation of the current state of the document.
Expand Down Expand Up @@ -452,13 +456,13 @@ def _close(self):
async def save(self):
"""Save the document associated with this window.
If the document associated with a window hasn't been saved before, and the
document type defines a :meth:`~toga.Document.write` method, the user will be
prompted to provide a filename.
If the document associated with a window hasn't been saved before, the user will
be prompted to provide a filename.
:returns: True if the save was successful; False if the save was aborted.
:returns: True if the save was successful; False if the save was aborted, or the
document type doesn't define a :meth:`~toga.Document.write` method.
"""
if overridden(self.doc.write):
if self.doc._writable():
if self.doc.path:
# Document has been saved previously; save using that filename.
self.doc.save()
Expand All @@ -471,12 +475,12 @@ async def save_as(self):
"""Save the document associated with this window under a new filename.
The default implementation will prompt the user for a new filename, then save
the document with that new filename. If the document type doesn't define a
:meth:`~toga.Document.write` method, the save-as request will be ignored.
the document with that new filename.
:returns: True if the save was successful; False if the save was aborted.
:returns: True if the save was successful; False if the save was aborted, or the
document type doesn't define a :meth:`~toga.Document.write` method.
"""
if overridden(self.doc.write):
if self.doc._writable():
suggested_path = (
self.doc.path if self.doc.path else f"Untitled.{self.doc.extensions[0]}"
)
Expand Down
15 changes: 0 additions & 15 deletions core/src/toga/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,6 @@
WrappedHandlerT: TypeAlias = Callable[..., object]


def overridable(method: T) -> T:
"""Decorate the method as being user-overridable"""
method._overridden = True
return method


def overridden(coroutine_or_method: Callable) -> bool:
"""Has the user overridden this method?
This is based on the method *not* having a ``_overridden`` attribute. Overridable
default methods have this attribute; user-defined method will not.
"""
return not hasattr(coroutine_or_method, "_overridden")


class NativeHandler:
def __init__(self, handler: Callable[..., object]):
self.native = handler
Expand Down

0 comments on commit cebc052

Please sign in to comment.