├── .github └── workflows │ ├── .deploy_docs.yml │ ├── build_and_test.yml │ └── pypi_publish.yml ├── .gitignore ├── .readthedocs.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── codecov.yml ├── docs ├── basic_usage.md ├── builtin_types.md ├── dash_confs.md ├── dash_embed.md ├── imgs │ ├── arg_label_qt.png │ ├── bmi_cli.png │ ├── bmi_dash.png │ ├── bmi_qt.png │ ├── builtin_example_dash.png │ ├── builtin_example_qt.png │ ├── dash_app_flask_embed.gif │ ├── download_res_dash.gif │ ├── example.png │ ├── interactive_arg_dash.gif │ ├── logo-light.png │ ├── logo.png │ ├── long_str.png │ ├── meme_gui.png │ ├── person_ext_dash.png │ ├── person_ext_qt.png │ ├── print_person_dash.png │ ├── print_person_qt.png │ ├── qt_app_embed.gif │ ├── random_points_dash_app.gif │ ├── random_series_dash_app.gif │ ├── rename_example_qt.png │ ├── run_not_once.gif │ └── run_once.gif ├── index.md ├── qt_confs.md ├── qt_embed.md ├── requirements.txt ├── type_extension.md └── wrap_cli.md ├── mkdocs.yml ├── oneface ├── __init__.py ├── check.py ├── core.py ├── dash_app │ ├── __init__.py │ ├── app.py │ ├── embed.py │ └── input_item.py ├── qt.py ├── utils.py └── wrap_cli │ ├── __init__.py │ ├── __main__.py │ ├── example.yaml │ └── wrap.py ├── requirements.txt ├── setup.py └── tests ├── conftest.py ├── test_check.py ├── test_cli.py ├── test_dash.py ├── test_qt.py └── test_wrap_cli.py /.github/workflows/.deploy_docs.yml: -------------------------------------------------------------------------------- 1 | name: Deploy Docs 2 | on: 3 | push: 4 | branches: 5 | - master 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - uses: actions/setup-python@v2 12 | with: 13 | python-version: 3.x 14 | - run: pip install mkdocs-material 15 | - run: mkdocs gh-deploy --force 16 | -------------------------------------------------------------------------------- /.github/workflows/build_and_test.yml: -------------------------------------------------------------------------------- 1 | name: Python package 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | python-version: [3.8, 3.9, "3.10"] 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Python ${{ matrix.python-version }} 16 | uses: actions/setup-python@v2 17 | with: 18 | python-version: ${{ matrix.python-version }} 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install .[all] 23 | pip install .[test] 24 | - name: Lint with flake8 25 | run: | 26 | # stop the build if there are Python syntax errors or undefined names 27 | flake8 . --exclude tests/ --count --show-source --statistics 28 | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide 29 | flake8 . --exclude tests/ --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics 30 | - name: Test with pytest 31 | run: | 32 | pytest --ignore ./tests/test_qt.py --cov=./ tests/ --cov-report=xml 33 | - name: Upload to codecov 34 | uses: codecov/codecov-action@v2 35 | with: 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | fail_ci_if_error: true 38 | -------------------------------------------------------------------------------- /.github/workflows/pypi_publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will upload a Python Package using Twine when a release is created 2 | # For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries 3 | 4 | name: Upload Python Package 5 | 6 | on: 7 | release: 8 | types: [created] 9 | 10 | jobs: 11 | deploy: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Set up Python 18 | uses: actions/setup-python@v2 19 | with: 20 | python-version: '3.x' 21 | - name: Install dependencies 22 | run: | 23 | python -m pip install --upgrade pip 24 | pip install setuptools wheel twine 25 | - name: Build and publish 26 | env: 27 | TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} 28 | TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} 29 | run: | 30 | python setup.py sdist bdist_wheel 31 | twine upload dist/* 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 105 | __pypackages__/ 106 | 107 | # Celery stuff 108 | celerybeat-schedule 109 | celerybeat.pid 110 | 111 | # SageMath parsed files 112 | *.sage.py 113 | 114 | # Environments 115 | .env 116 | .venv 117 | env/ 118 | venv/ 119 | ENV/ 120 | env.bak/ 121 | venv.bak/ 122 | 123 | # Spyder project settings 124 | .spyderproject 125 | .spyproject 126 | 127 | # Rope project settings 128 | .ropeproject 129 | 130 | # mkdocs documentation 131 | /site 132 | 133 | # mypy 134 | .mypy_cache/ 135 | .dmypy.json 136 | dmypy.json 137 | 138 | # Pyre type checker 139 | .pyre/ 140 | 141 | # pytype static type analyzer 142 | .pytype/ 143 | 144 | # Cython debug symbols 145 | cython_debug/ 146 | 147 | # PyCharm 148 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 149 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 150 | # and can be added to the global gitignore or merged into this file. For a more nuclear 151 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 152 | #.idea/ 153 | 154 | # VSCode 155 | .vscode/ 156 | 157 | tmp/ -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # .readthedocs.yaml 2 | # Read the Docs configuration file 3 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 4 | 5 | # Required 6 | version: 2 7 | 8 | # Set the version of Python and other tools you might need 9 | build: 10 | os: ubuntu-20.04 11 | tools: 12 | python: "3.9" 13 | 14 | mkdocs: 15 | configuration: mkdocs.yml 16 | 17 | # Optionally declare the Python requirements required to build your docs 18 | python: 19 | install: 20 | - requirements: docs/requirements.txt 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023, Weize Xu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft docs/ 2 | 3 | include LICENSE 4 | include README.md 5 | include oneface/wrap_cli/example.yaml 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 |

oneFace is a Python library for automatically generating multiple interfaces(CLI, GUI, WebGUI) from a callable Python object or a command line program.

6 | 7 |

8 | 9 | Build Status 10 | 11 | 12 | codecov 13 | 14 | 15 | Documentation 16 | 17 | 18 | Install with PyPi 19 | 20 |

21 | 22 |
23 | 24 | 25 | oneFace is an easy way to create interfaces. 26 | 27 | In Python, just decorate your function and mark the type and range of the arguments: 28 | 29 | ```Python 30 | from oneface import one, Val 31 | 32 | @one 33 | def bmi(name: str, 34 | height: Val[float, [100, 250]] = 160, 35 | weight: Val[float, [0, 300]] = 50.0): 36 | BMI = weight / (height / 100) ** 2 37 | print(f"Hi {name}. Your BMI is: {BMI}") 38 | return BMI 39 | 40 | 41 | # run cli 42 | bmi.cli() 43 | # or run qt_gui 44 | bmi.qt_gui() 45 | # or run dash web app 46 | bmi.dash_app() 47 | ``` 48 | 49 | These code will generate the following interfaces: 50 | 51 | | CLI | Qt | Dash | 52 | | ---- | -- | ---- | 53 | | ![CLI](./docs/imgs/bmi_cli.png) | ![Qt](./docs/imgs/bmi_qt.png) | ![Dash](./docs/imgs/bmi_dash.png) | 54 | 55 | ### Wrap command line program 56 | 57 | Or you can wrap a command line using a config file: 58 | 59 | ```yaml 60 | # add.yaml 61 | # This is a demo app, use for add two numbers. 62 | name: add 63 | 64 | # mark the arguments in command with: {} 65 | command: python {verbose} -c 'print({a} + {b})' 66 | 67 | inputs: 68 | # describe the type and range of your arguments 69 | verbose: 70 | type: bool 71 | default: False 72 | true_insert: "-v" # insert '-v' to the command when the value is true 73 | false_insert: "" 74 | a: 75 | type: float 76 | range: [-100.0, 100.0] 77 | default: 0.0 78 | b: 79 | type: float 80 | range: [-100.0, 100.0] 81 | default: 10.0 82 | ``` 83 | 84 | Lanuch the app with: 85 | 86 | ```Bash 87 | $ python -m oneface.wrap_cli run add.yaml dash_app # run Dash app, or: 88 | $ python -m oneface.wrap_cli run add.yaml qt_gui # run Qt GUI app 89 | ``` 90 | 91 | ## Features 92 | 93 | + Generate CLI, Qt GUI, Dash Web app from a python function or a command line. 94 | + Automatically check the type and range of input parameters and pretty print them. 95 | + Easy extension of parameter types and GUI widgets. 96 | + Support for embedding the generated interface into a parent application. 97 | 98 | Detail usage see the [documentation](https://oneface.readthedocs.io/en/latest/). 99 | 100 | ## Installation 101 | 102 | To install oneFace with complete dependency: 103 | 104 | ``` 105 | $ pip install oneface[all] 106 | ``` 107 | 108 | Or install with just qt or dash dependency: 109 | 110 | ``` 111 | $ pip install oneface[qt] # qt 112 | $ pip install oneface[dash] # dash 113 | ``` 114 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "oneface/qt.py" 3 | -------------------------------------------------------------------------------- /docs/basic_usage.md: -------------------------------------------------------------------------------- 1 | 2 | # Basic Usage 3 | 4 | Using `one` decorate the function, and use `Val` mark type and range of the arguments. 5 | 6 | ```Python 7 | from oneface import one, Val 8 | 9 | @one 10 | def print_person(name: str, age: Val[int, [0, 120]]): 11 | print(f"{name} is {age} years old.") 12 | ``` 13 | 14 | **Note**: `Val(type, range)` is same to `Val[type, range]`. 15 | 16 | ```Python 17 | # This is same to the previous defination 18 | @one 19 | def print_person(name: str, age: Val(int, [0, 120])): 20 | print(f"{name} is {age} years old.") 21 | ``` 22 | 23 | You can also mark arguments using decorators in [`funcdesc`](https://github.com/Nanguage/funcdesc): 24 | 25 | ```Python 26 | from oneface import one 27 | from funcdesc import mark_input 28 | 29 | @one 30 | @mark_input("age", range=[0, 120]) 31 | def print_person(name: str, age: int): 32 | print(f"{name} is {age} years old.") 33 | 34 | ``` 35 | 36 | This code achieves the same effect as the previous example, and you can refer to the [`funcdesc`](https://github.com/Nanguage/funcdesc) for more information about the `mark_input` decorator. 37 | 38 | 39 | ## Type and range checking 40 | 41 | Functions decorated with `one` will automatically check the type and range of input parameters: 42 | 43 | ```Python 44 | >>> print_person("Tom", 20) 45 | Run: print_person 46 | Arguments table: 47 | 48 | Argument Type Range InputVal InputType 49 | name None Tom 50 | age [0, 120] 20 51 | 52 | Tom is 20 years old. 53 | ``` 54 | 55 | If we pass parameters with incorrect type or range, it will raise an exception: 56 | 57 | ```Python 58 | >>> print_person(100, -20) # incorrect input type and range 59 | Run: print_person 60 | Arguments table: 61 | 62 | Argument Type Range InputVal InputType 63 | name None 100 64 | age [0, 120] -20 65 | 66 | Traceback (most recent call last): 67 | File "C:\Users\Nangu\Desktop\oneFace\tmp\test1.py", line 9, in 68 | print_person(100, -20) 69 | File "C:\Users\Nangu\miniconda3\envs\oneface\lib\site-packages\funcdesc\guard.py", line 46, in __call__ 70 | self.check_inputs(pass_in, errors) 71 | File "C:\Users\Nangu\Desktop\oneFace\oneface\check.py", line 86, in check_inputs 72 | raise CheckError(errors) 73 | funcdesc.guard.CheckError: [TypeError("Value 100 is not in valid type()"), ValueError('Value -20 is not in a valid range([0, 120]).')] 74 | ``` 75 | 76 | ### Turn-off arguments print 77 | 78 | By default, oneface will pretty print the input arguments with a table. It can be turned off with the `print_args` parameter: 79 | 80 | ```Python 81 | @one(print_args=False) 82 | def print_person(name: str, age: Val[int, [0, 120]]): 83 | print(f"{name} is {age} years old.") 84 | 85 | >>> print_person("Tom", 20) 86 | Tom is 20 years old. 87 | ``` 88 | 89 | ## Create interfaces 90 | 91 | Create a python module `print_person.py`: 92 | 93 | ```Python 94 | from oneface import one, Arg 95 | 96 | @one 97 | def print_person(name: str, age: Arg[int, [0, 120]]): 98 | print(f"{name} is {age} years old.") 99 | 100 | print_person.cli() 101 | ``` 102 | 103 | This will create a Command Line Interface for `print_person` function. You can call this function in the Shell: 104 | 105 | ```Bash 106 | $ python print_person.py Tom 20 107 | Run: print_person 108 | Arguments table: 109 | 110 | Argument Type Range InputVal InputType 111 | name None Tom 112 | age [0, 120] 20 113 | 114 | Tom is 20 years old. 115 | ``` 116 | 117 | If you want change to another interface, just change the `.cli()` to `.qt_gui()` or `.dash_app()`. 118 | Then run this file again: 119 | 120 | ``` 121 | $ python print_person.py 122 | ``` 123 | 124 | You will got the Qt gui: 125 | 126 | ![print_person_qt](imgs/print_person_qt.png) 127 | 128 | Or Dash web app: 129 | ![print_person_dash](imgs/print_person_dash.png) 130 | -------------------------------------------------------------------------------- /docs/builtin_types.md: -------------------------------------------------------------------------------- 1 | # Built-in argument types 2 | 3 | oneFace support the following types: 4 | 5 | | Type | Example | Type check | Range check | Description | 6 | | ---- | ------- | ---------- | ----------- | ----------- | 7 | | str | `Val(str)` | `True` | `False` | String input. | 8 | | int | `Val(int, [0, 10])` | `True` | `True` | Int input. | 9 | | float | `Val(float, [0, 1])` | `True` | `True` | Float input. | 10 | | bool | `Val(bool)` | `True` | `False` | Bool input. | 11 | | OneOf | `Val(OneOf, ["a", "b", "c"])` | `False` | `True` | Input should be a element of the range. | 12 | | SubSet | `Val(SubSet, ["a", "b", "c"])` | `False` | `True` | Input should be a subset of the range. | 13 | | InputPath | `Val(InputPath)` | `True` | `True` | Input should be an exist file path(`str` or `pathlib.Path`). | 14 | | OutPath | `Val(OutputPath)` | `True` | `False` | Input should be a file path(`str` or `pathlib.Path`) | 15 | 16 | This example show all built-in types, name as `builtin_example.py`: 17 | 18 | ```Python 19 | from oneface.core import one, Val 20 | from funcdesc.types import (OneOf, SubSet, InputPath, OutputPath) 21 | 22 | @one 23 | def func(in_path: InputPath, 24 | out_path: OutputPath = "./test", 25 | a: Val[int, [0, 10], text="parameter (a)"] = 10, 26 | b: Val[float, [0, 1]] = 0.1, 27 | c: Val[str] = "aaaa", 28 | d: Val[bool] = False, 29 | e: Val[OneOf, ["a", "b", "c"]] = "a", 30 | f: Val[SubSet, ["a", "b", "c"]] = ["a"]): 31 | print(in_path, out_path) 32 | print(a, b, c, d, e, f) 33 | return a + b 34 | 35 | 36 | func.qt_gui() 37 | ``` 38 | 39 | Running the script will get: 40 | 41 | ![builtin_example_qt](./imgs/builtin_example_qt.png) 42 | 43 | Change the last line to `func.dash_app()` and run it again, you will get: 44 | 45 | ![builtin_example_dash](./imgs/builtin_example_dash.png) 46 | -------------------------------------------------------------------------------- /docs/dash_confs.md: -------------------------------------------------------------------------------- 1 | # Dash interface configs 2 | 3 | ## Hidden console 4 | 5 | oneface dash provides a terminal for displaying operational status. 6 | The `show_console` parameter is used to control whether it is displayed. 7 | 8 | ```Python 9 | from oneface import one, Val 10 | 11 | @one 12 | def bmi(name: str = "Tom", 13 | height: Val[float, [100, 250]] = 160, 14 | weight: Val[float, [0, 300]] = 50.0): 15 | BMI = weight / (height / 100) ** 2 16 | print(f"Hi {name}. Your BMI is: {BMI}") 17 | return BMI 18 | 19 | bmi.dash_app(show_console=False) 20 | ``` 21 | 22 | Will not show the console. 23 | 24 | ## Console refresh interval 25 | 26 | By default, the console is refreshed in 2 seconds (2000 microseconds). 27 | `console_interval` can be used to set the refresh interval 28 | 29 | ```Python 30 | bmi.dash_app(console_interval=1000) 31 | ``` 32 | 33 | Will set refresh interval to 1 second. 34 | 35 | ## Argument label 36 | 37 | By default, argument label is the variable name. But it can be explicitly set by `text` parameter: 38 | 39 | ```Python 40 | @one 41 | def bmi(name: Val(str, text="NAME"), # explicitly label setting 42 | height: Val(float, [100, 250]) = 160, 43 | weight: Val(float, [0, 300]) = 50.0): 44 | BMI = weight / (height / 100) ** 2 45 | print(f"Hi {name}. Your BMI is: {BMI}") 46 | return BMI 47 | ``` 48 | 49 | ## Init run 50 | 51 | By default, it is not called until the user clicks the run button. 52 | However, the initial call can be turned on by setting `init_run=True`: 53 | 54 | ```Python 55 | bmi.dash_app(init_run=True) 56 | ``` 57 | 58 | This will cause the `bmi` function to be called once automatically at the end of app initialization. 59 | In this case, all parameters need to have default values. 60 | 61 | ## Interactive parameter 62 | 63 | Interactive parameters rerun the function each time the input is changed. 64 | We can use `Val`'s parameter to mark the interactive mode, for example we mark `height` as interactive: 65 | 66 | ```Python 67 | @one 68 | def bmi(name: Val(str) = "Tom", 69 | height: Val(float, [100, 250], interactive=True) = 160, 70 | weight: Val(float, [0, 300]) = 50.0): 71 | BMI = weight / (height / 100) ** 2 72 | print(f"Hi {name}. Your BMI is: {BMI}") 73 | return BMI 74 | ``` 75 | 76 | ![interactive_arg_dash](./imgs/interactive_arg_dash.gif) 77 | 78 | And, if you pass `interactive = True` to the `.dash_app` method, it will mark all parameters as interactive: 79 | 80 | ```Python 81 | bmi.dash_app(interactive=True) 82 | ``` 83 | 84 | ## Result show type 85 | 86 | By default, the `result_show_type` is `'text'`, which means that the result will be displayed in text. 87 | In addition, the results can also be presented in other forms: 88 | 89 | 90 | ### Plotly figure type 91 | 92 | Dash app can integrate the plotly to drawing dynamic figures in HTML. 93 | By setting `result_show_type` to `'plotly'` and wrap a function return the plotly figure object, 94 | we can archieve this: 95 | 96 | ```Python 97 | from oneface import one, Val 98 | import plotly.express as px 99 | import numpy as np 100 | 101 | @one 102 | def draw_random_points(n: Val[int, [1, 10000]] = 100): 103 | x, y = np.random.random(n), np.random.random(n) 104 | fig = px.scatter(x=x, y=y) 105 | return fig 106 | 107 | draw_random_points.dash_app( 108 | result_show_type='plotly', 109 | debug=True) 110 | ``` 111 | 112 | ![random_points_dash](./imgs/random_points_dash_app.gif) 113 | 114 | ### Download type 115 | 116 | In many cases, the results of running a web application need to be downloaded as a file for the user. 117 | You can set the `result_show_type='download'` for this purpose. 118 | In this case, the target function should return the path to the result file: 119 | 120 | ```Python 121 | from oneface import one, Val 122 | 123 | @one 124 | def bmi(name: Val(str) = "Tom", 125 | height: Val(float, [100, 250], interactive=True) = 160, 126 | weight: Val(float, [0, 300]) = 50.0): 127 | BMI = weight / (height / 100) ** 2 128 | out_path = f"./{name}_bmi.txt" 129 | with open(out_path, 'w') as fo: 130 | fo.write(f"Hi {name}. Your BMI is: {BMI}") 131 | return out_path 132 | 133 | bmi.dash_app(result_show_type="download") 134 | ``` 135 | 136 | ![download_res_dash](./imgs/download_res_dash.gif) 137 | 138 | 139 | ### Custom result type 140 | 141 | You can custom the dash app layout by inherit the `oneface.dash_app.App` class. 142 | For example we can create a app draw a random series: 143 | 144 | ```Python 145 | # random_series.py 146 | from oneface.dash_app import App 147 | from oneface import Val, one 148 | import numpy as np 149 | import plotly.express as px 150 | from dash import Dash, Output, Input, dcc 151 | 152 | 153 | class PlotSeries(App): 154 | def __init__(self, func, **kwargs): 155 | super().__init__(func, **kwargs) 156 | 157 | def get_result_layout(self): 158 | # override the result layout definition 159 | layout = self.base_result_layout() 160 | layout += [ 161 | dcc.Graph(id='line-plot') 162 | ] 163 | return layout 164 | 165 | def add_result_callbacks(self, app: "Dash"): 166 | # override the result callback definition 167 | @app.callback( 168 | Output("line-plot", "figure"), 169 | Input("out", "data"), 170 | ) 171 | def plot(val): 172 | fig = px.line(val) 173 | return fig 174 | 175 | @one 176 | def random_series(n: Val[int, [0, 10000]] = 100): 177 | return np.random.random(n) * 100 178 | 179 | 180 | p = PlotSeries(random_series, debug=True) 181 | p() 182 | ``` 183 | 184 | Run this script, we get: 185 | 186 | ![random_series_dash_app](./imgs/random_series_dash_app.gif) 187 | 188 | ## Host and Port 189 | 190 | Specify the app's host and port: 191 | 192 | ```Python 193 | bmi.dash_app(host="0.0.0.0", port=9000) 194 | ``` 195 | 196 | ## debug mode 197 | 198 | The debug mode is useful for debugging errors, use `debug=True` to open it: 199 | 200 | ```Python 201 | bmi.dash_app(debug=True) 202 | ``` 203 | -------------------------------------------------------------------------------- /docs/dash_embed.md: -------------------------------------------------------------------------------- 1 | # Embeding dash to Flask web app 2 | 3 | You can embed the oneFace generated dash app in a Flask application 4 | to integrate a number of dash apps when needed, 5 | or to leverage the power of Flask for additional functionality. 6 | 7 | Here is an example where we have integrated the `add` and `mul` applications into a single Flask server: 8 | 9 | ```Python 10 | # demo_flask_integrate.py 11 | from flask import Flask, url_for 12 | from oneface.dash_app import flask_route 13 | from oneface.core import one 14 | from oneface.dash_app import app 15 | 16 | server = Flask("test_dash_app") 17 | 18 | @flask_route(server, "/add") 19 | @one 20 | def add(a: int, b: int) -> int: 21 | return a + b 22 | 23 | @flask_route(server, "/mul") 24 | @app(console_interval=500) 25 | @one 26 | def mul(a: int, b: int) -> int: 27 | return a * b 28 | 29 | @server.route("/") 30 | def index(): 31 | return f""" 32 |

Hello

33 | 34 |
35 |

You can run the following applications:

36 |
37 | 41 |
42 |
43 | """ 44 | 45 | server.run("127.0.0.1", 8088) 46 | ``` 47 | 48 | ![dash_flask_embed](./imgs/dash_app_flask_embed.gif) 49 | 50 | -------------------------------------------------------------------------------- /docs/imgs/arg_label_qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/arg_label_qt.png -------------------------------------------------------------------------------- /docs/imgs/bmi_cli.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/bmi_cli.png -------------------------------------------------------------------------------- /docs/imgs/bmi_dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/bmi_dash.png -------------------------------------------------------------------------------- /docs/imgs/bmi_qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/bmi_qt.png -------------------------------------------------------------------------------- /docs/imgs/builtin_example_dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/builtin_example_dash.png -------------------------------------------------------------------------------- /docs/imgs/builtin_example_qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/builtin_example_qt.png -------------------------------------------------------------------------------- /docs/imgs/dash_app_flask_embed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/dash_app_flask_embed.gif -------------------------------------------------------------------------------- /docs/imgs/download_res_dash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/download_res_dash.gif -------------------------------------------------------------------------------- /docs/imgs/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/example.png -------------------------------------------------------------------------------- /docs/imgs/interactive_arg_dash.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/interactive_arg_dash.gif -------------------------------------------------------------------------------- /docs/imgs/logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/logo-light.png -------------------------------------------------------------------------------- /docs/imgs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/logo.png -------------------------------------------------------------------------------- /docs/imgs/long_str.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/long_str.png -------------------------------------------------------------------------------- /docs/imgs/meme_gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/meme_gui.png -------------------------------------------------------------------------------- /docs/imgs/person_ext_dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/person_ext_dash.png -------------------------------------------------------------------------------- /docs/imgs/person_ext_qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/person_ext_qt.png -------------------------------------------------------------------------------- /docs/imgs/print_person_dash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/print_person_dash.png -------------------------------------------------------------------------------- /docs/imgs/print_person_qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/print_person_qt.png -------------------------------------------------------------------------------- /docs/imgs/qt_app_embed.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/qt_app_embed.gif -------------------------------------------------------------------------------- /docs/imgs/random_points_dash_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/random_points_dash_app.gif -------------------------------------------------------------------------------- /docs/imgs/random_series_dash_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/random_series_dash_app.gif -------------------------------------------------------------------------------- /docs/imgs/rename_example_qt.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/rename_example_qt.png -------------------------------------------------------------------------------- /docs/imgs/run_not_once.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/run_not_once.gif -------------------------------------------------------------------------------- /docs/imgs/run_once.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/docs/imgs/run_once.gif -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | oneFace is a Python library for automatically generating multiple interfaces(CLI, GUI, WebGUI) from a callable Python object. 4 | 5 | ## Features 6 | 7 | + Generate CLI, Qt GUI, Dash Web app from a python function or a command line. 8 | + Automatically check the type and range of input parameters and pretty print them. 9 | + Easy extension of parameter types and GUI widgets. 10 | + Support for embedding the generated interface into a parent application. 11 | 12 | 13 | ## Installation 14 | 15 | To install oneFace with complete dependency: 16 | 17 | ``` 18 | $ pip install "oneface[all]" 19 | ``` 20 | 21 | Or install with just qt or dash dependency: 22 | 23 | ``` 24 | $ pip install "oneface[qt]" # qt 25 | $ pip install "oneface[dash]" # dash 26 | ``` 27 | 28 | ### Qt bindings 29 | 30 | oneFace support different Qt bindings: PyQt6(default), PyQt5, PySide2, PySide6. It can be specified: 31 | 32 | ``` 33 | $ pip install "oneface[pyside2]" # for example 34 | ``` 35 | 36 | ## Example 37 | 38 | oneFace is an easy way to create interfaces in Python, 39 | just decorate your function and mark the **type** and **range** of the arguments: 40 | 41 | ```Python 42 | from oneface import one, Val 43 | 44 | @one 45 | def bmi(name: str, 46 | height: Val[float, [100, 250]] = 160, 47 | weight: Val[float, [0, 300]] = 50.0): 48 | BMI = weight / (height / 100) ** 2 49 | print(f"Hi {name}. Your BMI is: {BMI}") 50 | return BMI 51 | 52 | 53 | # run cli 54 | bmi.cli() 55 | # or run qt_gui 56 | bmi.qt_gui() 57 | # or run dash web app 58 | bmi.dash_app() 59 | ``` 60 | 61 | These code will generate the following interfaces: 62 | 63 | | CLI | Qt | Dash | 64 | | ---- | -- | ---- | 65 | | ![CLI](imgs/bmi_cli.png) | ![Qt](imgs/bmi_qt.png) | ![Dash](imgs/bmi_dash.png) | 66 | 67 | -------------------------------------------------------------------------------- /docs/qt_confs.md: -------------------------------------------------------------------------------- 1 | # Qt interface configs 2 | 3 | ## Window name 4 | 5 | By default the window name is the name of the function, but it can be changed by `name` parameter of `.qt_gui` method: 6 | 7 | ```Python 8 | from oneface import one, Val 9 | 10 | @one 11 | def bmi(name: str, 12 | height: Val[float, [100, 250]] = 160, 13 | weight: Val[float, [0, 300]] = 50.0): 14 | BMI = weight / (height / 100) ** 2 15 | print(f"Hi {name}. Your BMI is: {BMI}") 16 | return BMI 17 | 18 | bmi.qt_gui(name="BMI calculator") 19 | ``` 20 | 21 | ![rename_example_qt](./imgs/rename_example_qt.png) 22 | 23 | ## Argument label 24 | 25 | By default, argument label is the variable name. But it can be explicitly set by `text` parameter: 26 | 27 | ```Python 28 | @one 29 | def bmi(name: Val(str, text="NAME"), # explicitly label setting 30 | height: Val[float, [100, 250]] = 160, 31 | weight: Val[float, [0, 300]] = 50.0): 32 | BMI = weight / (height / 100) ** 2 33 | print(f"Hi {name}. Your BMI is: {BMI}") 34 | return BMI 35 | ``` 36 | ![arg_label_qt](./imgs/arg_label_qt.png) 37 | 38 | ## Run multiple times 39 | 40 | By default, oneface Qt interface run only once then exit, when click the run button. 41 | 42 | ![run_once](./imgs/run_once.gif) 43 | 44 | You can use the `run_once=False` to make it run multiple times: 45 | 46 | ```Python 47 | bmi.qt_gui(run_once=False) 48 | ``` 49 | 50 | ![run_once](./imgs/run_not_once.gif) 51 | 52 | ## Window size 53 | 54 | The `size` parameter is used to explicitly specify the window size: 55 | 56 | ```Python 57 | bmi.qt_gui(size=(400, 600)) # width and height 58 | ``` 59 | -------------------------------------------------------------------------------- /docs/qt_embed.md: -------------------------------------------------------------------------------- 1 | # Embeding generated window to a PyQt app 2 | 3 | You can integrate oneFace generated Qt windows by embedding them in a Qt application: 4 | 5 | ```Python 6 | # demo_qt_embed.py 7 | import sys 8 | from oneface.qt import gui 9 | from oneface import one 10 | from qtpy import QtWidgets 11 | 12 | app = QtWidgets.QApplication([]) 13 | 14 | 15 | @gui 16 | @one 17 | def add(a: int, b: int): 18 | res = a + b 19 | print(res) 20 | 21 | @gui 22 | @one 23 | def mul(a: int, b: int): 24 | res = a * b 25 | print(res) 26 | 27 | 28 | main_window = QtWidgets.QWidget() 29 | main_window.setWindowTitle("MyApp") 30 | main_window.setFixedSize(200, 100) 31 | layout = QtWidgets.QVBoxLayout(main_window) 32 | layout.addWidget(QtWidgets.QLabel("Apps:")) 33 | btn_open_add = QtWidgets.QPushButton("add") 34 | btn_open_mul = QtWidgets.QPushButton("mul") 35 | btn_open_add.clicked.connect(add.window.show) 36 | btn_open_mul.clicked.connect(mul.window.show) 37 | layout.addWidget(btn_open_add) 38 | layout.addWidget(btn_open_mul) 39 | main_window.show() 40 | 41 | sys.exit(app.exec()) 42 | ``` 43 | 44 | Run it: 45 | 46 | ![qt_embed](imgs/qt_app_embed.gif) 47 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | mkdocs 2 | mkdocs-material 3 | -------------------------------------------------------------------------------- /docs/type_extension.md: -------------------------------------------------------------------------------- 1 | # Type Extension 2 | 3 | You can easily extend the argument types in oneFace. 4 | 5 | ## Registration of type and range check 6 | 7 | For example you have a custom `Person` class: 8 | 9 | ```Python 10 | class Person: 11 | def __init__(self, name, age): 12 | self.name = name 13 | self.age = age 14 | ``` 15 | 16 | ### Register type check 17 | 18 | ```Python 19 | Val.register_type_check(Person) 20 | ``` 21 | 22 | This will allow oneface to check the type of the input parameter to make sure it is an instance of `Person`: 23 | 24 | ```Python 25 | @one 26 | def print_person(person: Person): 27 | print(f"{person.name} is {person.age} years old.") 28 | 29 | >>> print_person(["Tom", 10]) # Incorrect input type 30 | Run: print_person 31 | Arguments table: 32 | 33 | Argument Type Range InputVal InputType 34 | person None ['Tom', 10] 35 | 36 | Traceback (most recent call last): 37 | File "C:\Users\Nangu\Desktop\oneFace\tmp\test1.py", line 17, in 38 | print_person(["Tom", 10]) 39 | File "C:\Users\Nangu\miniconda3\envs\oneface\lib\site-packages\funcdesc\guard.py", line 46, in __call__ 40 | self.check_inputs(pass_in, errors) 41 | File "C:\Users\Nangu\Desktop\oneFace\oneface\check.py", line 86, in check_inputs 42 | raise CheckError(errors) 43 | funcdesc.guard.CheckError: [TypeError("Value ['Tom', 10] is not in valid type()")] 44 | ``` 45 | 46 | `Val.register_type_check` also allow you to define a custom type checker, for example: 47 | 48 | ```Python 49 | def check_person_type(val, tp): 50 | return ( 51 | isinstance(val, tp) and 52 | isinstance(val.name, str) and 53 | isinstance(val.age, int) 54 | ) 55 | 56 | Arg.register_type_check(Person, check_person_type) 57 | ``` 58 | 59 | This will not only check if the input value is an instance of `Preson`, but also ensure that its attributes are of the correct type: 60 | 61 | ```Python 62 | >>> print_person(Person("Tom", "10")) # Incorrect age type 63 | Run: print_person 64 | Arguments table: 65 | 66 | Argument Type Range InputVal InputType 67 | person None <__main__.Person object at 0x0000021B20DD2FD0> 68 | 69 | Traceback (most recent call last): 70 | File "C:\Users\Nangu\Desktop\oneFace\tmp\test1.py", line 24, in 71 | print_person(Person("Tom", "10")) 72 | File "C:\Users\Nangu\miniconda3\envs\oneface\lib\site-packages\funcdesc\guard.py", line 46, in __call__ 73 | self.check_inputs(pass_in, errors) 74 | File "C:\Users\Nangu\Desktop\oneFace\oneface\check.py", line 86, in check_inputs 75 | raise CheckError(errors) 76 | funcdesc.guard.CheckError: [TypeError("Value <__main__.Person object at 0x0000021B20DD2FD0> is not in valid type()")] 77 | ``` 78 | 79 | ### Register range check 80 | 81 | You can also register a range check for it, for example, to limit the age to a certain range: 82 | 83 | ```Python 84 | Val.register_range_check(Person, lambda val, range: range[0] <= val.age <= range[1]) 85 | ``` 86 | 87 | Mark the range in argument annotation: 88 | 89 | ```Python 90 | @one 91 | def print_person(person: Val[Person, [0, 100]]): 92 | print(f"{person.name} is {person.age} years old.") 93 | ``` 94 | 95 | This will limit the person's age in the range of 0~100: 96 | 97 | ```Python 98 | >>> print_person(Person("Tom", -10)) 99 | Run: print_person 100 | Arguments table: 101 | 102 | Argument Type Range InputVal InputType 103 | person [0, 100] <__main__.Person object at 104 | 0x000001E9148CAD30> 105 | 106 | Traceback (most recent call last): 107 | File "C:\Users\Nangu\Desktop\oneFace\tmp\test1.py", line 25, in 108 | print_person(Person("Tom", -10)) 109 | File "C:\Users\Nangu\miniconda3\envs\oneface\lib\site-packages\funcdesc\guard.py", line 46, in __call__ 110 | self.check_inputs(pass_in, errors) 111 | File "C:\Users\Nangu\Desktop\oneFace\oneface\check.py", line 86, in check_inputs 112 | raise CheckError(errors) 113 | funcdesc.guard.CheckError: [ValueError('Value <__main__.Person object at 0x000001E9148CAD30> is not in a valid range([0, 100]).')] 114 | ``` 115 | 116 | ## Registration of interface widgets 117 | 118 | If you want to generate the appropriate widget for your custom type, you should register it in the specific interface. 119 | 120 | ### Register widgets in Qt interface 121 | 122 | ```Python 123 | from oneface.qt import GUI, InputItem 124 | from qtpy import QtWidgets 125 | 126 | 127 | class PersonInputItem(InputItem): 128 | def init_layout(self): 129 | self.layout = QtWidgets.QVBoxLayout() 130 | 131 | def init_ui(self): 132 | self.name_input = QtWidgets.QLineEdit() 133 | self.age_input = QtWidgets.QSpinBox() 134 | if self.range: 135 | self.age_input.setMinimum(self.range[0]) 136 | self.age_input.setMaximum(self.range[1]) 137 | if self.default: 138 | self.name_input.setText(self.default.name) 139 | self.age_input.setValue(self.default.age) 140 | self.layout.addWidget(QtWidgets.QLabel("person:")) 141 | name_row = QtWidgets.QHBoxLayout() 142 | name_row.addWidget(QtWidgets.QLabel("name:")) 143 | name_row.addWidget(self.name_input) 144 | self.layout.addLayout(name_row) 145 | age_row = QtWidgets.QHBoxLayout() 146 | age_row.addWidget(QtWidgets.QLabel("age:")) 147 | age_row.addWidget(self.age_input) 148 | self.layout.addLayout(age_row) 149 | 150 | def get_value(self): 151 | return Person(self.name_input.text(), self.age_input.value()) 152 | 153 | 154 | GUI.register_widget(Person, PersonInputItem) 155 | ``` 156 | 157 | ![person_ext_qt](./imgs/person_ext_qt.png) 158 | 159 | 160 | ### Register widgets in Dash interface 161 | 162 | 163 | ```Python 164 | from oneface.dash_app import App, InputItem 165 | from dash import dcc 166 | 167 | class PersonInputItem(InputItem): 168 | def get_input(self): 169 | if self.default: 170 | default_val = f"Person('{self.default.name}', {self.default.age})" 171 | else: 172 | default_val = "" 173 | return dcc.Input( 174 | placeholder="example: Person('age', 20)", 175 | type="text", 176 | value=default_val, 177 | style={ 178 | "width": "100%", 179 | "height": "40px", 180 | "margin": "5px", 181 | "font-size": "20px", 182 | } 183 | ) 184 | 185 | 186 | App.register_widget(Person, PersonInputItem) 187 | App.register_type_convert(Person, lambda s: eval(s)) 188 | ``` 189 | 190 | !!! warning 191 | 192 | Currently, there is no good way to composite dash components. Here, for simplicity, we use the serialized input Person. The above code is not recommended for use in production environments. see [issue#1](https://github.com/Nanguage/oneFace/issues/1) 193 | 194 | ![person_ext_dash](./imgs/person_ext_dash.png) 195 | 196 | #### Another example: TextArea 197 | 198 | Here we give another example of using TextArea to get long string input. 199 | 200 | ```Python 201 | from oneface import one, Val 202 | from oneface.dash_app import App, InputItem 203 | from dash import dcc 204 | 205 | 206 | class LongStrInputItem(InputItem): 207 | def get_input(self): 208 | return dcc.Textarea( 209 | placeholder='Enter a value...', 210 | value=(self.default or ""), 211 | style={'width': '100%'} 212 | ) 213 | 214 | 215 | App.register_widget(str, LongStrInputItem) 216 | 217 | 218 | @one 219 | def print_doc(doc: Val(str)): 220 | print(doc) 221 | 222 | 223 | print_doc.dash_app() 224 | ``` 225 | 226 | ![long_str](./imgs/long_str.png) 227 | 228 | More details on the dash component definition can be found in the [dash documentation](https://dash.plotly.com/dash-core-components). 229 | -------------------------------------------------------------------------------- /docs/wrap_cli.md: -------------------------------------------------------------------------------- 1 | # Wrap command line 2 | 3 | Another way to use oneFace is to wrap a command line program. 4 | From the perspective of implementation, it's a layer of wrapping 5 | around the oneFace API. You can use a YAML configuration file to 6 | describe the command line to be wrapped and the type, range and default 7 | value of each parameter. 8 | 9 | Here is an example YAML file: 10 | 11 | ```YAML 12 | # example.yaml 13 | # The name of your app 14 | name: add 15 | 16 | # The target command to be wraped. 17 | # Use '{}' to mark all arguments. 18 | command: python -c "print({a} + {b})" 19 | 20 | # List all argument's type and range 21 | inputs: 22 | 23 | a: 24 | type: int 25 | range: [0, 10] 26 | 27 | b: 28 | type: int 29 | range: [-10, 10] 30 | default: 0 31 | 32 | # Interface specific config 33 | # These parameters will pass to the interface 34 | qt_config: 35 | # qt related config 36 | run_once: false 37 | 38 | dash_config: 39 | # Dash related config 40 | console_interval: 2000 41 | ``` 42 | 43 | You can generate this file to your working path, using: 44 | 45 | ```bash 46 | $ python -m oneface.wrap_cli generate ./example.yaml 47 | ``` 48 | 49 | Then you can lanuch the application with: 50 | 51 | ```bash 52 | $ python -m oneface.wrap_cli run example.yaml qt_gui # or 53 | $ python -m oneface.wrap_cli run example.yaml dash_app 54 | ``` 55 | 56 | 57 | ## Flag insertion 58 | 59 | Extra string can insert to the actually executed command when the 60 | parameter is a `bool` type. It's useful when the command contains some 61 | "Flag" parameters. For example: 62 | 63 | ```YAML 64 | name: add 65 | 66 | command: python {verbose} -c "print({a} + {b})" 67 | 68 | inputs: 69 | 70 | verbose: 71 | type: bool 72 | default: False 73 | true_insert: "-v" # insert "-v" flag when verbose is True 74 | false_insert: "" 75 | 76 | a: 77 | type: int 78 | range: [0, 10] 79 | 80 | b: 81 | type: int 82 | range: [-10, 10] 83 | default: 0 84 | ``` 85 | 86 | 87 | ## Configurations 88 | 89 | You can modify the configuration related to 90 | the specific interface([Qt](./qt_confs.md) and [Dash](./dash_confs.md)). 91 | Using the corresponding fields, for example: 92 | 93 | ```YAML 94 | qt_config: 95 | name: "my qt app" 96 | size: [300, 200] 97 | run_once: False 98 | 99 | dash_config: 100 | host: 127.0.0.1 101 | port: 8989 102 | show_console: True 103 | console_interval: 2000 104 | init_run: True 105 | ``` 106 | 107 | And parameter related configrations should set to the corresponding argument fields. 108 | For example: 109 | 110 | ```YAML 111 | inputs: 112 | 113 | a: 114 | type: int 115 | range: [0, 10] 116 | interactive: True # will let this argument interactive in dash app 117 | 118 | ``` 119 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: oneFace Docs 2 | 3 | repo_url: https://github.com/Nanguage/oneFace 4 | 5 | theme: material 6 | 7 | nav: 8 | - Home: index.md 9 | - Basic Usage: basic_usage.md 10 | - Built-in Types: builtin_types.md 11 | - Type Extension: type_extension.md 12 | - Interface configuration: 13 | - Qt configs: qt_confs.md 14 | - Dash configs: dash_confs.md 15 | - Wrap command line: wrap_cli.md 16 | - Embedding: 17 | - qt_embed.md 18 | - dash_embed.md 19 | 20 | markdown_extensions: 21 | - pymdownx.highlight: 22 | anchor_linenums: true 23 | - pymdownx.inlinehilite 24 | - pymdownx.snippets 25 | - pymdownx.superfences 26 | - admonition 27 | - pymdownx.details 28 | -------------------------------------------------------------------------------- /oneface/__init__.py: -------------------------------------------------------------------------------- 1 | from .core import one 2 | from funcdesc import Val 3 | 4 | __version__ = '0.2.2' 5 | 6 | __all__ = [one, Val] 7 | -------------------------------------------------------------------------------- /oneface/check.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | import functools 3 | 4 | from rich.console import Console 5 | from rich.table import Table 6 | from funcdesc.guard import Guard, TF2, CheckError 7 | from funcdesc.desc import Description, Value 8 | 9 | from .utils import get_callable_name 10 | 11 | 12 | def check_args(func=None, **kwargs): 13 | if func is None: 14 | return functools.partial(check_args, **kwargs) 15 | return CallWithCheck(func, **kwargs) 16 | 17 | 18 | console = Console() 19 | 20 | 21 | class CallWithCheck(Guard[TF2]): 22 | def __init__( 23 | self, 24 | func: TF2, 25 | desc: T.Optional[Description] = None, 26 | check_inputs: bool = True, 27 | check_outputs: bool = False, 28 | check_side_effect: bool = False, 29 | check_type: bool = True, 30 | check_range: bool = True, 31 | print_args: bool = True, 32 | name: T.Optional[str] = None, 33 | ) -> None: 34 | self.name = get_callable_name(func, name) 35 | self.is_print_args = print_args 36 | self.table: T.Optional[Table] = None 37 | super().__init__( 38 | func, desc, check_inputs, check_outputs, 39 | check_side_effect, check_type, check_range,) 40 | 41 | def print_args(self): 42 | if self.table is None: 43 | return 44 | if self.name: 45 | console.print(f"Run: [bold purple]{self.name}") 46 | console.print("Arguments table:\n") 47 | console.print(self.table) 48 | console.print() 49 | 50 | def check_value( 51 | self, 52 | arg: Value, 53 | val: T.Any, 54 | errors: T.List[Exception]): 55 | val_str = str(val) 56 | range_str = str(arg.range) 57 | tp_str = str(type(val)) 58 | ann_tp_str = str(arg.type) 59 | try: 60 | if self.is_check_type: 61 | arg.check_type(val) 62 | if self.is_check_range: 63 | arg.check_range(val) 64 | except Exception as e: 65 | errors.append(e) 66 | if isinstance(e, ValueError): 67 | val_str = f"[red]{val_str}[/red]" 68 | range_str = f"[red]{range_str}[/red]" 69 | elif isinstance(e, TypeError): 70 | ann_tp_str = f"[red]{ann_tp_str}[/red]" 71 | tp_str = f"[red]{tp_str}[/red]" 72 | else: 73 | raise e 74 | if self.is_print_args: 75 | self.table.add_row( 76 | arg.name, ann_tp_str, range_str, val_str, tp_str) 77 | 78 | def check_inputs(self, pass_in: dict, errors: list): 79 | if self.is_print_args: 80 | self.table = self.get_argument_table() 81 | for val in self.desc.inputs: 82 | self.check_value(val, pass_in[val.name], errors) 83 | if self.is_print_args: 84 | self.print_args() 85 | if len(errors) > 0: 86 | raise CheckError(errors) 87 | 88 | @staticmethod 89 | def get_argument_table(): 90 | table = Table( 91 | show_header=True, header_style="bold magenta", 92 | box=None) 93 | table.add_column("Argument") 94 | table.add_column("Type") 95 | table.add_column("Range") 96 | table.add_column("InputVal") 97 | table.add_column("InputType") 98 | return table 99 | -------------------------------------------------------------------------------- /oneface/core.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from .check import CallWithCheck 4 | 5 | 6 | def one(func=None, **kwargs): 7 | if func is None: 8 | return functools.partial(one, **kwargs) 9 | return One(func, **kwargs) 10 | 11 | 12 | class One(CallWithCheck): 13 | 14 | def cli(self): 15 | from fire import Fire 16 | Fire(self.__call__) 17 | 18 | def qt_gui(self, **kwargs): 19 | from .qt import GUI 20 | return GUI(self, **kwargs)() 21 | 22 | def dash_app(self, **kwargs): 23 | from .dash_app import App 24 | return App(self, **kwargs)() 25 | -------------------------------------------------------------------------------- /oneface/dash_app/__init__.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | from .app import App 4 | from .embed import flask_route 5 | 6 | 7 | def app(func=None, **kwargs): 8 | if func is None: 9 | return functools.partial(app, **kwargs) 10 | return App(func, **kwargs) 11 | 12 | 13 | __all__ = ["app", "App", "flask_route"] 14 | -------------------------------------------------------------------------------- /oneface/dash_app/app.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | import contextlib 3 | from io import StringIO 4 | 5 | from dash import Dash, dcc, html, Input, Output, State 6 | from dash.exceptions import PreventUpdate 7 | from ansi2html import Ansi2HTMLConverter 8 | import visdcc 9 | from funcdesc.types import ( 10 | OneOf, SubSet, InputPath, OutputPath 11 | ) 12 | from funcdesc.desc import NotDef 13 | from funcdesc.parse import parse_func 14 | 15 | from .input_item import ( 16 | InputItem, IntInputItem, FloatInputItem, StrInputItem, BoolInputItem, 17 | DropdownInputItem, MultiDropdownInputItem, 18 | ) 19 | from ..utils import AllowWrapInstanceMethod, get_callable_name 20 | 21 | 22 | class App(AllowWrapInstanceMethod): 23 | type_to_widget_constructor: T.Dict[str, "InputItem"] = {} 24 | convert_types: T.Dict[str, T.Callable] = {} 25 | 26 | def __init__( 27 | self, func, name=None, 28 | show_console=True, 29 | console_interval=2000, 30 | interactive=False, init_run=False, 31 | result_show_type="text", 32 | **server_args): 33 | self.func = func 34 | self.name = get_callable_name(func, name) 35 | self.show_console = show_console 36 | self.console_interval = console_interval 37 | self.interactive = interactive 38 | self.init_run = init_run 39 | self.result_show_type = result_show_type 40 | self.server_args = server_args 41 | self.input_names: T.Optional[T.List[str]] = None 42 | self.input_types: T.Optional[T.List[T.Type]] = None 43 | self.input_attrs: T.Optional[T.List[dict]] = None 44 | self.result: T.Optional[T.Any] = None 45 | self.dash_app: T.Optional[Dash] = None 46 | 47 | def get_layout(self): 48 | input_widgets = self.parse_args() 49 | sub_nodes = [ 50 | html.H3("Arguments"), 51 | *input_widgets, 52 | html.Br(), 53 | html.Button("Run", id="run-btn"), 54 | html.Div("", style={"height": "20px"}), 55 | ] 56 | if self.show_console: 57 | sub_nodes += self.get_console_layout() 58 | sub_nodes += self.get_result_layout() 59 | layout = html.Div(children=sub_nodes, style={ 60 | 'width': "60%", 61 | 'min-width': "400px", 62 | 'max-width': "800px", 63 | 'margin': "auto", 64 | }) 65 | return layout 66 | 67 | def get_console_layout(self): 68 | return [ 69 | html.H3("Console"), 70 | html.Div("", style={"height": "20px"}), 71 | dcc.Interval( 72 | id="console-interval", 73 | interval=self.console_interval, n_intervals=0), 74 | html.Iframe(id="console-out", style={ 75 | "width": "100%", 76 | "max-width": "100%", 77 | "height": "400px", 78 | "resize": "both" 79 | }), 80 | visdcc.Run_js(id="jsscroll", run="") 81 | ] 82 | 83 | def base_result_layout(self): 84 | return [ 85 | html.H3("Result"), 86 | dcc.Store(id="out") 87 | ] 88 | 89 | def get_result_layout(self): 90 | layout = self.base_result_layout() 91 | show_type = self.result_show_type 92 | if show_type == "text": 93 | layout += [ 94 | html.Div(id="show-text") 95 | ] 96 | elif show_type == "download": 97 | layout += [ 98 | html.Button("Download Result", id="res-download-btn"), 99 | dcc.Download(id="res-download-index") 100 | ] 101 | elif show_type == "plotly": 102 | layout += [ 103 | dcc.Graph(id='plotly-figure') 104 | ] 105 | else: 106 | raise NotImplementedError( 107 | f"The layout for result_show_type '{show_type}'" 108 | "is not defined") 109 | return layout 110 | 111 | def parse_args(self) -> T.List["html.Div"]: 112 | """Parse target function's arguments, 113 | return a list of input widgets.""" 114 | widgets, names, types, attrs = [], [], [], [] 115 | desc = parse_func(self.func) 116 | for a in desc.inputs: 117 | if a.type is None: 118 | continue 119 | constructor = self.type_to_widget_constructor[a.type.__name__] 120 | default = None if a.default is NotDef else a.default 121 | attr = a.kwargs 122 | widget = constructor( 123 | a.name, a.range, default, attrs=attr).widget 124 | widgets.append(widget) 125 | names.append(a.name) 126 | types.append(a.type) 127 | attrs.append(attr) 128 | self.input_names = names 129 | self.input_types = types 130 | self.input_attrs = attrs 131 | return widgets 132 | 133 | def get_dash_app(self, *args, **kwargs): 134 | name = self.name or self.func.__name__ 135 | css = ['https://codepen.io/chriddyp/pen/bWLwgP.css'] 136 | if 'external_stylesheets' not in kwargs: 137 | kwargs['external_stylesheets'] = css 138 | app = Dash(name, *args, **kwargs) 139 | app.layout = self.get_layout() 140 | self.add_callbacks(app) 141 | return app 142 | 143 | def add_callbacks(self, app: "Dash"): 144 | self.add_run_callbacks(app) 145 | self.add_result_callbacks(app) 146 | 147 | def add_result_callbacks(self, app: "Dash"): 148 | show_type = self.result_show_type 149 | if show_type == "text": 150 | self.add_text_callback(app) 151 | elif show_type == "download": 152 | self.add_download_callbacks(app) 153 | elif show_type == "plotly": 154 | self.add_plotly_callbacks(app) 155 | else: 156 | raise NotImplementedError( 157 | f"The callback for result_show_type '{show_type}'" 158 | "is not defined." 159 | ) 160 | 161 | def get_run_callback_decorator(self, app: "Dash"): 162 | inputs = [Input("run-btn", 'n_clicks')] 163 | for i, n in enumerate(self.input_names): 164 | is_interactive = ( 165 | self.interactive or 166 | (self.input_attrs[i].get('interactive') is True) 167 | ) 168 | id_ = f"input-{n}" 169 | if is_interactive: 170 | input = Input(id_, "value") 171 | else: 172 | input = State(id_, "value") 173 | inputs.append(input) 174 | output = Output("out", "data") 175 | deco = app.callback(output, *inputs) 176 | return deco 177 | 178 | def add_run_callbacks(self, app): 179 | console_buffer = StringIO() 180 | 181 | @self.get_run_callback_decorator(app) 182 | def run(n_clicks, *args): 183 | if (not self.init_run) and (n_clicks is None): 184 | raise PreventUpdate 185 | kwargs = dict(zip(self.input_names, args)) 186 | for i, (k, v) in enumerate(kwargs.items()): 187 | input_type = self.input_types[i] 188 | tp_name = input_type.__name__ 189 | if tp_name in self.convert_types: 190 | kwargs[k] = self.convert_types[tp_name](v) 191 | with contextlib.redirect_stdout(console_buffer), \ 192 | contextlib.redirect_stderr(console_buffer): 193 | self.result = self.func(**kwargs) 194 | return self.result 195 | 196 | if self.show_console: 197 | self.add_console_callbacks(app, console_buffer) 198 | 199 | def add_console_callbacks(self, app, console_buffer): 200 | @app.callback( 201 | Output("console-out", "srcDoc"), 202 | Input("console-interval", "n_intervals")) 203 | def update_console(n): 204 | conv = Ansi2HTMLConverter() 205 | console_buffer.seek(0) 206 | lines = console_buffer.readlines() 207 | html_ = conv.convert("".join(lines)) 208 | return html_ 209 | 210 | doc_cache = None 211 | 212 | @app.callback( 213 | Output('jsscroll', 'run'), 214 | Input('console-out', 'srcDoc')) 215 | def scroll(doc): 216 | scroll_cmd = """ 217 | var out = document.getElementById('console-out'); 218 | out.contentWindow.scrollTo(0, 999999999); 219 | """ 220 | nonlocal doc_cache 221 | if doc == doc_cache: 222 | cmd = "" 223 | else: 224 | cmd = scroll_cmd 225 | doc_cache = doc 226 | return cmd 227 | 228 | def add_text_callback(self, app: "Dash"): 229 | @app.callback( 230 | Output("show-text", "children"), 231 | Input("out", "data")) 232 | def show(text): 233 | return text 234 | 235 | def add_download_callbacks(self, app: "Dash"): 236 | @app.callback( 237 | Output("res-download-index", "data"), 238 | Input("res-download-btn", "n_clicks"), 239 | prevent_initial_call=True) 240 | def send_file(n_clicks): 241 | return dcc.send_file(self.result) 242 | 243 | def add_plotly_callbacks(self, app: "Dash"): 244 | @app.callback( 245 | Output("plotly-figure", "figure"), 246 | Input("out", "data"), 247 | ) 248 | def show(data): 249 | return data 250 | 251 | @classmethod 252 | def register_widget(cls, type, widget_constructor): 253 | cls.type_to_widget_constructor[type.__name__] = widget_constructor 254 | 255 | @classmethod 256 | def register_type_convert(cls, type, converter=None): 257 | if converter is None: 258 | converter = type 259 | cls.convert_types[type.__name__] = converter 260 | 261 | def __call__(self): 262 | self.dash_app = self.get_dash_app() 263 | self.dash_app.run_server(**self.server_args) 264 | 265 | 266 | App.register_widget(int, IntInputItem) 267 | App.register_widget(float, FloatInputItem) 268 | App.register_type_convert(float) 269 | App.register_widget(str, StrInputItem) 270 | App.register_widget(bool, BoolInputItem) 271 | App.register_type_convert(bool, lambda s: s == "True") 272 | App.register_widget(OneOf, DropdownInputItem) 273 | App.register_widget(SubSet, MultiDropdownInputItem) 274 | App.register_widget(InputPath, StrInputItem) 275 | App.register_widget(OutputPath, StrInputItem) 276 | -------------------------------------------------------------------------------- /oneface/dash_app/embed.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | 3 | from flask import Flask 4 | from dash import Dash 5 | 6 | from .app import App 7 | 8 | 9 | class RouteFunc(): 10 | def __init__(self, dash_app: "Dash", name: str): 11 | self.dash_app = dash_app 12 | self.__name__ = name 13 | 14 | def __call__(self): 15 | return self.dash_app.index() 16 | 17 | 18 | def flask_route( 19 | server: "Flask", rule: str, 20 | dash_app_kwargs: T.Optional[dict] = None, 21 | **options): 22 | def deco(func: T.Callable): 23 | if isinstance(func, App): 24 | dash_wrap = func 25 | else: 26 | dash_wrap = App(func) 27 | base_path = rule if rule.endswith('/') else (rule + "/") 28 | if dash_app_kwargs is None: 29 | d_kwargs = {} 30 | else: 31 | d_kwargs = dash_app_kwargs 32 | dash_app = dash_wrap.get_dash_app( 33 | server=server, url_base_pathname=base_path, 34 | **d_kwargs, 35 | ) 36 | dash_app_route = RouteFunc(dash_app, dash_wrap.name) 37 | server.route(rule, **options)(dash_app_route) 38 | return func 39 | return deco 40 | -------------------------------------------------------------------------------- /oneface/dash_app/input_item.py: -------------------------------------------------------------------------------- 1 | from dash import dcc, html 2 | 3 | 4 | class InputItem(object): 5 | def __init__(self, name, range, default, attrs=None): 6 | self.name = name 7 | self.range = range 8 | self.default = default 9 | self.attrs = attrs 10 | self.input = self.get_input() 11 | self.input.id = f"input-{name}" 12 | self.widget = self.get_widget() 13 | 14 | def get_input(self): 15 | pass 16 | 17 | def get_widget(self): 18 | label = self.attrs.get("text", self.name) 19 | return html.Div([ 20 | html.Div(f"{label}: ", style={ 21 | "margin-top": "10px", 22 | "font-size": "20px", 23 | }), 24 | self.input 25 | ]) 26 | 27 | 28 | class IntInputItem(InputItem): 29 | def get_input(self): 30 | _range = self.range or [0, 100] 31 | _default = _range[0] if (self.default is None) else self.default 32 | return dcc.Input( 33 | min=_range[0], max=_range[1], type="number", step=1, 34 | value=_default, style={ 35 | 'width': "100%", 36 | } 37 | ) 38 | 39 | 40 | class FloatInputItem(InputItem): 41 | def get_input(self): 42 | _range = self.range or [0, 100] 43 | _default = _range[0] if (self.default is None) else self.default 44 | return dcc.Slider( 45 | _range[0], _range[1], step=None, 46 | value=_default, 47 | ) 48 | 49 | 50 | class StrInputItem(InputItem): 51 | def get_input(self): 52 | return dcc.Input( 53 | placeholder="Enter a value...", 54 | type="text", 55 | value=(self.default or ""), 56 | style={ 57 | "width": "100%", 58 | "height": "40px", 59 | "margin": "5px", 60 | "font-size": "20px", 61 | } 62 | ) 63 | 64 | 65 | class BoolInputItem(InputItem): 66 | def get_input(self): 67 | _default = True if self.default is None else self.default 68 | return dcc.RadioItems( 69 | ["True", "False"], 70 | value=str(_default) 71 | ) 72 | 73 | 74 | class DropdownInputItem(InputItem): 75 | def get_input(self): 76 | return dcc.Dropdown( 77 | self.range, 78 | value=(self.default or self.range[0]) 79 | ) 80 | 81 | 82 | class MultiDropdownInputItem(InputItem): 83 | def get_input(self): 84 | return dcc.Dropdown( 85 | self.range, 86 | value=(self.default or self.range[0]), multi=True 87 | ) 88 | -------------------------------------------------------------------------------- /oneface/qt.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | import functools 3 | 4 | from qtpy import QtWidgets 5 | from qtpy import QtCore 6 | from funcdesc.parse import parse_func 7 | from funcdesc.desc import NotDef 8 | from funcdesc.types import ( 9 | InputPath, OutputPath, OneOf, SubSet 10 | ) 11 | 12 | from .utils import AllowWrapInstanceMethod, get_callable_name 13 | 14 | 15 | class Worker(QtCore.QObject): 16 | finished = QtCore.Signal() 17 | 18 | def __init__(self, func, func_kwargs, *args, **kwargs): 19 | super().__init__(*args, **kwargs) 20 | self.func = func 21 | self.func_kwargs = func_kwargs 22 | self.result = None 23 | 24 | def run(self): 25 | self.result = self.func(**self.func_kwargs) 26 | self.finished.emit() 27 | 28 | 29 | def gui(func=None, **kwargs): 30 | if func is None: 31 | return functools.partial(gui, **kwargs) 32 | return GUI(func, **kwargs) 33 | 34 | 35 | def get_app(): 36 | app = QtWidgets.QApplication.instance() 37 | if app is None: 38 | app = QtWidgets.QApplication([]) 39 | return app 40 | 41 | 42 | class GUI(AllowWrapInstanceMethod): 43 | 44 | type_to_widget_constructor = {} 45 | 46 | def __init__( 47 | self, 48 | func: T.Callable, name: T.Optional[str] = None, 49 | size: T.Optional[T.List] = None, 50 | run_once=True): 51 | self.func = func 52 | self.run_once = run_once 53 | self.result = None 54 | self.app = get_app() 55 | self.name = get_callable_name(func, name) 56 | self.window = QtWidgets.QWidget() 57 | self.window._oneface_wrap = self 58 | self.window.setWindowTitle(self.name) 59 | if size: 60 | self.window.setFixedSize(*size) 61 | self.arg_widgets = {} 62 | self.layout = QtWidgets.QVBoxLayout() 63 | self.compose_ui() 64 | self.connect_events() 65 | 66 | def compose_ui(self): 67 | self.compose_arg_widgets(self.layout) 68 | self.run_btn = QtWidgets.QPushButton("Run") 69 | self.layout.addWidget(self.run_btn) 70 | self.terminal = QtWidgets.QTextEdit() 71 | self.window.setLayout(self.layout) 72 | 73 | def compose_arg_widgets(self, layout: QtWidgets.QVBoxLayout): 74 | desc = parse_func(self.func) 75 | for a in desc.inputs: 76 | if a.type is None: 77 | continue 78 | if a.type.__name__ not in self.type_to_widget_constructor: 79 | raise NotImplementedError( 80 | f"Input widget constructor is not registered for {a.type}") 81 | constructor = self.type_to_widget_constructor[a.type.__name__] 82 | default = None if a.default is NotDef else a.default 83 | w = constructor(a.name, a.range, default, attrs=a.kwargs) 84 | self.arg_widgets[a.name] = w 85 | layout.addWidget(w) 86 | 87 | def connect_events(self): 88 | self.run_btn.clicked.connect(self.run_func) 89 | 90 | def get_args(self): 91 | kwargs = {} 92 | for n, w in self.arg_widgets.items(): 93 | kwargs[n] = w.get_value() 94 | return kwargs 95 | 96 | def run_func(self): 97 | kwargs = self.get_args() 98 | if self.run_once: 99 | self.run_btn.setEnabled(False) 100 | self.result = self.func(**kwargs) 101 | self.window.close() 102 | else: 103 | thread = self.thread = QtCore.QThread() 104 | worker = self.worker = Worker(self.func, kwargs) 105 | worker.moveToThread(thread) 106 | thread.started.connect(worker.run) 107 | worker.finished.connect(thread.quit) 108 | worker.finished.connect(worker.deleteLater) 109 | thread.finished.connect(thread.deleteLater) 110 | thread.start() 111 | self.run_btn.setEnabled(False) 112 | 113 | def finish(): 114 | self.result = worker.result 115 | self.run_btn.setEnabled(True) 116 | thread.finished.connect(finish) 117 | 118 | def __call__(self): 119 | self.window.show() 120 | self.app.exec() 121 | return self.result 122 | 123 | @classmethod 124 | def register_widget(cls, type_, widget_constructor): 125 | cls.type_to_widget_constructor[type_.__name__] = widget_constructor 126 | 127 | 128 | class InputItem(QtWidgets.QWidget): 129 | def __init__(self, name, range, default, attrs, *args, **kwargs): 130 | super().__init__(*args, **kwargs) 131 | self.name = name 132 | self.range = range 133 | self.default = default 134 | self.attrs = attrs 135 | self.init_layout() 136 | self.init_ui() 137 | self.setLayout(self.layout) 138 | 139 | def init_layout(self): 140 | self.layout = QtWidgets.QHBoxLayout() 141 | 142 | def init_ui(self, label_stretch=1): 143 | label = self.attrs.get('text', self.name) 144 | self.label = QtWidgets.QLabel(f"{label}:") 145 | if label_stretch: 146 | self.layout.addWidget(self.label, stretch=label_stretch) 147 | else: 148 | self.layout.addWidget(self.label) 149 | 150 | def get_value(self): 151 | return self.input.value() 152 | 153 | 154 | class IntInputItem(InputItem): 155 | def init_ui(self): 156 | super().init_ui() 157 | self.input = QtWidgets.QSpinBox() 158 | if self.range: 159 | self.input.setMinimum(self.range[0]) 160 | self.input.setMaximum(self.range[1]) 161 | if self.default: 162 | self.input.setValue(self.default) 163 | self.layout.addWidget(self.input, stretch=1) 164 | 165 | 166 | class FloatInputItem(InputItem): 167 | def init_ui(self): 168 | super().init_ui() 169 | self.input = QtWidgets.QDoubleSpinBox() 170 | self.input.setSingleStep(0.1) 171 | if self.range: 172 | self.input.setMinimum(self.range[0]) 173 | self.input.setMaximum(self.range[1]) 174 | if self.default: 175 | self.input.setValue(self.default) 176 | self.layout.addWidget(self.input, stretch=1) 177 | 178 | 179 | class StrInputItem(InputItem): 180 | def init_ui(self): 181 | super().init_ui() 182 | self.input = QtWidgets.QLineEdit() 183 | if self.default: 184 | self.input.setText(self.default) 185 | self.layout.addWidget(self.input, stretch=1) 186 | 187 | def get_value(self): 188 | return self.input.text() 189 | 190 | 191 | class BoolInputItem(InputItem): 192 | def init_ui(self): 193 | super().init_ui(label_stretch=2) 194 | self.bt = QtWidgets.QRadioButton("True") 195 | self.bf = QtWidgets.QRadioButton("False") 196 | self.layout.addWidget(self.bt, stretch=1) 197 | self.layout.addWidget(self.bf, stretch=1) 198 | if (self.default is False): 199 | self.bf.setChecked(True) 200 | else: 201 | self.bt.setChecked(True) 202 | 203 | def get_value(self): 204 | return self.bt.isChecked() 205 | 206 | 207 | class SelectionInputItem(InputItem): 208 | def init_ui(self): 209 | super().init_ui() 210 | self.input = QtWidgets.QComboBox() 211 | if self.range: 212 | self.input.addItems(self.range) 213 | if self.default: 214 | self.input.setCurrentText(self.default) 215 | self.layout.addWidget(self.input, stretch=1) 216 | 217 | def get_value(self): 218 | return self.input.currentText() 219 | 220 | 221 | class SubsetInputItem(InputItem): 222 | def init_layout(self): 223 | self.layout = QtWidgets.QVBoxLayout() 224 | 225 | def init_ui(self): 226 | super().init_ui(label_stretch=None) 227 | self.cb_layout = QtWidgets.QHBoxLayout() 228 | self.cbs = [] 229 | for it in self.range: 230 | cb = QtWidgets.QCheckBox(it) 231 | self.cb_layout.addWidget(cb) 232 | self.cbs.append(cb) 233 | self.layout.addLayout(self.cb_layout) 234 | if self.default: 235 | for val in self.default: 236 | self.cbs[self.range.index(val)].setChecked(True) 237 | 238 | def get_value(self): 239 | res = [] 240 | for i, val in enumerate(self.range): 241 | if self.cbs[i].isChecked(): 242 | res.append(val) 243 | return res 244 | 245 | 246 | class PathInputItem(InputItem): 247 | def init_layout(self): 248 | self.layout = QtWidgets.QVBoxLayout() 249 | 250 | def init_ui(self): 251 | super().init_ui(label_stretch=None) 252 | self.box = QtWidgets.QHBoxLayout() 253 | self.path_edit = QtWidgets.QLineEdit() 254 | self.dialog_open = QtWidgets.QPushButton("open") 255 | self.dialog_open.clicked.connect(self.get_path) 256 | self.box.addWidget(self.path_edit, stretch=2) 257 | if self.default: 258 | self.path_edit.setText(self.default) 259 | self.box.addWidget(self.dialog_open, stretch=1) 260 | self.layout.addLayout(self.box) 261 | 262 | def get_path(self): 263 | fname, _ = QtWidgets.QFileDialog.getOpenFileName( 264 | None, "Open file", "", "All files (*)") 265 | self.path_edit.setText(fname) 266 | 267 | def get_value(self): 268 | return self.path_edit.text() 269 | 270 | 271 | class OutPathInputItem(PathInputItem): 272 | def get_path(self): 273 | fname, _ = QtWidgets.QFileDialog.getSaveFileName( 274 | None, "Save file", "", "All files (*)") 275 | self.path_edit.setText(fname) 276 | 277 | 278 | GUI.register_widget(int, IntInputItem) 279 | GUI.register_widget(float, FloatInputItem) 280 | GUI.register_widget(str, StrInputItem) 281 | GUI.register_widget(bool, BoolInputItem) 282 | GUI.register_widget(OneOf, SelectionInputItem) 283 | GUI.register_widget(SubSet, SubsetInputItem) 284 | GUI.register_widget(InputPath, PathInputItem) 285 | GUI.register_widget(OutputPath, OutPathInputItem) 286 | -------------------------------------------------------------------------------- /oneface/utils.py: -------------------------------------------------------------------------------- 1 | import typing as T 2 | import functools 3 | 4 | 5 | class AllowWrapInstanceMethod(object): 6 | def __get__(self, obj, objtype): 7 | if not hasattr(self, "_bounded"): # bound only once 8 | target_func = self.func 9 | bound_mth = functools.partial(target_func, obj) 10 | self.func = bound_mth 11 | self._bounded = True 12 | return self 13 | 14 | 15 | def get_callable_name(func, name: T.Optional[str]) -> str: 16 | if name is not None: 17 | return name 18 | elif hasattr(func, "name"): 19 | return func.name 20 | else: 21 | return func.__name__ 22 | -------------------------------------------------------------------------------- /oneface/wrap_cli/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Nanguage/oneFace/6a443d16e4698a7f942541aa40c04fb63f8e9701/oneface/wrap_cli/__init__.py -------------------------------------------------------------------------------- /oneface/wrap_cli/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import typing as T 3 | import os.path as osp 4 | 5 | import fire 6 | 7 | from .wrap import wrap_cli, load_config 8 | from ..core import one 9 | 10 | 11 | FILE_DIR = osp.dirname(osp.abspath(__file__)) 12 | EXAMPLE_FILE = osp.join(FILE_DIR, "example.yaml") 13 | 14 | 15 | def gen(target_path: str = "./example.yaml"): 16 | print(f"Generate an example config file in: {target_path}") 17 | with open(EXAMPLE_FILE) as f: 18 | content = f.read() 19 | with open(target_path, 'w') as f: 20 | f.write(content) 21 | 22 | 23 | InterfaceTypes = T.Literal["cli", "qt_gui", "dash_app"] 24 | 25 | 26 | def run( 27 | config_path: str, interface: InterfaceTypes, 28 | print_cmd: bool = True, **kwargs): 29 | """ 30 | :param config_path: The path to your config(.yaml) file. 31 | :param interface: The interface type, 'qt_gui' | 'dash_app' | 'cli' 32 | :param print_cmd: Print the actually executed command or not. 33 | """ 34 | config = load_config(config_path) 35 | wrap = wrap_cli(config, print_cmd=print_cmd) 36 | of = one(wrap, **kwargs) 37 | if interface == "qt_gui": 38 | ret_code = of.qt_gui(**config.get('qt_config', {})) 39 | elif interface == "dash_app": 40 | ret_code = of.dash_app(**config.get('dash_config', {})) 41 | else: 42 | ret_code = of.cli() 43 | sys.exit(ret_code) 44 | 45 | 46 | if __name__ == "__main__": 47 | fire.Fire({ 48 | 'run': run, 49 | 'generate': gen, 50 | }) 51 | -------------------------------------------------------------------------------- /oneface/wrap_cli/example.yaml: -------------------------------------------------------------------------------- 1 | # The name of your app 2 | name: add 3 | 4 | # The target command to be wraped. 5 | # Use '{}' to mark all arguments. 6 | command: python -c "print({a} + {b})" 7 | 8 | # List all input argument's type and range 9 | inputs: 10 | 11 | a: 12 | type: int 13 | range: [0, 10] 14 | 15 | b: 16 | type: int 17 | range: [-10, 10] 18 | default: 0 19 | 20 | # Interface specific config 21 | # These parameters will pass to the interface 22 | qt_config: 23 | # qt related config 24 | run_once: false 25 | 26 | dash_config: 27 | # Dash related config 28 | console_interval: 2000 29 | -------------------------------------------------------------------------------- /oneface/wrap_cli/wrap.py: -------------------------------------------------------------------------------- 1 | import yaml 2 | from cmd2func import cmd2func 3 | from cmd2func.config import CLIConfig 4 | from funcdesc import mark_input 5 | 6 | 7 | def load_config(path: str) -> dict: 8 | with open(path) as f: 9 | conf = yaml.safe_load(f) 10 | return conf 11 | 12 | 13 | def wrap_cli(config: CLIConfig, print_cmd=True): 14 | func = cmd2func(config['command'], config, print_cmd=print_cmd) 15 | for name, arg in config['inputs'].items(): 16 | if 'range' in arg: 17 | func = mark_input(name, range=arg['range'])(func) 18 | func.name = config['name'] 19 | return func 20 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | funcdesc>=0.1.2 2 | cmd2func>=0.1.4 3 | rich 4 | fire 5 | qtpy 6 | pyYAML 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | import re 3 | 4 | 5 | classifiers = [ 6 | "Development Status :: 3 - Alpha", 7 | "Operating System :: OS Independent", 8 | "Programming Language :: Python", 9 | "Programming Language :: Python :: 3", 10 | "Programming Language :: Python :: 3.8", 11 | "Programming Language :: Python :: 3.9", 12 | "Programming Language :: Python :: 3.10", 13 | 'License :: OSI Approved :: MIT License', 14 | "Intended Audience :: Science/Research", 15 | "Intended Audience :: Developers", 16 | ] 17 | 18 | 19 | keywords = [ 20 | 'GUI', 21 | 'CLI', 22 | ] 23 | 24 | 25 | def get_version(): 26 | with open("oneface/__init__.py") as f: 27 | for line in f.readlines(): 28 | m = re.match("__version__ = '([^']+)'", line) 29 | if m: 30 | return m.group(1) 31 | raise IOError("Version information can not found.") 32 | 33 | 34 | def get_long_description(): 35 | return "See https://github.com/Nanguage/oneFace" 36 | 37 | 38 | def get_requirements_from_file(filename): 39 | requirements = [] 40 | with open(filename) as f: 41 | for line in f.readlines(): 42 | line = line.strip() 43 | if len(line) == 0: 44 | continue 45 | if line and not line.startswith('#'): 46 | requirements.append(line) 47 | return requirements 48 | 49 | 50 | def get_install_requires(): 51 | return get_requirements_from_file('requirements.txt') 52 | 53 | 54 | requires_test = ['pytest', 'pytest-cov', 'flake8'] 55 | requires_doc = [] 56 | with open("docs/requirements.txt") as f: 57 | for line in f: 58 | p = line.strip() 59 | if p: 60 | requires_doc.append(p) 61 | requires_dash = ['ansi2html', 'dash', 'visdcc'] 62 | 63 | 64 | setup( 65 | name='oneFace', 66 | author='Weize Xu', 67 | author_email='vet.xwz@gmail.com', 68 | version=get_version(), 69 | license='MIT', 70 | description='oneFace is a library for automatically generating multiple ' 71 | 'interfaces(CLI, GUI) from a callable Python object', 72 | long_description=get_long_description(), 73 | keywords=keywords, 74 | url='https://github.com/Nanguage/oneFace', 75 | packages=find_packages(), 76 | include_package_data=True, 77 | zip_safe=False, 78 | classifiers=classifiers, 79 | install_requires=get_install_requires(), 80 | extras_require={ 81 | 'test': requires_test, 82 | 'doc': requires_doc, 83 | 'dev': requires_test + requires_doc, 84 | 'dash': requires_dash, 85 | 'qt': ['pyqt6'], 86 | 'pyqt5': ['pyqt5'], 87 | 'pyqt6': ['pyqt6'], 88 | 'pyside2': ['PySide2'], 89 | 'pyside6': ['PySide6'], 90 | 'all': ['pyqt6'] + requires_dash, 91 | }, 92 | python_requires='>=3.8, <4', 93 | ) 94 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | def pytest_sessionstart(session): 4 | sys.path.insert(0, "./") 5 | -------------------------------------------------------------------------------- /tests/test_check.py: -------------------------------------------------------------------------------- 1 | from oneface.core import * 2 | from oneface.check import * 3 | from funcdesc import Val 4 | from funcdesc.guard import CheckError 5 | 6 | import pytest 7 | 8 | 9 | def test_arg_check(): 10 | @check_args(print_args=False) 11 | def func(a: Val(int, [0, 10]), b: Val(float, [0, 1]), k=10): 12 | return a 13 | assert func(10, 0.3) == 10 14 | with pytest.raises(CheckError) as e: 15 | func(11, 0.3) 16 | assert isinstance(e.value.args[0][0], ValueError) 17 | with pytest.raises(CheckError) as e: 18 | func(2.0, 0.3) 19 | assert isinstance(e.value.args[0][0], TypeError) 20 | with pytest.raises(CheckError) as e: 21 | func("str", 0.3) 22 | assert isinstance(e.value.args[0][0], TypeError) 23 | with pytest.raises(CheckError) as e: 24 | func(2, 10.0) 25 | assert isinstance(e.value.args[0][0], ValueError) 26 | with pytest.raises(CheckError) as e: 27 | func(-1, 1) 28 | assert isinstance(e.value.args[0][0], ValueError) 29 | assert isinstance(e.value.args[0][1], TypeError) 30 | func(2, 0.5) 31 | @check_args(print_args=False) 32 | def func(a: Val(bool)): 33 | pass 34 | with pytest.raises(CheckError) as e: 35 | func(1) 36 | @check_args(print_args=False) 37 | def func(a: Val(int)): 38 | return a 39 | with pytest.raises(CheckError) as e: 40 | func(1.0) 41 | assert isinstance(e.value.args[0][0], TypeError) 42 | 43 | 44 | def test_arg_register(): 45 | @check_args(print_args=False) 46 | def func(a: Val(list, None)): 47 | pass 48 | Val.register_type_check(list) 49 | func([1,2,3]) 50 | with pytest.raises(CheckError) as e: 51 | func(True) 52 | assert isinstance(e.value.args[0][0], TypeError) 53 | 54 | 55 | def test_print_args(): 56 | @check_args(print_args=True) 57 | def func(a: Val(int, [0, 10]), b: Val(float, [0, 1])): 58 | return a + b 59 | func(3, 0.5) 60 | with pytest.raises(CheckError) as e: 61 | func(-1, 1.0) 62 | with pytest.raises(CheckError) as e: 63 | func(0.1, 1) 64 | 65 | 66 | def test_class_method_arg_check(): 67 | class A(): 68 | def __init__(self, a): 69 | self.a = a 70 | 71 | @check_args(print_args=False) 72 | def mth1(self, b: Val(float, [0, 1])): 73 | return self.a + b 74 | 75 | a = A(10) 76 | assert a.mth1(0.1) == 10.1 77 | with pytest.raises(CheckError) as e: 78 | a.mth1(10.0) 79 | assert isinstance(e.value.args[0][0], ValueError) 80 | with pytest.raises(CheckError) as e: 81 | a.mth1(False) 82 | assert isinstance(e.value.args[0][0], TypeError) 83 | 84 | 85 | def test_docstring(): 86 | @check_args 87 | def func1(): 88 | "test" 89 | pass 90 | assert func1.__doc__ == "test" 91 | 92 | 93 | def test_implicit(): 94 | @one(print_args=False) 95 | def func(a: int): 96 | return a + 1 97 | assert func(1) == 2 98 | with pytest.raises(CheckError): 99 | func(1.0) 100 | @one(print_args=False) 101 | def func(a: Val[int, [0, 10]], b: Val[int, [0, 10]]): 102 | return a + b 103 | assert func(10, 10) == 20 104 | 105 | 106 | if __name__ == "__main__": 107 | #test_arg_check() 108 | #test_arg_register() 109 | #test_print_args() 110 | #test_class_method_arg_check() 111 | test_implicit() 112 | -------------------------------------------------------------------------------- /tests/test_cli.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, "./") 3 | import subprocess 4 | from oneface.core import * 5 | from funcdesc import Val 6 | import pytest 7 | 8 | 9 | @one 10 | def cli_func(a: Val(int, [0, 10])): 11 | print(a) 12 | 13 | 14 | current_file = "tests/test_cli.py" 15 | 16 | 17 | def test_cli(): 18 | p = subprocess.Popen(f"python {current_file} 10", shell=True, stdout=subprocess.PIPE) 19 | outs, errs = p.communicate() 20 | assert "\n10" in outs.decode("utf8") 21 | with pytest.raises(subprocess.CalledProcessError): 22 | subprocess.check_call(f"python {current_file} 100", shell=True) 23 | 24 | 25 | if __name__ == "__main__": 26 | cli_func.cli() 27 | -------------------------------------------------------------------------------- /tests/test_dash.py: -------------------------------------------------------------------------------- 1 | from oneface.dash_app import * 2 | from oneface.core import one 3 | from oneface.dash_app.embed import flask_route 4 | from funcdesc import Val 5 | 6 | from dash import dcc 7 | 8 | 9 | def test_app_create(): 10 | @app 11 | @one 12 | def func(a: Val(int, [0, 10]), b: Val(float, [0, 5])): 13 | return a + b 14 | 15 | assert func.get_dash_app() is not None 16 | assert func.input_names == ['a', 'b'] 17 | assert func.input_types == [int, float] 18 | 19 | 20 | def test_field_default_value(): 21 | @app 22 | @one 23 | def func(a: Val[int, [-10, 10]] = 0, 24 | b: int = 20): 25 | return a + b 26 | 27 | dash_app = func.get_dash_app() 28 | assert 0 == dash_app.layout.children[1].children[1].value 29 | assert 20 == dash_app.layout.children[2].children[1].value 30 | 31 | @app 32 | @one 33 | def func1(a: bool): 34 | return a 35 | 36 | dash_app = func1.get_dash_app() 37 | assert 'True' == dash_app.layout.children[1].children[1].value 38 | 39 | 40 | def test_download_show_type(): 41 | @app(result_show_type="download") 42 | @one 43 | def func(a: str): 44 | return "" 45 | 46 | dash_app = func.get_dash_app() 47 | assert isinstance(dash_app.layout.children[-1], dcc.Download) 48 | 49 | 50 | def test_plotly_show_type(): 51 | @app(result_show_type="plotly") 52 | @one 53 | def func(): 54 | import plotly.express as px 55 | fig = px.scatter(x=[0, 1, 2, 3, 4], y=[0, 1, 4, 9, 16]) 56 | return fig 57 | 58 | dash_app = func.get_dash_app() 59 | assert isinstance(dash_app.layout.children[-1], dcc.Graph) 60 | 61 | 62 | def test_embed(): 63 | from flask import Flask 64 | 65 | server = Flask("test") 66 | 67 | @flask_route(server, "/dash") 68 | @app 69 | @one 70 | def func(name: str): 71 | return name 72 | 73 | 74 | def test_on_native_func(): 75 | @app 76 | def func(a: int, b: float): 77 | return a + b 78 | 79 | assert func.get_dash_app() is not None 80 | assert func.input_names == ['a', 'b'] 81 | assert func.input_types == [int, float] 82 | 83 | class A(): 84 | @app 85 | def mth1(self, name: str, weight: float): 86 | return name, weight 87 | 88 | a = A() 89 | assert a.mth1.get_dash_app() is not None 90 | assert a.mth1.input_names == ['name', 'weight'] 91 | assert a.mth1.input_types == [str, float] 92 | -------------------------------------------------------------------------------- /tests/test_qt.py: -------------------------------------------------------------------------------- 1 | from oneface.core import * 2 | from oneface.check import * 3 | from oneface.qt import * 4 | from funcdesc import Val 5 | 6 | import pytest 7 | 8 | 9 | def test_int_input(): 10 | @gui 11 | @one 12 | def func(a: Val(int, [0, 10])): 13 | return a 14 | 15 | assert isinstance(func, GUI) 16 | kwargs = func.get_args() 17 | assert kwargs['a'] == 0 18 | func.run_func() 19 | assert func.result == 0 20 | 21 | 22 | def test_int_with_default(): 23 | @gui 24 | @one 25 | def func(a: Val(int, [0, 10]) = 1): 26 | return a 27 | 28 | assert isinstance(func, GUI) 29 | kwargs = func.get_args() 30 | assert kwargs['a'] == 1 31 | func.run_func() 32 | assert func.result == 1 33 | 34 | 35 | def test_set_text(): 36 | @gui 37 | @one 38 | def func(a: Val(int, [0, 10], text="text a")): 39 | return a 40 | 41 | assert func.arg_widgets['a'].label.text() == "text a:" 42 | 43 | 44 | def test_float_input(): 45 | @gui 46 | @one 47 | def func(a: Val(float, [0, 10])): 48 | return a 49 | 50 | assert isinstance(func, GUI) 51 | kwargs = func.get_args() 52 | assert kwargs['a'] == 0.0 53 | func.run_func() 54 | assert func.result == 0.0 55 | 56 | 57 | def test_str_input(): 58 | @gui 59 | @one 60 | def func(a: Val(str)): 61 | return a 62 | 63 | assert isinstance(func, GUI) 64 | kwargs = func.get_args() 65 | assert kwargs['a'] == "" 66 | func.run_func() 67 | assert func.result == "" 68 | 69 | 70 | def test_bool_input(): 71 | @gui 72 | @one 73 | def func(a: Val(bool), b: Val(bool) = False): 74 | return a, b 75 | 76 | assert isinstance(func, GUI) 77 | kwargs = func.get_args() 78 | assert kwargs['a'] == True 79 | assert kwargs['b'] == False 80 | func.run_func() 81 | assert func.result == (True, False) 82 | 83 | 84 | def test_selection_input(): 85 | @gui 86 | @one 87 | def func(a: Val(OneOf, ["a", "b"]) = "a"): 88 | return a 89 | 90 | assert isinstance(func, GUI) 91 | kwargs = func.get_args() 92 | assert kwargs['a'] == "a" 93 | func.run_func() 94 | assert func.result == "a" 95 | 96 | 97 | def test_subset_input(): 98 | @gui 99 | @one 100 | def func(a: Val(SubSet, ["a", "b"]) = ["a"]): 101 | return a 102 | 103 | assert isinstance(func, GUI) 104 | kwargs = func.get_args() 105 | assert kwargs['a'] == ["a"] 106 | func.run_func() 107 | assert func.result == ["a"] 108 | 109 | 110 | def test_inputpath_input(): 111 | @gui 112 | @one 113 | def func(a: Val(InputPath) = "a"): 114 | return a 115 | 116 | assert isinstance(func, GUI) 117 | kwargs = func.get_args() 118 | assert kwargs['a'] == "a" 119 | with pytest.raises(CheckError) as e: 120 | func.run_func() 121 | assert isinstance(e.value.args[0][0], ValueError) 122 | 123 | 124 | def test_on_native_func(): 125 | @gui 126 | def func(a: int, b: float): 127 | return a + b 128 | 129 | assert isinstance(func, GUI) 130 | 131 | class A(): 132 | @gui 133 | def mth1(self, name: str, weight: float): 134 | return name, weight 135 | 136 | a = A() 137 | assert isinstance(a.mth1, GUI) 138 | 139 | 140 | if __name__ == "__main__": 141 | test_set_text() 142 | -------------------------------------------------------------------------------- /tests/test_wrap_cli.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from io import StringIO 3 | import os.path as osp 4 | import sys 5 | 6 | from oneface.wrap_cli.wrap import wrap_cli, load_config 7 | 8 | 9 | HERE = osp.dirname(osp.abspath(__file__)) 10 | example_yaml = osp.join(HERE, "../oneface/wrap_cli/example.yaml") 11 | 12 | 13 | def test_load_config(): 14 | conf = load_config(example_yaml) 15 | assert 'command' in conf 16 | assert 'inputs' in conf 17 | 18 | 19 | def test_wrap_cli(): 20 | conf = load_config(example_yaml) 21 | wrap = wrap_cli(conf) 22 | wrap(1, 2) 23 | assert wrap.__signature__ is not None 24 | 25 | 26 | def test_retcode(): 27 | conf = load_config(example_yaml) 28 | wrap = wrap_cli(conf) 29 | assert 0 == wrap(40, 2) 30 | 31 | 32 | def test_stdout(): 33 | conf = load_config(example_yaml) 34 | wrap = wrap_cli(conf, print_cmd=False) 35 | console_buffer = StringIO() 36 | wrap.out_stream = console_buffer 37 | wrap(40, 2) 38 | assert console_buffer.getvalue().strip() == "42" 39 | 40 | 41 | def test_stderr(): 42 | conf = load_config(example_yaml) 43 | wrap = wrap_cli(conf) 44 | console_buffer = StringIO() 45 | wrap.err_stream = console_buffer 46 | wrap(40, "aaa") 47 | assert "NameError" in console_buffer.getvalue() 48 | 49 | 50 | def test_replace(): 51 | conf = { 52 | "name": "test", 53 | "command": "python {c} 'print(1)'", 54 | "inputs": { 55 | "c": { 56 | "type": "bool", 57 | "true_insert": "-c", 58 | "default": True 59 | }, 60 | }, 61 | } 62 | wrap = wrap_cli(conf, print_cmd=True) 63 | assert 0 == wrap(True) 64 | 65 | 66 | def test_stdout_block(): 67 | conf = { 68 | "name": "test", 69 | "command": "python {v} -c 'print(1)'", 70 | "inputs": { 71 | "v": { 72 | "type": "bool", 73 | "true_insert": "-v", 74 | "default": True 75 | }, 76 | }, 77 | } 78 | wrap = wrap_cli(conf, print_cmd=True) 79 | console_buffer = StringIO() 80 | with contextlib.redirect_stderr(console_buffer): 81 | assert 0 == wrap(True) 82 | --------------------------------------------------------------------------------