├── .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 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
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 | |  |  |  |
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 | 
127 |
128 | Or Dash web app:
129 | 
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 | 
42 |
43 | Change the last line to `func.dash_app()` and run it again, you will get:
44 |
45 | 
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 | 
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 | 
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 | 
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 | 
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 |
42 |
43 | """
44 |
45 | server.run("127.0.0.1", 8088)
46 | ```
47 |
48 | 
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 | |  |  |  |
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 | 
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 | 
37 |
38 | ## Run multiple times
39 |
40 | By default, oneface Qt interface run only once then exit, when click the run button.
41 |
42 | 
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 | 
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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------