first commit
This commit is contained in:
commit
417e54da96
5696 changed files with 900003 additions and 0 deletions
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,92 @@
|
|||
# Copyright (c) 2019-2021 Artem Bulgakov
|
||||
#
|
||||
# This file is distributed under the terms of the same license,
|
||||
# as the Kivy framework.
|
||||
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
|
||||
class ArgumentParserWithHelp(argparse.ArgumentParser):
|
||||
def parse_args(self, args=None, namespace=None):
|
||||
# Add help when no arguments specified
|
||||
if not args and not len(sys.argv) > 1:
|
||||
self.print_help()
|
||||
self.exit(1)
|
||||
return super().parse_args(args, namespace)
|
||||
|
||||
def error(self, message):
|
||||
# Add full help on error
|
||||
self.print_help()
|
||||
self.exit(2, f"\nError: {message}\n")
|
||||
|
||||
def format_help(self):
|
||||
# Add subparsers usage and help to full help text
|
||||
formatter = self._get_formatter()
|
||||
|
||||
# Get subparsers
|
||||
subparsers_actions = [
|
||||
action
|
||||
for action in self._actions
|
||||
if isinstance(action, argparse._SubParsersAction)
|
||||
]
|
||||
|
||||
# Description
|
||||
formatter.add_text(self.description)
|
||||
|
||||
# Usage
|
||||
formatter.add_usage(
|
||||
self.usage,
|
||||
self._actions,
|
||||
self._mutually_exclusive_groups,
|
||||
prefix="Usage:\n",
|
||||
)
|
||||
|
||||
# Subparsers usage
|
||||
for subparsers_action in subparsers_actions:
|
||||
for choice, subparser in subparsers_action.choices.items():
|
||||
formatter.add_usage(
|
||||
subparser.usage,
|
||||
subparser._actions,
|
||||
subparser._mutually_exclusive_groups,
|
||||
prefix="",
|
||||
)
|
||||
|
||||
# Positionals, optionals and user-defined groups
|
||||
for action_group in self._action_groups:
|
||||
if not any(
|
||||
[
|
||||
action in subparsers_actions
|
||||
for action in action_group._group_actions
|
||||
]
|
||||
):
|
||||
formatter.start_section(action_group.title)
|
||||
formatter.add_text(action_group.description)
|
||||
formatter.add_arguments(action_group._group_actions)
|
||||
formatter.end_section()
|
||||
else:
|
||||
# Process subparsers differently
|
||||
# Just show list of choices
|
||||
formatter.start_section(action_group.title)
|
||||
# formatter.add_text(action_group.description)
|
||||
for action in action_group._group_actions:
|
||||
for choice in action.choices:
|
||||
formatter.add_text(choice)
|
||||
formatter.end_section()
|
||||
|
||||
# Subparsers help
|
||||
for subparsers_action in subparsers_actions:
|
||||
for choice, subparser in subparsers_action.choices.items():
|
||||
formatter.start_section(choice)
|
||||
for action_group in subparser._action_groups:
|
||||
formatter.start_section(action_group.title)
|
||||
formatter.add_text(action_group.description)
|
||||
formatter.add_arguments(action_group._group_actions)
|
||||
formatter.end_section()
|
||||
formatter.end_section()
|
||||
|
||||
# Epilog
|
||||
formatter.add_text(self.epilog)
|
||||
|
||||
# Determine help from format above
|
||||
return formatter.format_help()
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,550 @@
|
|||
"""
|
||||
HotReload
|
||||
=========
|
||||
|
||||
.. versionadded:: 1.0.0
|
||||
|
||||
.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/hot-reload.png
|
||||
:align: center
|
||||
|
||||
.. rubric::
|
||||
Hot reload tool - is a fork of the project https://github.com/tito/kaki
|
||||
|
||||
.. note::
|
||||
Since the project is not developing, we decided to include it in the
|
||||
KivvMD library and hope that the further development of the hot reload
|
||||
tool in the KivyMD project will develop faster.
|
||||
|
||||
.. rubric::
|
||||
This library enhance Kivy frameworks with opiniated features such as:
|
||||
|
||||
- Auto reloading kv or py (watchdog required, limited to some uses cases);
|
||||
- Idle detection support;
|
||||
- Foreground lock (Windows OS only);
|
||||
|
||||
Usage
|
||||
-----
|
||||
|
||||
.. note::
|
||||
See `create project with hot reload <https://kivymd.readthedocs.io/en/latest/api/kivymd/tools/patterns/create_project/#create-project-with-hot-reload>`_
|
||||
for more information.
|
||||
|
||||
TODO
|
||||
----
|
||||
|
||||
- Add automatic reloading of Python classes;
|
||||
- Add save application state on reloading;
|
||||
|
||||
FIXME
|
||||
-----
|
||||
|
||||
- On Windows, hot reloading of Python files may not work;
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
import traceback
|
||||
from fnmatch import fnmatch
|
||||
from os.path import join, realpath
|
||||
|
||||
original_argv = sys.argv
|
||||
|
||||
from kivy.base import ExceptionHandler, ExceptionManager # NOQA E402
|
||||
from kivy.clock import Clock, mainthread # NOQA E402
|
||||
from kivy.factory import Factory # NOQA E402
|
||||
from kivy.lang import Builder # NOQA E402
|
||||
from kivy.logger import Logger # NOQA E402
|
||||
from kivy.properties import ( # NOQA E402
|
||||
BooleanProperty,
|
||||
DictProperty,
|
||||
ListProperty,
|
||||
NumericProperty,
|
||||
)
|
||||
|
||||
from kivymd.app import MDApp as BaseApp # NOQA E402
|
||||
|
||||
try:
|
||||
from monotonic import monotonic
|
||||
except ImportError:
|
||||
monotonic = None
|
||||
try:
|
||||
from importlib import reload
|
||||
|
||||
PY3 = True
|
||||
except ImportError:
|
||||
PY3 = False
|
||||
|
||||
import watchdog # NOQA
|
||||
|
||||
|
||||
class ExceptionClass(ExceptionHandler):
|
||||
def handle_exception(self, inst):
|
||||
if isinstance(inst, (KeyboardInterrupt, SystemExit)):
|
||||
return ExceptionManager.RAISE
|
||||
app = MDApp.get_running_app()
|
||||
if not app.DEBUG and not app.RAISE_ERROR:
|
||||
return ExceptionManager.RAISE
|
||||
app.set_error(inst, tb=traceback.format_exc())
|
||||
return ExceptionManager.PASS
|
||||
|
||||
|
||||
ExceptionManager.add_handler(ExceptionClass())
|
||||
|
||||
|
||||
class MDApp(BaseApp):
|
||||
"""HotReload Application class."""
|
||||
|
||||
DEBUG = BooleanProperty("DEBUG" in os.environ)
|
||||
"""
|
||||
Control either we activate debugging in the app or not.
|
||||
Defaults depend if 'DEBUG' exists in os.environ.
|
||||
|
||||
:attr:`DEBUG` is a :class:`~kivy.properties.BooleanProperty`.
|
||||
"""
|
||||
|
||||
FOREGROUND_LOCK = BooleanProperty(False)
|
||||
"""
|
||||
If `True` it will require the foreground lock on windows.
|
||||
|
||||
:attr:`FOREGROUND_LOCK` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to `False`.
|
||||
"""
|
||||
|
||||
KV_FILES = ListProperty()
|
||||
"""
|
||||
List of KV files under management for auto reloader.
|
||||
|
||||
:attr:`KV_FILES` is a :class:`~kivy.properties.ListProperty`
|
||||
and defaults to `[]`.
|
||||
"""
|
||||
|
||||
KV_DIRS = ListProperty()
|
||||
"""
|
||||
List of managed KV directories for autoloader.
|
||||
|
||||
:attr:`KV_DIRS` is a :class:`~kivy.properties.ListProperty`
|
||||
and defaults to `[]`.
|
||||
"""
|
||||
|
||||
AUTORELOADER_PATHS = ListProperty([(".", {"recursive": True})])
|
||||
"""
|
||||
List of path to watch for auto reloading.
|
||||
|
||||
:attr:`AUTORELOADER_PATHS` is a :class:`~kivy.properties.ListProperty`
|
||||
and defaults to `([(".", {"recursive": True})]`.
|
||||
"""
|
||||
|
||||
AUTORELOADER_IGNORE_PATTERNS = ListProperty(["*.pyc", "*__pycache__*"])
|
||||
"""
|
||||
List of extensions to ignore.
|
||||
|
||||
:attr:`AUTORELOADER_IGNORE_PATTERNS` is a :class:`~kivy.properties.ListProperty`
|
||||
and defaults to `['*.pyc', '*__pycache__*']`.
|
||||
"""
|
||||
|
||||
CLASSES = DictProperty()
|
||||
"""
|
||||
Factory classes managed by hotreload.
|
||||
|
||||
:attr:`CLASSES` is a :class:`~kivy.properties.DictProperty`
|
||||
and defaults to `{}`.
|
||||
"""
|
||||
|
||||
IDLE_DETECTION = BooleanProperty(False)
|
||||
"""
|
||||
Idle detection (if True, event on_idle/on_wakeup will be fired).
|
||||
Rearming idle can also be done with `rearm_idle()`.
|
||||
|
||||
:attr:`IDLE_DETECTION` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to `False`.
|
||||
"""
|
||||
|
||||
IDLE_TIMEOUT = NumericProperty(60)
|
||||
"""
|
||||
Default idle timeout.
|
||||
|
||||
:attr:`IDLE_TIMEOUT` is a :class:`~kivy.properties.NumericProperty`
|
||||
and defaults to `60`.
|
||||
"""
|
||||
|
||||
RAISE_ERROR = BooleanProperty(True)
|
||||
"""
|
||||
Raise error.
|
||||
When the `DEBUG` is activated, it will raise any error instead
|
||||
of showing it on the screen. If you still want to show the error
|
||||
when not in `DEBUG`, put this to `False`.
|
||||
|
||||
:attr:`RAISE_ERROR` is a :class:`~kivy.properties.BooleanProperty`
|
||||
and defaults to `True`.
|
||||
"""
|
||||
|
||||
__events__ = ["on_idle", "on_wakeup"]
|
||||
|
||||
def build(self):
|
||||
if self.DEBUG:
|
||||
Logger.info("{}: Debug mode activated".format(self.appname))
|
||||
self.enable_autoreload()
|
||||
self.patch_builder()
|
||||
self.bind_key(32, self.rebuild)
|
||||
if self.FOREGROUND_LOCK:
|
||||
self.prepare_foreground_lock()
|
||||
|
||||
self.state = None
|
||||
self.approot = None
|
||||
self.root = self.get_root()
|
||||
self.rebuild(first=True)
|
||||
|
||||
if self.IDLE_DETECTION:
|
||||
self.install_idle(timeout=self.IDLE_TIMEOUT)
|
||||
|
||||
return super().build()
|
||||
|
||||
def get_root(self):
|
||||
"""
|
||||
Return a root widget, that will contains your application.
|
||||
It should not be your application widget itself, as it may
|
||||
be destroyed and recreated from scratch when reloading.
|
||||
|
||||
By default, it returns a RelativeLayout, but it could be
|
||||
a Viewport.
|
||||
"""
|
||||
|
||||
return Factory.RelativeLayout()
|
||||
|
||||
def get_root_path(self):
|
||||
"""Return the root file path."""
|
||||
|
||||
return realpath(os.getcwd())
|
||||
|
||||
def build_app(self, first=False):
|
||||
"""
|
||||
Must return your application widget.
|
||||
|
||||
If `first` is set, it means that will be your first time ever
|
||||
that the application is built. Act according to it.
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def unload_app_dependencies(self):
|
||||
"""
|
||||
Called when all the application dependencies must be unloaded.
|
||||
Usually happen before a reload
|
||||
"""
|
||||
|
||||
for path_to_kv_file in self.KV_FILES:
|
||||
path_to_kv_file = realpath(path_to_kv_file)
|
||||
Builder.unload_file(path_to_kv_file)
|
||||
|
||||
for name, module in self.CLASSES.items():
|
||||
Factory.unregister(name)
|
||||
|
||||
for path in self.KV_DIRS:
|
||||
for path_to_dir, dirs, files in os.walk(path):
|
||||
for name_file in files:
|
||||
if os.path.splitext(name_file)[1] == ".kv":
|
||||
path_to_kv_file = os.path.join(path_to_dir, name_file)
|
||||
Builder.unload_file(path_to_kv_file)
|
||||
|
||||
def load_app_dependencies(self):
|
||||
"""
|
||||
Load all the application dependencies.
|
||||
This is called before rebuild.
|
||||
"""
|
||||
|
||||
for path_to_kv_file in self.KV_FILES:
|
||||
path_to_kv_file = realpath(path_to_kv_file)
|
||||
Builder.load_file(path_to_kv_file)
|
||||
|
||||
for name, module in self.CLASSES.items():
|
||||
Factory.register(name, module=module)
|
||||
|
||||
for path in self.KV_DIRS:
|
||||
for path_to_dir, dirs, files in os.walk(path):
|
||||
for name_file in files:
|
||||
if os.path.splitext(name_file)[1] == ".kv":
|
||||
path_to_kv_file = os.path.join(path_to_dir, name_file)
|
||||
Builder.load_file(path_to_kv_file)
|
||||
|
||||
def rebuild(self, *args, **kwargs):
|
||||
print("{}: Rebuild the application".format(self.appname))
|
||||
first = kwargs.get("first", False)
|
||||
try:
|
||||
if not first:
|
||||
self.unload_app_dependencies()
|
||||
|
||||
# In case the loading fail in the middle of building a widget
|
||||
# there will be existing rules context that will break later
|
||||
# instanciation.
|
||||
# Just clean it.
|
||||
Builder.rulectx = {}
|
||||
|
||||
self.load_app_dependencies()
|
||||
self.set_widget(None)
|
||||
self.approot = self.build_app()
|
||||
self.set_widget(self.approot)
|
||||
self.apply_state(self.state)
|
||||
except Exception as exc:
|
||||
import traceback
|
||||
|
||||
Logger.exception("{}: Error when building app".format(self.appname))
|
||||
self.set_error(repr(exc), traceback.format_exc())
|
||||
if not self.DEBUG and self.RAISE_ERROR:
|
||||
raise
|
||||
|
||||
@mainthread
|
||||
def set_error(self, exc, tb=None):
|
||||
print(tb)
|
||||
from kivy.core.window import Window
|
||||
from kivy.utils import get_color_from_hex
|
||||
|
||||
scroll = Factory.MDScrollView(
|
||||
scroll_y=0, md_bg_color=get_color_from_hex("#e50000")
|
||||
)
|
||||
lbl = Factory.Label(
|
||||
text_size=(Window.width - 100, None),
|
||||
size_hint_y=None,
|
||||
text="{}\n\n{}".format(exc, tb or ""),
|
||||
)
|
||||
lbl.bind(texture_size=lbl.setter("size"))
|
||||
scroll.add_widget(lbl)
|
||||
self.set_widget(scroll)
|
||||
|
||||
def bind_key(self, key, callback):
|
||||
"""Bind a key (keycode) to a callback (cannot be unbind)."""
|
||||
|
||||
from kivy.core.window import Window
|
||||
|
||||
def _on_keyboard(window, keycode, *args):
|
||||
if key == keycode:
|
||||
return callback()
|
||||
|
||||
Window.bind(on_keyboard=_on_keyboard)
|
||||
|
||||
@property
|
||||
def appname(self):
|
||||
"""Return the name of the application class."""
|
||||
|
||||
return self.__class__.__name__
|
||||
|
||||
def enable_autoreload(self):
|
||||
"""
|
||||
Enable autoreload manually. It is activated automatically
|
||||
if "DEBUG" exists in environ. It requires the `watchdog` module.
|
||||
"""
|
||||
|
||||
try:
|
||||
from watchdog.events import FileSystemEventHandler
|
||||
from watchdog.observers import Observer
|
||||
except ImportError:
|
||||
Logger.warn(
|
||||
"{}: Autoreloader is missing watchdog".format(self.appname)
|
||||
)
|
||||
return
|
||||
Logger.info("{}: Autoreloader activated".format(self.appname))
|
||||
rootpath = self.get_root_path()
|
||||
self.w_handler = handler = FileSystemEventHandler()
|
||||
handler.dispatch = self._reload_from_watchdog
|
||||
self._observer = observer = Observer()
|
||||
for path in self.AUTORELOADER_PATHS:
|
||||
options = {"recursive": True}
|
||||
if isinstance(path, (tuple, list)):
|
||||
path, options = path
|
||||
observer.schedule(handler, join(rootpath, path), **options)
|
||||
observer.start()
|
||||
|
||||
def prepare_foreground_lock(self):
|
||||
"""
|
||||
Try forcing app to front permanently to avoid windows
|
||||
pop ups and notifications etc.app.
|
||||
|
||||
Requires fake full screen and borderless.
|
||||
|
||||
.. note::
|
||||
This function is called automatically if `FOREGROUND_LOCK` is set
|
||||
"""
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
|
||||
LSFW_LOCK = 1
|
||||
ctypes.windll.user32.LockSetForegroundWindow(LSFW_LOCK)
|
||||
Logger.info("App: Foreground lock activated")
|
||||
except Exception:
|
||||
Logger.warn("App: No foreground lock available")
|
||||
|
||||
def set_widget(self, wid):
|
||||
"""
|
||||
Clear the root container, and set the new approot widget to `wid`.
|
||||
"""
|
||||
|
||||
self.root.clear_widgets()
|
||||
self.approot = wid
|
||||
if wid is None:
|
||||
return
|
||||
self.root.add_widget(self.approot)
|
||||
try:
|
||||
wid.do_layout()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# State management.
|
||||
def apply_state(self, state):
|
||||
"""Whatever the current state is, reapply the current state."""
|
||||
|
||||
# Idle management leave.
|
||||
def install_idle(self, timeout=60):
|
||||
"""
|
||||
Install the idle detector. Default timeout is 60s.
|
||||
Once installed, it will check every second if the idle timer
|
||||
expired. The timer can be rearm using :func:`rearm_idle`.
|
||||
"""
|
||||
|
||||
if monotonic is None:
|
||||
Logger.exception(
|
||||
"{}: Cannot use idle detector, monotonic is missing".format(
|
||||
self.appname
|
||||
)
|
||||
)
|
||||
self.idle_timer = None
|
||||
self.idle_timeout = timeout
|
||||
Logger.info(
|
||||
"{}: Install idle detector, {} seconds".format(
|
||||
self.appname, timeout
|
||||
)
|
||||
)
|
||||
Clock.schedule_interval(self._check_idle, 1)
|
||||
self.root.bind(
|
||||
on_touch_down=self.rearm_idle, on_touch_up=self.rearm_idle
|
||||
)
|
||||
|
||||
def rearm_idle(self, *args):
|
||||
"""Rearm the idle timer."""
|
||||
|
||||
if not hasattr(self, "idle_timer"):
|
||||
return
|
||||
if self.idle_timer is None:
|
||||
self.dispatch("on_wakeup")
|
||||
self.idle_timer = monotonic()
|
||||
|
||||
# Internals.
|
||||
def patch_builder(self):
|
||||
Builder.orig_load_string = Builder.load_string
|
||||
Builder.load_string = self._builder_load_string
|
||||
|
||||
def on_idle(self, *args):
|
||||
"""Event fired when the application enter the idle mode."""
|
||||
|
||||
def on_wakeup(self, *args):
|
||||
"""Event fired when the application leaves idle mode."""
|
||||
|
||||
@mainthread
|
||||
def _reload_from_watchdog(self, event):
|
||||
from watchdog.events import FileModifiedEvent
|
||||
|
||||
if not isinstance(event, FileModifiedEvent):
|
||||
return
|
||||
|
||||
for pat in self.AUTORELOADER_IGNORE_PATTERNS:
|
||||
if fnmatch(event.src_path, pat):
|
||||
return
|
||||
|
||||
if event.src_path.endswith(".py"):
|
||||
# source changed, reload it
|
||||
try:
|
||||
Builder.unload_file(event.src_path)
|
||||
self._reload_py(event.src_path)
|
||||
except Exception as e:
|
||||
import traceback
|
||||
|
||||
self.set_error(repr(e), traceback.format_exc())
|
||||
return
|
||||
|
||||
Clock.unschedule(self.rebuild)
|
||||
Clock.schedule_once(self.rebuild, 0.1)
|
||||
|
||||
def _builder_load_string(self, string, **kwargs):
|
||||
if "filename" not in kwargs:
|
||||
from inspect import getframeinfo, stack
|
||||
|
||||
caller = getframeinfo(stack()[1][0])
|
||||
kwargs["filename"] = caller.filename
|
||||
return Builder.orig_load_string(string, **kwargs)
|
||||
|
||||
def _check_idle(self, *args):
|
||||
if not hasattr(self, "idle_timer"):
|
||||
return
|
||||
if self.idle_timer is None:
|
||||
return
|
||||
if monotonic() - self.idle_timer > self.idle_timeout:
|
||||
self.idle_timer = None
|
||||
self.dispatch("on_idle")
|
||||
|
||||
def _reload_py(self, filename):
|
||||
# We don't have dependency graph yet, so if the module actually exists
|
||||
# reload it.
|
||||
|
||||
filename = realpath(filename)
|
||||
# Check if it's our own application file.
|
||||
try:
|
||||
mod = sys.modules[self.__class__.__module__]
|
||||
mod_filename = realpath(mod.__file__)
|
||||
except Exception:
|
||||
mod_filename = None
|
||||
|
||||
# Detect if it's the application class // main.
|
||||
if mod_filename == filename:
|
||||
return self._restart_app(mod)
|
||||
|
||||
module = self._filename_to_module(filename)
|
||||
if module in sys.modules:
|
||||
Logger.debug("{}: Module exist, reload it".format(self.appname))
|
||||
Factory.unregister_from_filename(filename)
|
||||
self._unregister_factory_from_module(module)
|
||||
reload(sys.modules[module])
|
||||
|
||||
def _unregister_factory_from_module(self, module):
|
||||
# Check module directly.
|
||||
to_remove = [
|
||||
x for x in Factory.classes if Factory.classes[x]["module"] == module
|
||||
]
|
||||
# Check class name.
|
||||
for x in Factory.classes:
|
||||
cls = Factory.classes[x]["cls"]
|
||||
if not cls:
|
||||
continue
|
||||
if getattr(cls, "__module__", None) == module:
|
||||
to_remove.append(x)
|
||||
|
||||
for name in set(to_remove):
|
||||
del Factory.classes[name]
|
||||
|
||||
def _filename_to_module(self, filename):
|
||||
orig_filename = filename
|
||||
rootpath = self.get_root_path()
|
||||
if filename.startswith(rootpath):
|
||||
filename = filename[len(rootpath) :]
|
||||
if filename.startswith("/"):
|
||||
filename = filename[1:]
|
||||
module = filename[:-3].replace("/", ".")
|
||||
Logger.debug(
|
||||
"{}: Translated {} to {}".format(
|
||||
self.appname, orig_filename, module
|
||||
)
|
||||
)
|
||||
return module
|
||||
|
||||
def _restart_app(self, mod):
|
||||
_has_execv = sys.platform != "win32"
|
||||
cmd = [sys.executable] + original_argv
|
||||
if not _has_execv:
|
||||
import subprocess
|
||||
|
||||
subprocess.Popen(cmd)
|
||||
sys.exit(0)
|
||||
else:
|
||||
try:
|
||||
os.execv(sys.executable, cmd)
|
||||
except OSError:
|
||||
os.spawnv(os.P_NOWAIT, sys.executable, cmd)
|
||||
os._exit(0)
|
Binary file not shown.
|
@ -0,0 +1,72 @@
|
|||
"""
|
||||
PyInstaller hooks
|
||||
=================
|
||||
|
||||
Add ``hookspath=[kivymd.hooks_path]`` to your .spec file.
|
||||
|
||||
Example of .spec file
|
||||
=====================
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
from kivy_deps import sdl2, glew
|
||||
|
||||
from kivymd import hooks_path as kivymd_hooks_path
|
||||
|
||||
path = os.path.abspath(".")
|
||||
|
||||
a = Analysis(
|
||||
["main.py"],
|
||||
pathex=[path],
|
||||
hookspath=[kivymd_hooks_path],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=None,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=None)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
*[Tree(p) for p in (sdl2.dep_bins + glew.dep_bins)],
|
||||
debug=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
name="app_name",
|
||||
console=True,
|
||||
)
|
||||
"""
|
||||
|
||||
__all__ = ("hooks_path", "get_hook_dirs", "get_pyinstaller_tests")
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import kivymd
|
||||
|
||||
hooks_path = str(Path(__file__).absolute().parent)
|
||||
"""Path to hook directory to use with PyInstaller.
|
||||
See :mod:`kivymd.tools.packaging.pyinstaller` for more information."""
|
||||
|
||||
|
||||
def get_hook_dirs():
|
||||
return [hooks_path]
|
||||
|
||||
|
||||
def get_pyinstaller_tests():
|
||||
return [os.path.join(kivymd.path, "tests", "pyinstaller")]
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print(hooks_path)
|
||||
print(get_hook_dirs())
|
||||
print(get_pyinstaller_tests())
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,42 @@
|
|||
"""
|
||||
PyInstaller hook for KivyMD
|
||||
===========================
|
||||
|
||||
Adds fonts, images and KV files to package.
|
||||
|
||||
All modules from uix directory are added by Kivy hook.
|
||||
"""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import kivymd
|
||||
|
||||
datas = [
|
||||
# Add `.ttf` files from the `kivymd/fonts` directory.
|
||||
(
|
||||
kivymd.fonts_path,
|
||||
str(Path("kivymd").joinpath(Path(kivymd.fonts_path).name)),
|
||||
),
|
||||
# Add files from the `kivymd/images` directory.
|
||||
(
|
||||
kivymd.images_path,
|
||||
str(Path("kivymd").joinpath(Path(kivymd.images_path).name)),
|
||||
),
|
||||
]
|
||||
|
||||
# Add `.kv. files from the `kivymd/uix` directory.
|
||||
for path_to_kv_file in Path(kivymd.uix_path).glob("**/*.kv"):
|
||||
datas.append(
|
||||
(
|
||||
str(Path(path_to_kv_file).parent.joinpath("*.kv")),
|
||||
str(
|
||||
Path("kivymd").joinpath(
|
||||
"uix",
|
||||
str(Path(path_to_kv_file).parent).split(
|
||||
str(Path("kivymd").joinpath("uix")) + os.sep
|
||||
)[1],
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,53 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import socket
|
||||
|
||||
import requests
|
||||
from firebase import firebase
|
||||
|
||||
|
||||
def get_connect(func, host="8.8.8.8", port=53, timeout=3):
|
||||
"""Checks for an active Internet connection."""
|
||||
|
||||
def wrapped(*args):
|
||||
try:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(
|
||||
(host, port)
|
||||
)
|
||||
return func(*args)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
class DataBase:
|
||||
"""
|
||||
Your methods for working with the database should be implemented in this
|
||||
class.
|
||||
"""
|
||||
|
||||
name = "Firebase"
|
||||
|
||||
def __init__(self):
|
||||
self.DATABASE_URL = "https://fir-db73a-default-rtdb.firebaseio.com/"
|
||||
# Address for users collections.
|
||||
self.USER_DATA = "Userdata"
|
||||
# RealTime Database attribute.
|
||||
self.real_time_firebase = firebase.FirebaseApplication(
|
||||
self.DATABASE_URL, None
|
||||
)
|
||||
|
||||
@get_connect
|
||||
def get_data_from_collection(self, name_collection: str) -> dict | bool:
|
||||
"""Returns data of the selected collection from the database."""
|
||||
|
||||
try:
|
||||
data = self.real_time_firebase.get(
|
||||
self.DATABASE_URL, name_collection
|
||||
)
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False
|
||||
|
||||
return data
|
|
@ -0,0 +1,134 @@
|
|||
"""
|
||||
Restdb.io API Wrapper
|
||||
---------------------
|
||||
|
||||
This package is an API Wrapper for the website `restdb.io <https://restdb.io>`_,
|
||||
which allows for online databases.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import socket
|
||||
|
||||
import requests
|
||||
|
||||
|
||||
def get_connect(func, host="8.8.8.8", port=53, timeout=3):
|
||||
"""Checks for an active Internet connection."""
|
||||
|
||||
def wrapped(*args):
|
||||
try:
|
||||
socket.setdefaulttimeout(timeout)
|
||||
socket.socket(socket.AF_INET, socket.SOCK_STREAM).connect(
|
||||
(host, port)
|
||||
)
|
||||
return func(*args)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
return wrapped
|
||||
|
||||
|
||||
class DataBase:
|
||||
name = "RestDB"
|
||||
|
||||
def __init__(self):
|
||||
database_url = "https://restdbio-5498.restdb.io"
|
||||
api_key = "7ce258d66f919d3a891d1166558765f0b4dbd"
|
||||
|
||||
self.HEADERS = {"x-apikey": api_key, "Content-Type": "application/json"}
|
||||
# Address for file collections.
|
||||
self.USER_MEDIA = f"{database_url}/media"
|
||||
# Address for users collections.
|
||||
self.USER_DATA = f"{database_url}/rest/userdata"
|
||||
|
||||
@get_connect
|
||||
def upload_file(self, path_to_file: str) -> dict | bool:
|
||||
"""
|
||||
Uploads a file to the database.
|
||||
You can upload a file to the database only from a paid account.
|
||||
"""
|
||||
|
||||
HEADERS = self.HEADERS.copy()
|
||||
del HEADERS["Content-Type"]
|
||||
payload = {}
|
||||
name_file = os.path.split(path_to_file)[1]
|
||||
files = [("file", (name_file, open(path_to_file, "rb"), name_file))]
|
||||
response = requests.post(
|
||||
url=self.USER_MEDIA,
|
||||
headers=HEADERS,
|
||||
data=payload,
|
||||
files=files,
|
||||
)
|
||||
|
||||
if response.status_code == 201:
|
||||
# {
|
||||
# "msg":"OK",
|
||||
# "uploadid": "ed1bca42334f68d873161641144e57b7",
|
||||
# "ids": ["62614fb90f9df71600284aa7"],
|
||||
# }
|
||||
json = response.json()
|
||||
if "msg" in json and json["msg"] == "OK":
|
||||
return json
|
||||
else:
|
||||
return False
|
||||
|
||||
@get_connect
|
||||
def get_data_from_collection(self, collection_address: str) -> bool | list:
|
||||
"""Returns data of the selected collection from the database."""
|
||||
|
||||
response = requests.get(url=collection_address, headers=self.HEADERS)
|
||||
if response.status_code != 200:
|
||||
return False
|
||||
else:
|
||||
return response.json()
|
||||
|
||||
@get_connect
|
||||
def delete_doc_from_collection(self, collection_address: str) -> bool:
|
||||
"""
|
||||
Delete data of the selected collection from the database.
|
||||
|
||||
:param collection_address: "database_url/id_collection".
|
||||
"""
|
||||
|
||||
response = requests.delete(collection_address, headers=self.HEADERS)
|
||||
if response.status_code == 200:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
@get_connect
|
||||
def add_doc_to_collection(
|
||||
self, data: dict, collection_address: str
|
||||
) -> bool:
|
||||
"""Add collection to the database."""
|
||||
|
||||
response = requests.post(
|
||||
url=collection_address,
|
||||
data=json.dumps(data),
|
||||
headers=self.HEADERS,
|
||||
)
|
||||
if response.status_code == 201:
|
||||
if "_id" in response.json():
|
||||
return response.json()
|
||||
else:
|
||||
return False
|
||||
|
||||
@get_connect
|
||||
def edit_data(
|
||||
self, collection: dict, collection_address: str, collection_id: str
|
||||
) -> bool:
|
||||
"""Modifies data in a collection of data in a database."""
|
||||
|
||||
response = requests.put(
|
||||
url=f"{collection_address}/{collection_id}",
|
||||
headers=self.HEADERS,
|
||||
json=collection,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
if "_id" in response.json():
|
||||
return True
|
||||
else:
|
||||
return False
|
Binary file not shown.
|
@ -0,0 +1,12 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-27 18:54+0300\n"
|
||||
"PO-Revision-Date: 2019-09-22 23:12+0300\n"
|
||||
"Last-Translator: KivyMD library https://github.com/kivymd/KivyMD\n"
|
||||
"Language-Team: KivyMD library https://github.com/kivymd/KivyMD\n"
|
||||
"Language: Russian\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
|
@ -0,0 +1,12 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: 0.0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-27 18:54+0300\n"
|
||||
"PO-Revision-Date: 2019-09-22 23:12+0300\n"
|
||||
"Last-Translator: KivyMD library https://github.com/kivymd/KivyMD\n"
|
||||
"Language-Team: KivyMD library https://github.com/kivymd/KivyMD\n"
|
||||
"Language: English\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
|
@ -0,0 +1 @@
|
|||
# This package is for additional application modules.
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,49 @@
|
|||
import gettext
|
||||
|
||||
from kivy.lang import Observable
|
||||
|
||||
|
||||
class Translation(Observable):
|
||||
"""Original source - https://github.com/tito/kivy-gettext-example."""
|
||||
|
||||
observers = []
|
||||
|
||||
def __init__(self, defaultlang, domian, resource_dir):
|
||||
super().__init__()
|
||||
self.ugettext = None
|
||||
self.lang = defaultlang
|
||||
self.domian = domian
|
||||
self.resource_dir = resource_dir
|
||||
self.switch_lang(self.lang)
|
||||
|
||||
def _(self, text):
|
||||
return self.ugettext(text)
|
||||
|
||||
def fbind(self, name, func, args, **kwargs):
|
||||
if name == "_":
|
||||
self.observers.append((func, args, kwargs))
|
||||
else:
|
||||
return super().fbind(name, func, *args, **kwargs)
|
||||
|
||||
def funbind(self, name, func, args, **kwargs):
|
||||
if name == "_":
|
||||
key = (func, args, kwargs)
|
||||
if key in self.observers:
|
||||
self.observers.remove(key)
|
||||
else:
|
||||
return super().funbind(name, func, *args, **kwargs)
|
||||
|
||||
def switch_lang(self, lang):
|
||||
locales = gettext.translation(
|
||||
self.domian, self.resource_dir, languages=[lang]
|
||||
)
|
||||
try:
|
||||
self.ugettext = locales.ugettext
|
||||
except AttributeError:
|
||||
self.ugettext = locales.gettext
|
||||
|
||||
for func, largs, kwargs in self.observers:
|
||||
try:
|
||||
func(largs, None, None)
|
||||
except ReferenceError:
|
||||
pass
|
|
@ -0,0 +1,18 @@
|
|||
# SOME DESCRIPTIVE TITLE.
|
||||
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
|
||||
# This file is distributed under the same license as the PACKAGE package.
|
||||
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
||||
#
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2021-10-27 18:54+0300\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
"Language: \n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=CHARSET\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,210 @@
|
|||
"""
|
||||
The script creates a new View package
|
||||
=====================================
|
||||
|
||||
The script creates a new View package in an existing project with an MVC
|
||||
template created using the create_project utility.
|
||||
|
||||
.. versionadded:: 1.0.0
|
||||
|
||||
.. seealso::
|
||||
|
||||
`Utility create_project <https://kivymd.readthedocs.io/en/latest/api/kivymd/tools/patterns/create_project/>`_
|
||||
|
||||
.. rubric:: Use a clean architecture for your applications.
|
||||
|
||||
To add a new view to an existing project that was created using the
|
||||
`create_project` utility, use the following command::
|
||||
|
||||
kivymd.add_view \\
|
||||
name_pattern \\
|
||||
path_to_project \\
|
||||
name_view
|
||||
|
||||
Example command::
|
||||
|
||||
kivymd.add_view \\
|
||||
MVC \\
|
||||
/Users/macbookair/Projects \\
|
||||
NewScreen
|
||||
|
||||
You can also add new views with responsive behavior to an existing project::
|
||||
|
||||
kivymd.add_view \\
|
||||
MVC \\
|
||||
/Users/macbookair/Projects \\
|
||||
NewScreen \\
|
||||
--use_responsive yes
|
||||
|
||||
For more information about adaptive design,
|
||||
`see here <https://kivymd.readthedocs.io/en/latest/components/responsivelayout/>`_.
|
||||
"""
|
||||
|
||||
__all__ = [
|
||||
"main",
|
||||
]
|
||||
|
||||
import os
|
||||
import re
|
||||
|
||||
from kivy import Logger
|
||||
|
||||
from kivymd.tools.argument_parser import ArgumentParserWithHelp
|
||||
from kivymd.tools.patterns.create_project import (
|
||||
chek_camel_case_name_project,
|
||||
create_common_responsive_module,
|
||||
create_controller,
|
||||
create_model,
|
||||
create_view,
|
||||
)
|
||||
|
||||
screens_data = """%s
|
||||
|
||||
screens = {%s
|
||||
}"""
|
||||
|
||||
screns_comment = """# The screen's dictionary contains the objects of the models and controllers
|
||||
# of the screens of the application.
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
"""The function of adding a new view to the project."""
|
||||
|
||||
global screens_data
|
||||
|
||||
parser = create_argument_parser()
|
||||
args = parser.parse_args()
|
||||
|
||||
# pattern_name isn't used currently, will be used if new patterns is added in future
|
||||
pattern_name = args.pattern # noqa F841
|
||||
path_to_project = args.directory
|
||||
name_view = args.name
|
||||
use_responsive = args.use_responsive
|
||||
|
||||
if not os.path.exists(path_to_project):
|
||||
parser.error(f"Project <{path_to_project}> does not exist...")
|
||||
|
||||
if name_view[-6:] != "Screen":
|
||||
parser.error(
|
||||
f"The name of the <{name_view}> screen should contain the word "
|
||||
f"'Screen' at the end.\n"
|
||||
"For example - '--name_screen MyFirstScreen ...'"
|
||||
)
|
||||
|
||||
if name_view in os.listdir(os.path.join(path_to_project, "View")):
|
||||
parser.error(
|
||||
f"The <{name_view}> view also exists in the <{path_to_project}> project..."
|
||||
)
|
||||
|
||||
# Create model.
|
||||
name_database = (
|
||||
"yes"
|
||||
if "database.py" in os.listdir(os.path.join(path_to_project, "Model"))
|
||||
else "no"
|
||||
)
|
||||
module_name = chek_camel_case_name_project(name_view)
|
||||
if not module_name:
|
||||
parser.error(
|
||||
"The name of the screen should be written in camel case style. "
|
||||
"\nFor example - 'MyFirstScreen'"
|
||||
)
|
||||
module_name = "_".join([name.lower() for name in module_name])
|
||||
path_to_project = path_to_project
|
||||
create_model(name_view, module_name, name_database, path_to_project)
|
||||
|
||||
# Create controller.
|
||||
# FIXME: This is not a very good solution in order to understand whether
|
||||
# a project uses a hot reload or not. Because the string
|
||||
# 'from kivymd.tools.hotreload.app import MDApp' in the project can just
|
||||
# be commented out and the project does not actually use hot reload.
|
||||
with open(os.path.join(path_to_project, "main.py")) as main_module:
|
||||
if "from kivymd.tools.hotreload.app import MDApp" in main_module.read():
|
||||
use_hotreload = "yes"
|
||||
else:
|
||||
use_hotreload = "no"
|
||||
create_controller(
|
||||
name_view, module_name, use_hotreload, path_to_project
|
||||
)
|
||||
# Create View.
|
||||
if use_responsive == "no":
|
||||
create_view(name_view, module_name, [], path_to_project)
|
||||
else:
|
||||
create_view(name_view, module_name, [name_view], path_to_project)
|
||||
create_common_responsive_module([name_view], path_to_project)
|
||||
# Create 'View.screens.py module'.
|
||||
create_screens_data(name_view, module_name, path_to_project)
|
||||
Logger.info(
|
||||
f"KivyMD: The {name_view} view has been added to the project..."
|
||||
)
|
||||
|
||||
|
||||
def create_screens_data(
|
||||
name_view: str, module_name: str, path_to_project: str
|
||||
) -> None:
|
||||
with open(
|
||||
os.path.join(path_to_project, "View", "screens.py")
|
||||
) as screen_module:
|
||||
screen_module = screen_module.read()
|
||||
imports = re.findall(
|
||||
"from Model.*Model|from Controller.*Controller", screen_module
|
||||
)
|
||||
screens = ""
|
||||
path_to_view = os.path.join(path_to_project, "View")
|
||||
|
||||
for name in os.listdir(path_to_view):
|
||||
if os.path.isdir(os.path.join(path_to_view, name)):
|
||||
res = re.findall("[A-Z][a-z]*", name)
|
||||
if res and len(res) == 2 and res[-1] == "Screen":
|
||||
screens += (
|
||||
"\n '%s': {"
|
||||
"\n 'model': %s,"
|
||||
"\n 'controller': %s,"
|
||||
"\n },"
|
||||
% (
|
||||
f"{res[0].lower()} {res[1].lower()}",
|
||||
f'{"".join(res)}Model',
|
||||
f'{"".join(res)}Controller',
|
||||
)
|
||||
)
|
||||
|
||||
imports.append(f"from Model.{module_name} import {name_view}Model")
|
||||
imports.append(
|
||||
f"from Controller.{module_name} import {name_view}Controller"
|
||||
)
|
||||
imports.insert(0, screns_comment)
|
||||
screens = screens_data % ("\n".join(imports), screens)
|
||||
|
||||
with open(
|
||||
os.path.join(path_to_project, "View", "screens.py"), "w"
|
||||
) as screen_module:
|
||||
screen_module.write(screens)
|
||||
|
||||
|
||||
def create_argument_parser() -> ArgumentParserWithHelp:
|
||||
parser = ArgumentParserWithHelp(
|
||||
prog="create_project.py",
|
||||
allow_abbrev=False,
|
||||
)
|
||||
parser.add_argument(
|
||||
"pattern",
|
||||
help="the name of the pattern with which the project will be created.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"directory",
|
||||
help="the directory of the project to which you want to add a new view.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"name",
|
||||
help="the name of the view to add to an existing project.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--use_responsive",
|
||||
default="no",
|
||||
help="whether to create a view with responsive behavior.",
|
||||
)
|
||||
return parser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue