Skip to content

Commit 4da9371

Browse files
committed
feat: add pywebio.config()
1 parent 31b26d0 commit 4da9371

File tree

6 files changed

+130
-35
lines changed

6 files changed

+130
-35
lines changed

docs/guide.rst

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -663,8 +663,6 @@ And if the files have been modified after run `path_deploy() <pywebio.platform.p
663663

664664
You can also use the command ``pywebio-path-deploy`` to start a server just like using `path_deploy() <pywebio.platform.path_deploy>`. For more information, refer ``pywebio-path-deploy --help``
665665

666-
In Server mode, you can use `pywebio.platform.seo()` to set the `SEO <https://en.wikipedia.org/wiki/Search_engine_optimization>`_ information. If ``seo()`` is not used, the `docstring <https://www.python.org/dev/peps/pep-0257/>`_ of the task function will be regarded as SEO information by default.
667-
668666
.. attention::
669667

670668
Note that in Server mode, all functions from ``input``, ``output`` and ``session`` modules can only be called in the context of task functions. For example, the following code is **not allowed**::

pywebio/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from .exceptions import SessionException, SessionClosedException, SessionNotFoundException
99
from .platform import start_server
1010
from .platform.bokeh import try_install_bokeh_hook
11+
from .platform.utils import config
1112
from .utils import STATIC_PATH
1213

1314
try_install_bokeh_hook()

pywebio/platform/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,12 +110,13 @@
110110
111111
Other
112112
--------------
113-
.. autofunction:: pywebio.platform.seo
113+
.. autofunction:: pywebio.config
114114
.. autofunction:: pywebio.platform.run_event_loop
115115
116116
"""
117117

118118
from .httpbased import run_event_loop
119119
from .tornado import start_server
120120
from .utils import seo
121+
from .utils import config
121122
from .path_deploy import path_deploy_http, path_deploy

pywebio/platform/tpl/index.html

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,12 @@
1717
<link rel="stylesheet" href="{{ base_url }}codemirror/base16-light.min.css">
1818
<link rel="stylesheet" href="{{ base_url }}css/toastify.min.css">
1919
<link rel="stylesheet" href="{{ base_url }}css/app.css">
20+
{% for css in css_file %}
21+
{% if css %}<link rel="stylesheet" href="{{ css }}">{% end %}
22+
{% end %}
23+
{% if css_style %}
24+
<style>{% raw css_style %}</style>
25+
{% end %}
2026
</head>
2127
<body>
2228
<div class="pywebio">
@@ -60,9 +66,12 @@
6066
$('#output-container').html('<div class="alert alert-danger" role="alert"> Sorry, this website does not support IE browser. ☹ </div>');
6167
</script>
6268
<script src="{{ base_url }}js/pywebio.min.js"></script>
63-
64-
<script src="{{ base_url }}js/require.min.js"></script> <!-- JS module loader -->
69+
{% for js in js_file %}
70+
{% if js %}<script src="{{ js }}"></script>{% end %}
71+
{% end %}
6572
{% if script %}
73+
<script src="{{ base_url }}js/require.min.js"></script> <!-- JS module loader -->
74+
6675
<script>
6776

6877
require.config({
@@ -92,6 +101,8 @@
92101
});
93102
</script>
94103
{% end %}
95-
104+
{% if js_code %}
105+
<script>{% raw js_code %}</script>
106+
{% end %}
96107
</body>
97108
</html>

pywebio/platform/utils.py

Lines changed: 104 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ..__version__ import __version__ as version
1313
from ..exceptions import PyWebIOWarning
1414
from ..utils import isgeneratorfunction, iscoroutinefunction, get_function_name, get_function_doc, \
15-
get_function_seo_info
15+
get_function_attr
1616

1717
"""
1818
The maximum size in bytes of a http request body or a websocket message, after which the request or websocket is aborted
@@ -25,7 +25,9 @@
2525

2626
BOOTSTRAP_VERSION = '4.4.1'
2727

28-
AppMeta = namedtuple('App', 'title description')
28+
_global_config = {'title': 'PyWebIO Application'}
29+
config_keys = ['title', 'description', 'js_file', 'js_code', 'css_style', 'css_file']
30+
AppMeta = namedtuple('App', config_keys)
2931

3032
_here_dir = path.dirname(path.abspath(__file__))
3133
_index_page_tpl = template.Template(open(path.join(_here_dir, 'tpl', 'index.html'), encoding='utf8').read())
@@ -50,9 +52,10 @@ def render_page(app, protocol, cdn):
5052

5153
bootstrap_css = bootstrap_css_url()
5254

53-
return _index_page_tpl.generate(title=meta.title or 'PyWebIO Application',
54-
description=meta.description, protocol=protocol,
55-
script=True, content='', base_url=cdn, bootstrap_css=bootstrap_css)
55+
return _index_page_tpl.generate(title=meta.title, description=meta.description, protocol=protocol,
56+
script=True, content='', base_url=cdn, bootstrap_css=bootstrap_css,
57+
js_file=meta.js_file or [], js_code=meta.js_code, css_style=meta.css_style,
58+
css_file=meta.css_file or [])
5659

5760

5861
def bootstrap_css_url():
@@ -64,6 +67,7 @@ def bootstrap_css_url():
6467
bootswatch_themes = {'flatly', 'yeti', 'cerulean', 'pulse', 'journal', 'cosmo', 'sandstone', 'simplex', 'minty',
6568
'slate', 'superhero', 'lumen', 'spacelab', 'materia', 'litera', 'sketchy', 'cyborg', 'solar',
6669
'lux', 'united', 'darkly'}
70+
6771
if theme_name in bootswatch_themes:
6872
return 'https://cdn.jsdelivr.net/npm/bootswatch@{version}/dist/{theme}/bootstrap.min.css'.format(
6973
version=BOOTSTRAP_VERSION, theme=theme_name)
@@ -92,10 +96,10 @@ def cdn_validation(cdn, level='warn', stacklevel=3):
9296

9397

9498
def parse_app_metadata(func):
95-
"""解析pywebio app元数据"""
96-
seo_info = get_function_seo_info(func)
97-
if seo_info:
98-
return AppMeta(*seo_info)
99+
"""Get metadata form pywebio task function, fallback to global config in empty meta field."""
100+
prefix = '_pywebio_'
101+
attrs = get_function_attr(func, [prefix + k for k in config_keys])
102+
meta = AppMeta(**{k: attrs.get(prefix + k) for k in config_keys})
99103

100104
doc = get_function_doc(func)
101105
parts = doc.strip().split('\n\n', 1)
@@ -104,7 +108,16 @@ def parse_app_metadata(func):
104108
else:
105109
title, description = parts[0], ''
106110

107-
return AppMeta(title, description)
111+
if not meta.title:
112+
meta = meta._replace(title=title, description=description)
113+
114+
# fallback to global config
115+
for key in config_keys:
116+
if not getattr(meta, key, None) and _global_config.get(key):
117+
kwarg = {key: _global_config.get(key)}
118+
meta = meta._replace(**kwarg)
119+
120+
return meta
108121

109122

110123
_app_list_tpl = template.Template("""
@@ -274,7 +287,7 @@ def seo(title, description=None, app=None):
274287
:param str description: Application description
275288
:param callable app: PyWebIO task function
276289
277-
If not ``seo()`` is not used, the `docstring <https://www.python.org/dev/peps/pep-0257/>`_ of the task function will be regarded as SEO information by default.
290+
If ``seo()`` is not used, the `docstring <https://www.python.org/dev/peps/pep-0257/>`_ of the task function will be regarded as SEO information by default.
278291
279292
``seo()`` can be used in 2 ways: direct call and decorator::
280293
@@ -299,18 +312,88 @@ def hello():
299312
])
300313
301314
.. versionadded:: 1.1
315+
.. deprecated:: 1.4
316+
Use :func:`pywebio.config` instead.
302317
"""
318+
import warnings
319+
warnings.warn("`pywebio.platform.seo()` is deprecated since v1.4 and will remove in the future version, "
320+
"use `pywebio.config` instead", DeprecationWarning, stacklevel=2)
303321

304322
if app is not None:
305-
return seo(title, description)(app)
306-
307-
def decorator(func):
308-
try:
309-
func = partial(func)
310-
func._pywebio_title = title
311-
func._pywebio_description = description or ''
312-
except Exception:
323+
return config(title=title, description=description)(app)
324+
325+
return config(title=title, description=description)
326+
327+
328+
def config(*, title=None, description=None, js_code=None, js_file=[], css_style=None, css_file=[]):
329+
"""PyWebIO application configuration
330+
331+
:param str title: Application title
332+
:param str description: Application description
333+
:param str js_code: The javascript code that you want to inject to page.
334+
:param str/list js_file: The javascript files that inject to page, can be a URL in str or a list of it.
335+
:param str css_style: The CSS style that you want to inject to page.
336+
:param str/list css_file: The CSS files that inject to page, can be a URL in str or a list of it.
337+
338+
``config()`` can be used in 2 ways: direct call and decorator.
339+
If you call ``config()`` directly, the configuration will be global.
340+
If you use ``config()`` as decorator, the configuration will only work on single PyWebIO application function.
341+
::
342+
343+
config(title="My application")
344+
345+
@config(css_style="* { color:red }")
346+
def app():
347+
put_text("hello PyWebIO")
348+
349+
``title`` and ``description`` are used for SEO, which are provided when indexed by search engines.
350+
If no ``title`` and ``description`` set for a PyWebIO application function,
351+
the `docstring <https://www.python.org/dev/peps/pep-0257/>`_ of the function will be used as title and description by default::
352+
353+
def app():
354+
\"""Application title
355+
356+
Application description...
357+
(A empty line is used to separate the description and title)
358+
\"""
313359
pass
314-
return func
315360
316-
return decorator
361+
The above code is equal to::
362+
363+
@config(title="Application title", description="Application description...")
364+
def app():
365+
pass
366+
367+
.. versionadded:: 1.4
368+
"""
369+
if isinstance(js_file, str):
370+
js_file = [js_file]
371+
if isinstance(css_file, str):
372+
css_file = [css_file]
373+
374+
configs = locals()
375+
376+
377+
class Decorator:
378+
def __init__(self):
379+
self.called = False
380+
381+
def __call__(self, func):
382+
self.called = True
383+
try:
384+
func = partial(func) # to make a copy of the function
385+
for key, val in configs.items():
386+
if val:
387+
setattr(func, '_pywebio_%s' % key, val)
388+
except Exception:
389+
pass
390+
return func
391+
392+
def __del__(self): # if not called as decorator, set the config to global
393+
if self.called:
394+
return
395+
396+
global _global_config
397+
_global_config = configs
398+
399+
return Decorator()

pywebio/utils.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -180,18 +180,19 @@ def get_function_doc(func):
180180
return inspect.getdoc(func) or ''
181181

182182

183-
def get_function_seo_info(func):
184-
"""获取使用 pywebio.platform.utils.seo() 设置在函数上的SEO信息
185-
"""
186-
if hasattr(func, '_pywebio_title'):
187-
return func._pywebio_title, func._pywebio_description
183+
def get_function_attr(func, attrs):
184+
"""Get the attribute values of the given function, even if the function is decorated by `functools.partial` """
185+
values = {attr: getattr(func, attr) for attr in attrs if hasattr(func, attr)}
188186

189187
while isinstance(func, functools.partial):
190188
func = func.func
191-
if hasattr(func, '_pywebio_title'):
192-
return func._pywebio_title, func._pywebio_description
189+
values.update({
190+
attr: getattr(func, attr)
191+
for attr in attrs
192+
if hasattr(func, attr) and attr not in values
193+
})
193194

194-
return None
195+
return values
195196

196197

197198
class LimitedSizeQueue(queue.Queue):

0 commit comments

Comments
 (0)