pyulix 1.2.0b2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pyulix-1.2.0b2/LICENSE +21 -0
- pyulix-1.2.0b2/NOTICE +8 -0
- pyulix-1.2.0b2/PKG-INFO +132 -0
- pyulix-1.2.0b2/README.md +112 -0
- pyulix-1.2.0b2/pyproject.toml +36 -0
- pyulix-1.2.0b2/setup.cfg +4 -0
- pyulix-1.2.0b2/src/pyulix/__init__.py +12 -0
- pyulix-1.2.0b2/src/pyulix/__init__.pyi +4 -0
- pyulix-1.2.0b2/src/pyulix/_api.py +225 -0
- pyulix-1.2.0b2/src/pyulix/_api.pyi +20 -0
- pyulix-1.2.0b2/src/pyulix/_api_schema.json.gz +0 -0
- pyulix-1.2.0b2/src/pyulix/_bridge.py +367 -0
- pyulix-1.2.0b2/src/pyulix/java/__init__.py +4 -0
- pyulix-1.2.0b2/src/pyulix/java/__init__.pyi +32438 -0
- pyulix-1.2.0b2/src/pyulix/py.typed +0 -0
- pyulix-1.2.0b2/src/pyulix.egg-info/PKG-INFO +132 -0
- pyulix-1.2.0b2/src/pyulix.egg-info/SOURCES.txt +22 -0
- pyulix-1.2.0b2/src/pyulix.egg-info/dependency_links.txt +1 -0
- pyulix-1.2.0b2/src/pyulix.egg-info/top_level.txt +1 -0
- pyulix-1.2.0b2/tests/test_bridge.py +271 -0
- pyulix-1.2.0b2/tests/test_desktop.py +116 -0
- pyulix-1.2.0b2/tests/test_generated_api.py +50 -0
- pyulix-1.2.0b2/tests/test_support_facades.py +117 -0
- pyulix-1.2.0b2/tests/test_typed_objects.py +182 -0
pyulix-1.2.0b2/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 oculix-org
|
|
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.
|
pyulix-1.2.0b2/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
Pyulix is an independent fork of Operix, maintained by Ajay Rakde.
|
|
2
|
+
It is neither affiliated with nor endorsed by the OculiX project.
|
|
3
|
+
Original work by Julien Mer and the Operix/OculiX contributors.
|
|
4
|
+
Original copyright and MIT license notices are preserved in LICENSE.
|
|
5
|
+
Support for this fork: https://github.com/ajayrakde/Operix/issues
|
|
6
|
+
|
|
7
|
+
The separately downloaded JVM engine and its bundled dependencies retain
|
|
8
|
+
their own licenses and notices. The Pyulix MIT license does not replace them.
|
pyulix-1.2.0b2/PKG-INFO
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyulix
|
|
3
|
+
Version: 1.2.0b2
|
|
4
|
+
Summary: Pyulix: independent Python visual automation bridge
|
|
5
|
+
Author: Ajay Rakde
|
|
6
|
+
Maintainer: Ajay Rakde
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/ajayrakde/Operix
|
|
9
|
+
Project-URL: Repository, https://github.com/ajayrakde/Operix
|
|
10
|
+
Project-URL: Issues, https://github.com/ajayrakde/Operix/issues
|
|
11
|
+
Keywords: visual-testing,automation,ocr,sikuli,gui-testing
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Testing
|
|
15
|
+
Requires-Python: >=3.8
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
License-File: NOTICE
|
|
19
|
+
Dynamic: license-file
|
|
20
|
+
|
|
21
|
+
> **Independent fork:** Pyulix is maintained by **Ajay Rakde** and is neither affiliated with nor endorsed by the OculiX project. Original work by Julien Mer and contributors; original copyright and MIT license notices are preserved. [Support](https://github.com/ajayrakde/Operix/issues).
|
|
22
|
+
|
|
23
|
+
# Pyulix (Python)
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
Python wrapper for [OculiX](https://github.com/oculix-org/Oculix) — visual automation for the real world.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install --pre pyulix
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requirements: Python 3.8+, Java 17+ on `PATH` (Eclipse Temurin or Azul Zulu).
|
|
36
|
+
|
|
37
|
+
The OculiX engine (~200 MB fat JAR) is downloaded on first use into `~/.pyulix/lib/`.
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from pyulix import Screen, App, VNCScreen
|
|
43
|
+
|
|
44
|
+
screen = Screen()
|
|
45
|
+
screen.click("login.png")
|
|
46
|
+
screen.type("admin")
|
|
47
|
+
screen.click("submit.png")
|
|
48
|
+
screen.wait("dashboard.png", 10)
|
|
49
|
+
|
|
50
|
+
# Open a desktop app
|
|
51
|
+
calc = App.open("calc")
|
|
52
|
+
screen.click("button_7.png")
|
|
53
|
+
|
|
54
|
+
# Drive a remote VNC display
|
|
55
|
+
vnc = VNCScreen.start("10.0.0.42", 5900, "", 1920, 1080)
|
|
56
|
+
vnc.click("logo.png")
|
|
57
|
+
vnc.stop()
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## With pytest
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import pytest
|
|
64
|
+
from pyulix import Screen, App
|
|
65
|
+
|
|
66
|
+
@pytest.fixture
|
|
67
|
+
def calc():
|
|
68
|
+
a = App.open("calculator")
|
|
69
|
+
yield a
|
|
70
|
+
a.close()
|
|
71
|
+
|
|
72
|
+
def test_addition(calc):
|
|
73
|
+
s = Screen()
|
|
74
|
+
s.click("button_7.png")
|
|
75
|
+
s.click("button_plus.png")
|
|
76
|
+
s.click("button_3.png")
|
|
77
|
+
s.click("button_equals.png")
|
|
78
|
+
assert s.exists("result_10.png")
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT
|
|
84
|
+
|
|
85
|
+
## Java API parity
|
|
86
|
+
|
|
87
|
+
This fork exposes the complete pinned Oculix 4.0.0 public method surface: 211
|
|
88
|
+
public types with named Python methods, constructor/field access and every Java
|
|
89
|
+
overload's arguments. `from pyulix import Screen, OCR, Pattern` uses the new independent namespace.
|
|
90
|
+
Every class is also available through `pyulix.java`; nested classes keep names
|
|
91
|
+
such as `OCR.Options` and `OCR.PSM`.
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
from pyulix import Screen, Pattern, Match, OCR, Location, JavaCallback, ObserverCallBack
|
|
95
|
+
|
|
96
|
+
screen = Screen()
|
|
97
|
+
match = screen.find(Pattern("button.png").similar(0.8))
|
|
98
|
+
assert isinstance(match, Match)
|
|
99
|
+
match.click()
|
|
100
|
+
screen.type("hello", 0)
|
|
101
|
+
|
|
102
|
+
options = OCR.Options().language("eng").psm(OCR.PSM.SINGLE_LINE)
|
|
103
|
+
text = OCR.readText("button.png", options)
|
|
104
|
+
|
|
105
|
+
# Ambiguous Java overloads can be selected explicitly.
|
|
106
|
+
options.psm.overload("org.sikuli.script.OCR$PSM")("SINGLE_LINE")
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
`exists()` returns `Match` or `None`, matching Java; truth checks continue to work.
|
|
110
|
+
Java interface callbacks accept `JavaCallback(interface_name, handler)` and
|
|
111
|
+
observer methods accept a Python function directly. Arrays/collections become
|
|
112
|
+
Python sequences; maps become dictionaries; Java iterators support iteration.
|
|
113
|
+
|
|
114
|
+
This distribution uses the matching 1.2.0b2 JVM bridge from `ajayrakde/Operix`.
|
|
115
|
+
The Python namespace is `pyulix`; bridge downloads are cached under `~/.pyulix/lib/`.
|
|
116
|
+
Java classes and upstream Maven dependency names are unchanged.
|
|
117
|
+
|
|
118
|
+
## Migration from oculix-operix
|
|
119
|
+
|
|
120
|
+
Install `pyulix` with `pip install --pre pyulix`, then change `from oculix ...`
|
|
121
|
+
to `from pyulix ...` (including submodules). No `oculix` compatibility
|
|
122
|
+
package is installed. Remove the old `oculix-operix` distribution if no longer
|
|
123
|
+
needed. Existing published versions are historical and do not acquire this rename.
|
|
124
|
+
|
|
125
|
+
## Status and attribution
|
|
126
|
+
|
|
127
|
+
Beta: overload ambiguity, Java `Any` types, and platform-specific desktop/OCR
|
|
128
|
+
behavior still require care. API coverage is not a guarantee that every argument
|
|
129
|
+
combination works. See the repository's `docs/` for validation and limitations.
|
|
130
|
+
|
|
131
|
+
Original work: Julien Mer and contributors. See LICENSE and NOTICE.
|
|
132
|
+
The separately downloaded JVM engine and native dependencies have their own licenses.
|
pyulix-1.2.0b2/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
> **Independent fork:** Pyulix is maintained by **Ajay Rakde** and is neither affiliated with nor endorsed by the OculiX project. Original work by Julien Mer and contributors; original copyright and MIT license notices are preserved. [Support](https://github.com/ajayrakde/Operix/issues).
|
|
2
|
+
|
|
3
|
+
# Pyulix (Python)
|
|
4
|
+
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Python wrapper for [OculiX](https://github.com/oculix-org/Oculix) — visual automation for the real world.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install --pre pyulix
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requirements: Python 3.8+, Java 17+ on `PATH` (Eclipse Temurin or Azul Zulu).
|
|
16
|
+
|
|
17
|
+
The OculiX engine (~200 MB fat JAR) is downloaded on first use into `~/.pyulix/lib/`.
|
|
18
|
+
|
|
19
|
+
## Quickstart
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from pyulix import Screen, App, VNCScreen
|
|
23
|
+
|
|
24
|
+
screen = Screen()
|
|
25
|
+
screen.click("login.png")
|
|
26
|
+
screen.type("admin")
|
|
27
|
+
screen.click("submit.png")
|
|
28
|
+
screen.wait("dashboard.png", 10)
|
|
29
|
+
|
|
30
|
+
# Open a desktop app
|
|
31
|
+
calc = App.open("calc")
|
|
32
|
+
screen.click("button_7.png")
|
|
33
|
+
|
|
34
|
+
# Drive a remote VNC display
|
|
35
|
+
vnc = VNCScreen.start("10.0.0.42", 5900, "", 1920, 1080)
|
|
36
|
+
vnc.click("logo.png")
|
|
37
|
+
vnc.stop()
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## With pytest
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
import pytest
|
|
44
|
+
from pyulix import Screen, App
|
|
45
|
+
|
|
46
|
+
@pytest.fixture
|
|
47
|
+
def calc():
|
|
48
|
+
a = App.open("calculator")
|
|
49
|
+
yield a
|
|
50
|
+
a.close()
|
|
51
|
+
|
|
52
|
+
def test_addition(calc):
|
|
53
|
+
s = Screen()
|
|
54
|
+
s.click("button_7.png")
|
|
55
|
+
s.click("button_plus.png")
|
|
56
|
+
s.click("button_3.png")
|
|
57
|
+
s.click("button_equals.png")
|
|
58
|
+
assert s.exists("result_10.png")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## License
|
|
62
|
+
|
|
63
|
+
MIT
|
|
64
|
+
|
|
65
|
+
## Java API parity
|
|
66
|
+
|
|
67
|
+
This fork exposes the complete pinned Oculix 4.0.0 public method surface: 211
|
|
68
|
+
public types with named Python methods, constructor/field access and every Java
|
|
69
|
+
overload's arguments. `from pyulix import Screen, OCR, Pattern` uses the new independent namespace.
|
|
70
|
+
Every class is also available through `pyulix.java`; nested classes keep names
|
|
71
|
+
such as `OCR.Options` and `OCR.PSM`.
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from pyulix import Screen, Pattern, Match, OCR, Location, JavaCallback, ObserverCallBack
|
|
75
|
+
|
|
76
|
+
screen = Screen()
|
|
77
|
+
match = screen.find(Pattern("button.png").similar(0.8))
|
|
78
|
+
assert isinstance(match, Match)
|
|
79
|
+
match.click()
|
|
80
|
+
screen.type("hello", 0)
|
|
81
|
+
|
|
82
|
+
options = OCR.Options().language("eng").psm(OCR.PSM.SINGLE_LINE)
|
|
83
|
+
text = OCR.readText("button.png", options)
|
|
84
|
+
|
|
85
|
+
# Ambiguous Java overloads can be selected explicitly.
|
|
86
|
+
options.psm.overload("org.sikuli.script.OCR$PSM")("SINGLE_LINE")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`exists()` returns `Match` or `None`, matching Java; truth checks continue to work.
|
|
90
|
+
Java interface callbacks accept `JavaCallback(interface_name, handler)` and
|
|
91
|
+
observer methods accept a Python function directly. Arrays/collections become
|
|
92
|
+
Python sequences; maps become dictionaries; Java iterators support iteration.
|
|
93
|
+
|
|
94
|
+
This distribution uses the matching 1.2.0b2 JVM bridge from `ajayrakde/Operix`.
|
|
95
|
+
The Python namespace is `pyulix`; bridge downloads are cached under `~/.pyulix/lib/`.
|
|
96
|
+
Java classes and upstream Maven dependency names are unchanged.
|
|
97
|
+
|
|
98
|
+
## Migration from oculix-operix
|
|
99
|
+
|
|
100
|
+
Install `pyulix` with `pip install --pre pyulix`, then change `from oculix ...`
|
|
101
|
+
to `from pyulix ...` (including submodules). No `oculix` compatibility
|
|
102
|
+
package is installed. Remove the old `oculix-operix` distribution if no longer
|
|
103
|
+
needed. Existing published versions are historical and do not acquire this rename.
|
|
104
|
+
|
|
105
|
+
## Status and attribution
|
|
106
|
+
|
|
107
|
+
Beta: overload ambiguity, Java `Any` types, and platform-specific desktop/OCR
|
|
108
|
+
behavior still require care. API coverage is not a guarantee that every argument
|
|
109
|
+
combination works. See the repository's `docs/` for validation and limitations.
|
|
110
|
+
|
|
111
|
+
Original work: Julien Mer and contributors. See LICENSE and NOTICE.
|
|
112
|
+
The separately downloaded JVM engine and native dependencies have their own licenses.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyulix"
|
|
7
|
+
version = "1.2.0b2"
|
|
8
|
+
description = "Pyulix: independent Python visual automation bridge"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE", "NOTICE"]
|
|
12
|
+
requires-python = ">=3.8"
|
|
13
|
+
authors = [{name = "Ajay Rakde"}]
|
|
14
|
+
maintainers = [{name = "Ajay Rakde"}]
|
|
15
|
+
keywords = ["visual-testing", "automation", "ocr", "sikuli", "gui-testing"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 4 - Beta",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Topic :: Software Development :: Testing",
|
|
20
|
+
]
|
|
21
|
+
dependencies = []
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/ajayrakde/Operix"
|
|
25
|
+
Repository = "https://github.com/ajayrakde/Operix"
|
|
26
|
+
|
|
27
|
+
Issues = "https://github.com/ajayrakde/Operix/issues"
|
|
28
|
+
|
|
29
|
+
[tool.setuptools.packages.find]
|
|
30
|
+
where = ["src"]
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
testpaths = ["tests"]
|
|
34
|
+
|
|
35
|
+
[tool.setuptools.package-data]
|
|
36
|
+
pyulix = ["_api_schema.json.gz", "py.typed", "*.pyi", "**/*.pyi"]
|
pyulix-1.2.0b2/setup.cfg
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Complete named Python facade for the pinned Oculix Java API.
|
|
2
|
+
|
|
3
|
+
Java method names, constructors and overload arguments are preserved.
|
|
4
|
+
Use ``method.overload(*java_type_names)`` for ambiguous null/reference calls.
|
|
5
|
+
"""
|
|
6
|
+
from ._bridge import Bridge, BridgeError, RemoteObject, RemoteWrapper, default_bridge
|
|
7
|
+
from ._api import JavaObject, JavaCallback, java_class
|
|
8
|
+
from .java import *
|
|
9
|
+
from .java import __all__ as _java_names
|
|
10
|
+
|
|
11
|
+
__version__ = '1.2.0b2'
|
|
12
|
+
__all__ = list(_java_names) + ['Bridge', 'BridgeError', 'JavaObject', 'JavaCallback', 'java_class', 'default_bridge']
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
"""Java overload binding, fields and typed facades for the generated API."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import inspect
|
|
5
|
+
import gzip
|
|
6
|
+
import json
|
|
7
|
+
import keyword
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ._bridge import RemoteWrapper, RemoteObject, register_wrapper, _WRAPPER_TYPES
|
|
12
|
+
|
|
13
|
+
_SCHEMA = json.loads(gzip.decompress(Path(__file__).with_name('_api_schema.json.gz').read_bytes()))
|
|
14
|
+
_TYPES = {}
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _default_bridge():
|
|
18
|
+
import pyulix
|
|
19
|
+
return pyulix.default_bridge()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _identifier(name):
|
|
23
|
+
return name + '_' if keyword.iskeyword(name) or name in {'self', 'cls'} else name
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _bind(member, args, kwargs):
|
|
27
|
+
parameters = member['parameters']
|
|
28
|
+
values = list(args)
|
|
29
|
+
varargs = member['varargs']
|
|
30
|
+
fixed = len(parameters) - int(varargs)
|
|
31
|
+
if not varargs and len(values) > len(parameters): raise TypeError('Too many positional arguments')
|
|
32
|
+
for i, p in enumerate(parameters):
|
|
33
|
+
name = _identifier(p['name'])
|
|
34
|
+
if name in kwargs:
|
|
35
|
+
if i < len(args): raise TypeError('Multiple values for ' + name)
|
|
36
|
+
if i != len(values): raise TypeError('Missing preceding argument')
|
|
37
|
+
values.append(kwargs[name])
|
|
38
|
+
elif i >= len(values) and i < fixed:
|
|
39
|
+
raise TypeError('Missing argument ' + name)
|
|
40
|
+
known = {_identifier(p['name']) for p in parameters}
|
|
41
|
+
if set(kwargs) - known: raise TypeError('Unknown argument: ' + ', '.join(set(kwargs) - known))
|
|
42
|
+
if varargs:
|
|
43
|
+
# Both Java's explicit array form and Python's expanded positional form.
|
|
44
|
+
final = values[-1] if values else None
|
|
45
|
+
remote = final._remote if isinstance(final, RemoteWrapper) else final
|
|
46
|
+
explicit_array = (final is None or isinstance(final, (list, tuple))
|
|
47
|
+
or isinstance(final, (bytes, bytearray)) and parameters[-1]['type'] == 'byte[]'
|
|
48
|
+
or isinstance(remote, RemoteObject) and remote._class.startswith('['))
|
|
49
|
+
if len(values) == len(parameters) and explicit_array:
|
|
50
|
+
pass
|
|
51
|
+
else: values = values[:fixed] + [values[fixed:]]
|
|
52
|
+
elif len(values) != len(parameters): raise TypeError('Wrong argument count')
|
|
53
|
+
return values
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class BoundJavaMethod:
|
|
57
|
+
def __init__(self, descriptor, instance, owner):
|
|
58
|
+
self.descriptor, self.instance, self.owner = descriptor, instance, owner
|
|
59
|
+
self.__name__ = descriptor.name
|
|
60
|
+
self.__doc__ = descriptor.__doc__
|
|
61
|
+
self.overloads = tuple(descriptor.members)
|
|
62
|
+
member = next((m for m in descriptor.members if m['static'] or instance is not None), descriptor.members[0])
|
|
63
|
+
params = [inspect.Parameter(_identifier(p['name']), inspect.Parameter.VAR_POSITIONAL
|
|
64
|
+
if member['varargs'] and i == len(member['parameters']) - 1 else inspect.Parameter.POSITIONAL_OR_KEYWORD)
|
|
65
|
+
for i, p in enumerate(member['parameters'])]
|
|
66
|
+
self.__signature__ = inspect.Signature(params)
|
|
67
|
+
|
|
68
|
+
def overload(self, *parameter_types):
|
|
69
|
+
"""Choose an exact Java overload where Python values cannot disambiguate it."""
|
|
70
|
+
members = [m for m in self.descriptor.members if tuple(p['type'] for p in m['parameters']) == parameter_types]
|
|
71
|
+
if not members: raise TypeError('No Java overload with types ' + repr(parameter_types))
|
|
72
|
+
return BoundJavaMethod(JavaMethod(self.descriptor.name, members), self.instance, self.owner)
|
|
73
|
+
|
|
74
|
+
def __call__(self, *args, **kwargs):
|
|
75
|
+
bridge = self.instance._remote._bridge if self.instance is not None else _default_bridge()
|
|
76
|
+
candidates = []
|
|
77
|
+
for member in self.descriptor.members:
|
|
78
|
+
if self.instance is None and not member['static'] and self.__name__ != '<init>': continue
|
|
79
|
+
try: values = _bind(member, args, kwargs)
|
|
80
|
+
except TypeError: continue
|
|
81
|
+
prepared = []
|
|
82
|
+
for parameter, value in zip(member['parameters'], values):
|
|
83
|
+
if callable(value) and not isinstance(value, JavaCallback):
|
|
84
|
+
interface = parameter['type']
|
|
85
|
+
if interface == 'java.lang.Object': interface = 'org.sikuli.script.ObserverCallBack'
|
|
86
|
+
value = JavaCallback(interface, value)
|
|
87
|
+
prepared.append(value)
|
|
88
|
+
candidates.append((member, prepared))
|
|
89
|
+
if not candidates:
|
|
90
|
+
raise TypeError(f'No matching argument names/count for {self.owner.JAVA_CLASS}.{self.__name__}; see .overloads')
|
|
91
|
+
# Java owns assignability, interface inheritance, unboxing and conversion.
|
|
92
|
+
# Python only binds argument names/counts; stubs never gate runtime values.
|
|
93
|
+
if len(candidates) > 1:
|
|
94
|
+
index = bridge.resolve_overload(self.owner.JAVA_CLASS, self.__name__, candidates)
|
|
95
|
+
else: index = 0
|
|
96
|
+
member, prepared = candidates[index]
|
|
97
|
+
for parameter, value in zip(member['parameters'], prepared):
|
|
98
|
+
if value is None and parameter.get('non_null'):
|
|
99
|
+
raise TypeError(f"{self.owner.JAVA_CLASS}.{self.__name__}: {parameter['name']} "
|
|
100
|
+
f"must not be None ({parameter['type']})")
|
|
101
|
+
types = [p['type'] for p in member['parameters']]
|
|
102
|
+
if self.__name__ == '<init>': return bridge.create(self.owner.JAVA_CLASS, prepared, parameter_types=types)
|
|
103
|
+
if member['static']:
|
|
104
|
+
return bridge.call_static(self.owner.JAVA_CLASS, self.__name__, prepared, parameter_types=types)
|
|
105
|
+
return bridge.call(self.instance._remote._ref, self.__name__, prepared, parameter_types=types)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class JavaMethod:
|
|
109
|
+
"""A declared, finite set of Java overloads; no arbitrary method forwarding."""
|
|
110
|
+
def __init__(self, name, members):
|
|
111
|
+
self.name, self.members = name, members
|
|
112
|
+
self.__doc__ = '\n'.join(m['name'] + '(' + ', '.join(p['type'] + ' ' + p['name'] for p in m['parameters']) + ')' for m in members)
|
|
113
|
+
|
|
114
|
+
def __get__(self, instance, owner=None):
|
|
115
|
+
return BoundJavaMethod(self, instance, owner)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class JavaField:
|
|
119
|
+
def __init__(self, field): self.field = field
|
|
120
|
+
def __get__(self, instance, owner=None):
|
|
121
|
+
if instance is None and not self.field['static']: return self
|
|
122
|
+
bridge = instance._remote._bridge if instance is not None else _default_bridge()
|
|
123
|
+
req = {'class': owner.JAVA_CLASS, 'field': self.field['name']}
|
|
124
|
+
if instance is not None and not self.field['static']: req = {'ref': instance._remote._ref, 'field': self.field['name']}
|
|
125
|
+
from ._bridge import _decode
|
|
126
|
+
return _decode(bridge, bridge._request(req))
|
|
127
|
+
def __set__(self, instance, value):
|
|
128
|
+
self.set(type(instance), instance, value)
|
|
129
|
+
def set(self, owner, instance, value):
|
|
130
|
+
if self.field['final']: raise AttributeError('Java field is final: ' + self.field['name'])
|
|
131
|
+
bridge = instance._remote._bridge if instance is not None else _default_bridge()
|
|
132
|
+
from ._bridge import _encode
|
|
133
|
+
req = {'class': owner.JAVA_CLASS, 'field': self.field['name'], 'value': _encode(value, bridge)}
|
|
134
|
+
if instance is not None and not self.field['static']: req['ref'] = instance._remote._ref
|
|
135
|
+
bridge._request(req)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
class JavaMeta(type):
|
|
139
|
+
def overload(cls, *parameter_types):
|
|
140
|
+
"""Select an exact public constructor, including null reference calls."""
|
|
141
|
+
members = _SCHEMA.get(cls.JAVA_CLASS, {}).get('constructors', [])
|
|
142
|
+
return BoundJavaMethod(JavaMethod('<init>', members), None, cls).overload(*parameter_types)
|
|
143
|
+
def __setattr__(cls, name, value):
|
|
144
|
+
descriptor = next((p.__dict__[name] for p in cls.__mro__ if name in p.__dict__), None)
|
|
145
|
+
if isinstance(descriptor, JavaField): descriptor.set(cls, None, value)
|
|
146
|
+
else: super().__setattr__(name, value)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class JavaObject(RemoteWrapper, metaclass=JavaMeta):
|
|
150
|
+
JAVA_CLASS = 'java.lang.Object'
|
|
151
|
+
def __init__(self, *args, **kwargs):
|
|
152
|
+
schema = _SCHEMA.get(self.JAVA_CLASS)
|
|
153
|
+
if schema is None: result = _default_bridge().create(self.JAVA_CLASS, list(args))
|
|
154
|
+
else:
|
|
155
|
+
if not schema['constructors']: raise TypeError(self.JAVA_CLASS + ' has no public constructors')
|
|
156
|
+
result = BoundJavaMethod(JavaMethod('<init>', schema['constructors']), None, type(self))(*args, **kwargs)
|
|
157
|
+
self._remote = result._remote if isinstance(result, RemoteWrapper) else result
|
|
158
|
+
self._remote._bridge._cache[self._remote._ref] = self
|
|
159
|
+
@property
|
|
160
|
+
def _ref(self): return self._remote._ref
|
|
161
|
+
@property
|
|
162
|
+
def _bridge(self): return self._remote._bridge
|
|
163
|
+
def __getattr__(self, name):
|
|
164
|
+
if any(_SCHEMA.get(parent.JAVA_CLASS, {}).get('opaque') for parent in type(self).__mro__
|
|
165
|
+
if hasattr(parent, 'JAVA_CLASS')):
|
|
166
|
+
raise AttributeError(f'{self._remote._class} is an opaque Java handle; use the public owner API')
|
|
167
|
+
if self.JAVA_CLASS in _SCHEMA: raise AttributeError(name)
|
|
168
|
+
return super().__getattr__(name)
|
|
169
|
+
def __iter__(self):
|
|
170
|
+
if self._remote._class.startswith('java.util.'):
|
|
171
|
+
iterator = self if self._remote._kind == 'iterator' or 'Iterator' in self._remote._class else self._call('iterator')
|
|
172
|
+
while iterator._call('hasNext'): yield iterator._call('next')
|
|
173
|
+
else: raise TypeError(self.JAVA_CLASS + ' is not an iterable')
|
|
174
|
+
def __repr__(self): return f'<{type(self).__name__} {self._remote._class}#{self._remote._ref}>'
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class JavaCallback:
|
|
178
|
+
"""Python implementation of a Java interface or ObserverCallBack.
|
|
179
|
+
|
|
180
|
+
handler may be a function (single method) or an object/dict of method handlers.
|
|
181
|
+
Keep this object alive while Java may invoke it; the bridge retains it until stop.
|
|
182
|
+
"""
|
|
183
|
+
def __init__(self, interface, handler):
|
|
184
|
+
self.interface = interface.JAVA_CLASS if hasattr(interface, 'JAVA_CLASS') else interface
|
|
185
|
+
self.handler = handler
|
|
186
|
+
def dispatch(self, method, args):
|
|
187
|
+
if isinstance(self.handler, dict): function = self.handler[method]
|
|
188
|
+
elif callable(self.handler): function = self.handler
|
|
189
|
+
else: function = getattr(self.handler, method)
|
|
190
|
+
return function(*args)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def java_class(name, *, parent=None):
|
|
194
|
+
if name in _TYPES: return _TYPES[name]
|
|
195
|
+
cls = JavaMeta(name.rsplit('.', 1)[-1].replace('$', '_'), (parent or JavaObject,), {'JAVA_CLASS': name, '__module__': 'pyulix.java'})
|
|
196
|
+
_TYPES[name] = cls
|
|
197
|
+
register_wrapper(name, cls)
|
|
198
|
+
return cls
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def load_api(namespace):
|
|
202
|
+
todo = set(_SCHEMA)
|
|
203
|
+
while todo:
|
|
204
|
+
ready = sorted(n for n in todo if _SCHEMA[n]['superclass'] not in todo)
|
|
205
|
+
for name in ready:
|
|
206
|
+
row = _SCHEMA[name]
|
|
207
|
+
base = _TYPES.get(row['superclass'], JavaObject)
|
|
208
|
+
attrs = {'JAVA_CLASS': name, '__module__': 'pyulix.java', '__doc__': 'Java ' + name}
|
|
209
|
+
groups = {}
|
|
210
|
+
for m in row['methods']: groups.setdefault(m['name'], []).append(m)
|
|
211
|
+
for mname, members in groups.items():
|
|
212
|
+
descriptor = JavaMethod(mname, members)
|
|
213
|
+
attrs[_identifier(mname)] = descriptor
|
|
214
|
+
attrs[mname] = descriptor
|
|
215
|
+
for f in row['fields']:
|
|
216
|
+
if _identifier(f['name']) not in attrs: attrs[_identifier(f['name'])] = JavaField(f)
|
|
217
|
+
cls = JavaMeta(row['python_name'], (base,), attrs)
|
|
218
|
+
_TYPES[name] = cls
|
|
219
|
+
register_wrapper(name, cls)
|
|
220
|
+
namespace[row['python_name']] = cls
|
|
221
|
+
todo -= set(ready)
|
|
222
|
+
for name, cls in _TYPES.items():
|
|
223
|
+
if '$' in name and name.rsplit('$', 1)[0] in _TYPES:
|
|
224
|
+
setattr(_TYPES[name.rsplit('$', 1)[0]], name.rsplit('$', 1)[1], cls)
|
|
225
|
+
namespace['__all__'] = sorted(row['python_name'] for row in _SCHEMA.values())
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Typing facade: known Java classes expose their generated declarations only."""
|
|
2
|
+
from typing import Any, ClassVar
|
|
3
|
+
from ._bridge import Bridge
|
|
4
|
+
|
|
5
|
+
class JavaObject:
|
|
6
|
+
JAVA_CLASS: ClassVar[str]
|
|
7
|
+
_ref: str
|
|
8
|
+
_bridge: Bridge
|
|
9
|
+
def __init__(self, *args: Any, **kwargs: Any) -> None: ...
|
|
10
|
+
def _call(self, method: str, *args: Any) -> Any: ...
|
|
11
|
+
@classmethod
|
|
12
|
+
def _wrap(cls, value: Any) -> Any: ...
|
|
13
|
+
|
|
14
|
+
class JavaCallback:
|
|
15
|
+
interface: str
|
|
16
|
+
handler: Any
|
|
17
|
+
def __init__(self, interface: Any, handler: Any) -> None: ...
|
|
18
|
+
def dispatch(self, method: str, args: Any) -> Any: ...
|
|
19
|
+
|
|
20
|
+
def java_class(name: str, *, parent: Any = None) -> Any: ...
|
|
Binary file
|