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 [![Build Status](https://github.com/stefanhoelzl/vue.py/workflows/CI/badge.svg)](https://github.com/stefanhoelzl/vue.py/actions) [![PyPI](https://img.shields.io/pypi/v/vuepy.svg)](https://pypi.org/project/vuepy/) [![License](https://img.shields.io/pypi/l/vuepy.svg)](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 = """ """ 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 = """

{{ checked }}

""" ``` ================================================ 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 = """

Go to Foo Go to Bar

""" 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 ================================================ ================================================ 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 }} """ @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 ================================================

Latest vue.py Commits

stefanhoelzl/vue.py@{{ current_branch }}

================================================ 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) ![TodoMVC Screenshot](https://raw.githubusercontent.com/stefanhoelzl/vue.py/gh-pages/examples/todo_mvc/screenshot.png) ## SVG Graph [Demo](https://stefanhoelzl.github.io/vue.py/examples/svg_graph) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/svg_graph) ![SVG Graph Screenshot](https://raw.githubusercontent.com/stefanhoelzl/vue.py/gh-pages/examples/svg_graph/screenshot.png) ## Markdown Editor [Demo](https://stefanhoelzl.github.io/vue.py/examples/markdown_editor) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/markdown_editor) ![Markdown Editor Screenshot](https://raw.githubusercontent.com/stefanhoelzl/vue.py/gh-pages/examples/markdown_editor/screenshot.png) ## GitHub Commits [Demo](https://stefanhoelzl.github.io/vue.py/examples/github_commits) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/github_commits) ![GitHub Commits Screenshot](https://raw.githubusercontent.com/stefanhoelzl/vue.py/gh-pages/examples/github_commits/screenshot.png) ## Grid Component [Demo](https://stefanhoelzl.github.io/vue.py/examples/grid_component) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/grid_component) ![Grid Component Screenshot](https://raw.githubusercontent.com/stefanhoelzl/vue.py/gh-pages/examples/grid_component/screenshot.png) ## Tree View [Demo](https://stefanhoelzl.github.io/vue.py/examples/tree_view) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/tree_view) ![Tree View Screenshot](https://raw.githubusercontent.com/stefanhoelzl/vue.py/gh-pages/examples/tree_view/screenshot.png) ## Modal Component [Demo](https://stefanhoelzl.github.io/vue.py/examples/modal_component) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/modal_component) ![Modal Component Screenshot](https://raw.githubusercontent.com/stefanhoelzl/vue.py/gh-pages/examples/modal_component/screenshot.png) ## Elastic Header [Demo](https://stefanhoelzl.github.io/vue.py/examples/elastic_header) / [Source](https://github.com/stefanhoelzl/vue.py/tree/master/examples/elastic_header) ![Elastic Header Screenshot](https://raw.githubusercontent.com/stefanhoelzl/vue.py/gh-pages/examples/elastic_header/screenshot.png) ## 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 ================================================

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 ================================================
{{stat.value}}
{{ stats }}

* input[type="range"] requires IE10 or above.

================================================ 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 ================================================

todos

================================================ 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 = """
    {{ text }}
    """ 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 = """

    {{ checked }}

    """ CustomVModel.register("custom-vmodel") class App(VueComponent): clicked = False template = """

    {{ clicked }}

    """ 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 = """ """ 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 = """

    {{ text }}

    """ 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 = """

    {{ new_val }}

    """ 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 = """

    {{ message }}

    """ 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 = """ """ 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 = ( "
    " "
    DONT SHOW
    " "
    " "
    " ) 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 = ( "
      " "
    1. {{ item }}
    2. " "
    " ) 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 = "" 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 = ( "
    " "

    {{ clicked }}

    " " " "
    " ) 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 = """

    HEADER

    """ 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 = """

    {{ text }}

    {{ 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 = """

    Go to Foo Go to Bar

    """ 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 = """

    User

    """ 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 = """

    Go to Foo Go to Bar


    """ 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)