Source code for toui.pages

"""
A module that creates web pages or windows.
"""
import os
import time
from bs4 import BeautifulSoup
import webview
import json
from toui.elements import Element
from toui._javascript_templates import custom_func, get_script
from copy import copy
from toui._helpers import warn, info, debug, selector_to_str
from toui._signals import Signal
from toui._defaults import view_func


class _PageSignal(Signal):
    """
    Creates signals that will be sent to JavaScript.

    These signals are related the methods of the `Page` object.
    """

    no_return_functions = ["add_function"]

    @staticmethod
    def from_str(**kwargs):
        js_func = "_setDoc"
        js_args = []
        js_kwargs = {"doc": kwargs['html_str']}
        return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}

    @staticmethod
    def from_bs4_soup(**kwargs):
        js_func = "_setDoc"
        js_args = []
        js_kwargs = {"doc": str(kwargs['bs4_soup'])}
        return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}

    @staticmethod
    def add_function(**kwargs):
        value = kwargs['return_value']
        if value != "":
            value = custom_func(value)
        js_func = "_addScript"
        js_args = []
        js_kwargs = {"script": value}
        return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}

    @staticmethod
    def _open_another_page(**kwargs):
        js_func = "_goTo"
        js_args = []
        js_kwargs = {"url": kwargs['url']}
        return {'func': js_func, 'args': js_args, 'kwargs': js_kwargs}


[docs]class Page: """ Creates an HTML page that can be used as a window or as a webpage. Attributes ---------- url: str The URL of the page. window: pywebview.Window, default = None A `pywebview.Window` object. It is automatically created when adding the `Page` to `DesktopApp` and running the app. Examples -------- Importing the class: >>> from toui import Page Creating a `Page` from an HTML file: >>> path = "../examples/assets/test1.html" >>> page = Page(html_file=path) Creating a `Page` from a string: >>> page = Page(html_str="<html><h1>Hello</h1></html>") >>> page <html><h1>Hello</h1></html> Creating a page with a URL: >>> page = Page(html_str="<html><h1>Hello</h1></html>", url="/") Getting an element in the `Page` from its id: >>> page = Page(html_str='<html><h1 id="heading">Hello</h1></html>', url="/") >>> element = page.get_element(element_id="heading") >>> element <h1 id="heading">Hello</h1> See Also -------- `pywebview api`: https://pywebview.flowrl.com/guide/api.html """ def __init__(self, html_file=None, html_str=None, url=None): """ Parameters ---------- html_file: str The path to the HTML file. html_str: str A string containing HTML code. url: str, optional If the page was used as a webpage, this will be the URL of the page. """ if html_file: self._html_file = html_file with open(html_file, "rt") as file: original_html = file.read() if url is None: url = "/" + os.path.basename(html_file) elif html_str: original_html = html_str self._html_file = None else: original_html = "<html></html>" self._html_file = None self._html = BeautifulSoup(original_html, features="html.parser") if self._html.find("html") is None: self._html = BeautifulSoup(f"<html>{original_html}</html>", features="html.parser") if url is None: url = "/" self.url = url # Other attributes self.window = None # Other internal attributes self._app = None self._signal_mode = False self._signals = [] self._functions = {} self._basic_view_func = lambda: view_func(self) self._view_func = self._basic_view_func def __str__(self): return self.to_str() def __repr__(self): return self.to_str() def __copy__(self): new_pg = Page(html_file=self._html_file, url=self.url) new_pg.from_bs4_soup(self.to_bs4_soup()) new_pg.window = copy(self.window) new_pg._signal_mode = self._signal_mode new_pg._app = self._app return new_pg
[docs] @_PageSignal() def from_str(self, html_str): """ Converts HTML code to a `Page` object. Parameters ---------- html_str: str HTML code. """ self._html = BeautifulSoup(html_str, features="html.parser")
[docs] def to_str(self): """ Converts the `Page` object to HTML code. Returns ------- str """ return str(self._html)
[docs] def from_html_file(self, html_path): """ Reads an HTML file and converts it to an `Page` object. Parameters ---------- html_path: str """ with open(html_path, "rt") as file: self.from_str(file.read())
[docs] def to_html_file(self, html_path): """ Converts the `Page` object to an HTML file. Parameters ---------- html_path: str """ with open(html_path, "w") as file: file.write(self.to_str())
[docs] @_PageSignal() def from_bs4_soup(self, bs4_soup): """ Converts a `bs4.BeautifulSoap` object to a `Page` object. The `bs4.BeautifulSoap` object will be copied before converting. Parameters ---------- bs4_soup: bs4.BeautifulSoap See Also -------- bs4 """ self._html = copy(bs4_soup)
[docs] def to_bs4_soup(self): """ Converts the `Page` object to a `bs4.BeautifulSoap` object. The `Page` object will be copied before converting. Returns ------- bs4.BeautifulSoap See Also -------- bs4: https://beautiful-soup-4.readthedocs.io/en/latest/ """ return copy(self._html)
[docs] def get_element(self, element_id, do_copy=False): """ Gets an element from its ``id`` attribute. You can imagine this function as ``document.getElementById`` in JavaScript. Creating a page and getting an element by its ``id``: >>> page = Page(html_str='<html><h1 id="heading">Hello</h1></html>', url="/") >>> element = page.get_element(element_id="heading") >>> element <h1 id="heading">Hello</h1> Parameters ---------- element_id: str do_copy: bool, default = False If ``True``, the element will be copied. Returns ------- element: Element If the element was found, an `Element` object will be returned. Otherwise ``None`` will be returned. """ bs4_tag = self._html.find(id=element_id) if bs4_tag is None: return None element = Element() if do_copy: element.from_bs4_tag(bs4_tag) else: element._from_bs4_tag_no_copy(bs4_tag) element._parent_page = self element._signal_mode = self._signal_mode element._selector = {"selector": f"[id={element_id}]"} return element
[docs] def get_elements(self, tag_name=None, class_name=None, name=None, do_copy=False, attrs=None): """ Get elements from the `Page` by their tag name and attributes. Parameters ---------- tag_name: str, default=None The tag name of the elements. class_name: str, default=None The value of the ``class`` attribute of the elements. name: str, default=None The value of the ``name`` attribute of the elements. do_copy: bool If ``True``, the elements will be copied. attrs: dict, default=None Attributes other than ``class`` and ``name`` can be specified in this dictionary. Returns ------- elements_list: List A list of `Element` objects that match the parameters. """ if attrs is None: attrs = {} if class_name: attrs['class'] = class_name if name: attrs['name'] = name bs4_tags = self._html.find_all(name=tag_name, attrs=attrs) elements_list = [] for tag_num, bs4_tag in enumerate(bs4_tags): element = Element() if do_copy: element.from_bs4_tag(bs4_tag) else: element._from_bs4_tag_no_copy(bs4_tag) element._parent_page = self element._signal_mode = self._signal_mode element._selector = {"selector": selector_to_str(tag_name=tag_name, class_name=class_name, name=name, attrs=attrs), "number": tag_num} elements_list.append(element) return elements_list
[docs] def get_html_element(self): """ Gets the first ``<html>`` element. Returns ------- Element None If the ``<html>`` element was not found. """ elements = self.get_elements("html") if len(elements) > 0: return elements[0]
[docs] def get_body_element(self): """ Gets the first ``<body>`` element. Returns ------- Element None If the ``<body>`` element was not found. """ elements = self.get_elements("body") if len(elements) > 0: return elements[0]
[docs] @_PageSignal() def add_function(self, func): """ Adds a function to the `Page`. This function can be called from an HTML element. Examples -------- Consider an HTML code that contains the following: >>> html_with_function = '<html><button onclick="printValue()">Print</button></html>' Notice that the ``<button>`` element contains the attribute ``onclick`` calling a Python function `printValue`. However, this function is not yet added to the HTML page. To add the function, define it in Python and add it to a `Page`: >>> def printValue(): ... print("value") >>> page = Page(html_str=html_with_function) >>> page.add_function(printValue) Now create a `Website` and add the `Page` to it: >>> from toui import Website >>> app = Website() >>> app.add_pages(page) Parameters ---------- function Function to be added to the page. Returns ------- None """ if self._app: self._app._add_function(func) old_functions = copy(self._functions) self._functions[func.__name__] = func if func.__name__ in old_functions: return "" script_element = Element("script") script_element.set_content(custom_func(func.__name__)) self.get_elements(tag_name="html")[0].add_content(script_element) return func.__name__
[docs] def on_url_request(self, func, display_return_value=False): """ Sets a function that will be called when the user types the URL in a browser or when a request is sent to the URL. You can view it as the `view_func` in Flask. Parameters ---------- func: Callable The function that will be called when a request is sent to the URL. display_return_value: bool, default=False If ``True``, the browser will display the return value of the function when the URL is loaded. If ``False``, the return value will be ignored. """ def new_func(): original_return = self._basic_view_func() new_return = func() if display_return_value: return new_return else: return original_return self._view_func = new_func
def _add_script(self, template_type="web"): script_tag = Element("script") script_content = get_script(template_type) script_tag.set_content(script_content) self.get_elements(tag_name="html")[0].add_content(script_tag) def _create_window(self, name, api, assets_folder): with _TempHTML(directory=assets_folder, html=self.to_str(), win_kwargs={"title": name, "js_api": api}) as temp_html: self.window = temp_html.win time.sleep(1) api.window = self.window return self.window def _create_first_window(self, name, api, assets_folder): self.window = webview.create_window(title=name, js_api=api) def func(): with _TempHTML(directory=assets_folder, html=self.to_str(), win=self.window) as temp_html: time.sleep(1) api.window = self.window return func def _evaluate_js(self, func, kwargs): data_from_js = "" def wait_then_get_result(result): nonlocal data_from_js data_from_js = result codejs = f""" var kwargs = JSON.parse(\'{json.dumps(kwargs)}\') {func}(kwargs) """ debug("EVALUATE: " + codejs) self.window.evaluate_js(codejs, callback=wait_then_get_result) return data_from_js @_PageSignal() def _open_another_page(self, url): return
class _TempHTML: def __init__(self, directory, win=None, html="", win_args=(), win_kwargs=None): self.directory = directory self.win = win self.html = html self.win_args = win_args if win_kwargs is None: win_kwargs = {} self.win_kwargs = win_kwargs def __enter__(self): i = "" name = f"~toui{i}.html" while os.path.exists(f"{self.directory}/{name}"): if i == "": i = 1 else: i += 1 name = f"~toui{i}.html" self.file = f"{self.directory}/{name}" file = open(self.file, "w") if self.win: self.win.load_url(self.file) else: self.win = webview.create_window(*self.win_args, **self.win_kwargs, url=self.file) file.write(self.html) return self def __exit__(self, exc_type, exc_val, exc_tb): os.remove(self.file) if __name__ == "__main__": import doctest doctest.testmod()