Repository: stefanhoelzl/vue.py
Branch: master
Commit: 581e764d57e2
Files: 158
Total size: 245.3 KB
Directory structure:
gitextract_pwes50fh/
├── .editorconfig
├── .github/
│ └── workflows/
│ └── push.yaml
├── .gitignore
├── .gitpod.Dockerfile
├── .gitpod.yml
├── CONTRIBUTING.md
├── LICENSE
├── MANIFEST.in
├── Makefile
├── README.md
├── docs/
│ ├── _config.yml
│ ├── _layouts/
│ │ └── default.html
│ ├── docs/
│ │ ├── index.md
│ │ ├── management/
│ │ │ ├── cli.md
│ │ │ └── configuration.md
│ │ ├── pyjs_bridge.md
│ │ └── vue_concepts/
│ │ ├── computed_properties.md
│ │ ├── custom_directives.md
│ │ ├── custom_vmodel.md
│ │ ├── data_methods.md
│ │ ├── extend.md
│ │ ├── filter.md
│ │ ├── instance_components.md
│ │ ├── lifecycle_hooks.md
│ │ ├── plugins_mixins.md
│ │ ├── props.md
│ │ ├── render_function.md
│ │ ├── vue-router.md
│ │ └── vuex.md
│ └── planning.md
├── examples/
│ ├── elastic_header/
│ │ ├── app.py
│ │ ├── header-view.html
│ │ ├── main.html
│ │ ├── style.css
│ │ └── vuepy.yml
│ ├── element_ui/
│ │ ├── app.py
│ │ ├── components/
│ │ │ └── navigation.py
│ │ ├── navigation.html
│ │ ├── style.css
│ │ └── vuepy.yml
│ ├── github_commits/
│ │ ├── app.py
│ │ ├── commits.html
│ │ ├── data.json
│ │ ├── style.css
│ │ └── vuepy.yml
│ ├── grid_component/
│ │ ├── app.py
│ │ ├── form.html
│ │ ├── grid.html
│ │ ├── style.css
│ │ └── vuepy.yml
│ ├── index.md
│ ├── markdown_editor/
│ │ ├── app.py
│ │ ├── editor.html
│ │ ├── style.css
│ │ └── vuepy.yml
│ ├── modal_component/
│ │ ├── app.py
│ │ ├── main.html
│ │ ├── modal-template.html
│ │ ├── style.css
│ │ └── vuepy.yml
│ ├── svg_graph/
│ │ ├── app-template.html
│ │ ├── app.py
│ │ ├── polygraph-template.html
│ │ ├── style.css
│ │ └── vuepy.yml
│ ├── todo_mvc/
│ │ ├── app-template.html
│ │ ├── app.py
│ │ ├── style.css
│ │ └── vuepy.yml
│ └── tree_view/
│ ├── app-template.html
│ ├── app.py
│ ├── style.css
│ ├── tree-template.html
│ └── vuepy.yml
├── pyproject.toml
├── requirements.txt
├── setup.py
├── stubs/
│ ├── browser.py
│ ├── javascript.py
│ └── local_storage.py
├── tests/
│ ├── __init__.py
│ ├── cli/
│ │ └── test_provider.py
│ ├── pytest.ini
│ ├── selenium/
│ │ ├── .gitignore
│ │ ├── chromedriver.py
│ │ ├── conftest.py
│ │ ├── pytest.ini
│ │ ├── test_api.py
│ │ ├── test_examples.py
│ │ ├── test_guide/
│ │ │ ├── test_components/
│ │ │ │ ├── test_custom_events.py
│ │ │ │ └── test_props.py
│ │ │ ├── test_essentials/
│ │ │ │ ├── test_components_basics.py
│ │ │ │ ├── test_computed_properties.py
│ │ │ │ ├── test_event_handler.py
│ │ │ │ ├── test_instance.py
│ │ │ │ ├── test_introduction.py
│ │ │ │ └── test_list_rendering.py
│ │ │ └── test_reusability_composition/
│ │ │ ├── test_filters.py
│ │ │ ├── test_mixins.py
│ │ │ └── test_render_function.py
│ │ ├── test_vuerouter.py
│ │ └── test_vuex.py
│ ├── test_install.py
│ └── unit/
│ ├── test_bridge/
│ │ ├── __init__.py
│ │ ├── mocks.py
│ │ ├── test_dict.py
│ │ ├── test_jsobject.py
│ │ ├── test_list.py
│ │ ├── test_vue.py
│ │ └── test_vuex.py
│ ├── test_transformers/
│ │ ├── conftest.py
│ │ ├── test_component.py
│ │ ├── test_router.py
│ │ └── test_store.py
│ ├── test_utils.py
│ └── test_vue.py
├── vue/
│ ├── __init__.py
│ ├── bridge/
│ │ ├── __init__.py
│ │ ├── dict.py
│ │ ├── list.py
│ │ ├── object.py
│ │ ├── vue_instance.py
│ │ └── vuex_instance.py
│ ├── decorators/
│ │ ├── __init__.py
│ │ ├── action.py
│ │ ├── base.py
│ │ ├── components.py
│ │ ├── computed.py
│ │ ├── custom.py
│ │ ├── data.py
│ │ ├── directive.py
│ │ ├── extends.py
│ │ ├── filters.py
│ │ ├── getter.py
│ │ ├── lifecycle_hook.py
│ │ ├── method.py
│ │ ├── mixins.py
│ │ ├── model.py
│ │ ├── mutation.py
│ │ ├── plugin.py
│ │ ├── prop.py
│ │ ├── render.py
│ │ ├── routes.py
│ │ ├── state.py
│ │ ├── template.py
│ │ └── watcher.py
│ ├── router.py
│ ├── store.py
│ ├── transformers.py
│ ├── utils.py
│ └── vue.py
└── vuecli/
├── __init__.py
├── cli.py
├── index.html
└── provider/
├── __init__.py
├── flask.py
├── provider.py
└── static.py
================================================
FILE CONTENTS
================================================
================================================
FILE: .editorconfig
================================================
root = true
# Unix-style newlines with a newline ending every file
[*]
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
charset = utf-8
# 4 space indentation
[*.{py,ini}]
indent_style = space
indent_size = 4
# 2 space indentation
[*.{css,js,html,yml}]
indent_style = space
indent_size = 2
[Makefile]
indent_style = tab
================================================
FILE: .github/workflows/push.yaml
================================================
name: CI
on:
pull_request:
branches:
- master
push:
branches:
- '**'
tags:
- 'release-candidate'
defaults:
run:
shell: bash
jobs:
cleanup:
runs-on: ubuntu-20.04
steps:
- name: Clean Up Release Candiate Tag
if: ${{ github.ref == 'refs/tags/release-candidate' }}
uses: dev-drprasad/delete-tag-and-release@v0.2.0
with:
tag_name: release-candidate
delete_release: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
all:
runs-on: ubuntu-20.04
steps:
# Setup
- name: Checkout Repository
uses: actions/checkout@v3
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
- name: Setup Python
uses: actions/setup-python@v1
with:
python-version: 3.8
- uses: browser-actions/setup-chrome@latest
- name: Setup environment
run: |
make env.up
# Build and Test
- name: Run CI jobs
run: |
make ci
# Publish documentation
- name: Set default env variables
run: |
echo "GH_PAGES_BRANCH=gh-pages-test" >> $GITHUB_ENV
- name: Update env variables for release
if: startsWith(github.ref, 'refs/tags/v')
run: |
echo "GH_PAGES_BRANCH=gh-pages" >> $GITHUB_ENV
- name: Deploy to GitHub Pages
uses: crazy-max/ghaction-github-pages@v2
if: ${{ github.event_name != 'pull_request' }}
with:
target_branch: ${{ env.GH_PAGES_BRANCH }}
build_dir: gh-pages-build
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Release
- name: Check Commit Messages
run: |
release check-commit-messages
- name: Generate Changelog
run: |
release changelog > changelog.md
- name: Delete Previous Master Github Release
if: ${{ github.ref == 'refs/heads/master' }}
uses: dev-drprasad/delete-tag-and-release@v0.2.0
with:
tag_name: master
delete_release: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Master Github Release
if: ${{ github.ref == 'refs/heads/master' }}
run: |
gh release create master ./dist/*.whl -F changelog.md --prerelease --target master
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Github Release
if: ${{ github.ref == 'refs/tags/release-candidate' }}
run: |
gh release create v`release version` ./dist/*.whl -F changelog.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish PyPI
if: ${{ github.ref == 'refs/tags/release-candidate' }}
uses: pypa/gh-action-pypi-publish@master
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
repository_url: https://upload.pypi.org/legacy/
skip_existing: false
================================================
FILE: .gitignore
================================================
venv
.idea
.vscode
.pytest_cache
__pycache__
gh-pages-build
debug
examples/*/screenshot.png
vuecli/js
examples_static
vuepy.egg-info
dist
build
vue/__version__.py
changelog.md
================================================
FILE: .gitpod.Dockerfile
================================================
FROM gitpod/workspace-full
USER gitpod
# Install Google key
RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
RUN sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list'
# Install custom tools, runtime, etc.
RUN sudo apt-get update && sudo apt-get install -y google-chrome-stable && sudo rm -rf /var/lib/apt/lists/*
================================================
FILE: .gitpod.yml
================================================
image:
file: .gitpod.Dockerfile
tasks:
- init: make env.up
command: make serve
ports:
- port: 8000
onOpen: open-browser
- port: 8001
onOpen: ignore
- port: 5000
onOpen: ignore
================================================
FILE: CONTRIBUTING.md
================================================
# Contributing a PR
First of: Thanks for contributing a PR to this project!!
There a four main guidelines I try to follow in this projects:
* Write clean code according to Bob Martins book
* Have tests!!
* Unit tests and selenium tests
* In general I try to use the examples in the vue.js documentation as test cases to make sure vue.py works as vue.js
* If a new feature is implemented, please also provide documentation
* Each commit needs a certain format `[type] commit message`
* This allows a automated generation of the changelog
* PRs get squashed and merged, so commit messages in PRs can be arbitrary
* type can be one of the following
* feature: use when adding new features
* bugfix: use when a bug gets fixed but function stays the same
* internal: use when refactoring or no user-facing changed are made
* docs: use when updating documentation
* tooling: use when changing tooling
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2018 Stefan Hoelzl
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: MANIFEST.in
================================================
include LICENSE
================================================
FILE: Makefile
================================================
PYTHONPATH=.:stubs
.PHONY: env.pip
env.pip:
pip install -r requirements.txt
pip install -e .
.PHONY: env.chrome
env.chrome:
python tests/selenium/chromedriver.py
.PHONY: env.up
env.up: env.pip env.chrome
.PHONY: env.down
env.down:
git clean -xdf --exclude .idea --exclude venv --exclude debug
pip freeze > /tmp/vuepy-delete-requirements.txt
pip uninstall -y -r /tmp/vuepy-delete-requirements.txt
.PHONY: serve
serve:
python -m http.server 8000
.PHONY: run
run:
cd ${APP} && vue-cli deploy flask
.PHONY: tests.selenium
tests.selenium:
PYTHONPATH=$(PYTHONPATH) pytest tests/selenium
.PHONY: tests.unit
tests.unit:
PYTHONPATH=$(PYTHONPATH) pytest tests/unit
.PHONY: tests.cli
tests.cli:
PYTHONPATH=$(PYTHONPATH) pytest tests/cli
.PHONY: tests
tests:
PYTHONPATH=$(PYTHONPATH) pytest tests/${TEST}
.PHONY: format
format:
black --target-version py38 .
.PHONY: lint
lint:
black --target-version py38 --check .
.PHONY: build
build:
python setup.py sdist bdist_wheel
.PHONY: docs
docs:
rm -Rf gh-pages-build
mkdir gh-pages-build
cp -Rf docs/* README.md vue gh-pages-build
cp -Rf examples_static gh-pages-build/examples
cp examples/index.md gh-pages-build/examples
mkdir gh-pages-build/tests
cp -R tests/selenium/_html/* gh-pages-build/tests
mkdir gh-pages-build/js
vue-cli package gh-pages-build/js
.PHONY: ci
ci: lint tests build docs
================================================
FILE: README.md
================================================
# vue.py
[](https://github.com/stefanhoelzl/vue.py/actions)
[](https://pypi.org/project/vuepy/)
[](LICENSE)
use [Vue.js](https://www.vuejs.org) with pure Python
vue.py provides Python bindings for [Vue.js](https://www.vuejs.org).
It uses [brython](https://github.com/brython-dev/brython) to run Python in the browser.
Here is a simple example of an vue.py component
```python
from browser import alert
from vue import VueComponent
class HelloVuePy(VueComponent):
greeting = "Hello vue.py"
def greet(self, event):
alert(self.greeting)
template = """
click me
"""
HelloVuePy("#app")
```
## Installation
```bash
$ pip install vuepy
```
## Development Status
The goal is to provide a solution to write fully-featured Vue applications in pure Python.
To get an overview what currently is supported, have a look at the [Documentation](https://stefanhoelzl.github.io/vue.py/docs/).
Have a look [here](https://stefanhoelzl.github.io/vue.py/planning.html) to see whats planned!
See also the [Limitations](https://stefanhoelzl.github.io/vue.py/docs/pyjs_bridge.html)
## Documentation
Documentation for the last release is available [here](https://stefanhoelzl.github.io/vue.py/docs/).
Documentation fo the current master branch can be found [here](https://github.com/stefanhoelzl/vue.py/blob/master/docs/docs/index.md).
Examples can be found [here](https://stefanhoelzl.github.io/vue.py/examples).
These are vue.py versions of the [Vue.js examples](https://vuejs.org/v2/examples/)
## Performance
Initial loading times of `vue.py` apps can be very long.
Especially when loading a lot of python files.
Still figuring out how to solve this.
Have not done any peformance tests, but havent noticed any issues with performance
as soon as the app was fully loaded.
## Development
### Getting Started
Open in [gitpod.io](https://gitpod.io#github.com/stefanhoelzl/vue.py)
Get the code
```bash
$ git clone https://github.com/stefanhoelzl/vue.py.git
$ cd vue.py
```
Optionally you can create a [venv](https://docs.python.org/3.8/library/venv.html)
```bash
$ python -m venv venv
$ source venv/bin/activate
```
Install required python packages, the chromedriver for selenium and brython
```bash
$ make env.up
```
Format the code
```bash
$ make format
```
Run tests
```bash
$ make tests # runs all tets
$ make tests.unit # runs unit tests
$ make tests.selenium # runs selenium tests
$ make tests.cli # runs cli tests
$ make tests TEST=cli/test_provider.py::TestRenderIndex::test_defaults # run explicit test
```
Run an example
```bash
$ make run APP=examples/tree_view # makes example available on port 5000
```
Reset your development environment
_(clean up, reinstall packages and redownload needed files)_
```bash
$ make env.down
$ make env.up
```
Publish a new release
```bash
$ release release-candidate
```
### Contributing
see [CONTRIBUTING](https://github.com/stefanhoelzl/vue.py/blob/master/CONTRIBUTING.md)
## License
This project is licensed under the MIT License - see the [LICENSE](https://github.com/stefanhoelzl/vue.py/blob/master/LICENSE) file for details
================================================
FILE: docs/_config.yml
================================================
theme: jekyll-theme-cayman
title: vue.py
description: Pythonic Vue.js
include:
- __init__.py
- __entry_point__.py
navigation:
- title: Start
link: /
- title: Documentation
link: docs
- title: Gallery
link: examples
- title: Demo
link: https://stefanhoelzl.github.io/mqtt-dashboard/
================================================
FILE: docs/_layouts/default.html
================================================
{% seo %}
Skip to the content.
{{ content }}
================================================
FILE: docs/docs/index.md
================================================
# Documentation
`vue.py` provides bindings for [Vue.js](https://vuejs.org/).
If you are not familiar with [Vue.js](https://vuejs.org/) read the [Vue.js Guide](https://vuejs.org/v2/guide/)
and then get back here to learn how to use [Vue.js](https://vuejs.org/) with pure Python.
## Installation
Install `vue.py` via `pip`
```bash
$ pip install vuepy
```
or with flask include to deploy apps
```bash
$ pip install vuepy[flask]
```
or from current master branch
```bash
$ pip install git+https://github.com/stefanhoelzl/vue.py@master
```
## First Application
Create a folder for your app
```bash
$ mkdir app
$ cd app
```
and as last step create a `app.py` where you create your Vue Component
```python
from vue import *
class App(VueComponent):
msg = "Hello vue.py!"
template = "{{msg}}
"
App("#app")
```
deploy your app
```bash
$ vue-cli deploy flask
```
Now goto [http://localhost:5000](http://localhost:5000) and see your first vue.py app.
## Demo App
Checkout the [MQTT-Dashboard](https://github.com/stefanhoelzl/mqtt-dashboard/blob/master/app/app.py).
It's a little test project to demonstrate some `vue.py` features:
* uses the Browsers local storage to implement a vue-plugin in python
* uses a vue.js plugin
* uses some vue.js components
* uses a vuex store
## How to use Vue.js concepts
* [Instance and Components](vue_concepts/instance_components.md)
* [Data and Methods](vue_concepts/data_methods.md)
* [Computed Properties and Watchers](vue_concepts/computed_properties.md)
* [Props](vue_concepts/props.md)
* [Lifecycle Hooks](vue_concepts/lifecycle_hooks.md)
* [Customize V-Model](vue_concepts/custom_vmodel.md)
* [Filter](vue_concepts/filter.md)
* [Custom Directives](vue_concepts/custom_directives.md)
* [Plugins and Mixins](vue_concepts/plugins_mixins.md)
* [Extend](vue_concepts/extend.md)
* [Render Function](vue_concepts/render_function.md)
* [Vuex](vue_concepts/vuex.md)
* [Vue Router](vue_concepts/vue-router.md)
## Management
* [Configuration](management/configuration.md)
* [vue-cli](management/cli.md)
## Python/Javascript Bridge
[here](pyjs_bridge.md)
================================================
FILE: docs/docs/management/cli.md
================================================
# Command Line Interface
`vue.py` provides a command line tool `vue-cli` to deploy your application.
## Deployment
A `vue.py` application can be deployed via several provider.
Get help about the available provider and their arguments
```bash
$ vue-cli deploy -h
```
This installs all the required packages for e.g. the flask provider
```bash
pip install vuepy[flask]
```
### Flask
With a flask live deployment your application is accessible on
[http://localhost:5000](http://localhost:5000).
```bash
$ vue-cli deploy flask
```
This is the best deployment method when debugging.
#### Configuration
`vuepy.yml` can be used to set `HOST`, `PORT` and [Flask Builtin Configuration Values](https://flask.palletsprojects.com/en/2.0.x/config/#builtin-configuration-values)
```yaml
provider:
flask:
HOST: "0.0.0.0"
PORT: 5001
```
### Static
With a static deployment everything your application needs,
gets packaged into a single folder,
which can be served by your favorite web server.
```bash
$ vue-cli deploy static --package
```
* `destination` specifies the path where your application should be deployed to.
* `--package` (optional) packages the python code into the vuepy.js file.
================================================
FILE: docs/docs/management/configuration.md
================================================
# Configuration
Your `vue.py` application can be customized via a `vuepy.yml` file
located in your application folder.
## Stylesheets
If you want to use custom CSS stylsheets, add this section to the configuration file:
```yaml
stylesheets:
-
-
```
## Scripts
### Javascript Libraries
If you want to use custom javascript libraries, add this section to the configuration file:
```yaml
scripts:
-
-
```
or if combined with [extensions](#Extensions) or [custom versions](#Custom-Versions)
```yaml
scripts:
"local_lib_name":
"lib_name":
```
### Extensions
`vue.py` comes with some vue.js extensions builtin:
* [vuex](https://vuex.vuejs.org)
* [vue-router](https://router.vuejs.org)
The extensions can be activated as followed:
```yaml
scripts:
vuex: true
vue-router: true
```
By default all extensions are deactivated to avoid loading unnecessary files.
### Custom Versions
`vue.py` comes with vue.js and brython built-in.
If different versions can be used as followed:
```yaml
scripts:
vue:
brython:
vuex:
vue-router:
```
## EntryPoint
By default the `app.py` in your project directory is the entry point for your app.
If you want to point to a custom entry point `custom.py`, add this section:
```yaml
entry_point: custom
```
## Templates
Since writing HTML in python strings can be tedious
you can write your templates in .html files
and link them as your template string.
```yaml
templates:
myhtml: my.html
```
```python
from vue import VueComponent
class MyComponent(VueComponent):
template = "#myhtml"
```
================================================
FILE: docs/docs/pyjs_bridge.md
================================================
# Python/Javascript Bridge
## Call Javascript Functions
`vue.py` provides some utilities to access Javascript libraris.
You can load Javascript libraries dynamically
```python
from vue.utils import js_load
marked = js_load("https://unpkg.com/marked@0.3.6")
html = marked("# Title")
```
This perfoms an synchrous ajax call and therefore is not recommended for responsive applications.
Furthermore prevent some browser (e.g. Chrome) load from external ressources with ajax.
Therefore a second method is provided to acces a already loaded Javascript library.
```html
```
```python
from vue.utils import js_lib
marked = js_lib("marked")
html = marked("# Title")
```
This uses the optimized methods a Browser uses to load all dependencies.
And provides also access to all object in the Javascript namespace.
## Vue Reactivity
To keep the reactivity of Vue.js and at the same time providing a
Pythonic interface, all attribuets of vue.py components are wrapped in
custom types.
These types provide the same interfaes than native python types, but use
the javascript types in the background.
Just making dicts out of Javascript object, method calls, would look
rather unusual.
```python
element['focus']()
```
To avoid this, wrapped dicts can also access items as attributes, this leads to
more readable code
```python
element.focus()
```
By wrapping the javascript types, it is also possible
to improve the original Vue.js behavior. In Vue.js this is forbidden.
```javascript
var vm = new Vue({
data: {
reactive: {yes: 0}
}
})
// `vm.reactive.yes` is now reactive
vm.no = 2
// `vm.reactive.no` is NOT reactive
```
Your have to use `Vue.set()`. vue.py takes care of this under the hood.
```python
class App(VueComponent):
reactive = {"yes": 0}
app = App("#element")
# `vm.reactive.yes` is now reactive
app.reactive["also"] = 2 # `vm.reactive.also` is now also reactive
```
## Limitations
## Usable Types
For now vue.py only supports basics types (int, float, str, bool, list, dict), since these can be converted fairly simple to their Javascript equivalentive.
Writing own classes and using them for Component properties may not work.
This may change in the future, but for now it is not planned to work on this issue.
## Due To Wrapping Types
Due to restrictions of Brython in combination with the reactivity system in Vue.js are custom wrapper around component data and props neccessary.
This is done mostly in the background, there are some limitations to consider.
### When Native Python Types Are Assumed
The wrapper around lists and dictionaries provide the same interface than native python types but due to restrictions in Brython, they are no subclasses of `list`/`dict`.
This can lead to problems when passing this methods to other native python methods.
Therefore a helper is provided to convert a wrapped Javascript object into a native python type.
```python
import json
from vue import VueComponent, computed
from vue.bridge import Object
class MyComponent(VueComponent):
template = "{{ content_as_json }}
"
content = [{"a": 1}]
@computed
def content_as_json(self):
# Will break because self.content is not a native python type
# json.dumps does not know how to serialize this types
return json.dumps(sef.content)
# vue.py provides a method to convert the wrapper types
return json.dumps(Object.to_py(self.content))
```
**When converting to native python types reactivity may get lost!**
### When Native Javascript Types Are Assumed
A similar problem exists when passing wrapper variables to native javascript methods.
Brython can convert native Python types like lists and dicts to their javascript equivalent.
Since the wrapper types are not real lists/dicts Brython cannot convert them.
```python
from vue import VueComponent, computed
from vue.bridge import Object
from vue.utils import js_lib
js_json = js_lib("JSON")
class MyComponent(VueComponent):
template = "{{ content_as_json }}
"
content = [{"a": 1}]
@computed
def content_as_json(self):
# Will break because self.content is not a native javascript type
# JSON.stringify does not know how to serialize this types
return js_json.stringify(self.content)
# vue.py provides a method to convert the wrapper types
return js_json.stringify(Object.to_js(self.content))
```
### Are These Limitations Forever?
I hope not!
Currently the main reason for this limitations is [Brython Issue 893](https://github.com/brython-dev/brython/issues/893).
When this one gets fixed, the wrapper classes can be subclasses of native python types and Brython should be able to do the right conversions under the hood.
================================================
FILE: docs/docs/vue_concepts/computed_properties.md
================================================
# Computed Properties
Computed properties can be defined with the `@computed` decorator
```python
from vue import VueComponent, computed
class ComponentWithMethods(VueComponent):
message = "Hallo vue.py"
@computed
def reversed(self):
return "".join(reversed(self.message))
```
computed setters are defined similar to plain python setters
```python
class ComputedSetter(VueComponent):
message = "Hallo vue.py"
@computed
def reversed_message(self):
return self.message[::-1]
@reversed_message.setter
def reversed_message(self, reversed_message):
self.message = reversed_message[::-1]
```
# Watchers
Watchers can be defined with the `@watch` decorator.
```python
from vue import VueComponent, watch
class Watch(VueComponent):
message = ""
@watch("message")
def log_message_changes(self, new, old):
print("'message' changed from '{}' to '{}'".format(old, new)
```
`deep` and `immediate` watchers can be configured via arguments
```python
from vue import VueComponent, watch
class Watch(VueComponent):
message = ""
@watch("message", deep=True, immediate=True)
def log_message_changes(self, new, old):
print("'message' changed from '{}' to '{}'".format(old, new)
```
================================================
FILE: docs/docs/vue_concepts/custom_directives.md
================================================
# Custom Directives
## Local Registration
For [function directives](https://vuejs.org/v2/guide/custom-directive.html#Function-Shorthand)
is just a decorator necessary
```python
from vue import VueComponent, directive
class CustomDirective(VueComponent):
@staticmethod
@directive
def custom_focus(el, binding, vnode, old_vnode, *args):
pass
```
To define custom hook functions, add the directive name as argument to
the decorator
```python
from vue import VueComponent, directive
class CustomDirective(VueComponent):
@staticmethod
@directive("focus")
def component_updated(el, binding, vnode, old_vnode, *args):
# implement 'componentUpdated' hook function here
pass
@staticmethod
@directive("focus")
def inserted(el, binding, vnode, old_vnode, *args):
# implement 'inserted' hook function here
pass
```
To avoid code duplication when adding the same hook function to different hooks,
the hooks can be specified as decorator arguments.
```python
from vue import VueComponent, directive
class CustomDirective(VueComponent):
@staticmethod
@directive("focus", "component_updated", "inserted")
def combined_hook(el, binding, vnode, old_vnode, *args):
# implement function for 'componentUpdated' and 'inserted' hook here
pass
```
**The Vue.js hook `componentUpdated` is called `component_updated` to be more pythonic**
The `@staticmethod` decorator is only necessary to avoid IDE checker errors.
Underscores in directive names get replaced by dashes, so `custom_focus` gets `v-custom-focus`.
## Global Registration
Global directives can be created by sub-classing `VueDirective`.
```python
from vue import Vue, VueDirective
class MyDirective(VueDirective):
def bind(el, binding, vnode, old_vnode):
pass
def component_updated(el, binding, vnode, old_vnode):
pass
Vue.directive("my-directive", MyDirective)
```
and for function directives just pass the function to `Vue.directive`
```python
from vue import Vue
def my_directive(el, binding, vnode, old_vnode):
pass
Vue.directive("my-directive", my_directive)
```
`vue.py` offeres a shorthand, if you like to take the **lower-cased** name
of the function/directive-class as directive name.
```python
from vue import Vue
def my_directive(el, binding, vnode, old_vnode):
pass
Vue.directive(my_directive) # directive name is 'my_directive'
```
## Retrieve Global Directives
Getter for global directives works similar to Vue.js
```python
from vue import Vue
directive = Vue.directive('directive-name')
```
================================================
FILE: docs/docs/vue_concepts/custom_vmodel.md
================================================
# Customize V-Model
To customize the event and prop used by `v-model` a class variable of the type `Model()` can be defined.
```python
from vue import VueComponent, Model
class CustomVModel(VueComponent):
model = Model(prop="checked", event="change")
checked: bool
template = """
"""
```
================================================
FILE: docs/docs/vue_concepts/data_methods.md
================================================
# Data
All class variables of a `vue.py` component are available as data fields in the Vue instance.
```python
class ComponentWithData(VueComponent):
data_field = "value"
```
to initialize a data field with a prop you can use the `@data` decorator
```python
from vue import VueComponent, data
class ComponentWithData(VueComponent):
prop: str
@data
def initialized_with_prop(self):
return self.prop
```
# Methods
Similar to data fields all methods of the `vue.py` component are available as methods.
```python
from vue import VueComponent
class ComponentWithMethods(VueComponent):
counter = 0
def increase(self):
self.counter += 1
```
Methods used as event handler, must have a (optional) argument for the event.
```python
from vue import VueComponent
class ComponentWithMethods(VueComponent):
def handle(self, ev):
print(ev)
```
## self
All attributes of the Vue instance are available as attributes of `self` (e.g. methods, computed properties, props etc.).
================================================
FILE: docs/docs/vue_concepts/extend.md
================================================
# Extend
Vue.js uses `Vue.extend` or the `extends` Component attribute to extend
components. Per default `vue.py` sticks to the Pythonic way and uses the
python class inheritance behavior.
```python
from vue import VueComponent
class Base(VueComponent):
def created(self):
print("Base")
class Sub(Base):
def created(self):
super().created()
print("Sub")
```
which outputs on the console
```
Base
Sub
```
To use the merging strategies Vue.js applies when using `extends`
in `vue.py` just set the `extends` attribute to `True`
```python
from vue import VueComponent
class Base(VueComponent):
def created(self):
print("Base")
class Sub(Base):
extends = True
def created(self):
print("Sub")
```
which outputs on the console
```
Base
Sub
```
The `extend` attribute can also be a native Vue.js component to extend from this.
```
from vue import *
from vue.utils import js_lib
NativeVueJsComponent = js_lib("NativeVueJsComponent")
class Sub(VueComponent):
extends = NativeVueJsComponent
```
## Template Slots
Vue.js does not support extending templates out-of-the-box and it is
[recommended](https://vuejsdevelopers.com/2017/06/11/vue-js-extending-components/)
to use third-party libraries like [pug](https://pugjs.org/api/getting-started.html).
With `vue.py` a feature called `template_slots` is included to extend templates.
A base component can define slots in the template
and a sub component can fill the slots with the attribute `template_slots`.
The base component can also define default values for the slots.
```python
from vue import VueComponent
class Base(VueComponent):
template_slots = {
"heading": "Default Heading",
"footer": "Default Footer",
}
template = """
{heading}
{content}
{footer}
"""
class Sub(Base):
template_slots = {
"heading": "My Custom Heading",
"content": "content..."
}
```
The `Sub` component gets rendered as
```html
My Custom Heading
content...
Default Footer
```
If you only have one slot in your component, the `template_slots` attribute
can be the template string
```python
from vue import VueComponent
class Base(VueComponent):
template = "{} "
class Sub(Base):
template_slots = "heading"
```
The `Sub` component gets rendered as
```html
heading
```
Mixing both is also possible
```python
class Base(VueComponent):
template_slots = {"pre": "DEFAULT", "post": "DEFAULT"}
template = "{pre} {} {post}
"
class WithSlots(Base):
template_slots = {"pre": "PRE", "default": "SUB"}
class WithDefault(Base):
template_slots = "SUB"
```
The `WithSlots` component gets rendered as
```html
PRE SUB DEFAULT
```
The `WithDefault` component gets rendered as
```html
DEFAULT SUB DEFAULT
```
================================================
FILE: docs/docs/vue_concepts/filter.md
================================================
# Filter
## Local Registration
Local registration of filters is done with the `@filters` decorator.
```python
from vue import VueComponent, filters
class ComponentWithFilter(VueComponent):
message = "Message"
@filters
def lower_case(value):
return value.lower()
template = "{{ message | lower_case }}
"
```
To avoid errors on source code checking errors in modern IDEs, an additional `@staticmethod` decorator can be added
```python
from vue import VueComponent, filters
class ComponentWithFilter(VueComponent):
@staticmethod
@filters
def lower_case(value):
return value.lower()
```
## Global Registration
Global registration of filters works similar to Vue.js
```python
from vue import Vue
Vue.filter("capitalize", str.capitalize)
```
Additionally in vue.py it is allowd to only pass a function to `Vue.filter`.
In this case the filter gets registered under the function name.
```python
from vue import Vue
def my_filter(val):
return "filtered({})".format(val)
Vue.filter(my_filter)
```
================================================
FILE: docs/docs/vue_concepts/instance_components.md
================================================
# Components
## Define
A Vue component can be defined by writing a sub-class of `VueComponent`
```python
from vue import VueComponent
class MyComponent(VueComponent):
pass
```
## Registration
Every component has to be [registered](https://vuejs.org/v2/guide/components-registration.html) to be available in other components.
### Local Registration
```python
from vue import VueComponent
class MyComponent(VueComponent):
components = [
MyVuePyComponent,
AnotherNativeVueJsComponent,
]
```
The component to register can be either a `vue.py` component or a native
Vue.js component loaded with `js_lib` or `js_import`
### Global Registration
```python
from vue import Vue
# For vue.py components or native Vue.js component loaded with js_lib or js_import
Vue.component(MyComponent)
Vue.component("my-custom-name", MyComponent)
# Only for vue.py components
MyComponent.register()
MyComponent.register("my-custom-name")
```
## Template
The component html template can be defined with a class variable called `template`
```python
from vue import VueComponent
class MyComponent(VueComponent):
template = """
Hallo vue.py!
"""
```
`vue.py` templates look the same than Vue.js templates. This means inline expressions must be javascript!!.
```python
from vue import VueComponent
class MyComponent(VueComponent):
message = "Hallo vue.py!"
template = """
{{ message.split('').reverse().join('') }}
"""
```
# Instance
## Start
To start a component as Vue application, just pass a css selector at initialization
```
App("#app")
```
## Prop Data
[propsData](https://vuejs.org/v2/api/#propsData) can be passed in as a dictionary.
```python
App("#app", props_data={"prop": "value"})
```
## API
### Dollar Methods
$-methods like `$emit` can be called by omitting the `$`
```python
from vue import VueComponent
class MyComponent(VueComponent):
def created(self):
self.emit("creation", "Arg")
```
In the case your Component has another attribute with the same name, you can use a workaround and directly call `getattr()`
```python
from vue import VueComponent
class MyComponent(VueComponent):
emit = "already used"
def created(self):
getattr(self, "$emit")("creation", "Arg")
```
================================================
FILE: docs/docs/vue_concepts/lifecycle_hooks.md
================================================
# Lifecycle Hooks
Certain names for component methods are reserved, to specify lifecycle hooks.
```python
from vue import VueComponent
class ComponentLifecycleHooks(VueComponent):
def before_create(self):
print("on beforeCreate")
def created(self):
print("on created")
def before_mount(self):
print("on beforeMount")
def mounted(self):
print("on mounted")
def before_update(self):
print("on beforeUpdate")
def updated(self):
print("on updated")
def before_destroy(self):
print("on beforeDestroy")
def destroyed(self):
print("on destroyed")
```
================================================
FILE: docs/docs/vue_concepts/plugins_mixins.md
================================================
# Mixins
## Write Mixins
Mixins are created by sub-classing from `VueMixin` and from there it is
similar to write a `VueComponent`
```python
from vue import VueMixin, computed
class MyMixin(VueMixin):
prop: str
value = "default"
def created(self):
pass
@computed
def upper_value(self):
return self.value.upper()
```
## Use Mixins
### Local Registration
Local registration of a Mixin within a Component works just like in Vue.js.
```python
from vue import VueComponent
class MyComponent(VueComponent):
mixins = [MyPyMixin, AnotherVueJsMixin]
```
Mixins wirtten in Vue.js and `vue.py` can be mixed.
### Global Registration
Global registration of a Mixin works also just like in Vue.js.
```python
from vue import Vue
Vue.mixin(MyMixin)
```
# Plugins
## Write Plugins
A plugin can be written by sub-classing `VuePlugin` and implementing
the function `install` similar to Vue.js.
```python
from vue import Vue, VuePlugin, VueMixin
class MyPlugin(VuePlugin):
class MyMixin(VueMixin):
def created(self):
pass
# 4) Within a Mixin, new instance methods can be defined
def my_method(self, args):
pass
@staticmethod
def global_method():
pass
@staticmethod
def install(*args, **kwargs):
# 1) Add a global method or property
Vue.my_global_method = MyPlugin.global_method
# 2) Add a global assed
Vue.directive(MyDirective)
# 3) Inject Mixins
Vue.mixin(MyPlugin.MyMixin)
```
## Use Plugins
Using plugins works like in Vue.js
```python
from vue import Vue
Vue.use(MyPlugin)
```
`vue.py` supports also using native Vue.js plugins.
================================================
FILE: docs/docs/vue_concepts/props.md
================================================
# Props
A prop is defined by adding a type hint to a class variable.
```python
from vue import VueComponent
class ComponentWithData(VueComponent):
prop: str
```
## Types
Unlike Vue.js, vue.py enforces prop types (if not type hint is provided, it is a data field).
The following types are currently supported:
* `int`
* `float`
* `str`
* `bool`
* `list`
* `dict`
## Default
By assigning a value to a prop, the default value can be defined.
```python
from vue import VueComponent
class ComponentWithData(VueComponent):
prop: str = "default"
```
## Required
If no default value is given, the prop is automatically required.
## Validator
With the `@validator` decorator are prop validators defined
```python
from vue import VueComponent, validator
class ComponentWithData(VueComponent):
prop: int
@validator("prop")
def prop_must_be_greater_than_100(self, value):
return value > 100
```
================================================
FILE: docs/docs/vue_concepts/render_function.md
================================================
# Render Function
A render function can be defined by overwriting the `render` method.
```python
from vue import VueComponent
class ComponentWithData(VueComponent):
def render(self, create_element):
return create_element("h1", "Title")
```
## Accessing Slots
Slots can be accessed as a dictionary.
```python
from vue import VueComponent
class ComponentWithSlots(VueComponent):
def render(self, create_element):
return create_element(f"div", self.slots.get("default"))
```
It is recommened to access the dictionary via the `get`-method to avoid failures
when no children for the slot are provided.
## Passing Props
```python
from vue import VueComponent
class ComponentWithProps(VueComponent):
prop: str = "p"
template = "
"
ComponentWithProps.register()
class Component(VueComponent):
def render(self, create_element):
return create_element(
"ComponentWithProps", {"props": {"prop": "p"}},
)
```
================================================
FILE: docs/docs/vue_concepts/vue-router.md
================================================
# Vue Router
## Define
A vue router can be used by writing a sub-class of `VueRouter`,
setting some `VueRoute`s
and set the `router` parameter when initializing a app.
```python
from vue import VueComponent, VueRouter, VueRoute
class Foo(VueComponent):
template = "foo
"
class Bar(VueComponent):
template = "bar
"
class Router(VueRouter):
routes = [
VueRoute("/foo", Foo),
VueRoute("/bar", Bar),
]
class App(VueComponent):
template = """
"""
App("#app", router=Router())
```
enable the vue-router extension in the `vuepy.yml` [config file](../management/configuration.md):
```yaml
scripts:
vue-router: true
```
================================================
FILE: docs/docs/vue_concepts/vuex.md
================================================
# Vuex
## Define
A Vuex store can be defined by writing a sub-class of `VueStore`
and used by setting the `store` parameter when initializing a app.
```python
from vue import VueComponent, VueStore
class Store(VueStore):
pass
class App(VueComponent):
pass
App("#app", store=Store())
```
enable the vuex extension in the `vuepy.yml` [config file](../management/configuration.md):
```yaml
scripts:
vuex: true
```
## State Variables
```python
from vue import VueComponent, VueStore
class Store(VueStore):
greeting = "Hello Store"
class App(VueComponent):
def created(self):
print(self.store.greeting)
App("#app", store=Store())
```
## Getter
```python
from vue import VueComponent, VueStore, getter
class Store(VueStore):
greeting = "Hello"
@getter
def get_greeting(self):
return self.greeting
@getter
def personalized_greeting(self, name):
return "{} {}".format(self.greeting, name)
class App(VueComponent):
def created(self):
print(self.store.get_greeting) # "Hello"
print(self.store.personalized_greeting("Store")) # "Hello Store"
App("#app", store=Store())
```
## Mutations
Unlike `Vue.js` mutations in `vue.py` can have multiple arguments and
even keyword-arguments
```python
from vue import VueComponent, VueStore, mutation
class Store(VueStore):
greeting = ""
@mutation
def set_greeting(self, greeting, name=None):
self.greeting = greeting
if name:
self.greeting += " " + name
class App(VueComponent):
def created(self):
self.store.commit("set_greeting", "Hello", name="Store")
print(self.store.greeting) # "Hello Store"
App("#app", store=Store())
```
## Mutations
Similar to mutations actions in `vue.py` can have multiple arguments and
even keyword-arguments.
```python
from vue import VueComponent, VueStore, mutation
class Store(VueStore):
@action
def greet(self, greeting, name=None):
if name:
greeting += " " + name
print(greeting)
class App(VueComponent):
def created(self):
self.store.dispatch("greet", "Hello", name="Store")
App("#app", store=Store())
```
## Plugins
```python
class Plugin(VueStorePlugin):
def initialize(self, store):
store.message = "Message"
def subscribe(self, mut, *args, **kwargs):
print(mut, args, kwargs)
class Store(VueStore):
plugins = [Plugin().install] # list can also contain native vuex plugins
message = ""
@mutation
def msg(self, prefix, postfix=""):
pass
class ComponentUsingGetter(VueComponent):
@computed
def message(self):
return self.store.message
def created(self):
self.store.commit("msg", "Hallo", postfix="!")
template = "{{ message }}
"
```
================================================
FILE: docs/planning.md
================================================
# Future Plans
## Performance
* How to improve loading times?
* create benchmarks
## Tools
* Docker deployment
## Vue.py Universe
* store synchronization
* with local/session storage
* over WebSockets with python backend
* desktop toolkit
* based on [pywebview](https://github.com/r0x0r/pywebview) ??
## Vue.js Features
* full access to Vue object (global configuration etc.)
* ...
## Internals
* write tests for decorators
## Docs
* embed examples in gallery
================================================
FILE: examples/elastic_header/app.py
================================================
from vue import VueComponent, data, computed
from vue.bridge import Object
from vue.utils import js_lib
dynamics = js_lib("dynamics")
class DraggableHeaderView(VueComponent):
template = "#header-view"
dragging = False
@data
def c(self):
return {"x": 160, "y": 160}
@data
def start(self):
return {"x": 0, "y": 0}
@computed
def header_path(self):
return f'M0,0 L320,0 320,160Q{self.c["x"]},{self.c["y"]} 0,160'
@computed
def content_position(self):
dy = self.c["y"] - 160
dampen = 2 if dy > 0 else 4
return {"transform": f"translate3d(0,{dy / dampen}px,0)"}
def start_drag(self, e):
e = e["changedTouches"][0] if "changedTouches" in e else e
self.dragging = True
self.start["x"] = e.pageX
self.start["y"] = e.pageY
def on_drag(self, e):
e = e["changedTouches"][0] if "changedTouches" in e else e
if self.dragging:
self.c["x"] = 160 + (e.pageX - self.start["x"])
dy = e.pageY - self.start["y"]
dampen = 1.5 if dy > 0 else 4
self.c["y"] = int(160 + dy / dampen)
def stop_drag(self, _):
if self.dragging:
self.dragging = False
dynamics.animate(
Object.to_js(self.c),
{"x": 160, "y": 160},
{"type": dynamics.spring, "duration": 700, "friction": 280},
)
DraggableHeaderView.register()
class App(VueComponent):
template = "#main"
App("#app")
================================================
FILE: examples/elastic_header/header-view.html
================================================
================================================
FILE: examples/elastic_header/main.html
================================================
Elastic Draggable SVG Header
with vue.py
+ dynamics.js
Note this is just an effect demo
- there are of course many additional details
if you want to use this in production,
e.g. handling responsive sizes, reload threshold and content scrolling.
Those are out of scope for this quick little hack.
However, the idea is that you can hide them as internal details
of a vue.py component and expose a simple Web-Component-like interface.
================================================
FILE: examples/elastic_header/style.css
================================================
h1 {
font-weight: 300;
font-size: 1.8em;
margin-top: 0;
}
a {
color: #fff;
}
.draggable-header-view {
background-color: #fff;
box-shadow: 0 4px 16px rgba(0,0,0,.15);
width: 320px;
height: 560px;
overflow: hidden;
margin: 30px auto;
position: relative;
font-family: 'Roboto', Helvetica, Arial, sans-serif;
color: #fff;
font-size: 14px;
font-weight: 300;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.draggable-header-view .bg {
position: absolute;
top: 0;
left: 0;
z-index: 0;
}
.draggable-header-view .header, .draggable-header-view .content {
position: relative;
z-index: 1;
padding: 30px;
box-sizing: border-box;
}
.draggable-header-view .header {
height: 160px;
}
.draggable-header-view .content {
color: #333;
line-height: 1.5em;
}
================================================
FILE: examples/elastic_header/vuepy.yml
================================================
templates:
header-view: header-view.html
main: main.html
stylesheets:
- style.css
scripts:
- https://unpkg.com/dynamics.js@1.1.5/lib/dynamics.js
================================================
FILE: examples/element_ui/app.py
================================================
from vue import VueComponent
from .components import navigation
navigation.register()
class App(VueComponent):
template = "#navigation"
navigation_menu = [
{
"id": "one",
"title": "Navigation One",
"icon": "el-icon-location",
"children": [
{"group": "Group One"},
{"id": "one.one", "title": "Item One"},
{"id": "one.two", "title": "Item Two"},
{"group": "Group Two"},
{"id": "one.hree", "title": "Item Three"},
{
"id": "one.four",
"title": "Item Four",
"children": [{"id": "one.four.five", "title": "Item Five"}],
},
],
},
{"id": "two", "title": "Navigation Two", "icon": "el-icon-menu"},
{
"id": "three",
"title": "Navigation Three",
"icon": "el-icon-document",
"disabled": True,
},
{"id": "four", "title": "Navigation Four", "icon": "el-icon-setting"},
]
def clicked(self, item):
print(item)
self.notify.info(
{"title": "Navigation", "message": item.get("title", "NO TITLE")}
)
App("#app")
================================================
FILE: examples/element_ui/components/navigation.py
================================================
from vue import VueComponent, computed
class NavigationItem(VueComponent):
item: dict
template = """
{{ item.group }}
{{ item.title }}
{{ item.title }}
"""
@computed
def item_tag(self):
if self.is_submenu:
return "el-submenu"
return "el-menu-item"
@computed
def is_menu_item(self):
return not self.is_group_header and not self.is_submenu
@computed
def is_group_header(self):
return "group" in self.item
@computed
def is_submenu(self):
return "children" in self.item
class NavigationMenu(VueComponent):
content: list
template = """
"""
def register():
NavigationItem.register()
NavigationMenu.register()
================================================
FILE: examples/element_ui/navigation.html
================================================
================================================
FILE: examples/element_ui/style.css
================================================
.navigation-menu:not(.el-menu--collapse) {
width: 200px;
min-height: 400px;
}
================================================
FILE: examples/element_ui/vuepy.yml
================================================
templates:
navigation: navigation.html
stylesheets:
- style.css
- https://unpkg.com/element-ui/lib/theme-chalk/index.css
scripts:
- https://unpkg.com/element-ui/lib/index.js
================================================
FILE: examples/github_commits/app.py
================================================
from vue import VueComponent, filters, watch
from browser import window, ajax
url = "https://api.github.com/repos/stefanhoelzl/vue.py/commits?per_page=10&sha={}"
if window.location.hash == "#testing":
url = "data.json"
class App(VueComponent):
template = "#commits"
branches = ["master", "2948e6b"]
current_branch = "master"
commits = []
def created(self):
self.fetch_data()
@watch("current_branch")
def fetch_data_on_current_branch_change(self, new, old):
self.fetch_data()
@staticmethod
@filters
def truncate(value):
return value.split("\n", 1)[0]
@staticmethod
@filters
def format_date(value):
return value.replace("T", " ").replace("Z", "")
def fetch_data(self):
self.commits = []
req = ajax.ajax()
req.open("GET", url.format(self.current_branch), True)
req.bind("complete", self.loaded)
req.send()
def loaded(self, ev):
self.commits = window.JSON.parse(ev.text)
App("#app")
================================================
FILE: examples/github_commits/commits.html
================================================
================================================
FILE: examples/github_commits/data.json
================================================
[
{
"sha": "0a644f825e780555e62d2e4647786d2a3eb06afc",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOjBhNjQ0ZjgyNWU3ODA1NTVlNjJkMmU0NjQ3Nzg2ZDJhM2ViMDZhZmM=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T20:53:14Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-21T10:45:58Z"
},
"message": "[testing] refactored VueComponentFactory out and separated decorators",
"tree": {
"sha": "e04efd554a872923b487128493f29046de778387",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/e04efd554a872923b487128493f29046de778387"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/0a644f825e780555e62d2e4647786d2a3eb06afc",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/0a644f825e780555e62d2e4647786d2a3eb06afc",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/0a644f825e780555e62d2e4647786d2a3eb06afc",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/0a644f825e780555e62d2e4647786d2a3eb06afc/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "37fb44c39be49ed40d7dce3d4d5978853e4d82f0",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/37fb44c39be49ed40d7dce3d4d5978853e4d82f0",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/37fb44c39be49ed40d7dce3d4d5978853e4d82f0"
}
]
},
{
"sha": "37fb44c39be49ed40d7dce3d4d5978853e4d82f0",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOjM3ZmI0NGMzOWJlNDllZDQwZDdkY2UzZDRkNTk3ODg1M2U0ZDgyZjA=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T20:06:42Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T20:06:42Z"
},
"message": "[refactoring] renamed Vue to VueInstance",
"tree": {
"sha": "f98aa2b72ca181035a1f6a988a02dbb6d8b3d530",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/f98aa2b72ca181035a1f6a988a02dbb6d8b3d530"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/37fb44c39be49ed40d7dce3d4d5978853e4d82f0",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/37fb44c39be49ed40d7dce3d4d5978853e4d82f0",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/37fb44c39be49ed40d7dce3d4d5978853e4d82f0",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/37fb44c39be49ed40d7dce3d4d5978853e4d82f0/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "5f907e1325d56ba0736f9702d29a523d8a2d00fb",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/5f907e1325d56ba0736f9702d29a523d8a2d00fb",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/5f907e1325d56ba0736f9702d29a523d8a2d00fb"
}
]
},
{
"sha": "5f907e1325d56ba0736f9702d29a523d8a2d00fb",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOjVmOTA3ZTEzMjVkNTZiYTA3MzZmOTcwMmQyOWE1MjNkOGEyZDAwZmI=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T19:54:39Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T19:54:39Z"
},
"message": "[docs] directives docs updated",
"tree": {
"sha": "7bf96997338f7352247027ee3dad9af699c9f0cb",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/7bf96997338f7352247027ee3dad9af699c9f0cb"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/5f907e1325d56ba0736f9702d29a523d8a2d00fb",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/5f907e1325d56ba0736f9702d29a523d8a2d00fb",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/5f907e1325d56ba0736f9702d29a523d8a2d00fb",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/5f907e1325d56ba0736f9702d29a523d8a2d00fb/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "94a4affca971fc463dd200affd34e2389aef4c0d",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/94a4affca971fc463dd200affd34e2389aef4c0d",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/94a4affca971fc463dd200affd34e2389aef4c0d"
}
]
},
{
"sha": "94a4affca971fc463dd200affd34e2389aef4c0d",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOjk0YTRhZmZjYTk3MWZjNDYzZGQyMDBhZmZkMzRlMjM4OWFlZjRjMGQ=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T19:41:14Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T19:52:42Z"
},
"message": "[feature] full local directives supported",
"tree": {
"sha": "0c2fa597175f483f83b87ae6be83d4938906467f",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/0c2fa597175f483f83b87ae6be83d4938906467f"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/94a4affca971fc463dd200affd34e2389aef4c0d",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/94a4affca971fc463dd200affd34e2389aef4c0d",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/94a4affca971fc463dd200affd34e2389aef4c0d",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/94a4affca971fc463dd200affd34e2389aef4c0d/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "9ad2d7a56ca8cad51366bd317cab9591c65e72dc",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/9ad2d7a56ca8cad51366bd317cab9591c65e72dc",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/9ad2d7a56ca8cad51366bd317cab9591c65e72dc"
}
]
},
{
"sha": "9ad2d7a56ca8cad51366bd317cab9591c65e72dc",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOjlhZDJkN2E1NmNhOGNhZDUxMzY2YmQzMTdjYWI5NTkxYzY1ZTcyZGM=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T18:40:26Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T18:40:26Z"
},
"message": "[docs] updated informations to py/js bridge",
"tree": {
"sha": "a33ad0f207db1f77786792d31dc11a8785562210",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/a33ad0f207db1f77786792d31dc11a8785562210"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/9ad2d7a56ca8cad51366bd317cab9591c65e72dc",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/9ad2d7a56ca8cad51366bd317cab9591c65e72dc",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/9ad2d7a56ca8cad51366bd317cab9591c65e72dc",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/9ad2d7a56ca8cad51366bd317cab9591c65e72dc/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "cd655a5bf24284aa0544e84810f3a5eb298336e7",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/cd655a5bf24284aa0544e84810f3a5eb298336e7",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/cd655a5bf24284aa0544e84810f3a5eb298336e7"
}
]
},
{
"sha": "cd655a5bf24284aa0544e84810f3a5eb298336e7",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOmNkNjU1YTViZjI0Mjg0YWEwNTQ0ZTg0ODEwZjNhNWViMjk4MzM2ZTc=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T18:05:02Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T18:05:02Z"
},
"message": "[docs] updated limitations",
"tree": {
"sha": "40c98949632915ce909caf5abf651f94bebc6002",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/40c98949632915ce909caf5abf651f94bebc6002"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/cd655a5bf24284aa0544e84810f3a5eb298336e7",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/cd655a5bf24284aa0544e84810f3a5eb298336e7",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/cd655a5bf24284aa0544e84810f3a5eb298336e7",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/cd655a5bf24284aa0544e84810f3a5eb298336e7/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "3f4b7ae5db7c445acade2e5421cc58d508eab755",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/3f4b7ae5db7c445acade2e5421cc58d508eab755",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/3f4b7ae5db7c445acade2e5421cc58d508eab755"
}
]
},
{
"sha": "3f4b7ae5db7c445acade2e5421cc58d508eab755",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOjNmNGI3YWU1ZGI3YzQ0NWFjYWRlMmU1NDIxY2M1OGQ1MDhlYWI3NTU=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T17:46:58Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T17:46:58Z"
},
"message": "[examples] separate app and components",
"tree": {
"sha": "56d5e522c853a6ff1312ea4c59e030e09b9f005e",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/56d5e522c853a6ff1312ea4c59e030e09b9f005e"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/3f4b7ae5db7c445acade2e5421cc58d508eab755",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/3f4b7ae5db7c445acade2e5421cc58d508eab755",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/3f4b7ae5db7c445acade2e5421cc58d508eab755",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/3f4b7ae5db7c445acade2e5421cc58d508eab755/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "cdd9ea9bfbb1993c6d62370456f8cd3f7a182aae",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/cdd9ea9bfbb1993c6d62370456f8cd3f7a182aae",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/cdd9ea9bfbb1993c6d62370456f8cd3f7a182aae"
}
]
},
{
"sha": "cdd9ea9bfbb1993c6d62370456f8cd3f7a182aae",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOmNkZDllYTliZmJiMTk5M2M2ZDYyMzcwNDU2ZjhjZDNmN2ExODJhYWU=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T17:45:44Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T17:45:44Z"
},
"message": "[feature] wrap types for directives and filter methods",
"tree": {
"sha": "32d8ffca8bbfc96855e1f1388371f48b9cfac544",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/32d8ffca8bbfc96855e1f1388371f48b9cfac544"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/cdd9ea9bfbb1993c6d62370456f8cd3f7a182aae",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/cdd9ea9bfbb1993c6d62370456f8cd3f7a182aae",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/cdd9ea9bfbb1993c6d62370456f8cd3f7a182aae",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/cdd9ea9bfbb1993c6d62370456f8cd3f7a182aae/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "6ced874bbcc8d6db932b6714f30669bf8c5e5e91",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/6ced874bbcc8d6db932b6714f30669bf8c5e5e91",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/6ced874bbcc8d6db932b6714f30669bf8c5e5e91"
}
]
},
{
"sha": "6ced874bbcc8d6db932b6714f30669bf8c5e5e91",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOjZjZWQ4NzRiYmNjOGQ2ZGI5MzJiNjcxNGYzMDY2OWJmOGM1ZTVlOTE=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T17:31:31Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T17:31:31Z"
},
"message": "[refactoring] separate wrapping decorator from inject vue instance decorator",
"tree": {
"sha": "4f264bde73993a57ed958ffee2de060083f68ea1",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/4f264bde73993a57ed958ffee2de060083f68ea1"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/6ced874bbcc8d6db932b6714f30669bf8c5e5e91",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/6ced874bbcc8d6db932b6714f30669bf8c5e5e91",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/6ced874bbcc8d6db932b6714f30669bf8c5e5e91",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/6ced874bbcc8d6db932b6714f30669bf8c5e5e91/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "c9f4b8309fa288d82e33b10fe7dfecb7e4cb7e55",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/c9f4b8309fa288d82e33b10fe7dfecb7e4cb7e55",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/c9f4b8309fa288d82e33b10fe7dfecb7e4cb7e55"
}
]
},
{
"sha": "c9f4b8309fa288d82e33b10fe7dfecb7e4cb7e55",
"node_id": "MDY6Q29tbWl0MTM5NDg4NjkwOmM5ZjRiODMwOWZhMjg4ZDgyZTMzYjEwZmU3ZGZlY2I3ZTRjYjdlNTU=",
"commit": {
"author": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T17:19:34Z"
},
"committer": {
"name": "Stefan Hoelzl",
"email": "stefan.hoelzl@posteo.de",
"date": "2018-07-20T17:19:34Z"
},
"message": "[feature] access dict items also as attributes",
"tree": {
"sha": "ec4eb5123a1f44f8a8166671de14697ff7c44437",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/trees/ec4eb5123a1f44f8a8166671de14697ff7c44437"
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/git/commits/c9f4b8309fa288d82e33b10fe7dfecb7e4cb7e55",
"comment_count": 0,
"verification": {
"verified": false,
"reason": "unsigned",
"signature": null,
"payload": null
}
},
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/c9f4b8309fa288d82e33b10fe7dfecb7e4cb7e55",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/c9f4b8309fa288d82e33b10fe7dfecb7e4cb7e55",
"comments_url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/c9f4b8309fa288d82e33b10fe7dfecb7e4cb7e55/comments",
"author": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"committer": {
"login": "stefanhoelzl",
"id": 1478183,
"node_id": "MDQ6VXNlcjE0NzgxODM=",
"avatar_url": "https://avatars0.githubusercontent.com/u/1478183?v=4",
"gravatar_id": "",
"url": "https://api.github.com/users/stefanhoelzl",
"html_url": "https://github.com/stefanhoelzl",
"followers_url": "https://api.github.com/users/stefanhoelzl/followers",
"following_url": "https://api.github.com/users/stefanhoelzl/following{/other_user}",
"gists_url": "https://api.github.com/users/stefanhoelzl/gists{/gist_id}",
"starred_url": "https://api.github.com/users/stefanhoelzl/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/stefanhoelzl/subscriptions",
"organizations_url": "https://api.github.com/users/stefanhoelzl/orgs",
"repos_url": "https://api.github.com/users/stefanhoelzl/repos",
"events_url": "https://api.github.com/users/stefanhoelzl/events{/privacy}",
"received_events_url": "https://api.github.com/users/stefanhoelzl/received_events",
"type": "User",
"site_admin": false
},
"parents": [
{
"sha": "f9b50084be79b819be262d2e8a37f68d80a182b2",
"url": "https://api.github.com/repos/stefanhoelzl/vue.py/commits/f9b50084be79b819be262d2e8a37f68d80a182b2",
"html_url": "https://github.com/stefanhoelzl/vue.py/commit/f9b50084be79b819be262d2e8a37f68d80a182b2"
}
]
}
]
================================================
FILE: examples/github_commits/style.css
================================================
#demo {
font-family: 'Helvetica', Arial, sans-serif;
}
a {
text-decoration: none;
color: #f66;
}
li {
line-height: 1.5em;
margin-bottom: 20px;
}
.author, .date {
font-weight: bold;
}
================================================
FILE: examples/github_commits/vuepy.yml
================================================
templates:
commits: commits.html
stylesheets:
- style.css
================================================
FILE: examples/grid_component/app.py
================================================
from vue import VueComponent, data, computed, filters
class GridComponent(VueComponent):
template = "#grid"
content: list
columns: list
filter_key: str
sort_key = ""
@data
def sort_orders(self):
return {key: False for key in self.columns}
@computed
def filtered_data(self):
return list(
sorted(
filter(
lambda f: self.filter_key.lower() in f["name"].lower(), self.content
),
reverse=self.sort_orders.get(self.sort_key, False),
key=lambda c: c.get(self.sort_key, self.columns[0]),
)
)
@staticmethod
@filters
def capitalize(value):
return value.capitalize()
def sort_by(self, key):
self.sort_key = key
self.sort_orders[key] = not self.sort_orders[key]
GridComponent.register("demo-grid")
class App(VueComponent):
template = "#form"
search_query = ""
grid_columns = ["name", "power"]
grid_data = [
{"name": "Chuck Norris", "power": float("inf")},
{"name": "Bruce Lee", "power": 9000},
{"name": "Jackie Chan", "power": 7000},
{"name": "Jet Li", "power": 8000},
]
App("#app")
================================================
FILE: examples/grid_component/form.html
================================================
================================================
FILE: examples/grid_component/grid.html
================================================
{{ key | capitalize }}
{{ entry[key] }}
================================================
FILE: examples/grid_component/style.css
================================================
body {
font-family: Helvetica Neue, Arial, sans-serif;
font-size: 14px;
color: #444;
}
table {
border: 2px solid #42b983;
border-radius: 3px;
background-color: #fff;
}
th {
background-color: #42b983;
color: rgba(255,255,255,0.66);
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
td {
background-color: #f9f9f9;
}
th, td {
min-width: 120px;
padding: 10px 20px;
}
th.active {
color: #fff;
}
th.active .arrow {
opacity: 1;
}
.arrow {
display: inline-block;
vertical-align: middle;
width: 0;
height: 0;
margin-left: 5px;
opacity: 0.66;
}
.arrow.asc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-bottom: 4px solid #fff;
}
.arrow.dsc {
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #fff;
}
================================================
FILE: examples/grid_component/vuepy.yml
================================================
templates:
form: form.html
grid: grid.html
stylesheets:
- style.css
================================================
FILE: examples/index.md
================================================
# Example Gallery
## TodoMVC
[Demo](https://stefanhoelzl.github.io/vue.py/examples/todo_mvc) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/todo_mvc)

## SVG Graph
[Demo](https://stefanhoelzl.github.io/vue.py/examples/svg_graph) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/svg_graph)

## Markdown Editor
[Demo](https://stefanhoelzl.github.io/vue.py/examples/markdown_editor) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/markdown_editor)

## GitHub Commits
[Demo](https://stefanhoelzl.github.io/vue.py/examples/github_commits) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/github_commits)

## Grid Component
[Demo](https://stefanhoelzl.github.io/vue.py/examples/grid_component) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/grid_component)

## Tree View
[Demo](https://stefanhoelzl.github.io/vue.py/examples/tree_view) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/tree_view)

## Modal Component
[Demo](https://stefanhoelzl.github.io/vue.py/examples/modal_component) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/modal_component)

## Elastic Header
[Demo](https://stefanhoelzl.github.io/vue.py/examples/elastic_header) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/elastic_header)

## Run Examples Local
```bash
$ git clone https://github.com/stefanhoelzl/vue.py.git
$ cd vue.py
$ make env.up
$ make run APP=examples/
```
Goto [http://localhost:5000](http://localhost:5000)
================================================
FILE: examples/markdown_editor/app.py
================================================
from vue import VueComponent, computed
from vue.utils import js_lib
marked = js_lib("marked")
class App(VueComponent):
template = "#editor-template"
input = "# Editor"
@computed
def compiled_markdown(self):
return marked(self.input, {"sanitize": True})
def update(self, event):
self.input = event.target.value
App("#app")
================================================
FILE: examples/markdown_editor/editor.html
================================================
================================================
FILE: examples/markdown_editor/style.css
================================================
html, body, #editor {
margin: 0;
height: 100%;
font-family: 'Helvetica Neue', Arial, sans-serif;
color: #333;
}
textarea, #editor div {
display: inline-block;
width: 49%;
height: 100%;
vertical-align: top;
box-sizing: border-box;
padding: 0 20px;
}
textarea {
border: none;
border-right: 1px solid #ccc;
resize: none;
outline: none;
background-color: #f6f6f6;
font-size: 14px;
font-family: 'Monaco', courier, monospace;
padding: 20px;
}
code {
color: #f66;
}
================================================
FILE: examples/markdown_editor/vuepy.yml
================================================
templates:
editor-template: editor.html
stylesheets:
- style.css
scripts:
- https://unpkg.com/marked@0.3.6
================================================
FILE: examples/modal_component/app.py
================================================
from vue import VueComponent
class Modal(VueComponent):
template = "#modal-template"
Modal.register()
class App(VueComponent):
template = "#main"
show_modal = False
App("#app")
================================================
FILE: examples/modal_component/main.html
================================================
Show Modal
custom header
================================================
FILE: examples/modal_component/modal-template.html
================================================
================================================
FILE: examples/modal_component/style.css
================================================
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .5);
display: table;
transition: opacity .3s ease;
}
.modal-wrapper {
display: table-cell;
vertical-align: middle;
}
.modal-container {
width: 300px;
margin: 0px auto;
padding: 20px 30px;
background-color: #fff;
border-radius: 2px;
box-shadow: 0 2px 8px rgba(0, 0, 0, .33);
transition: all .3s ease;
font-family: Helvetica, Arial, sans-serif;
}
.modal-header h3 {
margin-top: 0;
color: #42b983;
}
.modal-body {
margin: 20px 0;
}
.modal-default-button {
float: right;
}
/*
* The following styles are auto-applied to elements with
* transition="modal" when their visibility is toggled
* by Vue.js.
*
* You can easily play with the modal transition by editing
* these styles.
*/
.modal-enter {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
================================================
FILE: examples/modal_component/vuepy.yml
================================================
templates:
modal-template: modal-template.html
main: main.html
stylesheets:
- style.css
================================================
FILE: examples/svg_graph/app-template.html
================================================
================================================
FILE: examples/svg_graph/app.py
================================================
import math
from vue import VueComponent, computed
stats = [
{"label": "A", "value": 100},
{"label": "B", "value": 100},
{"label": "C", "value": 100},
{"label": "D", "value": 100},
{"label": "E", "value": 100},
{"label": "F", "value": 100},
]
def value_to_point(value, index, total):
x = 0
y = -value * 0.8
angle = math.pi * 2 / total * index
cos = math.cos(angle)
sin = math.sin(angle)
tx = x * cos - y * sin + 100
ty = x * sin + y * cos + 100
return tx, ty
class AxisLabel(VueComponent):
template = '{{stat.label}} '
stat: dict
index: int
total: int
@computed
def point(self):
return value_to_point(+int(self.stat["value"]) + 10, self.index, self.total)
AxisLabel.register()
class Polygraph(VueComponent):
template = "#polygraph-template"
stats: list
@computed
def points(self):
return " ".join(
map(
lambda e: ",".join(
str(p)
for p in value_to_point(int(e[1]["value"]), e[0], len(self.stats))
),
enumerate(self.stats),
)
)
Polygraph.register()
class App(VueComponent):
template = "#app-template"
new_label = ""
stats = stats
@computed
def disable_remove(self):
return len(self.stats) <= 3
def add(self, event):
event.preventDefault()
if self.new_label:
self.stats.append({"label": self.new_label, "value": 100})
self.new_label = ""
def remove(self, stat):
del self.stats[self.stats.index(stat)]
App("#app")
================================================
FILE: examples/svg_graph/polygraph-template.html
================================================
================================================
FILE: examples/svg_graph/style.css
================================================
body {
font-family: Helvetica Neue, Arial, sans-serif;
}
polygon {
fill: #42b983;
opacity: .75;
}
circle {
fill: transparent;
stroke: #999;
}
text {
font-family: Helvetica Neue, Arial, sans-serif;
font-size: 10px;
fill: #666;
}
label {
display: inline-block;
margin-left: 10px;
width: 20px;
}
#raw {
position: absolute;
top: 0;
left: 300px;
}
================================================
FILE: examples/svg_graph/vuepy.yml
================================================
templates:
app-template: app-template.html
polygraph-template: polygraph-template.html
stylesheets:
- style.css
================================================
FILE: examples/todo_mvc/app-template.html
================================================
================================================
FILE: examples/todo_mvc/app.py
================================================
from browser.local_storage import storage
from browser import window
import json
from vue import VueComponent, computed, filters, watch, directive
from vue.bridge import Object
STORAGE_KEY = "todos-vue.py"
class ToDoStorage:
NEXT_UID = 0
@classmethod
def next_uid(cls):
uid = cls.NEXT_UID
cls.NEXT_UID += 1
return uid
@classmethod
def fetch(cls):
cls.NEXT_UID = 0
todos = json.loads(storage.get(STORAGE_KEY, "[]"))
return [{"id": cls.next_uid(), **todo} for todo in todos]
@staticmethod
def save(todos):
storage[STORAGE_KEY] = json.dumps(todos)
class VisibilityFilters:
def __new__(cls, visibility):
try:
return getattr(VisibilityFilters, visibility)
except AttributeError:
return False
@staticmethod
def all(todos):
return [todo for todo in todos]
@staticmethod
def active(todos):
return [todo for todo in todos if not todo.get("completed", False)]
@staticmethod
def completed(todos):
return [todo for todo in todos if todo.get("completed", False)]
class App(VueComponent):
template = "#app-template"
todos = ToDoStorage.fetch()
new_todo = ""
edited_todo = None
edit_cache = ""
visibility = "all"
@watch("todos", deep=True)
def save_todos(self, new, old):
ToDoStorage.save(Object.to_py(new))
@computed
def filtered_todos(self):
return VisibilityFilters(self.visibility)(self.todos)
@computed
def remaining(self):
return len(VisibilityFilters.active(self.todos))
@computed
def all_done(self):
return self.remaining == 0
@all_done.setter
def all_done(self, value):
for todo in self.todos:
todo["completed"] = value
@staticmethod
@filters
def pluralize(n):
return "item" if n == 1 else "items"
def add_todo(self, ev=None):
value = self.new_todo.strip()
if not value:
return
self.todos.append(
{"id": ToDoStorage.next_uid(), "title": value, "completed": False}
)
self.new_todo = ""
def remove_todo(self, todo):
del self.todos[self.todos.index(todo)]
def edit_todo(self, todo):
self.edit_cache = todo["title"]
self.edited_todo = todo
def done_edit(self, todo):
if not self.edited_todo:
return
self.edited_todo = None
todo["title"] = todo["title"].strip()
if not todo["title"]:
self.remove_todo(todo)
def cancel_edit(self, todo):
self.edited_todo = None
todo.title = self.edit_cache
def remove_completed(self, ev=None):
self.todos = VisibilityFilters.active(self.todos)
@staticmethod
@directive
def todo_focus(el, binding, vnode, old_vnode, *args):
if binding.value:
el.focus()
app = App("#app")
def on_hash_change(ev):
visibility = window.location.hash.replace("#", "").replace("/", "")
if VisibilityFilters(visibility):
app.visibility = visibility
else:
window.location.hash = ""
app.visibility = "all"
window.bind("hashchange", on_hash_change)
================================================
FILE: examples/todo_mvc/style.css
================================================
[v-cloak] { display: none; }
================================================
FILE: examples/todo_mvc/vuepy.yml
================================================
templates:
app-template: app-template.html
stylesheets:
- style.css
- https://unpkg.com/todomvc-app-css@2.0.6/index.css
================================================
FILE: examples/tree_view/app-template.html
================================================
(You can double click on an item to turn it into a folder.)
================================================
FILE: examples/tree_view/app.py
================================================
from vue import VueComponent, computed
demo_data = {
"name": "My Tree",
"children": [
{"name": "hello"},
{"name": "wat"},
{
"name": "child folder",
"children": [
{
"name": "child folder",
"children": [{"name": "hello"}, {"name": "wat"}],
},
{"name": "hello"},
{"name": "wat"},
{
"name": "child folder",
"children": [{"name": "hello"}, {"name": "wat"}],
},
],
},
],
}
class Tree(VueComponent):
template = "#tree-template"
model: dict
open = False
@computed
def is_folder(self):
return len(self.model.get("children", ())) > 0
def toggle(self, ev=None):
if self.is_folder:
self.open = not self.open
def change_type(self, ev=None):
if not self.is_folder:
self.model["children"] = []
self.add_child()
self.open = True
def add_child(self, ev=None):
self.model["children"].append({"name": "new stuff"})
Tree.register()
class App(VueComponent):
template = "#app-template"
tree_data = demo_data
App("#app")
================================================
FILE: examples/tree_view/style.css
================================================
body {
font-family: Menlo, Consolas, monospace;
color: #444;
}
.tree {
cursor: pointer;
}
.bold {
font-weight: bold;
}
ul {
padding-left: 1em;
line-height: 1.5em;
list-style-type: dot;
}
================================================
FILE: examples/tree_view/tree-template.html
================================================
{{ model.name }}
[{{ open ? '-' : '+' }}]
================================================
FILE: examples/tree_view/vuepy.yml
================================================
templates:
tree-template: tree-template.html
app-template: app-template.html
stylesheets:
- style.css
================================================
FILE: pyproject.toml
================================================
[build-system]
requires = [
"setuptools",
"requests",
"python-project-tools@git+https://github.com/stefanhoelzl/python-project-tools.git"
]
[tool.python-project-tools]
start-commit = "f4256454256ddfe54a8be6dea493d3fc915ef1a2"
================================================
FILE: requirements.txt
================================================
# provider
Flask==2.2.2
# packaging
wheel==0.38.4
# testing
pytest==7.2.1
selenium==4.8.0
pyderman==3.3.2
requests==2.28.2
# linting
black==23.1.0
# releasing
semver==2.13.0
git+https://github.com/stefanhoelzl/python-project-tools.git
================================================
FILE: setup.py
================================================
from pathlib import Path
from setuptools import setup
import requests
from tools import release
def make_version():
version = release.version()
Path("vue/__version__.py").write_text(f'__version__ = "{version}"\n')
return version
def fetch_vue_cli_js_file(source: str, name: str) -> str:
js_data_base = Path(__file__).parent / "vuecli" / "js"
js_data_base.mkdir(exist_ok=True)
dest_path = js_data_base / name
resp = requests.get(source)
assert resp.ok
dest_path.write_bytes(resp.content)
return f"js/{name}"
setup(
name="vuepy",
version=make_version(),
description="Pythonic Vue",
long_description=Path("README.md").read_text(),
long_description_content_type="text/markdown",
classifiers=[
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.6",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX :: Linux",
"Operating System :: Microsoft :: Windows",
"Topic :: Internet :: WWW/HTTP",
"Topic :: Software Development",
"Topic :: Software Development :: Libraries :: Application Frameworks",
],
keywords="web reactive gui framework",
url="https://stefanhoelzl.github.io/vue.py/",
author="Stefan Hoelzl",
author_email="stefan.hoelzl@posteo.de",
license="MIT",
packages=["vuecli", "vuecli.provider", "vue", "vue.bridge", "vue.decorators"],
install_requires=["brython==3.8.9", "Jinja2>=2.10", "pyyaml>=5.1"],
extras_require={"flask": ["Flask>=1.0"]},
package_data={
"vuecli": [
"index.html",
"loading.gif",
fetch_vue_cli_js_file("https://unpkg.com/vue@2.6.14/dist/vue.js", "vue.js"),
fetch_vue_cli_js_file(
"https://raw.githubusercontent.com/vuejs/vue/dev/LICENSE", "LICENSE_VUE"
),
fetch_vue_cli_js_file(
"https://unpkg.com/vuex@3.6.2/dist/vuex.js", "vuex.js"
),
fetch_vue_cli_js_file(
"https://raw.githubusercontent.com/vuejs/vuex/master/LICENSE",
"LICENSE_VUEX",
),
fetch_vue_cli_js_file(
"https://unpkg.com/vue-router@3.5.1/dist/vue-router.js", "vue-router.js"
),
fetch_vue_cli_js_file(
"https://raw.githubusercontent.com/vuejs/vue-router/dev/LICENSE",
"LICENSE_VUE_ROUTER",
),
]
},
entry_points={
"console_scripts": ["vue-cli=vuecli.cli:main"],
"vuecli.provider": [
"static=vuecli.provider.static:Static",
"flask=vuecli.provider.flask:Flask",
],
},
zip_safe=False,
)
================================================
FILE: stubs/browser.py
================================================
"""stub to avoid import errors"""
import local_storage
def load(path):
...
def bind(target, ev):
...
class window:
String = str
Number = int
Boolean = bool
class Object:
def __init__(self, obj):
...
@staticmethod
def assign(target, *sources):
...
@staticmethod
def keys(obj):
...
@staticmethod
def bind(el, ev):
...
class location:
hash = ""
class Array:
def __init__(self, *objs):
...
@classmethod
def isArray(cls, obj):
...
class Vuex:
class Store:
@classmethod
def new(cls, *args, **kwargs):
...
class VueRouter:
@classmethod
def new(cls, *args, **kwargs):
...
class Vue:
@classmethod
def new(cls, *args, **kwargs):
...
@classmethod
def component(cls, name, opts=None):
...
@classmethod
def set(cls, obj, key, value):
...
@classmethod
def delete(cls, obj, key):
...
@classmethod
def use(cls, plugin, *args, **kwargs):
...
@classmethod
def directive(cls, name, directive=None):
...
@classmethod
def filter(cls, name, method):
...
@classmethod
def mixin(cls, mixin):
...
class timer:
@staticmethod
def set_interval(fn, interval):
...
class ajax:
class ajax:
def open(self, method, url, asnc):
...
def bind(self, ev, method):
...
def send(self):
...
================================================
FILE: stubs/javascript.py
================================================
"""stub to avoid import errors"""
def this():
return None
================================================
FILE: stubs/local_storage.py
================================================
storage = dict()
================================================
FILE: tests/__init__.py
================================================
================================================
FILE: tests/cli/test_provider.py
================================================
from xml.etree import ElementTree
import yaml
import pytest
from vuecli.provider.provider import Provider
@pytest.fixture
def render_index(tmp_path):
def render(config=None):
tmp_path.joinpath("vuepy.yml").write_text(yaml.dump(config or {}))
provider = Provider(tmp_path)
return provider.render_index()
return render
def parse_index(index):
et = ElementTree.fromstring(index)
return {
"stylesheets": [e.attrib["href"] for e in et.findall("head/link")],
"scripts": [e.attrib["src"] for e in et.findall("head/script")],
"templates": {
e.attrib["id"]: e.text.strip()
for e in et.findall("body/script[@type='x-template']")
},
"brython": et.find("body").attrib["onload"],
}
class TestRenderIndex:
def test_defaults(self, render_index):
index = render_index()
assert parse_index(index) == {
"stylesheets": [],
"scripts": ["vuepy.js", "vue.js"],
"templates": {},
"brython": "brython();",
}
def test_custom_stylesheets(self, render_index):
index = render_index({"stylesheets": ["first.css", "second.css"]})
assert parse_index(index)["stylesheets"] == ["first.css", "second.css"]
@pytest.mark.parametrize(
"ext, js", [("vuex", "vuex.js"), ("vue-router", "vue-router.js")]
)
def test_enable_builtin_script(self, render_index, ext, js):
index = render_index({"scripts": {ext: True}})
assert js in parse_index(index)["scripts"]
@pytest.mark.parametrize("ext", ["vue", "brython", "vuex", "vue-router"])
def test_customize_builtin_script(self, render_index, ext):
index = render_index({"scripts": {ext: "custom"}})
assert "custom" in parse_index(index)["scripts"]
def test_custom_script(self, render_index):
index = render_index({"scripts": ["myscript.js"]})
assert "myscript.js" in parse_index(index)["scripts"]
def test_custom_template(self, render_index, tmp_path):
tmp_path.joinpath("my.html").write_text("content")
index = render_index({"templates": {"my": "my.html"}})
assert parse_index(index)["templates"] == {"my": "content"}
def test_custom_brython_args(self, render_index):
index = render_index({"brython_args": {"debug": 10}})
assert parse_index(index)["brython"] == "brython({ debug: 10 });"
================================================
FILE: tests/pytest.ini
================================================
[pytest]
addopts =
--new-first
--failed-first
--capture=no
--tb=short
================================================
FILE: tests/selenium/.gitignore
================================================
_html
chromedriver
================================================
FILE: tests/selenium/chromedriver.py
================================================
import re
from subprocess import run
import pyderman
import requests
LatestReleaseUrl = (
"https://chromedriver.storage.googleapis.com/LATEST_RELEASE_{version}"
)
chrome_version_output = (
run(["google-chrome", "--version"], capture_output=True)
.stdout.decode("utf-8")
.strip()
)
print(chrome_version_output)
chrome_major_version = re.search("Google Chrome (\d+)", chrome_version_output).group(1)
chromedriver_version = requests.get(
LatestReleaseUrl.format(version=chrome_major_version)
).text.strip()
print(f"Chromedriver Version {chromedriver_version}")
pyderman.install(
browser=pyderman.chrome,
file_directory="tests/selenium",
filename="chromedriver",
overwrite=True,
version=chromedriver_version,
)
================================================
FILE: tests/selenium/conftest.py
================================================
import re
import os
import json
import inspect
from pathlib import Path
from contextlib import contextmanager
from textwrap import dedent
from threading import Thread
from http.server import HTTPServer, SimpleHTTPRequestHandler
from http.client import HTTPConnection
import yaml
import pytest
from vuecli.provider.static import Static
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as ec
from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
from selenium.common.exceptions import NoSuchElementException
Address = "localhost"
Port = 8001
BaseUrl = f"http://{Address}:{Port}"
TEST_PATH = Path(__file__).parent
CHROME_DRIVER_PATH = TEST_PATH / "chromedriver"
HTML_OUTPUT_PATH = TEST_PATH / "_html"
APP_URL = BaseUrl + "/{}/{}/deploy"
EXAMPLE_URL = BaseUrl + "/examples_static/{}"
EXAMPLE_SCREENSHOT_PATH = "examples_static/{}/screenshot.png"
DEFAULT_TIMEOUT = 5
@pytest.fixture(scope="session")
def http_server():
timeout = 10
class RequestHandler(SimpleHTTPRequestHandler):
protocol_version = "HTTP/1.0"
def log_message(self, *args):
pass
with HTTPServer((Address, Port), RequestHandler) as httpd:
thread = Thread(target=httpd.serve_forever, daemon=True)
thread.start()
c = HTTPConnection(Address, Port, timeout=timeout)
c.request("GET", "/", "")
assert c.getresponse().status == 200
c.close()
try:
yield httpd
finally:
httpd.shutdown()
thread.join(timeout=timeout)
@pytest.fixture(scope="session")
def selenium_session(http_server):
with SeleniumSession() as session:
yield session
@pytest.fixture()
def selenium(selenium_session, request):
selenium_session.request = request
yield selenium_session
selenium_session.request = None
class ErrorLogException(Exception):
def __init__(self, errors):
formatted_errors = []
for error in errors:
formatted_error = {**error}
formatted_error["message"] = formatted_error["message"].split("\\n")
formatted_errors.append(formatted_error)
super().__init__(json.dumps(formatted_errors, indent=2))
self.errors = errors
class SeleniumSession:
def __init__(self):
self.driver = None
self.request = None
self.allowed_errors = []
self.logs = []
self._screenshot_file = None
def __getattr__(self, item):
return getattr(self.driver, item)
def __enter__(self):
options = webdriver.ChromeOptions()
options.add_argument("headless")
options.add_argument("disable-gpu")
options.add_argument("disable-dev-shm-usage")
options.add_argument("no-sandbox")
options.set_capability("goog:loggingPrefs", {"browser": "ALL"})
self.driver = webdriver.Chrome(
service=webdriver.chrome.service.Service(
executable_path=CHROME_DRIVER_PATH,
),
options=options,
)
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.driver.close()
def get_logs(self):
new_logs = self.driver.get_log("browser")
self.logs.extend(new_logs)
return new_logs
def clear_logs(self):
self.allowed_errors.clear()
self.get_logs()
self.logs.clear()
@contextmanager
def url(self, url):
self.clear_logs()
self.driver.get(url)
try:
yield
finally:
self.analyze_logs()
@contextmanager
def app(self, app, config=None, files=None):
test_name = self.request.function.__name__
self._create_app_content(test_name, app, config or {}, files or {})
url_base = str(self._app_output_path.relative_to(Path(".").absolute()))
url = APP_URL.format(url_base, test_name)
with self.url(url):
yield
@property
def _app_output_path(self):
sub_path = Path(self.request.node.nodeid.split("::", 1)[0])
try:
sub_path = sub_path.relative_to("selenium")
except ValueError:
pass # WORKAROUND when running single tests from PyCharm
output_path = HTML_OUTPUT_PATH / sub_path
output_path.mkdir(exist_ok=True, parents=True)
return output_path
def _create_app_content(self, test_name, app, config, files):
path = self._app_output_path / test_name
path.mkdir(exist_ok=True, parents=True)
code = "from vue import *\n\n\n"
code += dedent("\n".join(inspect.getsource(app).split("\n")))
code += """\n\napp = {}("#app")\n""".format(app.__name__)
(path / "app.py").write_text(code)
(path / "vuepy.yml").write_text(yaml.dump(config))
for filename, content in files.items():
(path / filename).write_text(content)
provider = Static(path)
provider.setup()
provider.deploy(path / "deploy")
@contextmanager
def example(self, hash_=None):
test_name = self.request.function.__name__
name = test_name[5:]
self._screenshot_file = Path(EXAMPLE_SCREENSHOT_PATH.format(name))
url = EXAMPLE_URL.format(name)
provider = Static("examples/{}".format(name))
provider.setup()
provider.deploy("examples_static/{}".format(name), package=True)
if hash_:
url = "{}#{}".format(url, hash_)
with self.url(url):
try:
yield
finally:
self.screenshot()
def screenshot(self):
if self._screenshot_file:
self.driver.save_screenshot(str(self._screenshot_file))
self._screenshot_file = None
def analyze_logs(self):
errors = []
exceptions = [
r"[^ ]+ \d+"
r" Synchronous XMLHttpRequest on the main thread is deprecated"
r" because of its detrimental effects to the end user's experience."
r" For more help, check https://xhr.spec.whatwg.org/.",
r"[^ ]+ (\d+|-) {}".format(
re.escape(
"Failed to load resource:"
" the server responded with a status of 404 (File not found)"
)
),
]
self.get_logs()
for log in self.logs:
if log["level"] != "INFO":
for exception in exceptions + self.allowed_errors:
if re.match(exception, log["message"]):
break
else:
if log["source"] not in ["deprecation"]:
errors.append(log)
if errors:
raise ErrorLogException(errors)
def element_has_text(self, id_, text, timeout=DEFAULT_TIMEOUT):
return WebDriverWait(self.driver, timeout).until(
ec.text_to_be_present_in_element((By.ID, id_), text)
)
def element_with_tag_name_has_text(self, tag_name, text, timeout=DEFAULT_TIMEOUT):
return WebDriverWait(self.driver, timeout).until(
ec.text_to_be_present_in_element((By.TAG_NAME, tag_name), text)
)
def element_present(self, id_, timeout=DEFAULT_TIMEOUT):
return WebDriverWait(self.driver, timeout).until(
ec.presence_of_element_located((By.ID, id_))
)
def element_with_tag_name_present(self, tag, timeout=DEFAULT_TIMEOUT):
return WebDriverWait(self.driver, timeout).until(
ec.presence_of_element_located((By.TAG_NAME, tag))
)
def element_not_present(self, id_, timeout=DEFAULT_TIMEOUT):
def check(driver_):
try:
driver_.find_element(by=By.ID, value=id_)
except NoSuchElementException:
return True
return False
return WebDriverWait(self.driver, timeout).until(check)
def element_attribute_has_value(
self, id_, attribute, value, timeout=DEFAULT_TIMEOUT
):
def check(driver_):
element = driver_.find_element(by=By.ID, value=id_)
if element.get_attribute(attribute) == value:
return element
else:
return False
return WebDriverWait(self.driver, timeout).until(check)
================================================
FILE: tests/selenium/pytest.ini
================================================
[pytest]
addopts =
-s
# allow debug console display values
--tb=short
# do not display standard traceback display of python but a more
# compact one
================================================
FILE: tests/selenium/test_api.py
================================================
from vue import *
def test_app_with_props_and_data(selenium):
def app_with_props_data(el):
class App(VueComponent):
text: str
template = """
{{ text }}
"""
return App(el, props_data={"text": "TEXT"})
with selenium.app(app_with_props_data):
assert selenium.element_has_text("el", "TEXT")
def test_emit_method(selenium):
def call_emit(el):
class Emitter(VueComponent):
template = "
"
def created(self):
self.emit("creation", "YES")
Emitter.register()
class App(VueComponent):
text = "NO"
template = """
"""
def change(self, ev=None):
self.text = ev
return App(el)
with selenium.app(call_emit):
assert selenium.element_has_text("el", "YES")
def test_extend(selenium):
def extended_component(el):
class Base(VueComponent):
template = "{{ components_string }}
"
comps = []
def created(self):
self.comps.append("BASE")
@computed
def components_string(self):
return " ".join(self.comps)
class Sub(Base):
extends = True
def created(self):
self.comps.append("SUB")
@computed
def components_string(self):
comps = super().components_string()
return f"SUB({comps})"
return Sub(el)
with selenium.app(extended_component):
assert selenium.element_has_text("comps", "SUB(BASE SUB)")
def test_extend_from_dict(selenium):
class Component(VueComponent):
template = "{{ done }}
"
done = "NO"
extends = {"created": lambda: print("CREATED BASE")}
def created(self):
print("CREATED SUB")
self.done = "YES"
with selenium.app(Component):
assert selenium.element_has_text("done", "YES")
assert "CREATED BASE" in selenium.logs[-2]["message"]
assert "CREATED SUB" in selenium.logs[-1]["message"]
================================================
FILE: tests/selenium/test_examples.py
================================================
import time
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
def test_markdown_editor(selenium):
with selenium.example():
time.sleep(0.5)
element = selenium.element_present("markdown")
element.clear()
element.send_keys("# Title\n\n")
element.send_keys("* item one\n")
element.send_keys("* item two\n")
element.send_keys("\n")
element.send_keys("_italic_\n")
element.send_keys("\n")
element.send_keys("`some code`\n")
element.send_keys("\n")
element.send_keys("**bold**\n")
element.send_keys("\n")
element.send_keys("## Sub Title\n")
element.send_keys("\n")
selenium.element_has_text("sub-title", "Sub Title")
def test_grid_component(selenium):
with selenium.example():
time.sleep(0.5)
query = selenium.element_present("query")
query.clear()
query.send_keys("j")
power = selenium.element_present("power")
power.click()
rows = selenium.driver.find_elements(by=By.TAG_NAME, value="td")
assert "Jet Li" == rows[0].text
assert "8000" == rows[1].text
assert "Jackie Chan" == rows[2].text
assert "7000" == rows[3].text
def test_tree_view(selenium):
with selenium.example():
time.sleep(0.5)
lis = selenium.find_elements(by=By.TAG_NAME, value="li")
lis[0].click()
lis[3].click()
ActionChains(selenium.driver).double_click(lis[8]).perform()
assert selenium.find_elements(by=By.TAG_NAME, value="li")[9].text == "new stuff"
def test_svg_graph(selenium):
with selenium.example():
time.sleep(0.5)
selenium.find_elements(by=By.TAG_NAME, value="button")[5].click()
a = selenium.find_elements(by=By.TAG_NAME, value="input")[0]
d = selenium.find_elements(by=By.TAG_NAME, value="input")[3]
ActionChains(selenium.driver).click_and_hold(a).move_by_offset(
20, 0
).release().perform()
ActionChains(selenium.driver).click_and_hold(d).move_by_offset(
5, 0
).release().perform()
polygon = selenium.find_elements(by=By.TAG_NAME, value="polygon")[0]
assert 5 == len(polygon.get_attribute("points").split(" "))
def test_modal_component(selenium):
with selenium.example():
time.sleep(1)
show_button = selenium.element_present("show-modal")
show_button.click()
time.sleep(1)
assert selenium.element_present("modal_view", timeout=2)
def test_todo_mvc(selenium):
with selenium.example():
time.sleep(0.5)
title_input = selenium.element_present("title-input")
title_input.clear()
title_input.send_keys("new todo")
title_input.send_keys(Keys.ENTER)
title_input.send_keys("completed")
title_input.send_keys(Keys.ENTER)
toggle_buttons = selenium.driver.find_elements(by=By.CLASS_NAME, value="toggle")
assert 2 == len(toggle_buttons)
toggle_buttons[1].click()
selenium.element_present("show-active").click()
labels = selenium.driver.find_elements(by=By.TAG_NAME, value="label")
assert 1 == len(labels)
assert "new todo" == labels[0].text
selenium.element_present("show-all").click()
labels = selenium.driver.find_elements(by=By.TAG_NAME, value="label")
assert 2 == len(labels)
assert "new todo" == labels[0].text
assert "completed" == labels[1].text
def test_github_commits(selenium):
with selenium.example(hash_="testing"):
assert selenium.element_with_tag_name_present("ul")
time.sleep(2)
assert 10 == len(selenium.driver.find_elements(by=By.TAG_NAME, value="li"))
def test_elastic_header(selenium):
with selenium.example():
assert selenium.element_present("header")
header = selenium.find_element(by=By.ID, value="header")
content = selenium.find_element(by=By.ID, value="content")
assert (
content.get_attribute("style") == "transform:"
" translate3d(0px, 0px, 0px);"
)
ActionChains(selenium.driver).click_and_hold(header).move_by_offset(
xoffset=0, yoffset=100
).perform()
selenium.screenshot()
assert (
content.get_attribute("style") == "transform:"
" translate3d(0px, 33px, 0px);"
)
ActionChains(selenium.driver).release().perform()
time.sleep(1)
assert (
content.get_attribute("style") == "transform:"
" translate3d(0px, 0px, 0px);"
)
================================================
FILE: tests/selenium/test_guide/test_components/test_custom_events.py
================================================
from vue import *
from selenium.webdriver.common.by import By
def test_customize_v_model(selenium):
def app(el):
class CustomVModel(VueComponent):
model = Model(prop="checked", event="change")
checked: bool
template = """
"""
CustomVModel.register("custom-vmodel")
class App(VueComponent):
clicked = False
template = """
"""
return App(el)
with selenium.app(app):
assert selenium.element_has_text("instance", "false")
assert selenium.element_has_text("component", "false")
selenium.find_element(by=By.ID, value="c").click()
assert selenium.element_has_text("component", "true")
assert selenium.element_has_text("instance", "true")
================================================
FILE: tests/selenium/test_guide/test_components/test_props.py
================================================
import pytest
from vue import *
def test_prop_types(selenium):
def app(el):
class SubComponent(VueComponent):
prop: int
content = ""
template = "{{ content }}
"
def created(self):
assert isinstance(self.prop, int)
self.content = "text"
SubComponent.register()
class App(VueComponent):
template = """
"""
return App(el)
with selenium.app(app):
assert selenium.element_has_text("component", "text")
def test_prop_default(selenium):
def app(el):
class SubComponent(VueComponent):
prop: int = 100
content = ""
template = "{{ content }}
"
def created(self):
assert 100 == self.prop
self.content = "text"
SubComponent.register()
class App(VueComponent):
template = """
"""
return App(el)
with selenium.app(app):
assert selenium.element_has_text("component", "text")
def test_prop_required(selenium):
def app(el):
class SubComponent(VueComponent):
prop: int
content = ""
template = "{{ content }}
"
def created(self):
self.content = "text"
SubComponent.register()
class App(VueComponent):
template = """
SUB
"""
return App(el)
with pytest.raises(Exception) as excinfo:
with selenium.app(app):
selenium.element_has_text("component", "text")
assert "[Vue warn]: Missing required prop:" in excinfo.value.errors[0]["message"]
def test_prop_as_initial_value(selenium):
def app(el):
class SubComponent(VueComponent):
prop: str
@data
def cnt(self):
return self.prop
template = "{{ cnt }}
"
SubComponent.register()
class App(VueComponent):
template = """
"""
return App(el)
with selenium.app(app):
assert selenium.element_has_text("component", "text")
def test_dont_allow_write_prop(selenium):
def app(el):
class SubComponent(VueComponent):
prop: str
def created(self):
self.prop = "HALLO"
template = "{{ prop }}
"
SubComponent.register()
class App(VueComponent):
template = """
"""
return App(el)
with pytest.raises(Exception):
with selenium.app(app):
with pytest.raises(TimeoutError):
selenium.element_has_text("component", "HALLO")
def test_prop_validator(selenium):
def app(el):
class SubComponent(VueComponent):
prop: str
@validator("prop")
def is_text(self, value):
return "text" == value
template = "{{ prop }}
"
SubComponent.register()
class App(VueComponent):
template = """
"""
return App(el)
with pytest.raises(Exception) as excinfo:
with selenium.app(app):
assert selenium.element_has_text("component", "not text")
assert (
"[Vue warn]: Invalid prop: custom validator check failed for prop"
in excinfo.value.errors[0]["message"]
)
================================================
FILE: tests/selenium/test_guide/test_essentials/test_components_basics.py
================================================
from vue import *
from selenium.webdriver.common.by import By
def test_data_must_be_function(selenium):
def app(el):
class ClickCounter(VueComponent):
count = 0
template = """
{{ count }}
"""
ClickCounter.register()
class App(VueComponent):
template = """
"""
return App(el)
with selenium.app(app):
assert selenium.element_has_text("btn0", "0")
assert selenium.element_has_text("btn1", "0")
selenium.find_element(by=By.ID, value="btn1").click()
assert selenium.element_has_text("btn0", "0")
assert selenium.element_has_text("btn1", "1")
def test_register_with_name(selenium):
def app(el):
class SubComponent(VueComponent):
template = """
TEXT
"""
SubComponent.register("another-name")
class App(VueComponent):
template = """
"""
return App(el)
with selenium.app(app):
assert selenium.element_has_text("component", "TEXT")
def test_passing_data_with_props(selenium):
def app(el):
class SubComponent(VueComponent):
prop: str
template = """
{{ prop }}
"""
SubComponent.register()
class App(VueComponent):
template = """
"""
return App(el)
with selenium.app(app):
assert selenium.element_has_text("component", "message")
def test_emit_event(selenium):
def app(el):
class SubComponent(VueComponent):
template = """
"""
SubComponent.register()
class App(VueComponent):
text = ""
def handler(self, value):
self.text = value
template = """
"""
return App(el)
with selenium.app(app):
assert selenium.element_present("component")
selenium.find_element(by=By.ID, value="component").click()
assert selenium.element_has_text("content", "value")
================================================
FILE: tests/selenium/test_guide/test_essentials/test_computed_properties.py
================================================
from vue import *
def test_basics(selenium):
class ComputedPropertiesBasics(VueComponent):
message = "message"
@computed
def reversed_message(self):
return self.message[::-1]
template = """
{{ message }}
{{ reversed_message }}
"""
with selenium.app(ComputedPropertiesBasics):
assert selenium.element_has_text("reversed", "egassem")
def test_watch(selenium):
class Watch(VueComponent):
message = "message"
new_val = ""
@watch("message")
def _message(self, new, old):
self.new_val = new
def created(self):
self.message = "changed"
template = """
"""
with selenium.app(Watch):
assert selenium.element_has_text("change", "changed")
def test_computed_setter(selenium):
class ComputedSetter(VueComponent):
message = ""
@computed
def reversed_message(self):
return self.message[::-1]
@reversed_message.setter
def reversed_message(self, reversed_message):
self.message = reversed_message[::-1]
def created(self):
self.reversed_message = "olleh"
template = """
"""
with selenium.app(ComputedSetter):
assert selenium.element_has_text("msg", "hello")
================================================
FILE: tests/selenium/test_guide/test_essentials/test_event_handler.py
================================================
from vue import *
from selenium.webdriver.common.by import By
def test_inline_handlers(selenium):
class InlineHandler(VueComponent):
message = ""
def change(self, to):
self.message = to
template = """
{{ message }}
"""
with selenium.app(InlineHandler):
assert selenium.element_has_text("btn", "")
selenium.find_element(by=By.ID, value="btn").click()
assert selenium.element_has_text("btn", "changed")
================================================
FILE: tests/selenium/test_guide/test_essentials/test_instance.py
================================================
from vue import *
def test_lifecycle_hooks(selenium):
def lifecycle_hooks(el):
class ComponentLifecycleHooks(VueComponent):
text: str
template = "{{ text }}
"
def before_create(self):
print("lh: before_created", self)
def created(self):
print("lh: created", self)
def before_mount(self):
print("lh: before_mount", self)
def mounted(self):
print("lh: mounted", self)
def before_update(self):
print("lh: before_update", self)
def updated(self):
print("lh: updated", self)
def before_destroy(self):
print("lh: before_destroy", self)
def destroyed(self):
print("lh: destroyed", self)
ComponentLifecycleHooks.register("clh")
class App(VueComponent):
show = True
text = "created"
def mounted(self):
self.text = "mounted"
def updated(self):
self.show = False
template = (
" "
"
"
)
return App(el)
with selenium.app(lifecycle_hooks):
selenium.element_present("after")
logs = list(filter(lambda l: "lh: " in l["message"], selenium.get_logs()))
for idx, log_message in enumerate(
[
"lh: before_create",
"lh: created",
"lh: before_mount",
"lh: mounted",
"lh: before_update",
"lh: updated",
"lh: before_destroy",
"lh: destroyed",
]
):
assert log_message in logs[idx]["message"]
================================================
FILE: tests/selenium/test_guide/test_essentials/test_introduction.py
================================================
from vue import *
from selenium.webdriver.common.by import By
def test_declarative_rendering(selenium):
class DeclarativeRendering(VueComponent):
message = "MESSAGE CONTENT"
template = "{{ message }}
"
with selenium.app(DeclarativeRendering):
assert selenium.element_has_text("content", "MESSAGE CONTENT")
def test_bind_element_title(selenium):
class BindElementTitle(VueComponent):
title = "TITLE"
template = "
"
with selenium.app(BindElementTitle):
assert selenium.element_attribute_has_value("withtitle", "title", "TITLE")
def test_if_condition(selenium):
class IfCondition(VueComponent):
show = False
template = (
""
)
with selenium.app(IfCondition):
assert selenium.element_present("present")
assert selenium.element_not_present("notpresent")
def test_for_loop(selenium):
class ForLoop(VueComponent):
items = ["0", "1", "2"]
template = (
""
" {{ item }} "
" "
)
with selenium.app(ForLoop):
for idx in range(3):
assert selenium.element_has_text(str(idx), str(idx))
def test_on_click_method(selenium):
class OnClickMethod(VueComponent):
message = "message"
template = "{{ message }} "
def reverse(self, event):
self.message = "".join(reversed(self.message))
with selenium.app(OnClickMethod):
assert selenium.element_has_text("btn", "message")
selenium.find_element(by=By.ID, value="btn").click()
assert selenium.element_has_text("btn", "egassem")
def test_v_model(selenium):
class VModel(VueComponent):
clicked = False
template = (
""
)
with selenium.app(VModel):
assert selenium.element_has_text("p", "false")
selenium.find_element(by=By.ID, value="c").click()
assert selenium.element_has_text("p", "true")
def test_component(selenium):
def components(el):
class SubComponent(VueComponent):
template = """
"""
SubComponent.register()
class App(VueComponent):
template = """
"""
return App(el)
with selenium.app(components):
assert selenium.element_has_text("header", "HEADER")
def test_component_with_props(selenium):
def components_with_properties(el):
class SubComponent(VueComponent):
text: str
sub = "SUB"
template = """
{{ sub }}
"""
SubComponent.register()
class App(VueComponent):
template = """
"""
return App(el)
with selenium.app(components_with_properties):
assert selenium.element_has_text("header", "TEXT")
assert selenium.element_has_text("sub", "SUB")
================================================
FILE: tests/selenium/test_guide/test_essentials/test_list_rendering.py
================================================
from vue import *
def test_mutation_methods(selenium):
class MutationMethods(VueComponent):
array = [1, 2, 3]
template = "
"
def created(self):
print(self.array) # 1,2,3
print(self.array.pop()) # 3
print(self.array) # 1,2
self.array.append(4)
print(self.array) # 1,2,4
print(self.array.pop(0)) # 1
print(self.array) # 2,4
self.array[0:0] = [6, 4]
print(self.array) # 6,4,2,4
self.array.insert(2, 8)
print(self.array) # 6,4,8,2,4
del self.array[3]
print(self.array) # 6,4,8,4
self.array.sort(key=lambda a: 0 - a)
print(self.array) # 8,6,4,4
self.array.reverse()
print(self.array) # 4,4,6,8
with selenium.app(MutationMethods):
selenium.element_present("done")
logs = [
l["message"].split(" ", 2)[-1][:-3][1:] for l in selenium.get_logs()[-11:]
]
assert logs == [
"[1, 2, 3]",
"3",
"[1, 2]",
"[1, 2, 4]",
"1",
"[2, 4]",
"[6, 4, 2, 4]",
"[6, 4, 8, 2, 4]",
"[6, 4, 8, 4]",
"[8, 6, 4, 4]",
"[4, 4, 6, 8]",
]
================================================
FILE: tests/selenium/test_guide/test_reusability_composition/test_filters.py
================================================
from vue import *
def test_local_filter(selenium):
class ComponentWithFilter(VueComponent):
message = "Message"
@staticmethod
@filters
def lower_case(value):
return value.lower()
template = "{{ message | lower_case }}
"
with selenium.app(ComponentWithFilter):
assert selenium.element_has_text("content", "message")
def test_global_filter(selenium):
def app(el):
Vue.filter("lower_case", lambda v: v.lower())
class ComponentUsesGlobalFilter(VueComponent):
message = "Message"
template = "{{ message | lower_case }}
"
return ComponentUsesGlobalFilter(el)
with selenium.app(app):
assert selenium.element_has_text("content", "message")
================================================
FILE: tests/selenium/test_guide/test_reusability_composition/test_mixins.py
================================================
from vue import *
def test_local_mixin(selenium):
def app(el):
class MyMixin(VueMixin):
def created(self):
print("created")
@staticmethod
@filters
def lower_case(value):
return value.lower()
class ComponentUsesGlobalFilter(VueComponent):
message = "Message"
mixins = [MyMixin]
template = "{{ message | lower_case }}
"
return ComponentUsesGlobalFilter(el)
with selenium.app(app):
assert selenium.element_has_text("content", "message")
logs = list(filter(lambda l: "created" in l["message"], selenium.logs))
assert 1 == len(logs)
================================================
FILE: tests/selenium/test_guide/test_reusability_composition/test_render_function.py
================================================
from vue import *
from selenium.webdriver.common.by import By
def test_basics(selenium):
def app(el):
class ComponentWithRenderFunction(VueComponent):
level = 3
def render(self, create_element):
return create_element(f"h{self.level}", "Title")
return ComponentWithRenderFunction(el)
with selenium.app(app):
assert selenium.element_with_tag_name_has_text("h3", "Title")
def test_slots(selenium):
def app(el):
class WithSlots(VueComponent):
def render(self, create_element):
return create_element(f"p", self.slots.get("default"))
WithSlots.register()
class Component(VueComponent):
template = "
"
return Component(el)
with selenium.app(app):
div = selenium.element_with_tag_name_present("p")
assert len(div.find_elements(by=By.TAG_NAME, value="p")) == 2
def test_empty_slots(selenium):
def app(el):
class WithSlots(VueComponent):
def render(self, create_element):
return create_element(f"div", self.slots.get("default"))
WithSlots.register()
class Component(VueComponent):
template = " "
return Component(el)
with selenium.app(app):
pass
def test_props(selenium):
def app(el):
class ComponentWithProps(VueComponent):
prop: str = "p"
template = "
"
ComponentWithProps.register()
class ComponentRendersWithAttrs(VueComponent):
def render(self, create_element):
return create_element("ComponentWithProps", {"props": {"prop": "p"}})
return ComponentRendersWithAttrs(el)
with selenium.app(app):
assert selenium.element_present("p")
================================================
FILE: tests/selenium/test_vuerouter.py
================================================
from selenium.webdriver.common.by import By
VueRouterConfig = {"scripts": {"vue-router": True}}
def test_routes(selenium):
def app(el):
from vue import VueComponent, VueRouter, VueRoute
class Foo(VueComponent):
template = 'foo
'
class Bar(VueComponent):
text = "bar"
template = '{{ text }}
'
class Router(VueRouter):
routes = [VueRoute("/foo", Foo), VueRoute("/bar", Bar)]
class ComponentUsingRouter(VueComponent):
template = """
"""
return ComponentUsingRouter(el, router=Router())
with selenium.app(app, config=VueRouterConfig):
assert selenium.element_present("foo")
selenium.find_element(by=By.ID, value="foo").click()
assert selenium.element_has_text("content", "foo")
assert selenium.element_present("bar")
selenium.find_element(by=By.ID, value="bar").click()
assert selenium.element_has_text("content", "bar")
def test_dynamic_route_matching(selenium):
def app(el):
from vue import VueComponent, VueRouter, VueRoute
class User(VueComponent):
template = '{{ $route.params.id }}
'
class Router(VueRouter):
routes = [VueRoute("/user/:id", User)]
class ComponentUsingRouter(VueComponent):
template = """
"""
return ComponentUsingRouter(el, router=Router())
with selenium.app(app, config=VueRouterConfig):
assert selenium.element_present("link")
selenium.find_element(by=By.ID, value="link").click()
assert selenium.element_has_text("user", "123")
def test_named_routes(selenium):
def app(el):
from vue import VueComponent, VueRouter, VueRoute
class FooTop(VueComponent):
template = ''
class FooBottom(VueComponent):
template = 'foo bottom
'
class BarTop(VueComponent):
template = ''
class BarBottom(VueComponent):
template = 'bar bottom
'
class Router(VueRouter):
routes = [
VueRoute("/foo", components={"default": FooBottom, "top": FooTop}),
VueRoute("/bar", components={"default": BarBottom, "top": BarTop}),
]
class ComponentUsingRouter(VueComponent):
template = """
"""
return ComponentUsingRouter(el, router=Router())
with selenium.app(app, config=VueRouterConfig):
assert selenium.element_present("foo")
selenium.find_element(by=By.ID, value="foo").click()
assert selenium.element_has_text("header", "foo top")
assert selenium.element_has_text("body", "foo bottom")
assert selenium.element_present("bar")
selenium.find_element(by=By.ID, value="bar").click()
assert selenium.element_has_text("header", "bar top")
assert selenium.element_has_text("body", "bar bottom")
def test_nested_routes_and_redirect(selenium):
def app(el):
from vue import VueComponent, VueRouter, VueRoute
class UserHome(VueComponent):
template = 'Home
'
class UserProfile(VueComponent):
template = 'Profile
'
class UserPosts(VueComponent):
template = 'Posts
'
class ComponentUsingRouter(VueComponent):
template = """
/user/foo
/user/foo/profile
/user/foo/posts
User {{ $route.params.id }}
"""
class Router(VueRouter):
routes = [
VueRoute("/", redirect="/user/foo"),
VueRoute(
"/user/:id",
ComponentUsingRouter,
children=[
VueRoute("", UserHome),
VueRoute("profile", UserProfile),
VueRoute("posts", UserPosts),
],
),
]
return ComponentUsingRouter(el, router=Router())
with selenium.app(app, config=VueRouterConfig):
assert selenium.element_present("link-home")
selenium.find_element(by=By.ID, value="link-home").click()
assert selenium.element_has_text("home", "Home")
assert selenium.element_present("link-profile")
selenium.find_element(by=By.ID, value="link-profile").click()
assert selenium.element_has_text("profile", "Profile")
assert selenium.element_present("link-posts")
selenium.find_element(by=By.ID, value="link-posts").click()
assert selenium.element_has_text("posts", "Posts")
================================================
FILE: tests/selenium/test_vuex.py
================================================
from vue import *
VuexConfig = {"scripts": {"vuex": True}}
def test_state(selenium):
def app(el):
class Store(VueStore):
message = "Message"
class ComponentUsingStore(VueComponent):
@computed
def message(self):
return self.store.message
template = "{{ message }}
"
return ComponentUsingStore(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message")
def test_mutation_noargs(selenium):
def app(el):
class Store(VueStore):
message = ""
@mutation
def mutate_message(self):
self.message = "Message"
class ComponentUsingMutation(VueComponent):
@computed
def message(self):
self.store.commit("mutate_message")
return self.store.message
template = "{{ message }}
"
return ComponentUsingMutation(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message")
def test_mutation(selenium):
def app(el):
class Store(VueStore):
message = ""
@mutation
def mutate_message(self, new_message):
self.message = new_message
class ComponentUsingMutation(VueComponent):
@computed
def message(self):
self.store.commit("mutate_message", "Message")
return self.store.message
template = "{{ message }}
"
return ComponentUsingMutation(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message")
def test_mutation_kwargs(selenium):
def app(el):
class Store(VueStore):
message = ""
@mutation
def mutate_message(self, new_message, postfix=""):
self.message = new_message + postfix
class ComponentUsingMutation(VueComponent):
@computed
def message(self):
self.store.commit("mutate_message", "Message", postfix="!")
return self.store.message
template = "{{ message }}
"
return ComponentUsingMutation(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message!")
def test_action(selenium):
def app(el):
class Store(VueStore):
message = ""
@mutation
def mutate_message(self, new_message):
self.message = new_message
@action
def change_message(self, new_message):
self.commit("mutate_message", new_message)
class ComponentUsingAction(VueComponent):
def created(self):
self.store.dispatch("change_message", "Message")
@computed
def message(self):
return self.store.message
template = "{{ message }}
"
return ComponentUsingAction(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message")
def test_action_noargs(selenium):
def app(el):
class Store(VueStore):
message = ""
@mutation
def mutate_message(self, new_message):
self.message = new_message
@action
def change_message(self):
self.commit("mutate_message", "Message")
class ComponentUsingAction(VueComponent):
def created(self):
self.store.dispatch("change_message")
@computed
def message(self):
return self.store.message
template = "{{ message }}
"
return ComponentUsingAction(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message")
def test_action_kwargs(selenium):
def app(el):
class Store(VueStore):
message = ""
@mutation
def mutate_message(self, new_message):
self.message = new_message
@action
def change_message(self, new_message, postfix=""):
self.commit("mutate_message", new_message + postfix)
class ComponentUsingAction(VueComponent):
def created(self):
self.store.dispatch("change_message", "Message", postfix="!")
@computed
def message(self):
return self.store.message
template = "{{ message }}
"
return ComponentUsingAction(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message!")
def test_getter_noargs(selenium):
def app(el):
class Store(VueStore):
message = "Message"
@getter
def msg(self):
return self.message
class ComponentUsingGetter(VueComponent):
@computed
def message(self):
return self.store.msg
template = "{{ message }}
"
return ComponentUsingGetter(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message")
def test_getter_method(selenium):
def app(el):
class Store(VueStore):
message = "Message"
@getter
def msg(self, prefix):
return prefix + self.message
class ComponentUsingGetter(VueComponent):
@computed
def message(self):
return self.store.msg("pre")
template = "{{ message }}
"
return ComponentUsingGetter(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "preMessage")
def test_getter_kwargs(selenium):
def app(el):
class Store(VueStore):
message = "Message"
@getter
def msg(self, prefix, postfix):
return prefix + self.message + postfix
class ComponentUsingGetter(VueComponent):
@computed
def message(self):
return self.store.msg("pre", "!")
template = "{{ message }}
"
return ComponentUsingGetter(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "preMessage!")
def test_plugin(selenium):
def app(el):
class Plugin(VueStorePlugin):
def initialize(self, store):
store.message = "Message"
def subscribe(self, state, mut, *args, **kwargs):
print(state.message, mut, args, kwargs)
class Store(VueStore):
plugins = [Plugin().install]
message = ""
@mutation
def msg(self, prefix, postfix=""):
pass
class ComponentUsingGetter(VueComponent):
@computed
def message(self):
return self.store.message
def created(self):
self.store.commit("msg", "Hallo", postfix="!")
template = "{{ message }}
"
return ComponentUsingGetter(el, store=Store())
with selenium.app(app, config=VuexConfig):
assert selenium.element_has_text("content", "Message")
last_log_message = selenium.get_logs()[-1]["message"]
expected_msg = "Message msg ('Hallo',) {'postfix': '!'}"
assert expected_msg in last_log_message
def test_using_state_within_native_vue_component(selenium):
def app(el):
class Store(VueStore):
message = "Message"
class ComponentUsingNativeComponent(VueComponent):
template = " "
return ComponentUsingNativeComponent(el, store=Store())
config = {"scripts": {"vuex": True, "my": "my.js"}}
myjs = """
Vue.component('native', {
template: '{{ message }}
',
computed: {
message () {
return this.$store.state.message
}
}
});
"""
with selenium.app(app, config=config, files={"my.js": myjs}):
assert selenium.element_has_text("content", "Message")
================================================
FILE: tests/test_install.py
================================================
import json
import subprocess
import urllib.request
import time
from contextlib import contextmanager
import pytest
from tools.release import version
def _raise_failed_process(proc, error_msg):
stdout = proc.stdout if isinstance(proc.stdout, bytes) else proc.stdout.read()
stderr = proc.stderr if isinstance(proc.stderr, bytes) else proc.stderr.read()
print(f"return-code: {proc.returncode}")
print("stdout")
print(stdout.decode("utf-8"))
print("stderr")
print(stderr.decode("utf-8"))
raise RuntimeError(error_msg)
def shell(*args, env=None, cwd=None):
proc = subprocess.run(args, env=env, cwd=cwd, capture_output=True)
if proc.returncode:
_raise_failed_process(proc, str(args))
@pytest.fixture
def wheel(scope="session"):
shell("make", "build")
return f"dist/vuepy-{version()}-py3-none-any.whl"
@pytest.fixture
def venv(tmp_path):
path = tmp_path / "venv"
shell("python", "-m", "venv", str(path))
return path
@pytest.fixture
def install(wheel, venv):
def _install(extra=None):
extra = f"[{extra}]" if extra else ""
shell(
"pip",
"install",
f"{wheel}{extra}",
env={"PATH": str(venv / "bin"), "PIP_USER": "no"},
)
return _install
@contextmanager
def background_task(*args, env=None, cwd=None):
proc = subprocess.Popen(
args, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
try:
yield
finally:
if proc.poll() is not None:
_raise_failed_process(proc, "background task finished early")
proc.kill()
proc.communicate()
def request(url, retries=0, retry_delay=0):
for retry in range(1 + retries):
try:
with urllib.request.urlopen(url) as response:
return response
except urllib.request.URLError:
if retry >= retries:
raise
time.sleep(retry_delay)
@pytest.fixture
def app(tmp_path):
app_path = tmp_path / "app"
app_path.mkdir()
return app_path
@pytest.fixture
def config(app):
def _config(values):
app.joinpath("vuepy.yml").write_text(json.dumps(values, indent=2))
return _config
def test_static(install, venv, tmp_path, app):
destination = tmp_path / "destination"
install()
shell(
"vue-cli",
"deploy",
"static",
str(destination),
env={"PATH": f"{venv / 'bin'}"},
cwd=str(app),
)
assert (destination / "index.html").is_file()
def test_flask(install, venv, app):
install(extra="flask")
with background_task(
"vue-cli", "deploy", "flask", env={"PATH": f"{venv / 'bin'}"}, cwd=str(app)
):
assert (
request("http://localhost:5000", retries=5, retry_delay=0.5).status == 200
)
def test_flask_settings(install, config, venv, app):
install(extra="flask")
config({"provider": {"flask": {"PORT": 5001}}})
with background_task(
"vue-cli", "deploy", "flask", env={"PATH": f"{venv / 'bin'}"}, cwd=str(app)
):
assert (
request("http://localhost:5001", retries=5, retry_delay=0.5).status == 200
)
================================================
FILE: tests/unit/test_bridge/__init__.py
================================================
================================================
FILE: tests/unit/test_bridge/mocks.py
================================================
class VueMock:
@staticmethod
def set(obj, key, value):
setattr(obj, key, value)
@staticmethod
def delete(obj, key):
delattr(obj, key)
class ObjectMock:
def __new__(cls, arg):
return arg
@staticmethod
def assign(target, *sources):
for source in sources:
target.attributes.update(source)
return target
@staticmethod
def keys(obj):
return [k for k in obj]
class ArrayMock:
def __init__(self, *items):
self._data = list(items)
def __getitem__(self, item):
return self._data[item]
def __setitem__(self, key, value):
self._data[key] = value
@property
def length(self):
return len(self._data)
def push(self, *items):
self._data.extend(items)
def splice(self, index, delete_count=None, *items):
delete_count = (
delete_count if delete_count is not None else len(self._data) - index
)
index = index if index >= 0 else len(self._data) + index
deleted = self._data[index : index + delete_count]
self._data = (
self._data[0:index] + list(items) + self._data[index + delete_count :]
)
return deleted
def slice(self, index, stop=None):
return self._data[index:stop]
def indexOf(self, obj, start=0):
try:
return self._data.index(obj, start)
except ValueError:
return -1
def reverse(self):
self._data.reverse()
return self._data
================================================
FILE: tests/unit/test_bridge/test_dict.py
================================================
from unittest import mock
import pytest
from tests.unit.test_bridge.mocks import ObjectMock, VueMock
from vue.bridge.dict import window, Dict
@pytest.fixture(scope="module", autouse=True)
def window_object():
with mock.patch.object(window, "Object", new=ObjectMock), mock.patch.object(
window, "Vue", new=VueMock
):
yield
class JsObjectMock:
def __init__(self, attribtes):
self.attributes = attribtes
def __getattr__(self, item):
return self.attributes[item]
def __setattr__(self, key, value):
if key == "attributes":
super().__setattr__(key, value)
else:
self.attributes[key] = value
def __delattr__(self, item):
del self.attributes[item]
def __iter__(self):
return iter(self.attributes)
def make_dict(dct):
return Dict(JsObjectMock(dct))
class TestDict:
def test_getitem(self):
assert "value" == make_dict({"key": "value"})["key"]
def test_items(self):
assert (("a", 1), ("b", 2)) == make_dict({"a": 1, "b": 2}).items()
def test_eq(self):
assert {"a": 1} != make_dict({"a": 2})
assert {"a": 0, "b": 1} == make_dict({"a": 0, "b": 1})
def test_keys(self):
assert ("a", "b") == make_dict({"a": 0, "b": 1}).keys()
def test_iter(self):
assert ["a", "b"] == list(iter(make_dict({"a": 0, "b": 1})))
def test_setitem(self):
d = make_dict({})
d["a"] = 1
assert 1 == d["a"]
def test_contains(self):
assert "a" in make_dict({"a": 0, "b": 1})
def test_setdefault(self):
d = make_dict({})
assert 1 == d.setdefault("a", 1)
assert 1 == d.setdefault("a", 2)
def test_len(self):
assert 2 == len(make_dict({"a": 0, "b": 1}))
def test_get(self):
assert 1 == make_dict({"a": 0, "b": 1}).get("b", "default")
assert "default" == make_dict({"a": 0, "b": 1}).get("c", "default")
def test_values(self):
assert (0, 1) == make_dict({"a": 0, "b": 1}).values()
def test_repr(self):
assert str({"a": 0, "b": 1}) == str(make_dict({"a": 0, "b": 1}))
def test_update(self):
d = make_dict({"a": 0, "b": 1})
d.update(a=2)
assert {"a": 2, "b": 1} == d
d.update(c=0)
assert {"a": 2, "b": 1, "c": 0} == d
d.update({"c": 3, "d": 0})
assert {"a": 2, "b": 1, "c": 3, "d": 0} == d
def test_bool(self):
assert not make_dict({})
assert make_dict({"a": 0})
def test_delitem(self):
d = make_dict({"a": 0, "b": 1})
del d["a"]
assert {"b": 1} == d
def test_pop(self):
d = make_dict({"a": 0, "b": 1})
assert 1 == d.pop("b")
assert {"a": 0} == d
def test_pop_default(self):
d = make_dict({"a": 0, "b": 1})
assert "default" == d.pop("c", "default")
assert {"a": 0, "b": 1} == d
def test_pop_key_error(self):
d = make_dict({"a": 0, "b": 1})
with pytest.raises(KeyError):
d.pop("c")
def test_popitem(self):
d = make_dict({"a": 2})
assert ("a", 2) == d.popitem()
assert {} == d
def test_clear(self):
d = make_dict({"a": 0, "b": 1})
d.clear()
assert not d
def test_set(self):
d = make_dict({"a": 0, "b": 1})
old_id = id(d)
d.__set__({"c": 1, "d": 2})
assert old_id == id(d)
assert {"c": 1, "d": 2} == d
def test_getattr(self):
d = make_dict({"a": 0, "b": 1})
assert 1 == d.b
def test_setattr(self):
d = make_dict({"a": 1})
d.a = 2
assert {"a": 2} == d
def test_str_toString(self):
d = make_dict({})
d.toString = lambda: "STRING"
assert "STRING" == str(d)
================================================
FILE: tests/unit/test_bridge/test_jsobject.py
================================================
from unittest import mock
from browser import window
from vue.bridge import Object
from vue.bridge.vue_instance import VueInstance
from .mocks import ArrayMock
class TestJSObjectWrapper:
def test_vue(self):
class This:
def _isVue(self):
return True
assert isinstance(Object.from_js(This()), VueInstance)
def test_array(self):
with mock.patch.object(window.Array, "isArray", return_value=True):
obj = Object.from_js(ArrayMock(1, 2, 3))
assert [1, 2, 3] == obj
def test_dict(self):
assert {"a": 1, "b": 2} == Object.from_js({"a": 1, "b": 2})
================================================
FILE: tests/unit/test_bridge/test_list.py
================================================
import pytest
from vue.bridge.list import List
from .mocks import ArrayMock
class TestList:
def test_len(self):
assert 3 == len(List(ArrayMock(1, 2, 3)))
def test_getitem(self):
assert 3 == List(ArrayMock(1, 2, 3))[2]
assert [2, 3] == List(ArrayMock(1, 2, 3, 4))[1:3]
assert 3 == List(ArrayMock(1, 2, 3))[-1]
assert [2] == List(ArrayMock(1, 2, 3))[-2:-1]
def test_delitem(self):
l = List(ArrayMock(1, 2, 3))
del l[1]
assert [1, 3] == l
def test_delitem_range(self):
l = List(ArrayMock(1, 2, 3, 4))
del l[1:3]
assert [1, 4] == l
def test_setitem(self):
l = List(ArrayMock(1, 2, 3))
l[1] = 5
assert [1, 5, 3] == l
def test_setitem_range(self):
l = List(ArrayMock(1, 2, 3))
l[:] = [5]
assert [5] == l
def test_setitem_negative(self):
l = List(ArrayMock(1, 2, 3, 4))
l[-3:-1] = [8, 9]
assert [1, 8, 9, 4] == l
def test_iter(self):
assert [1, 2, 3] == [i for i in List(ArrayMock(1, 2, 3))]
def test_eq(self):
assert [1, 2, 3] == List(ArrayMock(1, 2, 3))
def test_mul(self):
assert [1, 2, 1, 2, 1, 2] == List(ArrayMock(1, 2)) * 3
def test_index(self):
assert 3 == List(ArrayMock(1, 2, 3, 4)).index(4)
def test_index_start(self):
assert 4 == List(ArrayMock(4, 1, 2, 3, 4)).index(4, start=1)
def test_index_not_in_list(self):
with pytest.raises(ValueError):
List(ArrayMock(1, 2, 3)).index(4)
def test_extend(self):
l = List(ArrayMock(1, 2))
l.extend([3, 4])
assert [1, 2, 3, 4] == l
def test_contains(self):
assert 3 in List(ArrayMock(1, 2, 3))
def test_count(self):
assert 2 == List(ArrayMock(1, 2, 1)).count(1)
def test_repr(self):
assert "[1, 2, 3]" == repr(List(ArrayMock(1, 2, 3)))
def test_str(self):
assert "[1, 2, 3]" == str(List(ArrayMock(1, 2, 3)))
def test_append(self):
l = List(ArrayMock(1, 2))
l.append(3)
assert [1, 2, 3] == l
def test_insert(self):
l = List(ArrayMock(1, 3))
l.insert(1, 2)
assert [1, 2, 3] == l
def test_remove(self):
l = List(ArrayMock(1, 2, 1, 3))
l.remove(1)
assert [2, 3] == l
def test_pop(self):
l = List(ArrayMock(1, 2, 3))
assert 3 == l.pop()
def test_sort(self):
l = List(ArrayMock(4, 3, 6, 1))
l.sort()
assert [1, 3, 4, 6] == l
def test_reverse(self):
l = List(ArrayMock(4, 3, 6, 1))
l.reverse()
assert [1, 6, 3, 4] == l
def test_set(self):
l = List(ArrayMock(4, 3, 6, 1))
l.__set__([1, 2, 3, 4])
assert [1, 2, 3, 4] == l
================================================
FILE: tests/unit/test_bridge/test_vue.py
================================================
from unittest import mock
import pytest
from .mocks import ArrayMock
from vue.bridge.vue_instance import VueInstance
from browser import window
class TestVue:
def test_getattr(self):
class This:
def __init__(self):
self.attribute = "value"
this = This()
vue = VueInstance(this)
assert "value" == vue.attribute
this.attribute = "new_value"
assert "new_value" == vue.attribute
def test_get_dollar_attribute(self):
class This:
def __getattr__(self, item):
if item == "$dollar":
return "DOLLAR"
return super().__getattribute__(item)
vue = VueInstance(This())
assert "DOLLAR" == vue.dollar
with pytest.raises(AttributeError):
assert not vue.no_dollar
def test_setattr(self):
class This:
def __init__(self):
self.attribute = False
def __getattr__(self, item):
if item == "$props":
return ()
return self.__getattribute__(item)
this = This()
vue = VueInstance(this)
vue.attribute = True
assert vue.attribute
assert this.attribute
def test_set_attribute_with_set(self):
class This:
def __init__(self):
self.list = ArrayMock([1, 2, 3, 4])
def __getattr__(self, item):
if item == "$props":
return ()
return self.__getattribute__(item)
this = This()
vue = VueInstance(this)
list_id = id(this.list)
with mock.patch.object(window.Array, "isArray", return_value=True):
vue.list = [0, 1, 2]
assert [0, 1, 2] == vue.list._data
assert list_id == id(this.list)
================================================
FILE: tests/unit/test_bridge/test_vuex.py
================================================
from vue.bridge.vuex_instance import VuexInstance
class Getter:
def __init__(self, **kwargs):
self.vars = kwargs
def __getattr__(self, item):
return self.vars[item]
class Callable:
def __init__(self):
self.args = None
def __call__(self, *args):
self.args = args
def test_get_state():
vuex = VuexInstance(state=dict(i=1))
assert 1 == vuex.i
def test_get_root_state():
vuex = VuexInstance(root_state=dict(i=1))
assert 1 == vuex.i
def test_set_state():
state = dict(i=0)
vuex = VuexInstance(state=state)
vuex.i = 1
assert 1 == state["i"]
def test_set_root_state():
state = dict(i=0)
vuex = VuexInstance(root_state=state)
vuex.i = 1
assert 1 == state["i"]
def test_access_getter():
vuex = VuexInstance(getters=Getter(i=1))
assert 1 == vuex.i
def test_access_root_getter():
vuex = VuexInstance(root_getters=Getter(i=1))
assert 1 == vuex.i
def test_access_comit():
c = Callable()
vuex = VuexInstance(commit=c)
vuex.commit("mutation", 1, a=0)
assert "mutation" == c.args[0]
assert {"args": (1,), "kwargs": {"a": 0}} == c.args[1]
def test_access_dispatch():
c = Callable()
vuex = VuexInstance(dispatch=c)
vuex.dispatch("action", 1, a=0)
assert "action" == c.args[0]
assert {"args": (1,), "kwargs": {"a": 0}} == c.args[1]
================================================
FILE: tests/unit/test_transformers/conftest.py
================================================
from unittest import mock
from vue.bridge.list import window as list_window
import pytest
class ArrayMock(list):
def __new__(cls, *args):
return list(args)
@staticmethod
def isArray(obj):
return isinstance(obj, list)
@pytest.fixture(scope="module", autouse=True)
def window_object():
with mock.patch.object(list_window, "Array", new=ArrayMock):
yield
================================================
FILE: tests/unit/test_transformers/test_component.py
================================================
from unittest import mock
from vue import *
def test_empty():
class Empty(VueComponent):
pass
assert {} == Empty.init_dict()
def test_method():
class Component(VueComponent):
def do(self, event):
return self, event
vue_dict = Component.init_dict()
assert "do" in vue_dict["methods"]
method = vue_dict["methods"]["do"]
with mock.patch("vue.decorators.base.javascript.this", return_value="THIS"):
assert "SELF", "EVENT" == method("EVENT")
def test_method_as_coroutine():
class Component(VueComponent):
async def co(self):
return self
assert "co" in Component.init_dict()["methods"]
def test_data():
class Component(VueComponent):
attribute = 1
assert {"attribute": 1} == Component.init_dict()["data"]("THIS")
def test_data_as_property():
class Component(VueComponent):
@data
def attribute(self):
return self
assert {"attribute": "THIS"} == Component.init_dict()["data"]("THIS")
def test_props():
class Component(VueComponent):
prop: int
init_dict = Component.init_dict()
assert {"prop": {"type": int, "required": True}} == init_dict["props"]
def test_props_with_default():
class Component(VueComponent):
prop: int = 100
init_dict = Component.init_dict()
props = {"prop": {"type": int, "default": 100}}
assert props == init_dict["props"]
def test_props_validator():
class Component(VueComponent):
prop: int
@validator("prop")
def is_lt_100(self, value):
return value < 100
init_dict = Component.init_dict()
assert not init_dict["props"]["prop"]["validator"](100)
assert init_dict["props"]["prop"]["validator"](99)
def test_template():
class Component(VueComponent):
template = "TEMPLATE"
init_dict = Component.init_dict()
assert "TEMPLATE" == init_dict["template"]
def test_lifecycle_hooks():
class Component(VueComponent):
def before_create(self):
return self
def created(self):
return self
def before_mount(self):
return self
def mounted(self):
return self
def before_update(self):
return self
def updated(self):
return self
def before_destroy(self):
return self
def destroyed(self):
return self
init_dict = Component.init_dict()
assert "beforeCreate" in init_dict
assert "created" in init_dict
assert "beforeMount" in init_dict
assert "mounted" in init_dict
assert "beforeUpdate" in init_dict
assert "updated" in init_dict
assert "beforeDestroy" in init_dict
assert "destroyed" in init_dict
def test_customize_model():
class Component(VueComponent):
model = Model(prop="prop", event="event")
init_dict = Component.init_dict()
assert {"prop": "prop", "event": "event"} == init_dict["model"]
def test_filter():
class Component(VueComponent):
@staticmethod
@filters
def lower_case(value):
return value.lower()
init_dict = Component.init_dict()
assert "abc" == init_dict["filters"]["lower_case"]("Abc")
def test_watch():
class Component(VueComponent):
@watch("data")
def lower_case(self, new, old):
return old, new
init_dict = Component.init_dict()
result = init_dict["watch"]["data"]["handler"]("new", "old")
assert "new", "old" == result
def test_watch_deep():
class Component(VueComponent):
@watch("data", deep=True)
def lower_case(self, new, old):
return new, old
init_dict = Component.init_dict()
assert init_dict["watch"]["data"]["deep"]
def test_watch_immediate():
class Component(VueComponent):
@watch("data", immediate=True)
def lower_case(self, new, old):
return new, old
init_dict = Component.init_dict()
assert init_dict["watch"]["data"]["immediate"]
def test_function_directive():
class Component(VueComponent):
@staticmethod
@directive
def focus(el, binding, vnode, old_vnode):
return el, binding, vnode, old_vnode
init_dict = Component.init_dict()
res = ["el", "binding", "vnode", "old_vnode"]
assert res == init_dict["directives"]["focus"](
"el", "binding", "vnode", "old_vnode"
)
def test_full_directive_different_hooks():
class Component(VueComponent):
@staticmethod
@directive("focus")
def bind():
return "bind"
@staticmethod
@directive("focus")
def inserted():
return "inserted"
@staticmethod
@directive("focus")
def update():
return "update"
@staticmethod
@directive("focus")
def component_updated():
return "componentUpdated"
@staticmethod
@directive("focus")
def unbind():
return "unbind"
init_dict = Component.init_dict()
directive_map = init_dict["directives"]["focus"]
for fn_name in ("bind", "inserted", "update", "componentUpdated", "unbind"):
assert fn_name == directive_map[fn_name]()
def test_full_directive_single_hook():
class Component(VueComponent):
@staticmethod
@directive("focus", "bind", "inserted", "update", "component_updated", "unbind")
def hook():
return "hook"
init_dict = Component.init_dict()
directive_map = init_dict["directives"]["focus"]
for fn_name in ("bind", "inserted", "update", "componentUpdated", "unbind"):
assert "hook" == directive_map[fn_name]()
def test_directive_replace_dash():
class Component(VueComponent):
@staticmethod
@directive
def focus_dashed(el, binding, vnode, old_vnode):
return el, binding, vnode, old_vnode
init_dict = Component.init_dict()
assert "focus-dashed" in init_dict["directives"]
def test_mixins():
class Component(VueComponent):
mixins = [{"created": "fn"}]
assert [{"created": "fn"}] == Component.init_dict()["mixins"]
def test_vuepy_mixin():
class MyMixin(VueMixin):
pass
class Component(VueComponent):
mixins = [MyMixin]
assert [{}] == Component.init_dict()["mixins"]
def test_render_function():
class Component(VueComponent):
def render(self, create_element):
pass
assert "render" in Component.init_dict()
def test_attributes_from_base_class():
class Component(VueComponent):
template = "TEMPLATE"
class SubComponent(Component):
pass
assert "TEMPLATE" == SubComponent.init_dict()["template"]
def test_extends():
class Component(VueComponent):
template = "TEMPLATE"
class SubComponent(Component):
extends = True
assert {"template": "TEMPLATE"} == SubComponent.init_dict()["extends"]
def test_template_merging():
class Base(VueComponent):
template = "BASE {}
"
class Middle(Base):
template_slots = "MIDDLE {}"
class Sub(Middle):
template_slots = "SUB"
assert "BASE MIDDLE SUB
" == Sub.init_dict()["template"]
def test_template_merging_with_slots():
class Base(VueComponent):
template_slots = {"pre": "DEFAULT", "post": "DEFAULT"}
template = "{pre} {} {post}
"
class WithSlots(Base):
template_slots = {"pre": "PRE", "default": "SUB"}
class WithDefault(Base):
template_slots = "SUB"
assert "PRE SUB DEFAULT
" == WithSlots.init_dict()["template"]
assert "DEFAULT SUB DEFAULT
" == WithDefault.init_dict()["template"]
def test_components():
class Component(VueComponent):
components = [{"created": "fn"}]
assert [{"created": "fn"}] == Component.init_dict()["components"]
def test_vuepy_components():
class SubComponent(VueComponent):
pass
class Component(VueComponent):
components = [SubComponent]
assert [{}] == Component.init_dict()["components"]
================================================
FILE: tests/unit/test_transformers/test_router.py
================================================
from unittest.mock import Mock
from vue import *
class TestVueRoute:
def test_path_and_component(self):
route = VueRoute("/path", VueComponent)
assert route == {"path": "/path", "component": VueComponent.init_dict()}
def test_path_and_components(self):
route = VueRoute(
"/path", components={"default": VueComponent, "named": VueComponent}
)
assert route == {
"path": "/path",
"components": {
"default": VueComponent.init_dict(),
"named": VueComponent.init_dict(),
},
}
def test_path_and_components_and_children(self):
route = VueRoute(
"/path",
VueComponent,
children=[
VueRoute("/path", VueComponent),
VueRoute("/path2", VueComponent),
],
)
assert route == {
"path": "/path",
"component": VueComponent.init_dict(),
"children": [
{"path": "/path", "component": VueComponent.init_dict()},
{"path": "/path2", "component": VueComponent.init_dict()},
],
}
def test_path_and_redirect(self):
route = VueRoute("/path", redirect="/path2")
assert route == {"path": "/path", "redirect": "/path2"}
class TestVueRouter:
def test_routes(self):
class Router(VueRouter):
routes = [VueRoute("/path", VueComponent)]
routes = Router.init_dict()["routes"]
assert routes == [{"path": "/path", "component": VueComponent.init_dict()}]
def test_custom_router(self):
router_class_mock = Mock()
router_class_mock.new.return_value = "CustomRouter"
class Router(VueRouter):
RouterClass = router_class_mock
router = Router()
assert router == "CustomRouter"
router_class_mock.new.assert_called_with(Router.init_dict())
================================================
FILE: tests/unit/test_transformers/test_store.py
================================================
from vue import *
from vue.bridge import VuexInstance
def test_state():
class Store(VueStore):
attribute = 1
assert {"attribute": 1} == Store.init_dict()["state"]
def test_mutation():
class Store(VueStore):
@mutation
def mutation(self, payload):
return self, payload
store, arg = Store.init_dict()["mutations"]["mutation"]({}, {"args": (2,)})
assert 2 == arg
assert isinstance(store, VuexInstance)
def test_action():
class Context:
def __init__(self):
self.state = {}
self.getters = {}
self.rootState = {}
self.rootGetters = {}
self.commit = None
self.dispatch = None
class Store(VueStore):
@action
def action(self, payload):
return self, payload
store, arg = Store.init_dict()["actions"]["action"](Context(), {"args": (2,)})
assert 2 == arg
assert isinstance(store, VuexInstance)
def test_getter():
class Store(VueStore):
@staticmethod
@getter
def getter(self):
return self
vuex = Store.init_dict()["getters"]["getter"]({}, {})
assert isinstance(vuex, VuexInstance)
def test_getter_method():
class Store(VueStore):
@staticmethod
@getter
def getter(self, value):
return self, value
vuex, value = Store.init_dict()["getters"]["getter"]({}, {})(3)
assert isinstance(vuex, VuexInstance)
assert 3 == value
def test_plugin_registration():
class Store(VueStore):
plugins = [1]
assert [1] == Store.init_dict()["plugins"]
================================================
FILE: tests/unit/test_utils.py
================================================
import pytest
from unittest import mock
from vue import utils
from vue.utils import *
@pytest.fixture
def fix_load_and_window():
utils.CACHE.clear()
class WindowMock:
def __getattr__(self, item):
return item
class LoadMock:
def __init__(self):
self.call_count = 0
def __call__(self, path):
self.call_count += 1
path, mods = path.split(";")
mods = mods.split(",")
for mod in mods:
if mod:
setattr(WindowMock, mod, mod)
with mock.patch.object(utils, "load", new=LoadMock()), mock.patch.object(
utils, "window", new=WindowMock
):
yield
@pytest.mark.usefixtures("fix_load_and_window")
class TestJsLoad:
def test_single(self):
assert "a" == js_load("path;a")
def test_multiple(self):
assert {"a": "a", "b": "b"} == js_load("path;a,b")
def test_different(self):
assert "a" == js_load("first;a")
assert "b" == js_load("second;b")
assert 2 == utils.load.call_count
def test_using_cache(self):
assert "a" == js_load("path;a")
assert "a" == js_load("path;a")
assert 1 == utils.load.call_count
def test_none(self):
assert js_load("path;") is None
def test_ignore_dollar(self):
assert js_load("path;$test") is None
class TestJsLib:
def test_getattr_of_window(self):
class WindowMock:
attribute = "ATTRIBUTE"
with mock.patch.object(utils, "window", new=WindowMock):
assert "ATTRIBUTE" == js_lib("attribute")
def test_get_default(self):
class AttributeWithDefault:
default = "DEFAULT"
def __dir__(self):
return ["default"]
class WindowMock:
attribute = AttributeWithDefault()
with mock.patch.object(utils, "window", new=WindowMock):
assert "DEFAULT" == js_lib("attribute")
================================================
FILE: tests/unit/test_vue.py
================================================
from contextlib import contextmanager
from unittest import mock
import pytest
from vue import Vue, VueComponent, VueDirective, VueMixin
from vue.bridge.dict import window
@pytest.fixture(autouse=True)
def window_object():
with mock.patch.object(window, "Object", new=dict):
yield
class VueMock(mock.MagicMock):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.init_dict = None
self.register_name = None
self.directive_name = None
self._directive = None
@contextmanager
def new(self):
with mock.patch("vue.vue.window.Vue.new", new=self) as new:
yield self
self.init_dict = new.call_args[0][0]
@contextmanager
def component(self):
with mock.patch("vue.vue.window.Vue.component", new=self) as component:
component.side_effect = lambda *args, **kwargs: self.init_dict
yield self
self.init_dict = (
component.call_args[0][1] if len(component.call_args[0]) > 1 else component
)
self.register_name = component.call_args[0][0]
@contextmanager
def directive(self):
with mock.patch("vue.vue.window.Vue.directive", new=self) as drctv:
drctv.side_effect = lambda *args, **kwargs: self._directive
yield self
self.directive_name = drctv.call_args[0][0]
self._directive = drctv.call_args[0][1] if len(drctv.call_args[0]) > 1 else None
@contextmanager
def filter(self):
with mock.patch("vue.vue.window.Vue.filter", new=self) as flt:
yield self
flt._filter_name = flt.call_args[0][0]
flt._filter = flt.call_args[0][1]
@contextmanager
def mixin(self):
with mock.patch("vue.vue.window.Vue.mixin", new=self) as mixin:
yield self
mixin.init_dict = mixin.call_args[0][0]
@contextmanager
def use(self):
with mock.patch("vue.vue.window.Vue.use", new=self) as use:
yield self
use.plugin = use.call_args[0][0]
class TestVueComponent:
def test_el(self):
class Component(VueComponent):
pass
with VueMock().new() as new:
Component("app")
assert "app" == new.init_dict["el"]
def test_props_data(self):
class Component(VueComponent):
prop: str
with VueMock().new() as new:
Component("app", props_data={"prop": "PROP"})
assert {"prop": "PROP"} == new.init_dict["propsData"]
def test_register(self):
class Component(VueComponent):
pass
with VueMock().component() as component:
Component.register()
assert "Component" == component.register_name
with VueMock().component() as component:
Component.register("new-name")
assert "new-name" == component.register_name
class TestVue:
def test_directive(self):
class Drctv(VueDirective):
def bind(self):
pass
with VueMock().directive() as dirctv:
Vue.directive("directive", Drctv)
assert "directive" == dirctv.directive_name
assert "bind" in dirctv._directive
def test_directive_with_implicit_name(self):
class Drctv(VueDirective):
def bind(self):
pass
with VueMock().directive() as dirctv:
Vue.directive(Drctv)
assert "drctv" == dirctv.directive_name
assert "bind" in dirctv._directive
def test_function_directive(self):
def function_directive(a):
return a
with VueMock().directive() as dirctv:
Vue.directive("my-directive", function_directive)
assert "my-directive" == dirctv.directive_name
assert "a" == dirctv._directive("a")
def test_function_directive_with_implicit_name(self):
def function_directive(a):
return a
with VueMock().directive() as dirctv:
Vue.directive(function_directive)
assert "function_directive" == dirctv.directive_name
assert "a" == dirctv._directive("a")
def test_directive_getter(self):
with VueMock().directive() as drctv:
drctv._directive = "DIRECTIVE"
drctv.directive_name = "my-directive"
assert "DIRECTIVE" == Vue.directive("my-durective")
def test_filter(self):
with VueMock().filter() as filter_mock:
Vue.filter("my_filter", lambda val: "filtered({})".format(val))
assert "my_filter" == filter_mock._filter_name
assert "filtered(value)" == filter_mock._filter("value")
def test_filter_use_function_name(self):
def flt(v):
return "filtered({})".format(v)
with VueMock().filter() as filter_mock:
Vue.filter(flt)
assert "flt" == filter_mock._filter_name
assert "filtered(value)" == filter_mock._filter("value")
def test_mixin(self):
class Mixin(VueMixin):
def created(self):
return "created"
with VueMock().mixin() as mixin_mock:
Vue.mixin(Mixin)
assert "created" == mixin_mock.init_dict["created"]()
def test_use(self):
with VueMock().use() as use:
Vue.use("Plugin")
assert "Plugin" == use.plugin
def test_component(self):
with VueMock().component() as component:
Vue.component("my-component", {"a": 0})
assert {"a": 0} == component.init_dict
assert "my-component" == component.register_name
def test_vuepy_component(self):
class MyComponent(VueComponent):
pass
with VueMock().component() as component:
Vue.component("my-component", MyComponent)
assert {} == component.init_dict
assert "my-component" == component.register_name
def test_vuepy_component_implicit_naming(self):
class MyComponent(VueComponent):
pass
with VueMock().component() as component:
Vue.component(MyComponent)
assert {} == component.init_dict
assert "MyComponent" == component.register_name
def test_component_getter(self):
with VueMock().component() as comp:
comp.init_dict = {"a": 0}
component = Vue.component("my-component")
assert {"a": 0} == component
================================================
FILE: vue/__init__.py
================================================
from .__version__ import __version__
try:
from .vue import VueComponent, VueMixin, Vue, VueDirective, VuePlugin
from .store import VueStore, VueStorePlugin
from .router import VueRouter, VueRoute
from .decorators import (
computed,
validator,
directive,
filters,
watch,
data,
Model,
custom,
mutation,
action,
getter,
)
except ModuleNotFoundError:
pass
================================================
FILE: vue/bridge/__init__.py
================================================
from .object import Object
from .list import List
from .dict import Dict
from .vue_instance import VueInstance
from .vuex_instance import VuexInstance
================================================
FILE: vue/bridge/dict.py
================================================
from browser import window
from .object import Object
class Dict(Object):
@staticmethod
def __unwraps__():
return dict
@staticmethod
def __can_wrap__(obj):
return (str(type(obj)) == "") or (
obj.__class__.__name__ == "JSObject"
and not callable(obj)
and not isinstance(obj, dict)
)
def __eq__(self, other):
return other == {k: v for k, v in self.items()}
def __getitem__(self, item):
return Object.from_js(getattr(self._js, item))
def __iter__(self):
return (k for k in self.keys())
def pop(self, k, default=...):
if k not in self and not isinstance(default, type(Ellipsis)):
return default
item = self[k]
del self[k]
return item
def popitem(self):
key = self.keys()[0]
return key, self.pop(key)
def setdefault(self, k, default=None):
if k not in self:
self[k] = default
return self[k]
def __len__(self):
return len(self.items())
def __contains__(self, item):
return Object.to_js(item) in self.keys()
def __delitem__(self, key):
window.Vue.delete(self._js, Object.to_js(key))
def __setitem__(self, key, value):
if key not in self:
window.Vue.set(self._js, Object.to_js(key), Object.to_js(value))
else:
setattr(self._js, Object.to_js(key), Object.to_js(value))
def get(self, k, default=None):
if k not in self:
return default
return self[k]
def values(self):
return tuple(self[key] for key in self)
def update(self, _m=None, **kwargs):
if _m is None:
_m = {}
_m.update(kwargs)
window.Object.assign(self._js, Object.to_js(_m))
def clear(self):
while len(self) > 0:
self.popitem()
@classmethod
def fromkeys(cls, seq):
raise NotImplementedError()
def copy(self):
raise NotImplementedError()
def items(self):
return tuple((key, self[key]) for key in self)
def keys(self):
return tuple(Object.from_js(key) for key in window.Object.keys(self._js))
def __str__(self):
if hasattr(self, "toString") and callable(self.toString):
return self.toString()
return repr(self)
def __repr__(self):
return "{{{}}}".format(
", ".join("{!r}: {!r}".format(k, v) for k, v in self.items())
)
def __set__(self, new):
self.clear()
self.update(new)
def __bool__(self):
return len(self) > 0
def __getattr__(self, item):
try:
return self[item]
except KeyError:
raise AttributeError(item)
def __setattr__(self, key, value):
if key in ["_js"]:
return super().__setattr__(key, value)
self[key] = value
def __py__(self):
return {Object.to_py(k): Object.to_py(v) for k, v in self.items()}
def __js__(self):
if isinstance(self, dict):
return window.Object(
{Object.to_js(k): Object.to_js(v) for k, v in self.items()}
)
return self._js
Object.Default = Dict
================================================
FILE: vue/bridge/list.py
================================================
from browser import window
from .object import Object
class List(Object):
@staticmethod
def __unwraps__():
return list, tuple
@staticmethod
def __can_wrap__(obj):
return window.Array.isArray(obj) and not isinstance(obj, list)
def _slice(self, slc):
if isinstance(slc, int):
if slc < 0:
slc = len(self) + slc
return slc, slc + 1
start = slc.start if slc.start is not None else 0
stop = slc.stop if slc.stop is not None else len(self)
return start, stop
def __eq__(self, other):
return other == [i for i in self]
def __mul__(self, other):
return [i for i in self] * other
def index(self, obj, start=0, _stop=-1):
index = self._js.indexOf(Object.to_js(obj), start)
if index == -1:
raise ValueError("{} not in list".format(obj))
return index
def extend(self, iterable):
self._js.push(*(i for i in iterable))
def __len__(self):
return self._js.length
def __contains__(self, item):
try:
self.index(item)
return True
except ValueError:
return False
def __imul__(self, other):
raise NotImplementedError()
def count(self, obj):
return [i for i in self].count(obj)
def reverse(self):
self._js.reverse()
def __delitem__(self, key):
start, stop = self._slice(key)
self._js.splice(start, stop - start)
def __setitem__(self, key, value):
start, stop = self._slice(key)
value = value if isinstance(value, list) else [value]
self._js.splice(start, stop - start, *value)
def __getitem__(self, item):
start, stop = self._slice(item)
value = self._js.slice(start, stop)
if isinstance(item, int):
return Object.from_js(value[0])
return [Object.from_js(i) for i in value]
def __reversed__(self):
raise NotImplementedError()
def __rmul__(self, other):
raise NotImplemented()
def append(self, obj):
self._js.push(Object.to_js(obj))
def insert(self, index, obj):
self._js.splice(index, 0, Object.to_js(obj))
def remove(self, obj):
index = self._js.indexOf(Object.to_js(obj))
while index != -1:
del self[self._js.indexOf(Object.to_js(obj))]
index = self._js.indexOf(Object.to_js(obj))
def __iadd__(self, other):
raise NotImplemented()
def __iter__(self):
def _iter(lst):
for i in range(lst.__len__()):
yield lst[i]
return _iter(self)
def pop(self, index=-1):
return Object.from_js(self._js.splice(index, 1)[0])
def sort(self, key=None, reverse=False):
self[:] = sorted(self, key=key, reverse=reverse)
def __add__(self, other):
raise NotImplemented()
def clear(self):
raise NotImplemented()
def copy(self):
raise NotImplemented()
def __set__(self, new):
self[:] = new
def __repr__(self):
return "[{}]".format(", ".join(repr(i) for i in self))
def __py__(self):
return [Object.to_py(item) for item in self]
def __js__(self):
if isinstance(self, (list, tuple)):
return window.Array(*[Object.to_js(item) for item in self])
return self._js
Object.SubClasses.append(List)
================================================
FILE: vue/bridge/object.py
================================================
class Object:
SubClasses = []
Default = None
@classmethod
def sub_classes(cls):
return cls.SubClasses + ([cls.Default] if cls.Default else [])
@classmethod
def from_js(cls, jsobj):
for sub_class in cls.sub_classes():
if sub_class.__can_wrap__(jsobj):
return sub_class(jsobj)
return jsobj
@classmethod
def to_js(cls, obj):
if isinstance(obj, Object):
return obj.__js__()
for sub_class in cls.sub_classes():
if isinstance(obj, sub_class.__unwraps__()):
return sub_class.__js__(obj)
return obj
@classmethod
def to_py(cls, obj):
obj = Object.from_js(obj)
if isinstance(obj, Object):
return obj.__py__()
for sub_class in cls.sub_classes():
if isinstance(obj, sub_class.__unwraps__()):
return sub_class.__py__(obj)
return obj
@staticmethod
def __can_wrap__(obj):
return False
@staticmethod
def __unwraps__():
return ()
def __init__(self, js):
self._js = js
def __js__(self):
return self._js
def __py__(self):
return self
================================================
FILE: vue/bridge/vue_instance.py
================================================
from .object import Object
from .vuex_instance import VuexInstance
class VueInstance(Object):
@staticmethod
def __can_wrap__(obj):
return hasattr(obj, "_isVue") and obj._isVue
@property
def store(self):
store = self.__getattr__("store")
return VuexInstance(
state=store.state,
getters=store.getters,
commit=store.commit,
dispatch=store.dispatch,
)
def __getattr__(self, item):
try:
return Object.from_js(getattr(self._js, item))
except AttributeError:
if not item.startswith("$"):
return self.__getattr__("${}".format(item))
raise
def __setattr__(self, key, value):
if key in ["_js"]:
object.__setattr__(self, key, value)
elif hasattr(getattr(self, key), "__set__"):
getattr(self, key).__set__(value)
else:
if key not in dir(getattr(self._js, "$props", [])):
setattr(self._js, key, value)
Object.SubClasses.append(VueInstance)
================================================
FILE: vue/bridge/vuex_instance.py
================================================
class VuexInstance:
def __init__(
self,
state=None,
getters=None,
root_state=None,
root_getters=None,
commit=None,
dispatch=None,
):
self.__state__ = state if state else {}
self.__getter__ = getters
self.__root_getter__ = root_getters
self.__root_state__ = root_state if root_state else {}
self.__commit__ = commit
self.__dispatch__ = dispatch
def __getattr__(self, item):
item = item.replace("$", "")
if item in ["__state__", "__root_state__"]:
return {}
if item in self.__state__:
return self.__state__[item]
if hasattr(self.__getter__, item):
return getattr(self.__getter__, item)
if item in self.__root_state__:
return self.__root_state__[item]
if hasattr(self.__root_getter__, item):
return getattr(self.__root_getter__, item)
return super().__getattribute__(item)
def __setattr__(self, key, value):
key = key.replace("$", "")
if key in self.__state__:
self.__state__[key] = value
elif key in self.__root_state__:
self.__root_state__[key] = value
else:
super().__setattr__(key, value)
def commit(self, mutation_name, *args, **kwargs):
self.__commit__(mutation_name, {"args": args, "kwargs": kwargs})
def dispatch(self, mutation_name, *args, **kwargs):
self.__dispatch__(mutation_name, {"args": args, "kwargs": kwargs})
================================================
FILE: vue/decorators/__init__.py
================================================
from .computed import computed
from .prop import validator
from .directive import directive, DirectiveHook
from .filters import filters
from .watcher import watch
from .data import data
from .model import Model
from .custom import custom
from .mutation import mutation
from .action import action
from .getter import getter
================================================
FILE: vue/decorators/action.py
================================================
from .base import pyjs_bridge, VueDecorator
from vue.bridge import VuexInstance
class Action(VueDecorator):
def __init__(self, name, value):
self.__key__ = f"actions.{name}"
self.__value__ = value
def action(fn):
def wrapper(context, *payload):
payload = payload[0] if payload else {"args": (), "kwargs": {}}
return fn(
VuexInstance(
state=context.state,
getters=context.getters,
root_state=context.rootState,
root_getters=context.rootGetters,
commit=context.commit,
dispatch=context.dispatch,
),
*payload.get("args", ()),
**payload.get("kwargs", {}),
)
return Action(fn.__name__, pyjs_bridge(wrapper))
================================================
FILE: vue/decorators/base.py
================================================
from vue.bridge import Object
import javascript
class VueDecorator:
__key__ = None
__value__ = None
def pyjs_bridge(fn, inject_vue_instance=False):
def wrapper(*args, **kwargs):
args = (javascript.this(), *args) if inject_vue_instance else args
args = tuple(Object.from_js(arg) for arg in args)
kwargs = {k: Object.from_js(v) for k, v in kwargs.items()}
return Object.to_js(fn(*args, **kwargs))
wrapper.__name__ = fn.__name__
return wrapper
================================================
FILE: vue/decorators/components.py
================================================
from .base import VueDecorator
class Components(VueDecorator):
__key__ = "components"
def __init__(self, *mixins):
self.__value__ = list(mixins)
================================================
FILE: vue/decorators/computed.py
================================================
from .base import pyjs_bridge, VueDecorator
class Computed(VueDecorator):
def __init__(self, fn):
self.__key__ = f"computed.{fn.__name__}"
self.__name__ = fn.__name__
self.__call__ = pyjs_bridge(fn)
self._setter = None
def setter(self, fn):
self._setter = pyjs_bridge(fn, inject_vue_instance=True)
return self
@property
def __value__(self):
vue_object = {"get": self.__call__}
if self._setter:
vue_object["set"] = self._setter
return vue_object
def computed(fn):
return Computed(fn)
================================================
FILE: vue/decorators/custom.py
================================================
from .base import pyjs_bridge, VueDecorator
class Custom(VueDecorator):
def __init__(self, fn, key, name=None, static=False):
self.__key__ = f"{key}.{name if name is not None else fn.__name__}"
self.__value__ = pyjs_bridge(fn, inject_vue_instance=not static)
def custom(key, name=None, static=False):
def wrapper(fn):
return Custom(fn, key, name, static)
return wrapper
================================================
FILE: vue/decorators/data.py
================================================
from .base import pyjs_bridge, VueDecorator
class Data(VueDecorator):
def __init__(self, name, value):
self.__key__ = f"data.{name}"
self.__value__ = value
def data(fn):
return Data(fn.__name__, pyjs_bridge(fn))
================================================
FILE: vue/decorators/directive.py
================================================
from .base import pyjs_bridge, VueDecorator
def map_hook(hook_name):
if hook_name == "component_updated":
return "componentUpdated"
return hook_name
class DirectiveHook(VueDecorator):
def __init__(self, fn, hooks=(), name=None):
name = name if name else fn.__name__
self.__key__ = f"directives.{name.replace('_', '-')}"
self.__value__ = pyjs_bridge(fn)
if hooks:
self.__value__ = {map_hook(hook): self.__value__ for hook in hooks}
def _directive_hook(name, hooks):
def wrapper(fn):
_hooks = (fn.__name__,) if not hooks else hooks
return DirectiveHook(fn, hooks=_hooks, name=name)
return wrapper
def directive(fn, *hooks):
if callable(fn):
return DirectiveHook(fn)
return _directive_hook(fn, hooks)
================================================
FILE: vue/decorators/extends.py
================================================
from .base import VueDecorator
class Extends(VueDecorator):
__key__ = "extends"
def __init__(self, init_dict):
self.__value__ = init_dict
================================================
FILE: vue/decorators/filters.py
================================================
from .base import pyjs_bridge, VueDecorator
class Filter(VueDecorator):
def __init__(self, fn, name):
self.name = name
self.__key__ = f"filters.{name}"
self.__value__ = pyjs_bridge(fn)
def filters(fn):
return Filter(fn, fn.__name__)
================================================
FILE: vue/decorators/getter.py
================================================
from .base import pyjs_bridge, VueDecorator
from vue.bridge import VuexInstance
class Getter(VueDecorator):
def __init__(self, name, value):
self.__key__ = f"getters.{name}"
self.__value__ = value
def getter(fn):
def wrapper(state, getters, *args):
if fn.__code__.co_argcount == 1:
return fn(VuexInstance(state=state, getters=getters))
else:
def getter_method(*args_, **kwargs):
return fn(VuexInstance(state=state, getters=getters), *args_, **kwargs)
return getter_method
return Getter(fn.__name__, pyjs_bridge(wrapper))
================================================
FILE: vue/decorators/lifecycle_hook.py
================================================
from .base import pyjs_bridge, VueDecorator
class LifecycleHook(VueDecorator):
mapping = {
"before_create": "beforeCreate",
"created": "created",
"before_mount": "beforeMount",
"mounted": "mounted",
"before_update": "beforeUpdate",
"updated": "updated",
"before_destroy": "beforeDestroy",
"destroyed": "destroyed",
}
def __init__(self, name, fn):
self.__key__ = self.mapping[name]
self.__value__ = pyjs_bridge(fn, inject_vue_instance=True)
def lifecycle_hook(name):
def wrapper(fn):
return LifecycleHook(name, fn)
return wrapper
================================================
FILE: vue/decorators/method.py
================================================
from .base import pyjs_bridge, VueDecorator
class Method(VueDecorator):
def __init__(self, fn):
if hasattr(fn, "__coroutinefunction__"):
fn = coroutine(fn)
self.__value__ = pyjs_bridge(fn, inject_vue_instance=True)
self.__key__ = f"methods.{fn.__name__}"
def coroutine(_coroutine):
def wrapper(*args, **kwargs):
import asyncio
return asyncio.ensure_future(_coroutine(*args, **kwargs))
wrapper.__name__ = _coroutine.__name__
return wrapper
def method(_method):
if hasattr(_method, "__coroutinefunction__"):
_method = coroutine(_method)
return Method(_method)
================================================
FILE: vue/decorators/mixins.py
================================================
from .base import VueDecorator
class Mixins(VueDecorator):
__key__ = "mixins"
def __init__(self, *mixins):
self.__value__ = list(mixins)
================================================
FILE: vue/decorators/model.py
================================================
from .base import VueDecorator
class Model(VueDecorator):
__key__ = "model"
def __init__(self, prop="value", event="input"):
self.prop = prop
self.event = event
@property
def __value__(self):
return {"prop": self.prop, "event": self.event}
================================================
FILE: vue/decorators/mutation.py
================================================
from vue.bridge import VuexInstance
from .base import pyjs_bridge, VueDecorator
class Mutation(VueDecorator):
def __init__(self, name, value):
self.__key__ = f"mutations.{name}"
self.__value__ = value
def mutation(fn):
def wrapper(state, payload):
return fn(
VuexInstance(state=state),
*payload.get("args", ()),
**payload.get("kwargs", {}),
)
return Mutation(fn.__name__, pyjs_bridge(wrapper))
================================================
FILE: vue/decorators/plugin.py
================================================
from .base import VueDecorator
class Plugin(VueDecorator):
__key__ = "plugins"
def __init__(self, plugins):
self.__value__ = list(plugins)
================================================
FILE: vue/decorators/prop.py
================================================
from browser import window
from .base import pyjs_bridge, VueDecorator
class Prop(VueDecorator):
type_map = {
int: window.Number,
float: window.Number,
str: window.String,
bool: window.Boolean,
list: window.Array,
object: window.Object,
dict: window.Object,
None: None,
}
def __init__(self, name, typ, mixin=None):
mixin = mixin if mixin else {}
self.__key__ = f"props.{name}"
self.__value__ = {"type": self.type_map[typ], **mixin}
class Validator(VueDecorator):
def __init__(self, prop, fn):
self.__key__ = f"props.{prop}.validator"
self.__value__ = pyjs_bridge(fn, inject_vue_instance=True)
def validator(prop):
def decorator(fn):
return Validator(prop, fn)
return decorator
================================================
FILE: vue/decorators/render.py
================================================
from .base import pyjs_bridge, VueDecorator
class Render(VueDecorator):
__key__ = "render"
def __init__(self, fn):
self.__value__ = pyjs_bridge(fn, inject_vue_instance=True)
================================================
FILE: vue/decorators/routes.py
================================================
from .base import VueDecorator
class Routes(VueDecorator):
__key__ = "routes"
def __init__(self, routes):
self.__value__ = list(routes)
================================================
FILE: vue/decorators/state.py
================================================
from .base import VueDecorator
class State(VueDecorator):
def __init__(self, name, value):
self.__key__ = f"state.{name}"
self.__value__ = value
================================================
FILE: vue/decorators/template.py
================================================
from .base import VueDecorator
class Template(VueDecorator):
__key__ = "template"
def __init__(self, template):
self.__value__ = template
================================================
FILE: vue/decorators/watcher.py
================================================
from .base import pyjs_bridge, VueDecorator
class Watcher(VueDecorator):
def __init__(self, name, fn, deep=False, immediate=False):
self.__key__ = f"watch.{name}"
self._fn = pyjs_bridge(fn, inject_vue_instance=True)
self._deep = deep
self._immediate = immediate
@property
def __value__(self):
return {"handler": self._fn, "deep": self._deep, "immediate": self._immediate}
def watch(name, deep=False, immediate=False):
def decorator(fn):
return Watcher(name, fn, deep, immediate)
return decorator
================================================
FILE: vue/router.py
================================================
from browser import window
from .transformers import Transformable, VueRouterTransformer
class VueRouter(Transformable):
RouterClass = None
@classmethod
def init_dict(cls):
return VueRouterTransformer.transform(cls)
def __new__(cls):
router_class = cls.RouterClass or window.VueRouter
return router_class.new(cls.init_dict())
class VueRoute:
def __new__(cls, path, component=None, components=None, **kwargs):
route = {"path": path, **kwargs}
if component is not None:
route["component"] = component.init_dict()
elif components is not None:
route["components"] = {
name: component.init_dict() for name, component in components.items()
}
return route
================================================
FILE: vue/store.py
================================================
from browser import window
from .transformers import Transformable, VueStoreTransformer
from .bridge import Object
from .bridge.vuex_instance import VuexInstance
class VueStore(Transformable):
@classmethod
def init_dict(cls):
return VueStoreTransformer.transform(cls)
def __new__(cls):
return Object.from_js(window.Vuex.Store.new(cls.init_dict()))
class VueStorePlugin:
def initialize(self, store):
raise NotImplementedError()
def subscribe(self, state, mutation, *args, **kwargs):
raise NotImplementedError()
def __subscribe__(self, muation_info, state):
self.subscribe(
VuexInstance(state=state),
muation_info["type"],
*muation_info["payload"]["args"],
**muation_info["payload"]["kwargs"],
)
def install(self, store):
self.initialize(
VuexInstance(
state=store.state,
getters=store.getters,
commit=store.commit,
dispatch=store.dispatch,
)
)
store.subscribe(self.__subscribe__)
================================================
FILE: vue/transformers.py
================================================
"""
Transformers are used to create dictionaries to initialize Vue-Objects from Python classes.
e.g.
```python
class Component(VueComponent):
prop: str
def method(self):
...
@computed
def computed_value(self):
...
```
will be transformed into
```javascript
{
props: {
prop: {
type: window.String,
}
},
method: // wrapper calling Component.method,
computed: {
computed_value: {
get: // wrapper calling Component.computed_value
}
}
}
```
"""
from .decorators.base import VueDecorator
from .decorators.prop import Prop
from .decorators.data import Data
from .decorators.computed import Computed
from .decorators.lifecycle_hook import LifecycleHook
from .decorators.method import Method
from .decorators.render import Render
from .decorators.mixins import Mixins
from .decorators.template import Template
from .decorators.directive import DirectiveHook
from .decorators.extends import Extends
from .decorators.components import Components
from .decorators.state import State
from .decorators.plugin import Plugin
from .decorators.routes import Routes
def _merge_templates(sub):
def get_template_slots(cls):
template_slots = getattr(cls, "template_slots", {})
if isinstance(template_slots, str):
template_slots = {"default": template_slots}
return template_slots
base = sub.__bases__[0]
template_merging = hasattr(base, "template") and getattr(
sub, "template_slots", False
)
if template_merging:
base_template = _merge_templates(base)
base_slots = get_template_slots(base)
sub_slots = get_template_slots(sub)
slots = dict(tuple(base_slots.items()) + tuple(sub_slots.items()))
default = slots.get("default")
return base_template.format(default, **slots)
return getattr(sub, "template", "{}")
class _TransformableType(type):
pass
class Transformable(metaclass=_TransformableType):
pass
class ClassAttributeDictTransformer:
"""
Takes all attributes of a class and creates a dictionary out of it.
For each attribute a decorator is retrieved.
The decorator is given by the `decorate` method wich should be overriden by sub-classes.
The decorator has a `__key__` attribute to determine where in the resulting dictionary
the value given by the `__value__` attribute should be stored.
The resulting dictionaries can be used to be passed to Vue as initializers.
"""
@classmethod
def transform(cls, transformable):
if not isinstance(transformable, _TransformableType):
return transformable
result = {}
for attribute_name, attribute_value in cls._iter_attributes(transformable):
decorator = cls.decorate(transformable, attribute_name, attribute_value)
if decorator is not None:
cls._inject_into(result, decorator.__key__, decorator.__value__)
return result
@classmethod
def decorate(cls, transformable, attribute_name, attribute_value):
if isinstance(attribute_value, VueDecorator):
return attribute_value
return None
@classmethod
def _iter_attributes(cls, transformable):
all_objects = set(dir(transformable))
all_objects.update(getattr(transformable, "__annotations__", {}).keys())
own_objects = (
all_objects - set(dir(cls._get_base(transformable))) - {"__annotations__"}
)
for attribute_name in own_objects:
yield attribute_name, getattr(transformable, attribute_name, None)
@classmethod
def _get_base(cls, transformable):
base = transformable.__bases__[0]
if base is Transformable:
return transformable
return cls._get_base(base)
@classmethod
def _inject_into(cls, destination, key, value):
keys = key.split(".")
value_key = keys.pop()
for key in keys:
destination = destination.setdefault(key, {})
if isinstance(destination.get(value_key), dict):
destination[value_key].update(value)
else:
destination[value_key] = value
class VueComponentTransformer(ClassAttributeDictTransformer):
"""
Takes a VueComponent-class and transforms it into a dictionary
which can be passed to e.g. window.Vue.new or window.Vue.component
"""
@classmethod
def transform(cls, transformable):
init_dict = super().transform(transformable)
_data = init_dict.get("data", None)
if not _data:
return init_dict
def get_initialized_data(this):
initialized_data = {}
for name, date in _data.items():
initialized_data[name] = date(this) if callable(date) else date
return initialized_data
init_dict.update(data=get_initialized_data)
return init_dict
@classmethod
def decorate(cls, transformable, attribute_name, attribute_value):
decorated = super().decorate(transformable, attribute_name, attribute_value)
if decorated is not None:
return decorated
if attribute_name in LifecycleHook.mapping:
return LifecycleHook(attribute_name, attribute_value)
if attribute_name == "template":
return Template(_merge_templates(transformable))
if attribute_name == "extends" and attribute_value:
if not attribute_value:
return None
extends = (
transformable.__bases__[0]
if isinstance(attribute_value, bool)
else attribute_value
)
return Extends(VueComponentTransformer.transform(extends))
if attribute_name == "mixins":
return Mixins(
*(VueComponentTransformer.transform(m) for m in attribute_value)
)
if attribute_name == "components":
return Components(
*(VueComponentTransformer.transform(m) for m in attribute_value)
)
if attribute_name == "render":
return Render(attribute_value)
if callable(attribute_value):
return Method(attribute_value)
if attribute_name in getattr(transformable, "__annotations__", {}):
mixin = {"required": True}
if attribute_name in dir(transformable):
mixin = {"default": getattr(transformable, attribute_name)}
return Prop(
attribute_name,
transformable.__annotations__[attribute_name],
mixin,
)
return Data(attribute_name, attribute_value)
class VueDirectiveTransformer(ClassAttributeDictTransformer):
"""
Takes a VueDirective-class and transforms it into a dictionary
which can be passed to window.Vue.directive
"""
@classmethod
def transform(cls, transformable):
default = {transformable.name: {}}
dct = super().transform(transformable)
return dct.get("directives", default).popitem()[1]
@classmethod
def decorate(cls, transformable, attribute_name, attribute_value):
if callable(attribute_value):
attribute_value = DirectiveHook(
attribute_value, hooks=(attribute_name,), name=transformable.name
)
return super().decorate(transformable, attribute_name, attribute_value)
class VueStoreTransformer(ClassAttributeDictTransformer):
"""
Takes a VueStore-class and transforms it into a dictionary
which can be passed to window.Vuex.Store.new
"""
@classmethod
def decorate(cls, transformable, attribute_name, attribute_value):
if attribute_name == "plugins":
return Plugin(attribute_value)
decorated = super().decorate(transformable, attribute_name, attribute_value)
if decorated is None:
return State(attribute_name, attribute_value)
return decorated
class VueRouterTransformer(ClassAttributeDictTransformer):
"""
Takes a VueStore-class and transforms it into a dictionary
which can be passed to window.VueRouter
"""
@classmethod
def decorate(cls, transformable, attribute_name, attribute_value):
if attribute_name == "routes":
return Routes(attribute_value)
return super().decorate(transformable, attribute_name, attribute_value)
================================================
FILE: vue/utils.py
================================================
from browser import window, load
CACHE = {}
def js_load(path):
if path in CACHE:
return CACHE[path]
before = dir(window)
load(path)
after = dir(window)
diff = set(after) - set(before)
mods = {module: getattr(window, module) for module in diff if "$" not in module}
if len(mods) == 0:
mods = None
elif len(mods) == 1:
mods = mods.popitem()[1]
CACHE[path] = mods
return mods
def js_lib(name):
attr = getattr(window, name)
if dir(attr) == ["default"]:
return attr.default
return attr
================================================
FILE: vue/vue.py
================================================
from browser import window
from .transformers import (
VueComponentTransformer,
Transformable,
VueDirectiveTransformer,
)
from .bridge import Object
from .decorators.directive import DirectiveHook
from .decorators.filters import Filter
class Vue:
@staticmethod
def directive(name, directive=None):
if directive is None and isinstance(name, str):
return window.Vue.directive(name)
if directive is None:
directive = name
name = directive.__name__.lower()
if not isinstance(directive, type):
class FunctionDirective(VueDirective):
d = DirectiveHook(directive)
directive = FunctionDirective
window.Vue.directive(name, VueDirectiveTransformer.transform(directive))
@staticmethod
def filter(method_or_name, method=None):
if not method:
method = method_or_name
name = method_or_name.__name__
else:
method = method
name = method_or_name
flt = Filter(method, name)
window.Vue.filter(flt.name, flt.__value__)
@staticmethod
def mixin(mixin):
window.Vue.mixin(VueComponentTransformer.transform(mixin))
@staticmethod
def use(plugin, *args, **kwargs):
window.Vue.use(plugin, *args, kwargs)
@staticmethod
def component(component_or_name, component=None):
if isinstance(component_or_name, str) and component is None:
return window.Vue.component(component_or_name)
if component is not None:
name = component_or_name
else:
component = component_or_name
name = component.__name__
window.Vue.component(name, VueComponentTransformer.transform(component))
class VueComponent(Transformable):
@classmethod
def init_dict(cls):
return VueComponentTransformer.transform(cls)
def __new__(cls, el, **kwargs):
init_dict = cls.init_dict()
init_dict.update(el=el)
for key, value in kwargs.items():
if key == "props_data":
key = "propsData"
init_dict.update({key: value})
return Object.from_js(window.Vue.new(Object.to_js(init_dict)))
@classmethod
def register(cls, name=None):
if name:
Vue.component(name, cls)
else:
Vue.component(cls)
class VueMixin(Transformable):
pass
class VueDirective(Transformable):
name = None
class VuePlugin:
@staticmethod
def install(*args, **kwargs):
raise NotImplementedError()
================================================
FILE: vuecli/__init__.py
================================================
================================================
FILE: vuecli/cli.py
================================================
import sys
import argparse
from tempfile import TemporaryDirectory as TempDir
from pathlib import Path
from vuecli.provider import RegisteredProvider
from vuecli.provider.static import Static as StaticProvider
from vue import __version__
def deploy(provider_class, arguments):
if provider_class is None:
print(
f"'pip install vuepy[{arguments.deploy}]' to use this provider",
file=sys.stderr,
)
sys.exit(1)
deploy_arguments = {
name.strip("-"): getattr(arguments, name.strip("-"), False)
for name in provider_class.Arguments
}
provider = provider_class(arguments.src)
provider.setup()
provider.deploy(**deploy_arguments)
def package(destination, app):
with TempDir() as apptemp, TempDir() as deploytemp:
appdir = Path(app if app else apptemp)
deploydir = Path(deploytemp)
provider = StaticProvider(appdir)
provider.setup()
provider.deploy(deploydir, package=True)
Path(destination, "vuepy.js").write_text(
(deploydir / "vuepy.js").read_text(encoding="utf-8")
)
def main():
cli = argparse.ArgumentParser(description="vue.py command line interface")
cli.add_argument("--version", action="version", version=f"vue.py {__version__}")
command = cli.add_subparsers(title="commands", dest="cmd")
deploy_cmd = command.add_parser("deploy", help="deploy application")
provider_cmd = deploy_cmd.add_subparsers(help="Provider")
for name, provider in RegisteredProvider.items():
sp = provider_cmd.add_parser(name)
sp.set_defaults(deploy=name)
if provider is not None:
for arg_name, config in provider.Arguments.items():
if isinstance(config, str):
config = {"help": config}
sp.add_argument(arg_name, **config)
deploy_cmd.add_argument(
"--src",
default=".",
nargs="?",
help="Path of the application to deploy (default: '.')",
)
package_cmd = command.add_parser("package", help="create vuepy.js")
package_cmd.add_argument(
"destination", default=".", nargs="?", help="(default: current directory)"
)
package_cmd.add_argument(
"--app", nargs="?", default=False, help="include application in package"
)
args = cli.parse_args()
if args.cmd == "deploy":
deploy(RegisteredProvider[args.deploy], args)
elif args.cmd == "package":
package(args.destination, "." if args.app is None else args.app)
else:
cli.print_help()
if __name__ == "__main__":
main()
================================================
FILE: vuecli/index.html
================================================
{% for script in scripts.values() %}
{% endfor %}
{% for stylesheet in stylesheets %}
{% endfor %}
{% for id, content in templates.items() %}
{% endfor %}
================================================
FILE: vuecli/provider/__init__.py
================================================
import pkg_resources as _pkgres
def _load(ep):
try:
return ep.load()
except ModuleNotFoundError:
return None
RegisteredProvider = {
entry_point.name: _load(entry_point)
for entry_point in _pkgres.iter_entry_points("vuecli.provider")
}
================================================
FILE: vuecli/provider/flask.py
================================================
import os
from pathlib import Path
from flask import Flask as FlaskApp, send_file, abort
from .provider import Provider
class Flask(Provider):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.app = FlaskApp(__name__)
def content(self, endpoint, route, content):
self.app.add_url_rule(route, endpoint, content)
def directory(self, endpoint, route, path, deep=False):
def view_func(filename):
full_path = Path(path, filename)
if not full_path.exists():
abort(404)
return send_file(str(full_path.absolute()))
flask_route = os.path.join(
route, "<{}filename>".format("path:" if deep else "")
)
self.app.add_url_rule(flask_route, endpoint, view_func)
def deploy(self):
flask_config = self.config.get("provider", {}).get("flask", {})
host = flask_config.pop("HOST", None)
port = flask_config.pop("PORT", None)
for key, value in flask_config.items():
self.app.config[key] = value
self.app.run(host=host, port=port)
================================================
FILE: vuecli/provider/provider.py
================================================
from pkg_resources import resource_filename, resource_string
from functools import partial
from pathlib import Path
import yaml
from jinja2 import Template
VuePath = resource_filename("vue", "")
IndexTemplate = resource_string("vuecli", "index.html")
StaticContents = {
"/loading.gif": resource_string("vuecli", "loading.gif"),
"/vuepy.js": b"\n".join(
[
resource_string("brython", "data/brython.js"),
resource_string("brython", "data/brython_stdlib.js"),
]
),
"/vue.js": resource_string("vuecli", "js/vue.js"),
"/vuex.js": resource_string("vuecli", "js/vuex.js"),
"/vue-router.js": resource_string("vuecli", "js/vue-router.js"),
}
class Provider:
Arguments = {}
def __init__(self, path=None):
self.path = Path(path if path else ".")
self.config = self.load_config()
@staticmethod
def _normalize_config(config):
default_scripts = {
"vuepy": "vuepy.js",
"vue": "vue.js",
"vuex": "vuex.js",
"vue-router": "vue-router.js",
}
scripts = {"vuepy": True, "vue": True}
custom_scripts = config.get("scripts", {})
if isinstance(custom_scripts, list):
custom_scripts = {k: k for k in custom_scripts}
scripts.update(custom_scripts)
config["scripts"] = {
k: default_scripts[k] if v is True else v for k, v in scripts.items() if v
}
def load_config(self):
config_file = Path(self.path, "vuepy.yml")
config = {}
if config_file.exists():
with open(config_file, "r") as fh:
config = yaml.safe_load(fh.read()) or config
self._normalize_config(config)
return config
def render_index(self):
brython_args = self.config.get("brython_args", {})
if brython_args:
joined = ", ".join(f"{k}: {v}" for k, v in brython_args.items())
brython_args = f"{{ {joined} }}"
else:
brython_args = ""
return Template(IndexTemplate.decode("utf-8")).render(
stylesheets=self.config.get("stylesheets", []),
scripts=self.config.get("scripts", {}),
templates={
id_: Path(self.path, template).read_text("utf-8")
for id_, template in self.config.get("templates", {}).items()
},
brython_args=brython_args,
)
def setup(self):
self.directory("application", "/", Path(self.path), deep=True)
self.directory("vuepy", "/vue", VuePath, deep=True)
entry_point = self.config.get("entry_point", "app")
self.content(
"entry_point", "/__entry_point__.py", lambda: f"import {entry_point}\n"
)
self.content("index", "/", lambda: self.render_index())
for route in StaticContents:
self.content(route, route, partial(StaticContents.get, route))
def content(self, endpoint, route, content):
raise NotImplementedError()
def directory(self, endpoint, route, path, deep=False):
raise NotImplementedError()
def deploy(self, **kwargs):
raise NotImplementedError()
================================================
FILE: vuecli/provider/static.py
================================================
import os
import sys
import shutil
from tempfile import TemporaryDirectory as TempDir
import subprocess
from pathlib import Path
from .provider import Provider
def copytree(src, dst, deep=True):
if not dst.exists():
dst.mkdir()
for item in os.listdir(src):
s = Path(src, item)
d = Path(dst, item)
if s.is_dir() and deep:
d.mkdir()
copytree(s, d)
elif s.is_file():
shutil.copy2(str(s), str(d))
class Static(Provider):
Arguments = {
"destination": "Path where the application should be deployed to",
"--package": {"action": "store_true", "help": "adds application to vuepy.js"},
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._tempdir = TempDir()
@property
def temppath(self):
return self._tempdir.name
def content(self, endpoint, route, content):
path = self.temppath / Path(route).relative_to("/")
if path.is_dir():
path = path / "index.html"
content = content()
mode = "w+" if isinstance(content, str) else "wb+"
with open(path, mode) as dest_file:
dest_file.write(content)
def directory(self, endpoint, route, path, deep=False):
dest = self.temppath / Path(route).relative_to("/")
copytree(Path(path), dest, deep=deep)
def deploy(self, destination, package=False):
try:
rel_depolypath = (
Path(destination).absolute().relative_to(Path(self.path).absolute())
)
except ValueError:
pass
else:
shutil.rmtree(str(Path(self.temppath) / rel_depolypath), ignore_errors=True)
if package:
self._create_package()
shutil.rmtree(destination, ignore_errors=True)
shutil.copytree(self.temppath, Path(destination))
self._tempdir.cleanup()
def _create_package(self):
self._brython("--make_package", "app")
Path(self.temppath, "vuepy.js").write_text(
Path(self.temppath, "vuepy.js").read_text(encoding="utf-8")
+ "\n"
+ Path(self.temppath, "app.brython.js").read_text(encoding="utf-8")
)
def _brython(self, *args):
completed_process = subprocess.run(
[sys.executable, "-m", "brython", *args],
cwd=str(self.temppath),
stdout=subprocess.PIPE,
)
if completed_process.returncode:
raise RuntimeError(completed_process.returncode)