diff --git a/doc/bridge.md b/doc/bridge.md new file mode 100644 index 000000000..b65be2d04 --- /dev/null +++ b/doc/bridge.md @@ -0,0 +1,225 @@ +# Type Mapping +One of the challenging tasks for the Java to Python bridge is to select the +correct set of interfaces to implement that presents the available behaviors +for each Python class as Java interfaces. + +This is represented in two ways. + + - A list of interfaces that describes the functional behavior of + the object in Java. + + - A dictionary that provides a mapping from Java function names + to Python callables. + +With these two objects we can create a JProxy that captures +the behaviors of the Python object in Java. + +We will break the wrappers into two types. + + - Protocol wrappers are entirely behavioral which look at the + dunder functions of the class an present that behavior to + user. + + - Concrete mappings are specializations to each of the + Python builtin classes. We would like to support + additional late specialized wrappers by the user. + (See Specialized Wrappers) + + +## Probing + +Probes will take place in two stages. First we need to collect +a list protocols that are supported for the object. These will be +provided as a bit mapped field. + +There are three ways we can probe for concrete types. We can consult the mro +to see what is the second type slot. For objects that haven't reordered +their mro this will be successful. We can consult the `tp_flags` for those +types that Python has already accelerated support to check for. +We can perform a list of isinstance searches. The last has the significant +downside that it will end up O(n). + + +Concrete Types: + +- PyByteArray +- PyBytes - inherits from byte `Py_TPFLAGS_BYTES_SUBCLASS` +- PyComplex +- PyDict - inherits from dict `Py_TPFLAGS_DICT_SUBCLASS` +- PyEnumerate +- PyLong - inherits from int `Py_TPFLAGS_LONG_SUBCLASS` +- PyExceptionBase - inherits from exception `Py_TPFLAGS_BASE_EXC_SUBCLASS` +- PyList - inherits from list `Py_TPFLAGS_LIST_SUBCLASS` +- PyMemoryView +- PyObject - Base class and special case when an object has len(mro)==1 +- PyRange +- PySet +- PySlice +- PyString - inherits from str `Py_TPFLAGS_UNICODE_SUBCLASS` +- PyTuple - inherits from list `Py_TPFLAGS_TUPLE_SUBCLASS` +- PyType - inherits from type `Py_TPFLAGS_TYPE_SUBCLASS` +- PyZip + + +Protocols include: + +- PyASync - Abstract interface for classes with the async behavior. + Identify with `tp_as_async` slot. + +- PyAttributes - Virtual map interface to the attributes of an object. + Python has two types of map object in getattr/setattr and getitem/setitem. + As we can't have the same function mapping for two behaviors we + will split into a seperate wrapper for this type. + +- PyBuffer - Abstract interface for objects which look like memory buffers. + Identify with `tp_as_buffer` + +- PyCallable - Abstraction for every way that an object can be called. + This will have methods to simplify keyword arguments in Java syntax. + Identify with `tp_call` slot. + +- PyComparable - Class with the `tp_richcompare` slot. + +- PyGenerator - An abstraction for an object which looks both like a iterable and a iterator. + Generators are troublesome as Java requires that every use of an iterable starts + back from the start of the collection. We will need create an illusion of this. + Identify with `tp_iter` and `tp_next` + +- PyIterable - Abstract interface that can be used as a Java Iterable. + Identify with `tp_iter` slot. + +- PyIter - Abstract interface that can be converted to a Java Iterator. + PyIter is not well compatible with the Java representation because + it requires a lot of state information to be wrapped, thus we + will need both a Python like and a Java like instance. The + concrete type PyIterator is compatible with the Java concept. + Identify with `tp_next` (the issue is many Python iter also + look like generators, so may be hard to distiguish.) + +- PySequence - Abstract interface for ordered list of items. + Identify with `Py_TPFLAGS_SEQUENCE` + +- PyMapping - Abstract interface that looks like a Java Map. + Identify with `Py_TPFLAGS_MAPPING` + +- PyNumber - Abstraction for class that supports some subset of numerical operations. + This one will be a bit hit or miss because number slots can mean + number like, matrix like, array like, or some other use of + operators. + + +One special case exists which happens when a Java object is passed through a +Python interface as a return. In such a case we have no way to satify the type +relationship of being a PyObject. Thus we will prove a PyObject wrapper that +holds the Java object. + +We will loosely separate these two types of iterfaces into two Java packages +`python.lang` and `python.protocol`. There are also numerous private internal +classes that are used in wrapping. + + +## Presentation + +Python objects should whereever possible conform to the nearest Java concept +if possible. This means the method names will often need to be remapped to +Java behaviors. This is somewhat burdensome when Java has a different +concept of what is returned or different argument orders. We don't need +to be fully complete here as the user always has the option of using the +builtin methods or using a string eval to get at unwrapped behavior. + +### Name Conflicts + +Python has two behaviors that share the same set of slots by have very +different meanings. Sequence and Mapping both use the dunder functions +getitem/setitem but in one case it can only accept an index and the other any +object. When these are wrapped they map to two difference collections on the +Java side which have extensive name conflicts. Thus the wrapping algorithm +must ensure that these behaviors are mutually exclusive. + +## Specialized Wrappers + +In addition, to the predefined wrappers of Python concrete classes, we may want +to provide the user with some way to add additional specializations. For +example, wrappers for specific classes in nump, scipy, and matplotlib may be +desireable. + +To support this we need a way for a Java module to register is wrappers and +add them interface and dictionaries produced by the probe. There is also +a minor issue that the interpret may not be active when the Jar is loaded. + +Potential implementations: + +- Initialization of a static field to call registerTypes() would be one option. +But Java is lazy in loading classes. That means that if we probe a class +before the corresponding class is loaded we will be stuck with a bad wrapper. + +- JNI onload function is guaranteed to be called when the jar is first +encountered. But is has the significant disadvantage that it must +be tied to a machine architeture. + +- Java module services may be able to force a static block to implement. +This will take experimentation as the behavior of this function is +not well documented. + + + +## Internal implemention + +### Converters + +We will need to define two converters for each Python type. One which gives +the most specific type such that if Java requests a specialized type it is +guaranteed to get it (or get a failure exception) and a second for the base +class PyObject in which Java has requested an untyped object. + + +### Probe Goals + +The system must endever to satisfy these goals. + +1) Minimize the number of probes for each object type. + (Probing will always be an expensive operation) + +2) Memory efficient + (many types will share the same list of interfaces and dictionary, which + means that whereever possible we will want to join like items in the table.) + + +To satify these goals we will use a caching strategy. + +We can consolidate the storage of our entities by using three maps. + +- WeakKeyDict(Type, (Interfaces[], Dict)) holds a cached copy of the results + of every probe. This will be consulted first to avoid unnecessary probe + calls. + +- Dict(Interfaces[], Interfaces[]) will hold the unique tuple of + interfaces that were the results of the probes. When caching we form + the dynamic desired tuple of interfaces, then consult this map to get + the correct instance to store in the cache. + +- Dict(Interfaces[], Dict) as the Java methods are fixed based on the + interfaces provided, we only need one dict for each unique set of interfaces. + +These will all be created at module load time. + + +### Limitations +As probing of classes only happens once, any monkey patching of classes will +not be reflected in the Java typing system. Any changes to the type dictionary +will likely result in broken behaviors in the Java representation. + + +### Exceptions + +Exceptions are a particularly challenging type to wrap. To present an exception +in Java, we need to deal with fact that both Python and Java choose to use +concrete types. There is no easy way to use a proxy. Thus Python +execptions will be represented in two pieces. An interface based one will +be used to wrap the Python class in a proxy. A concrete type in Java will +redirect the Java behaviors to Python proxy. This may lead to some +confusion as the user will can encounter both copies even if we try to +keep the automatically unwrapped. (Ie. Java captures an exception and +places it in a log, then Python is implementing a log listener and gets +the Java version of the wrapper.) + diff --git a/doc/conf.py b/doc/conf.py index 24091a14d..dcc71f916 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -146,7 +146,9 @@ def patch(self, key): mockModule._JPackage = _JPackage mockModule._JClassHints = _JClassHints mockModule._hasClass = lambda x: False +del mockModule.bootstrap sys.modules['_jpype'] = mockModule + import jpype import jpype.imports diff --git a/jpype/__init__.py b/jpype/__init__.py index cf191d97d..899f7f95d 100644 --- a/jpype/__init__.py +++ b/jpype/__init__.py @@ -40,6 +40,7 @@ from . import _jio # lgtm [py/import-own-module] from . import protocol # lgtm [py/import-own-module] from . import _jthread # lgtm [py/import-own-module] +from . import _jbridge # lgtm [py/import-own-module] __all__ = ['java', 'javax'] __all__.extend(_jinit.__all__) # type: ignore[name-defined] diff --git a/jpype/_core.py b/jpype/_core.py index 61b44a90d..60f12c783 100644 --- a/jpype/_core.py +++ b/jpype/_core.py @@ -22,6 +22,7 @@ from pathlib import Path import sys import typing +import weakref import _jpype from . import types as _jtypes @@ -29,6 +30,7 @@ from . import _jcustomizer from . import _jinit from . import _pykeywords +from . import _jbridge from ._jvmfinder import * @@ -459,6 +461,8 @@ def initializeResources(): _jpype.JPypeContext = _jpype.JClass('org.jpype.JPypeContext').getInstance() _jpype.JPypeClassLoader = _jpype.JPypeContext.getClassLoader() + _jbridge.initialize() + # Everything succeeded so started is now true. _JVM_started = True @@ -597,3 +601,16 @@ def removeShutdownHook(self, thread): _jpype.JVMNotRunning = JVMNotRunning + +# Support for Java bridge code +# Dictionary of Python types to Java Interfaces +_jpype._concrete = {} +# Dictionary of String to Java Interfaces +_jpype._protocol = {} +# Dictionary of Type to Tuple(Interface[], Dict) +_jpype._cache = weakref.WeakKeyDictionary() +# Dictionary of Tuple(Interface[]) to Tuple(Interface[]) +_jpype._cache_interfaces = {} +_jpype._cache_methods = {} +# Dictionary of Tuple(Interface) to Dict +_jpype._methods = {} diff --git a/jpype/_jbridge.py b/jpype/_jbridge.py new file mode 100644 index 000000000..f3920dc7e --- /dev/null +++ b/jpype/_jbridge.py @@ -0,0 +1,669 @@ +# ***************************************************************************** +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# See NOTICE file for details. +# +# ***************************************************************************** +import _jpype +from . import _jclass +from . import _jproxy +from . import types as _jtypes +from . import _jcustomizer +from collections.abc import Mapping, Sequence, MutableSequence +import itertools +from typing import MutableMapping, Callable, List + +__all__: List[str] = [] + +JImplements = _jproxy.JImplements +JProxy = _jproxy.JProxy +JOverride = _jclass.JOverride +JConversion = _jcustomizer.JConversion +JClass = _jclass.JClass + +################################################################################### +# Set up methods binds from Java to Python + +def _isTrue(x): + return x==True + +def _not(x): + return not x + +def _setitem(x, k, v): + x[k] = v + +def _getitem(x, k): + return x[k] + +def _setitem_str(x, k, v): + x[str(k)] = v + +def _getitem_str(x, k): + return x[str(k)] + +def _delitem(x, k): + del x[k] + +def _identity(x): + return x + +def _newdict(args): + out = dict() + for p,v in args.entrySet(): + out[p]=v + return out + +def _asfunc(x): + if hasattr(x,__call__): + return x + return None + +def _hasattr(x,k): + return hasattr(x, str(k)) + +def _getattr(x,k): + return getattr(x, str(k)) + +def _setattr(x,k,v): + setattr(x, str(k), v) + +def _delattr(x,k): + delattr(x, str(k)) + +def _getdict(x): + return x.__dict__ + +def _isinstance(x, args): + return isinstance(x, tuple(args)) + +def _contains(x, v): + return v in x + +def _putall(x, m): + for p,v in m.entrySet(): + x[p] = v + +def _containsvalue(x, v): + return v in x.values() + +def _clear(x): + x.clear() + +def _isempty(x): + return len(x) == 0 + +def _keyset(x): + return JClass("org.jpype.bridge.KeySet")(x) + +def _add(x,v): + return x+v + +def _sub(x,v): + return x-v + +def _mult(x,v): + return x*v + +def _div(x,v): + return x/v + +def _addassign(x,v): + x += v + return x + +def _subassign(x,v): + x -= v + return x + +def _multassign(x,v): + x *= v + return x + +def _divassign(x,v): + x /= v + return x + +def _matmul(x,v): + return x@v + +def _divmod(x,v): + return x//v + +def _pow(x,v): + return x**v + +def _remainder(x,v): + return x%v + +def _call(x, v, k): + if k is None: + return x(*v) + return x(*v, **k) + +def _range(*args): + return range(*args) + +def _map(x,f): + return map(f,x) + +def _set_union(x, args): + return x.union(*tuple(args)) + +def _set_intersect(x, args): + return x.intersect(*tuple(args)) + +def _set_difference(x, args): + return x.difference(*tuple(args)) + +def _set_symmetric_difference(x, args): + return x.symmetric_difference(*tuple(args)) + +def _set_add(s, v): + if v in s: + return False + s.add(v) + return True + +def _set_contains(s, v): + return v in s + +def _equals(x,y): + return x == y + +def _filter(x, f): + return filter(f,x) + +def _charat(x,y): + return x[y] + +def _str_subseq(x,s,e): + return x[s:e] + +def _c_add(x, *args): + if len(args)==1: + x.append(args[0]) + return True + x.insert(args[0], args[1]) + return None + +def _indexof(x, v): + try: + return x.index(v) + except ValueError: + return -1 + +def _c_set(x, i, v): + if i<0: + raise ValueError() + out = x[i] + x[i] = v + return out + +def _removeall(x, c): + c = set(c) + nl = [i for i in x if not i in c] + x.clear() + x.extend(nl) + +def _retainall(x, c): + c = set(c) + nl = [i for i in x if i in c] + x.clear() + x.extend(nl) + +def _real(x): + return x.real + +def _imag(x): + return x.imag + +def _items(x): + return x.items() + +def _values(x): + return x.values() + +def _sublist(x, s, e): + return x[s:e] + +def _start(x): + return x.start + +def _end(x): + return x.end + +def _step(x): + return x.step + + +_PyJPBackendMethods: MutableMapping[str, Callable] = { + "bytearray": bytearray, + "bytearray_fromhex": bytearray.fromhex, + "bytes": bytes, + "bytes_fromhex": bytes.fromhex, + "call": _call, + "complex": complex, + "delattr": _delattr, + "dict": dict, + "dir": dir, + "enumerate": enumerate, + "eval": eval, + "exec": exec, + "getDict": _getdict, + "getattr": _getattr, + "getitem": _getitem_str, + "hasattr": _hasattr, + "isinstance": _isinstance, + "iter": iter, + "list": list, + "memoryview": memoryview, + "newDict": _newdict, + "newSet": set, + "newTuple": tuple, + "next": next, + "object": object, + "range": _range, + "repr": repr, + "set": set, + "setattr": _setattr, + "setitem": _setitem_str, + "slice": slice, + "str": str, + "tee": itertools.tee, + "tuple": tuple, + "type": type, + "zip": zip, + "items": _items, + "values": _values, +} + + +# Now we need to set up the dictionary for the proxy object +# This seems trivial but the name binds the Java interface to existing functions +_PyObjectMethods: MutableMapping[str, Callable] = { + "getType": type, + "isInstance": isinstance, + "asAttributes": _identity, + "asSequence": _identity, + "asFunc": _asfunc, + "toInt": _identity, + "toFloat": _identity, + "hashCode": hash, + "equals": _equals, +} + +# Protocols + +_PyAttributesMethods: MutableMapping[str, Callable]= { + "asObject": _identity, + "get": _getattr, + "set": _setattr, + "remove": _delattr, + "has": _hasattr, + "dir": dir, + "dict": _getdict +} + +_PyCallableMethods: MutableMapping[str, Callable] = { + "asObject": _identity, +} + + +_PyMappingMethods: MutableMapping[str, Callable] = { + "asObject": _identity, + "clear": _clear, + "containsKey": _contains, + "containsValue": _containsvalue, + "get": _getitem, + "put": _c_set, + "remove": _delitem, + "putAll": _putall, +} + +_PyNumberMethods: MutableMapping[str, Callable] = { + "asObject": _identity, + "toInt": int, + "toFloat": float, + "toBool": bool, + "not": _not, + "add": _add, + "sub": _sub, + "mult": _mult, + "div": _div, + "addAssign": _addassign, + "subAssign": _subassign, + "multAssign": _multassign, + "divAssign": _divassign, + "matMult": _matmul, + "divMod": _divmod, + "pow": _pow, + "remainder": _remainder, +} + +_PySequenceMethods = { + "asObject": _identity, + "get": _getitem, + "set": _setitem, + "remove": _delitem, + "size": len, +} + +_PyIterableMethods: MutableMapping[str, Callable] = { + "any": any, + "all": all, + "iter": iter, + "map": _map, + "min": min, + "max": max, + "reversed": reversed, + "sorted": sorted, + "sum": sum, +} +_PyIterableMethods.update(_PyObjectMethods) + +_PyIterMethods: MutableMapping[str, Callable] = { + "filter": _filter, + "next": next, +} +_PyIterMethods.update(_PyObjectMethods) + +## Concrete types + +_PyBytesMethods: MutableMapping[str, Callable] = { + "decode": bytes.decode, + "translate": bytes.translate, +} +_PyBytesMethods.update(_PyObjectMethods) + +_PyByteArrayMethods: MutableMapping[str, Callable] = { + "decode": bytearray.decode, + "translate": bytearray.translate, +} +_PyByteArrayMethods.update(_PyObjectMethods) + +_PyComplexMethods: MutableMapping[str, Callable] = { + "real": _real, + "imag": _imag, + "conjugate": complex.conjugate +} +_PyComplexMethods.update(_PyObjectMethods) +_PyComplexMethods.update(_PyNumberMethods) + +_PyDictMethods: MutableMapping[str, Callable] = { + "clear": _clear, + "containsKey": _contains, + "containsValue": _containsvalue, + "get": _getitem, + "put": _c_set, + "remove": _delitem, + "putAll": _putall, +} +_PyDictMethods.update(_PyObjectMethods) + +_PyExcMethods: MutableMapping[str, Callable] = { + "getMessage": str, +} +_PyExcMethods.update(_PyObjectMethods) + +# enumerate, zip, range +_PyGeneratorMethods: MutableMapping[str, Callable] = { + "iter": iter +} +_PyGeneratorMethods.update(_PyObjectMethods) + + + +_PyListMethods: MutableMapping[str, Callable] = { + "add": _c_add, + "addAny": _c_add, + "clear": list.clear, + "contains": _contains, + "extend": list.extend, + "get": _getitem, + "indexOf": _indexof, + "insert": list.insert, + "removeAll": _removeall, + "retainAll": _retainall, + "set": _c_set, + "setAny": _setitem, + "size": len, + "subList": _sublist, +} +_PyListMethods.update(_PyIterableMethods) + +_PyMemoryViewMethods: MutableMapping[str, Callable] = {} +_PyMemoryViewMethods.update(_PyObjectMethods) + +_PySetMethods = { + "add": _set_add, + "clear": set.clear, + "contains": _set_contains, + "copy": set.copy, + "difference": _set_difference, + "discard": set.discard, + "intersect": _set_intersect, + "isDisjoint": set.isdisjoint, + "isSubset": set.issubset, + "isSuperset": set.issuperset, + "size": len, + "pop": set.pop, + "symmetricDifference": _set_symmetric_difference, + "union": _set_union, + "update": set.update, +} +_PySetMethods.update(_PyObjectMethods) + +_PySliceMethods: MutableMapping[str, Callable] = { + "start": _start, + "end": _end, + "step": _step, + "indices": slice.indices +} +_PySliceMethods.update(_PyObjectMethods) + +_PyStringMethods: MutableMapping[str, Callable] = { + "charAt": _charat, + "length": len, + "subSequence": _str_subseq, + "capitalize": str.capitalize, + "casefold": str.casefold, + "center": str.center, + "count": str.count, + "encode": str.encode, + "endsWith": str.endswith, + "expandTabs": str.expandtabs, + "find": str.find, + "format": str.format, + "formatMap": str.format_map, + "index": str.index, + "isAlnum": str.isalnum, + "isAlpha": str.isalpha, + "isAscii": str.isascii, + "isDecimal": str.isdecimal, + "isDigit": str.isdigit, + "isIdentifier": str.isidentifier, + "isLower": str.islower, + "isNumeric": str.isnumeric, + "isPrintable": str.isprintable, + "isSpace": str.isspace, + "isTitle": str.istitle, + "isUpper": str.isupper, + "join": str.join, + "lstrip": str.lstrip, + "partition": str.partition, +# "removePrefix": str.removeprefix, +# "removeSuffix": str.removesuffix, + "replace": str.replace, + "rfind": str.rfind, + "rindex": str.rindex, + "rpartition": str.rpartition, + "rsplit": str.rsplit, + "split": str.split, + "splitLines": str.splitlines, + "startsWith": str.startswith, + "swapCase": str.swapcase, + "title": str.title, + "translate": str.translate, + "upper": str.upper, + "zfill": str.zfill, +} +_PyStringMethods.update(_PyObjectMethods) + +_PyTupleMethods = { + "contains": _contains, + "get": _getitem, + "indexOf": _indexof, + "size": len, + "subList": _sublist, +} +_PyTupleMethods.update(_PyIterableMethods) + +_PyTypeMethods = { + "mro": type.mro, +} +_PyTypeMethods.update(_PyObjectMethods) + + +def initialize(): + return + # Install the handler + bridge = JClass("org.jpype.bridge.Interpreter").getInstance() + Backend = JClass("org.jpype.bridge.Backend") + backend = Backend@JProxy(Backend, dict=_PyJPBackendMethods) + + ################################################################################### + # Name our types into local scope + + # Concrete types + _PyBytes = JClass("python.lang.PyBytes") + _PyByteArray = JClass("python.lang.PyByteArray") + _PyComplex = JClass("python.lang.PyComplex") + _PyDict = JClass("python.lang.PyDict") + _PyEnumerate = JClass("python.lang.PyEnumerate") + _PyJavaObject = JClass("python.lang.PyJavaObject") + _PyList = JClass("python.lang.PyList") + _PyMemoryView = JClass("python.lang.PyMemoryView") + _PyObject = JClass("python.lang.PyObject") + _PyRange = JClass("python.lang.PyRange") + _PySet = JClass("python.lang.PySet") + _PySlice = JClass("python.lang.PySlice") + _PyString = JClass("python.lang.PyString") + _PyTuple = JClass("python.lang.PyTuple") + _PyType = JClass("python.lang.PyType") + _PyZip = JClass("python.lang.PyZip") + _PyExc = JClass("python.exception.PyExc") + + # Protocols + _PyAttributes = JClass("python.protocol.PyAttributes") + _PyCallable = JClass("python.protocol.PyCallable") + _PyGenerator = JClass("python.protocol.PyGenerator") + _PyIterable = JClass("python.protocol.PyIterable") + _PyIter = JClass("python.protocol.PyIter") + _PyMapping = JClass("python.protocol.PyMapping") + _PyNumber = JClass("python.protocol.PyNumber") + _PySequence = JClass("python.protocol.PySequence") + + ################################################################################### + # Bind the method tables + + # Define the method tables for each type here + _PyBytes._methods = _PyBytesMethods + _PyByteArray._methods = _PyByteArrayMethods + _PyDict._methods = _PyDictMethods + _PyEnumerate._methods = _PyGeneratorMethods + _PyGenerator._methods = _PyGeneratorMethods + _PyIterable._methods = _PyIterableMethods + _PyIter._methods = _PyIterMethods + _PyList._methods = _PyListMethods + _PyMemoryView._methods = _PyMemoryViewMethods + _PyObject._methods = _PyObjectMethods + _PyRange._methods = _PyGeneratorMethods + _PySet._methods = _PySetMethods + _PySlice._methods = _PySliceMethods + _PyString._methods = _PyStringMethods + _PyTuple._methods = _PyTupleMethods + _PyType._methods = _PyTypeMethods + _PyZip._methods = _PyGeneratorMethods + + _PyAttributes._methods = _PyAttributesMethods + _PyCallable._methods = _PyCallableMethods + _PyMapping._methods = _PyMappingMethods + _PyNumber._methods = _PyNumberMethods + _PySequence._methods = _PySequenceMethods + + + ################################################################################### + # Construct conversions between concrete types and protocols. + # + # This is a trivial task as we have JProxy which can add an interface to any Python + # object. We just need to make it tag in such a way that it automatically unwraps + # back to Python when passed from Java. + + # Bind the concrete types + @JConversion(_PyBytes, instanceof=bytes) + @JConversion(_PyByteArray, instanceof=bytearray) + @JConversion(_PyComplex, instanceof=complex) + @JConversion(_PyDict, instanceof=dict) + @JConversion(_PyEnumerate, instanceof=enumerate) + @JConversion(_PyExc, instanceof=BaseException) + @JConversion(_PyList, instanceof=list) + @JConversion(_PyMemoryView, instanceof=memoryview) + @JConversion(_PyRange, instanceof=range) + @JConversion(_PySet, instanceof=set) + @JConversion(_PySlice, instanceof=slice) + @JConversion(_PyString, instanceof=str) + @JConversion(_PyTuple, instanceof=tuple) + @JConversion(_PyType, instanceof=type) + @JConversion(_PyZip, instanceof=zip) + # Bind dunder + @JConversion(_PyIterable, attribute="__iter__") + @JConversion(_PyIter, attribute="__next__") + # Bind the protocols + @JConversion(_PyAttributes, instanceof=object) + @JConversion(_PyCallable, instanceof=object) + @JConversion(_PyMapping, instanceof=object) + @JConversion(_PyNumber, instanceof=object) + @JConversion(_PySequence, instanceof=object) + def _jconvert(jcls, obj): + return JProxy(jcls, dict=jcls._methods, inst=obj, convert=True) + + # Create a dispatch which will bind Python concrete types to Java. + # Most of the time people won't see them, but we can add Java interfaces to + # make those objects become Java like. + _conversionDispatch = { + bytes: _PyBytes, + dict: _PyDict, + list: _PyList, + memoryview: _PyMemoryView, + str: _PyString, + tuple: _PyTuple, + type: _PyType, + } + + # Next we bind the dispatch to the Java types using the dispatch + @JConversion(_PyObject, instanceof=object) + def _pyobject(jcls, obj): + if isinstance(obj, _jpype._JObject): + return _PyJavaObject(obj) + # See if there is a more advanced wrapper we can apply + if len(obj.__class__.__mro__) > 1: + jcls = _conversionDispatch.get(obj.__class__.__mro__[-2], jcls) + return _jconvert(jcls, obj) + + bridge.setBackend(backend) + + @JConversion(_PyObject, instanceof=_jpype._JObject) + def _pyobject(jcls, obj): + return _PyJavaObject(obj) diff --git a/native/bootstrap/ejp_natives.cpp b/native/bootstrap/ejp_natives.cpp new file mode 100644 index 000000000..ddea72c56 --- /dev/null +++ b/native/bootstrap/ejp_natives.cpp @@ -0,0 +1,48 @@ +/* +This is a fake module which is installed with the _jpype module to hold the prelaunch hooks. +*/ +#ifdef WIN32 +#include +#else +#if defined(_HPUX) && !defined(_IA64) +#include +#else +#include +#endif // HPUX +#endif +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +void* PyInit__jpypeb() +{ + return nullptr; +} + +/* Stock System.loadLibrary() does not work for us because they load the + shared library with local flags. We need a load which supports shared + used with all Python modules. +*/ +JNIEXPORT jlong JNICALL Java_org_jpype_bridge_BootstrapLoader_loadLibrary +(JNIEnv *env, jclass clazz, jstring lib) +{ + const char *path = env->GetStringUTFChars(lib, nullptr); + void *handle = nullptr; +#ifdef WIN32 + // it is not clear if Windows needs a bootstrap load +#else +#if defined(_HPUX) && !defined(_IA64) + handle = shl_load(path, BIND_DEFERRED | BIND_VERBOSE, 0L); +#else + handle = dlopen(path, RTLD_GLOBAL | RTLD_LAZY); +#endif +#endif + return (jlong) handle; +} + +#ifdef __cplusplus +} +#endif diff --git a/native/common/jp_bridge.cpp b/native/common/jp_bridge.cpp new file mode 100644 index 000000000..0172a2055 --- /dev/null +++ b/native/common/jp_bridge.cpp @@ -0,0 +1,285 @@ +#include +#include +#ifdef WIN32 +#include +#else +#include +#endif +#include "jpype.h" +#include "pyjp.h" +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +static void fail(JNIEnv *env, const char* msg) +{ + // This is a low frequency path so we don't need efficiency. + jclass runtimeException = env->FindClass("java/lang/RuntimeException"); + env->ThrowNew(runtimeException, msg); +} + +static void convertException(JNIEnv *env, JPypeException& ex) +{ + // This is a low frequency path so we don't need efficiency. + // We can't use ex.toJava() because this is part of initialization. + jclass runtimeException = env->FindClass("java/lang/RuntimeException"); + + // If it is a Java exception, we can simply throw it + if (ex.getExceptionType() == JPError::_java_error) + { + env->Throw(ex.getThrowable()); + return; + } + + // No guarantees that the exception will make it back so print it first + PyObject *err = PyErr_Occurred(); + if (err != NULL) + { + PyErr_Print(); + env->ThrowNew(runtimeException, "Exception in Python"); + } else + { + env->ThrowNew(runtimeException, "Internal error"); + } +} + +/* Arguments we need to push in. + * + * A list of module_search_paths so this can be used of limited/embedded deployments. + * A list of command line arguments so we can execute command line functionality. + */ +JNIEXPORT void JNICALL Java_org_jpype_bridge_Natives_start +(JNIEnv *env, jclass cls, jobjectArray modulePath, jobjectArray args, + jstring name, jstring prefix, jstring home, jstring exec_prefix, jstring executable, + jboolean isolated, jboolean faulthandler, jboolean quiet, jboolean verbose, + jboolean site_import, jboolean user_site, jboolean bytecode) +{ + + PyObject* jpype = nullptr; + PyObject* jpypep = nullptr; + JPContext* context; + PyObject *obj; + PyObject *obj2; + PyObject *obj3; + PyStatus status; + PyConfig config; + + PyGILState_STATE gstate; + jboolean isCopy; + const char *cstr; + int length; + std::string str; + jsize items; + wchar_t* wide_str; + jobject v; + std::list resources; + + try + { + if (isolated) + { + PyConfig_InitIsolatedConfig(&config); + } + else + { + PyConfig_InitPythonConfig(&config); + status = PyConfig_Read(&config); + if (PyStatus_Exception(status)) + goto error_config; + } + + config.faulthandler = faulthandler; + config.quiet = quiet; + config.site_import = site_import; + config.user_site_directory = user_site; + config.write_bytecode = bytecode; + config.verbose = verbose; + + if (name != nullptr) + { + cstr = env->GetStringUTFChars(name, &isCopy); + length = env->GetStringUTFLength(name); + str = transcribe(cstr, length, JPEncodingJavaUTF8(), JPEncodingUTF8()); + env->ReleaseStringUTFChars(name, cstr); + wide_str = Py_DecodeLocale(str.c_str(), NULL); + config.program_name = wide_str; + resources.push_back(wide_str); + } + + if (prefix != nullptr) + { + cstr = env->GetStringUTFChars(prefix, &isCopy); + length = env->GetStringUTFLength(prefix); + str = transcribe(cstr, length, JPEncodingJavaUTF8(), JPEncodingUTF8()); + env->ReleaseStringUTFChars(prefix, cstr); + wide_str = Py_DecodeLocale(str.c_str(), NULL); + config.prefix = wide_str; + resources.push_back(wide_str); + } + + if (home != nullptr) + { + cstr = env->GetStringUTFChars(home, &isCopy); + length = env->GetStringUTFLength(home); + str = transcribe(cstr, length, JPEncodingJavaUTF8(), JPEncodingUTF8()); + env->ReleaseStringUTFChars(home, cstr); + wide_str = Py_DecodeLocale(str.c_str(), NULL); + config.home = wide_str; + resources.push_back(wide_str); + } + + if (exec_prefix != nullptr) + { + cstr = env->GetStringUTFChars(exec_prefix, &isCopy); + length = env->GetStringUTFLength(exec_prefix); + str = transcribe(cstr, length, JPEncodingJavaUTF8(), JPEncodingUTF8()); + env->ReleaseStringUTFChars(exec_prefix, cstr); + wide_str = Py_DecodeLocale(str.c_str(), NULL); + config.exec_prefix = wide_str; + resources.push_back(wide_str); + } + + if (executable != nullptr) + { + cstr = env->GetStringUTFChars(executable, &isCopy); + length = env->GetStringUTFLength(executable); + str = transcribe(cstr, length, JPEncodingJavaUTF8(), JPEncodingUTF8()); + env->ReleaseStringUTFChars(executable, cstr); + wide_str = Py_DecodeLocale(str.c_str(), NULL); + config.executable = wide_str; + resources.push_back(wide_str); + } + + if (modulePath != nullptr) + { + config.module_search_paths_set = 1; + items = env->GetArrayLength(modulePath); + for (jsize i = 0; iGetObjectArrayElement(modulePath, i); + if (v == nullptr) + continue; + cstr = env->GetStringUTFChars((jstring)v, &isCopy); + length = env->GetStringUTFLength((jstring)v); + str = transcribe(cstr, length, JPEncodingJavaUTF8(), JPEncodingUTF8()); + env->ReleaseStringUTFChars((jstring)v, cstr); + wide_str = Py_DecodeLocale(str.c_str(), NULL); + PyWideStringList_Append(&config.module_search_paths, wide_str); + resources.push_back(wide_str); + } + } + + if (args != nullptr) + { + config.parse_argv = 1; + items = env->GetArrayLength(args); + for (jsize i = 0; iGetObjectArrayElement(args, i); + if (v == nullptr) + continue; + cstr = env->GetStringUTFChars((jstring)v, &isCopy); + length = env->GetStringUTFLength((jstring)v); + str = transcribe(cstr, length, JPEncodingJavaUTF8(), JPEncodingUTF8()); + env->ReleaseStringUTFChars((jstring)v, cstr); + wide_str = Py_DecodeLocale(str.c_str(), NULL); + PyWideStringList_Append(&config.argv, wide_str); + resources.push_back(wide_str); + } + if (PyStatus_Exception(status)) + goto error_config; + } + + // Get Python started +// PyImport_AppendInittab("_jpype", &PyInit__jpype); + status = Py_InitializeFromConfig(&config); + if (PyStatus_Exception(status)) + goto error_config; + + goto success_config; + +error_config: + PyConfig_Clear(&config); + fail(env, "configuration failed"); + for (std::list::iterator iter = resources.begin(); iter!=resources.end(); ++iter) + PyMem_Free(*iter); + return; + +success_config: + PyConfig_Clear(&config); +// for (std::list::iterator iter = resources.begin(); iter!=resources.end(); ++iter) +// PyMem_Free(*iter); +#if PY_VERSION_HEX<0x030a0000 + PyEval_InitThreads(); +#endif + + gstate = PyGILState_Ensure(); + jpype = PyImport_ImportModule("jpype"); + jpypep = PyImport_ImportModule("_jpype"); + if (jpypep == NULL) + { + fail(env, "_jpype module not found"); + return; + } + + // Import the Python side to create the hooks + if (jpype == NULL) + { + fail(env, "jpype module not found"); + return; + } + + PyJPModule_loadResources(jpypep); + + Py_DECREF(jpype); + Py_DECREF(jpypep); + + // Then attach the private module to the JVM + context = JPContext_global; + context->attachJVM(env); + JPJavaFrame frame = JPJavaFrame::external(env); + + // Initialize the resources in the jpype module + obj = PyObject_GetAttrString(jpype, "_core"); + obj2 = PyObject_GetAttrString(obj, "initializeResources"); + obj3 = PyTuple_New(0); + PyObject_Call(obj2, obj3, NULL); + Py_DECREF(obj); + Py_DECREF(obj2); + Py_DECREF(obj3); + + // Next, we need to release the state so we can return to Java. + PyGILState_Release(gstate); + return; + + } catch (JPypeException& ex) + { + convertException(env, ex); + } catch (...) + { + fail(env, "C++ exception during start"); + } +} + +JNIEXPORT void JNICALL Java_org_jpype_bridge_Natives_interactive +(JNIEnv *env, jclass cls) +{ + JPPyCallAcquire callback; + PyRun_InteractiveLoop(stdin, ""); +} + +JNIEXPORT void JNICALL Java_org_jpype_bridge_Natives_finish +(JNIEnv *env, jclass cls) +{ + JPPyCallAcquire callback; + Py_Finalize(); +} + + + +#ifdef __cplusplus +} +#endif diff --git a/native/jpype_module/nb-configuration.xml b/native/jpype_module/nb-configuration.xml index b7553f735..b06d56435 100644 --- a/native/jpype_module/nb-configuration.xml +++ b/native/jpype_module/nb-configuration.xml @@ -43,5 +43,9 @@ Any value defined here will override the pom.xml file value but is only applicab 80 true project + ${project.basedir}/licenseheader.txt + true + true + all diff --git a/native/jpype_module/pom.xml b/native/jpype_module/pom.xml index a10eac9f1..6c2975a9c 100644 --- a/native/jpype_module/pom.xml +++ b/native/jpype_module/pom.xml @@ -9,7 +9,7 @@ org.testng testng - 6.8.1 + 7.3.0 test @@ -46,6 +46,11 @@ + + + -Xlint + + diff --git a/native/jpype_module/src/main/java/exclude/org/jpype/Reflector0.java b/native/jpype_module/src/main/java/exclude/org/jpype/Reflector0.java index f4ff1891f..8241c1e5e 100644 --- a/native/jpype_module/src/main/java/exclude/org/jpype/Reflector0.java +++ b/native/jpype_module/src/main/java/exclude/org/jpype/Reflector0.java @@ -3,11 +3,22 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +/** + * Instance of JPypeReflector loaded during bootstrapping. + * + * Due to restrictions in the Java module system it is necessary that the + * Reflector by loaded in the unnamed module category with a special + * classloader. As such this class may not appear in the jar as a loaded class. + * Instead the class is stored in the META-INF directory as if it were class for + * Java version 0. When JPype loads it fetches this class from the jar and loads + * it with the required security privilages. + */ public class Reflector0 implements JPypeReflector { public Reflector0() - {} + { + } /** * Call a method using reflection. @@ -31,7 +42,6 @@ public Object callMethod(Method method, Object obj, Object[] args) return method.invoke(obj, args); } catch (InvocationTargetException ex) { -// ex.printStackTrace(); throw ex.getCause(); } } diff --git a/native/jpype_module/src/main/java/module-info.java b/native/jpype_module/src/main/java/module-info.java index 84de82f20..2ad2b3316 100644 --- a/native/jpype_module/src/main/java/module-info.java +++ b/native/jpype_module/src/main/java/module-info.java @@ -21,6 +21,7 @@ requires java.management; exports org.jpype; + exports org.jpype.bridge; exports org.jpype.html; exports org.jpype.javadoc; exports org.jpype.manager; @@ -28,4 +29,6 @@ exports org.jpype.pkg; exports org.jpype.proxy; exports org.jpype.ref; + exports python.lang; + exports python.exception; } diff --git a/native/jpype_module/src/main/java/org/jpype/JPypeClassLoader.java b/native/jpype_module/src/main/java/org/jpype/JPypeClassLoader.java index f56ae622c..e217f2703 100644 --- a/native/jpype_module/src/main/java/org/jpype/JPypeClassLoader.java +++ b/native/jpype_module/src/main/java/org/jpype/JPypeClassLoader.java @@ -28,6 +28,9 @@ import java.util.jar.JarEntry; import java.util.jar.JarFile; import java.net.URLDecoder; +import java.util.HashSet; +import java.util.logging.Level; +import java.util.logging.Logger; /** * Class loader for JPype. @@ -287,64 +290,67 @@ public void addURL(URL url) /** * Recreate missing directory entries for Jars that lack indexing. * - * Some jar files are missing the directory entries that prevents use from + * Some jar files are missing the directory entries that prevents us from * properly importing their contents. This procedure scans a jar file when * loaded to build missing directories. * - * @param path + * @param path Path to the jar file. */ void scanJar(Path path) { - if (!Files.exists(path)) - return; - if (Files.isDirectory(path)) + // Validate the input path + if (path == null || !Files.exists(path) || Files.isDirectory(path)) return; - try (JarFile jf = new JarFile(path.toFile())) + try (JarFile jarFile = new JarFile(path.toFile())) { - Enumeration entries = jf.entries(); - URI abs = path.toAbsolutePath().toUri(); - Set urls = new java.util.HashSet(); + Enumeration entries = jarFile.entries(); + URI absoluteUri = path.toAbsolutePath().toUri(); + Set directoryEntries = new HashSet<>(); + while (entries.hasMoreElements()) { - JarEntry next = entries.nextElement(); - String name = next.getName(); + JarEntry entry = entries.nextElement(); + String entryName = entry.getName(); - // Skip over META-INF - if (name.startsWith("META-INF/")) + // Skip META-INF directory + if (entryName.startsWith("META-INF/")) + { continue; + } - if (next.isDirectory()) - { - // If we find a directory entry then the jar has directories already + // If the jar already contains directory entries, exit early + if (entry.isDirectory()) return; - } - // Split on each separator in the name - int i = 0; + // Process the entry name to identify missing directories + int separatorIndex = 0; while (true) { - i = name.indexOf("/", i); - if (i == -1) + separatorIndex = entryName.indexOf("/", separatorIndex); + if (separatorIndex == -1) break; - String name2 = name.substring(0, i); - i++; + String directoryName = entryName.substring(0, separatorIndex); + separatorIndex++; - // Already have an entry no problem - if (urls.contains(name2)) + // Skip if the directory is already processed + if (!directoryEntries.add(directoryName)) continue; - // Add a new entry for the missing directory - String jar = "jar:" + abs + "!/" + name2 + "/"; - urls.add(name2); - this.addResource(name2, new URL(jar)); + // Construct the jar URL for the missing directory + URI jarUri = new URI("jar", absoluteUri + "!/" + directoryName + "/", null); + this.addResource(directoryName, jarUri.toURL()); } } - } catch (IOException ex) + } catch (IOException | URISyntaxException ex) { - // Anything goes wrong skip it + // Log the exception for debugging purposes + System.err.println("Failed to process jar file: " + path); + ex.printStackTrace(); } + // Log the exception for debugging purposes + } } diff --git a/native/jpype_module/src/main/java/org/jpype/JPypeContext.java b/native/jpype_module/src/main/java/org/jpype/JPypeContext.java index 34d952471..66965196e 100644 --- a/native/jpype_module/src/main/java/org/jpype/JPypeContext.java +++ b/native/jpype_module/src/main/java/org/jpype/JPypeContext.java @@ -64,8 +64,6 @@ * contents of JPJni. * * - * - * @author nelson85 */ public class JPypeContext { @@ -327,7 +325,7 @@ public void _addPost(Runnable run) /** * Helper function for collect rectangular, */ - private static boolean collect(List l, Object o, int q, int[] shape, int d) + private static boolean collect(List l, Object o, int q, int[] shape, int d) { if (Array.getLength(o) != shape[q]) return false; diff --git a/native/jpype_module/src/main/java/org/jpype/JPypeKeywords.java b/native/jpype_module/src/main/java/org/jpype/JPypeKeywords.java index f8ea1df04..934a71044 100644 --- a/native/jpype_module/src/main/java/org/jpype/JPypeKeywords.java +++ b/native/jpype_module/src/main/java/org/jpype/JPypeKeywords.java @@ -20,43 +20,103 @@ import java.util.Set; /** + * Utility class for handling reserved keywords in JPype. * - * @author nelson85 + * This class provides methods to manage a set of reserved keywords and offers + * functionality to wrap and unwrap names to avoid conflicts with these + * keywords. It also includes a method to safely process package names + * containing underscores. */ public class JPypeKeywords { + /** + * A set of reserved keywords. + * + * This set contains keywords that need special handling to avoid conflicts. + * Keywords can be added dynamically using the {@link #setKeywords(String[])} + * method. + */ final public static Set keywords = new HashSet<>(); + /** + * Adds keywords to the reserved keywords set. + * + * This method allows dynamic addition of keywords to the {@link #keywords} + * set. + * + * @param s An array of strings representing the keywords to add. + */ public static void setKeywords(String[] s) { keywords.addAll(Arrays.asList(s)); } + /** + * Wraps a name if it matches a reserved keyword. + * + * If the given name is a reserved keyword, this method appends an underscore + * (`_`) to the name to avoid conflicts. Otherwise, it returns the name + * unchanged. + * + * @param name The name to check and wrap if necessary. + * @return The wrapped name if it is a keyword, otherwise the original name. + */ public static String wrap(String name) { if (keywords.contains(name)) + { return name + "_"; + } return name; } + /** + * Unwraps a name that ends with an underscore (`_`). + * + * If the given name ends with an underscore and matches a reserved keyword + * (after removing the underscore), this method returns the original keyword. + * Otherwise, it returns the name unchanged. + * + * @param name The name to check and unwrap if necessary. + * @return The unwrapped name if it ends with an underscore and matches a + * keyword, otherwise the original name. + */ public static String unwrap(String name) { if (!name.endsWith("_")) + { return name; + } String name2 = name.substring(0, name.length() - 1); if (keywords.contains(name2)) - return name2;; + { + return name2; + } return name; } + /** + * Safely processes a package name by unwrapping parts containing underscores. + * + * This method splits the package name into its components (separated by + * dots), unwraps each part, and then reassembles the package name. It ensures + * that underscores are handled properly for each part of the package name. + * + * @param s The package name to process. + * @return The processed package name with unwrapped parts. + */ static String safepkg(String s) { if (!s.contains("_")) + { return s; + } String[] parts = s.split("\\."); for (int i = 0; i < parts.length; ++i) + { parts[i] = unwrap(parts[i]); + } return String.join(".", parts); } } diff --git a/native/jpype_module/src/main/java/org/jpype/JPypeReflector.java b/native/jpype_module/src/main/java/org/jpype/JPypeReflector.java index c6963724d..a521ab85a 100644 --- a/native/jpype_module/src/main/java/org/jpype/JPypeReflector.java +++ b/native/jpype_module/src/main/java/org/jpype/JPypeReflector.java @@ -3,17 +3,23 @@ import java.lang.reflect.Method; /** + * Interface for invoking methods using reflection in JPype. * - * @author nelson85 + * The {@code JPypeReflector} interface provides a mechanism to call methods + * dynamically using Java reflection. This is particularly useful for invoking + * caller-sensitive methods while ensuring proper stack frame creation for + * execution. */ public interface JPypeReflector { /** - * Call a method using reflection. + * Invokes a method using reflection. * - * This method creates a stackframe so that caller sensitive methods will - * execute properly. + * This method dynamically calls the specified method on the provided object + * (or class, if the method is static) using reflection. It ensures that + * caller-sensitive methods execute correctly by creating the appropriate + * stack frame during invocation. * * @param method is the method to call. * @param obj is the object to operate on, it will be null if the method is @@ -23,6 +29,5 @@ public interface JPypeReflector * @throws java.lang.Throwable throws whatever type the called method * produces. */ - public Object callMethod(Method method, Object obj, Object[] args) - throws Throwable; + public Object callMethod(Method method, Object obj, Object[] args) throws Throwable; } diff --git a/native/jpype_module/src/main/java/org/jpype/JPypeSignal.java b/native/jpype_module/src/main/java/org/jpype/JPypeSignal.java index 639d74235..a4fa58f1b 100644 --- a/native/jpype_module/src/main/java/org/jpype/JPypeSignal.java +++ b/native/jpype_module/src/main/java/org/jpype/JPypeSignal.java @@ -47,8 +47,8 @@ static void installHandlers() { try { - Class Signal = Class.forName("sun.misc.Signal"); - Class SignalHandler = Class.forName("sun.misc.SignalHandler"); + Class Signal = Class.forName("sun.misc.Signal"); + Class SignalHandler = Class.forName("sun.misc.SignalHandler"); main = Thread.currentThread(); Method method = Signal.getMethod("handle", Signal, SignalHandler); Object intr = Signal.getDeclaredConstructor(String.class).newInstance("INT"); diff --git a/native/jpype_module/src/main/java/org/jpype/JPypeUtilities.java b/native/jpype_module/src/main/java/org/jpype/JPypeUtilities.java index a73c65025..2f2474fd8 100644 --- a/native/jpype_module/src/main/java/org/jpype/JPypeUtilities.java +++ b/native/jpype_module/src/main/java/org/jpype/JPypeUtilities.java @@ -11,6 +11,7 @@ import java.util.Arrays; import java.util.function.Predicate; +@SuppressWarnings("unchecked") public class JPypeUtilities { @@ -21,11 +22,11 @@ public class JPypeUtilities .filter(m -> !Modifier.isFinal(m.getModifiers())) .toArray(Method[]::new); - private static final Predicate isSealed; + private static final Predicate> isSealed; static { - Predicate result = null; + Predicate> result = null; try { Method m = Class.class.getMethod("isSealed"); diff --git a/native/jpype_module/src/main/java/org/jpype/PyExceptionProxy.java b/native/jpype_module/src/main/java/org/jpype/PyExceptionProxy.java index ffbe93184..563b5ba48 100644 --- a/native/jpype_module/src/main/java/org/jpype/PyExceptionProxy.java +++ b/native/jpype_module/src/main/java/org/jpype/PyExceptionProxy.java @@ -2,7 +2,6 @@ /** * - * @author nelson85 */ public class PyExceptionProxy extends RuntimeException { diff --git a/native/jpype_module/src/main/java/org/jpype/bridge/Backend.java b/native/jpype_module/src/main/java/org/jpype/bridge/Backend.java new file mode 100644 index 000000000..f73321633 --- /dev/null +++ b/native/jpype_module/src/main/java/org/jpype/bridge/Backend.java @@ -0,0 +1,330 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package org.jpype.bridge; + +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.Future; +import python.lang.PyByteArray; +import python.lang.PyBytes; +import python.lang.PyComplex; +import python.lang.PyDict; +import python.lang.PyEnumerate; +import python.lang.PyFloat; +import python.lang.PyFrozenSet; +import python.lang.PyInt; +import python.lang.PyList; +import python.lang.PyMemoryView; +import python.lang.PyObject; +import python.lang.PyRange; +import python.lang.PySet; +import python.lang.PySlice; +import python.lang.PyString; +import python.lang.PyTuple; +import python.lang.PyType; +import python.lang.PyZip; +import python.lang.PyBuffer; +import python.lang.PyCallable; +import python.lang.PyIter; + +/** + * Backend for all Python entry points. + * + * This interface is implemented by Python to allow access to Java. It is not + * callable directly but serves as a bridge used by other support classes to + * provide functionality. + * + * The user should not interact with this interface directly. Instead, the + * user-facing API is available through `BuiltIn` and `Context`. + * + * Parameters are kept as generic as possible to ensure universal applicability + * across the Python-Java bridge. Return types are chosen to be the most + * specific to match Python's behavior. + * + * `CharSequence` is preferred over `String` to accommodate both Python `str` + * and Java `String` seamlessly. + * + * Avoid overloading these methods unnecessarily, as different implementations + * may be required depending on the arguments. + */ +public interface Backend +{ + + // Create a Python `bytearray` from the specified object. + PyByteArray bytearray(Object obj); + + // Create a Python `bytearray` from a hex-encoded string. + PyByteArray bytearrayFromHex(CharSequence str); + + // Create a Python `bytes` object from the specified object. + PyBytes bytes(Object obj); + + // Create a Python `bytes` object from a hex-encoded string. + PyBytes bytesFromHex(CharSequence str); + + // Call a Python callable with positional and keyword arguments. + PyObject call(PyCallable obj, PyTuple args, PyDict kwargs); + + // Call a Python callable asynchronously with positional and keyword arguments. + Future callAsync(PyCallable callable, PyTuple args, PyDict kwargs); + + // Call a Python callable asynchronously with a timeout. + Future callAsyncWithTimeout(PyCallable callable, PyTuple args, PyDict kwargs, long timeout); + + // Check if the specified value is contained within the given Python object. + boolean contains(Object obj, Object value); + + // Delete an item from a Python sequence by index. + void delitemByIndex(Object obj, int index); + + void delitemByObject(Object obj, Object index); + + PyObject delattrReturn(Object obj, Object key); + + // Delete an attribute from a Python object by name. + void delattrString(Object obj, CharSequence attrName); + + // Get a list of attributes for a Python object (equivalent to Python's `dir()`). + PyList dir(Object obj); + + // Create a Python `enumerate` object from the specified iterable. + PyEnumerate enumerate(Object obj); + + // Evaluate a Python expression with optional global and local variables. + PyObject eval(CharSequence source, PyDict globals, PyObject locals); + + // Execute a Python statement with optional global and local variables. + void exec(CharSequence source, PyDict globals, PyObject locals); + + // Get the `__dict__` attribute of a Python object. + PyDict getDict(Object obj); + + // Get the docstring of a Python callable. + String getDocString(PyCallable obj); + + // Get the signature of a Python callable. + PyObject getSignature(PyCallable obj); + + PyObject getattrDefault(PyObject obj, Object key, PyObject defaultValue); + + // Get an attribute from a Python object by key. + PyObject getattrObject(PyObject obj, Object key); + + // Get an attribute from a Python object by name. + PyObject getattrString(Object obj, CharSequence attrName); + + // Get an item from a Python mapping object by key. + PyObject getitemMappingObject(Object obj, Object key); + + // Get an item from a Python mapping object by string key. + PyObject getitemMappingString(Object obj, CharSequence key); + + // Get an item from a Python sequence by index. + PyObject getitemSequence(Object obj, int index); + + // Check if a Python object has a specific attribute. + boolean hasattrString(Object obj, CharSequence attrName); + + // Check if the specified object is callable in Python. + boolean isCallable(Object obj); + + // Check if a Python object is an instance of any of the specified types. + boolean isinstanceFromArray(Object obj, Object[] types); + + // Get the items of a Python mapping object. + PyObject items(PyObject obj); + + // Create a Python iterator from the specified object. + PyIter iter(Object obj); + + PyIter iterSet(Set obj); + + PyIter iterMap(Map obj); + + // Get the keys of a Python mapping object. + PyObject keys(Object dict); + + // Get the length of a Python object (equivalent to Python's `len()`). + int len(Object obj); + + // Create a Python `list` from the specified object. + PyList list(Object obj); + + // Clear all items in a Python mapping object. + void mappingClear(); + + // Clear all items in the specified Python mapping object. + void mappingClear(Object obj); + + // Check if all values in the collection are contained in the Python mapping object. + boolean mappingContainsAllValues(Object map, Collection c); + + // Check if the specified value is contained in the Python mapping object. + boolean mappingContainsValue(Object map, Object value); + + // Remove all keys in the collection from the Python mapping object. + boolean mappingRemoveAllKeys(Object map, Collection collection); + + // Remove all values in the collection from the Python mapping object. + boolean mappingRemoveAllValue(Object map, Collection collection); + + // Remove the specified value from the Python mapping object. + boolean mappingRemoveValue(Object map, Object value); + + // Retain only the specified keys in the Python mapping object. + boolean mappingRetainAllKeys(Object map, Collection collection); + + // Retain only the specified values in the Python mapping object. + boolean mappingRetainAllValue(Object map, Collection collection); + + // Create a Python `memoryview` from the specified object. + PyMemoryView memoryview(Object obj); + + // Create an empty Python `bytearray`. + PyByteArray newByteArray(); + + // Create a Python `bytearray` from a buffer. + PyByteArray newByteArrayFromBuffer(PyBuffer bytes); + + // Create a Python `bytearray` from an iterable. + PyByteArray newByteArrayFromIterable(Iterable iter); + + PyByteArray newByteArrayFromIterator(Iterable iterable); + + // Create a Python `bytearray` with a specified size. + PyByteArray newByteArrayOfSize(int size); + + // Create a Python `bytes` object from a buffer. + PyBytes newBytesFromBuffer(PyBuffer bytes); + + // Create a Python `bytes` object from an iterator. + PyBytes newBytesFromIterator(Iterable iter); + + // Create a Python `bytes` object with a specified size. + PyBytes newBytesOfSize(int size); + + // Create a Python `complex` number with real and imaginary parts. + PyComplex newComplex(double real, double imag); + + // Create an empty Python `dict`. + PyDict newDict(); + + // Create a Python `dict` from a Java `Map`. + PyDict newDict(Map map); + + // Create a Python `dict` from an iterable of key-value pairs. + PyDict newDictFromIterable(Iterable iterable); + + // Create a Python `enumerate` object from an iterable. + PyEnumerate newEnumerate(Iterable iterable); + + // Create a Python `float` from a double value. + PyFloat newFloat(double value); + + // Create a Python `frozenset` from an iterable. + PyFrozenSet newFrozenSet(Iterable iterable); + + // Create a Python `int` from a long value. + PyInt newInt(long value); + + // Create an empty Python `list`. + PyList newList(); + + // Create a Python `list` from an array of objects. + PyList newListFromArray(Object... elements); + + // Create a Python `list` from an iterable. + PyList newListFromIterable(Iterable iterable); + + // Create an empty Python `set`. + PySet newSet(); + + // Create a Python `set` from an iterable. + PySet newSetFromIterable(Iterable iterable); + + PyTuple newTuple(); + + // Create a Python `tuple` from an array of objects. + PyTuple newTupleFromArray(List elements); + + // Create a Python `tuple` from an iterable. + PyTuple newTupleFromIterator(Iterable iterable); + + // Create a Python `zip` object from multiple iterables. + PyZip newZip(Object[] iterables); + + // Get the next item from a Python iterator, with a stop value. + PyObject next(Object iterator, Object stop); + + // Create a generic Python object. + PyObject object(); + + // Create a Python `range` object with a stop value. + PyRange range(int stop); + + // Create a Python `range` object with start and stop values. + PyRange range(int start, int stop); + + // Create a Python `range` object with start, stop, and step values. + PyRange range(int start, int stop, int step); + + // Get the string representation of an object (equivalent to Python's `repr()`). + PyString repr(Object obj); + + // Create an empty Python `set`. + PySet set(); + + // Set an attribute on a Python object and return the updated object. + PyObject setattrReturn(PyObject obj, Object attrName, Object value); + + // Set an attribute on a Python object. + void setattrString(Object obj, CharSequence attrName, Object value); + + // Set an item in a Python mapping object by object key. + PyObject setitemFromObject(Object obj, Object key, Object value); + + // Set an item in a Python mapping object by string key. + void setitemFromString(Object obj, CharSequence key, Object value); + + void setitemMapping(Object obj, Object index, Object values); + + PyObject setitemSequence(Object obj, int index, Object value); + + // Create a Python `slice` object. + PySlice slice(Integer start, Integer stop, Integer step); + + // Convert an object to a Python `str`. + PyString str(Object obj); + + // Create a Python `tee` iterator. + PyIter teeIterator(PyIter iterator); + + // Get the type of a Python object. + PyType type(Object obj); + + // Get the values of a Python mapping object. + PyObject values(Object obj); + + // Get the `__dict__` attribute of a Python object. + PyDict vars(Object obj); + + PyZip zipFromArray(Object[] objects); + + // Create a Python `zip` object from multiple objects. + PyZip zipFromIterable(Iterable objects); +} diff --git a/native/jpype_module/src/main/java/org/jpype/bridge/BootstrapLoader.java b/native/jpype_module/src/main/java/org/jpype/bridge/BootstrapLoader.java new file mode 100644 index 000000000..61b3ebc03 --- /dev/null +++ b/native/jpype_module/src/main/java/org/jpype/bridge/BootstrapLoader.java @@ -0,0 +1,28 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package org.jpype.bridge; + +/** + * JPype Python Native Embedding. + * + * Loaded by _jpyne shared library. + */ +class BootstrapLoader +{ + + native static void loadLibrary(String library); + +} diff --git a/native/jpype_module/src/main/java/org/jpype/bridge/Context.java b/native/jpype_module/src/main/java/org/jpype/bridge/Context.java new file mode 100644 index 000000000..9def7c927 --- /dev/null +++ b/native/jpype_module/src/main/java/org/jpype/bridge/Context.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package org.jpype.bridge; + +import python.lang.PyBuiltIn; +import python.lang.PyDict; +import python.lang.PyObject; + +/** + * Represents an scope of variables in the Python interpreter. + * + * We can consider these to be modules houses in Java space. Each has its own + * set of variables. Python modules are shared between all scopes as they exist + * globally and we only have one interpreter. + * + * When wrapping a Python module the Java wrapper class should hold its own + * private scope object. + * + */ +public class Context extends PyBuiltIn +{ + + public final PyDict globalsDict; + public final PyObject localsDict; + + public Context() + { + if (!Interpreter.getInstance().isJava()) + throw new IllegalStateException("Java bridge must be active"); + this.globalsDict = Interpreter.backend.newDict(); + this.localsDict = globalsDict; + } + + public Context(PyDict globals, PyObject locals) + { + if (!Interpreter.getInstance().isJava()) + throw new IllegalStateException("Java bridge must be active"); + this.globalsDict = globals; + this.localsDict = locals; + } + + /** + * Evaluate a single statement in this context. + * + * @param source is a single Python statement. + * @return the result of the evaluation. + */ + public PyObject eval(String source) + { + return Interpreter.backend.eval(source, globalsDict, localsDict); + } + + /** + * Execute a block of code in this context. + * + * @param source + */ + public void exec(String source) + { + Interpreter.backend.exec(source, globalsDict, localsDict); + } + + public void importModule(String module) + { + Interpreter.backend.exec(String.format("import %s", module), globalsDict, localsDict); + } + + public void importModule(String module, String as) + { + Interpreter.backend.exec(String.format("import %s as %s", module, as), globalsDict, localsDict); + } + + /** + * Get the globals dictionary. + * + * @return + */ + PyDict globals() + { + return globalsDict; + } + + /** + * Get the locals mapping. + * + * @return + */ + PyObject locals() + { + return localsDict; + } + +} diff --git a/native/jpype_module/src/main/java/org/jpype/bridge/Interpreter.java b/native/jpype_module/src/main/java/org/jpype/bridge/Interpreter.java new file mode 100644 index 000000000..f0bafc90e --- /dev/null +++ b/native/jpype_module/src/main/java/org/jpype/bridge/Interpreter.java @@ -0,0 +1,591 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package org.jpype.bridge; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; +import python.lang.PyObject; + +/** + * Frontend for managing the Python interpreter within JPype. + * + *

+ * This class serves as the main entry point for interacting with the Python + * interpreter. It is a singleton that is created once to connect to Python. To + * start the interpreter, configure all necessary variables and then call + * {@link #start(String[])}.

+ * + *

+ * The {@code Interpreter} class provides methods for locating the Python + * executable, probing Python installations, and managing Python module paths. + * It also allows applications to operate interactively with the Python + * interpreter.

+ * + */ +public class Interpreter +{ + + /** + * Indicates whether the interpreter is active. + */ + private boolean active = false; + + /** + * Indicates whether the operating system is Windows. + */ + private final boolean isWindows = checkWindows(); + + /** + * Path to the JPype library. + */ + private String jpypeLibrary; + + /** + * Version of the JPype library. + */ + private String jpypeVersion; + + /** + * List of module paths used by the Python interpreter. + */ + private final List modulePaths = new ArrayList<>(); + + /** + * Path to the Python library. + */ + private String pythonLibrary; + + /** + * The minimum required version of JPype. + */ + static final String REQUIRED_VERSION = "1.6.0"; + + /** + * Probe pattern for Unix systems to retrieve the Python library location, + * `_jpype` module location, and version number. + */ + static final String UNIX_PROBE = "" + + "import sysconfig\n" + + "import os\n" + + "gcv = sysconfig.get_config_var\n" + + "print(os.path.join(gcv('LIBDIR'), gcv('LDLIBRARY')))\n" + + "import _jpype\n" + + "print(_jpype.__file__)\n" + + "print(_jpype.__version__)\n"; + + /** + * Probe pattern for Windows systems to retrieve the Python library location, + * `_jpype` module location, and version number. + */ + static final String WINDOWS_PROBE = "" + + "import sysconfig\n" + + "import os\n" + + "gcv = sysconfig.get_config_var\n" + + "print(os.path.join(gcv('BINDIR'), 'python'+gcv('VERSION')+'.dll'))\n" + + "import _jpype\n" + + "print(_jpype.__file__)\n" + + "print(_jpype.__version__)\n"; + + /** + * The backend used to interact with Python. + */ + static Backend backend = null; + + /** + * Singleton instance of the {@code Interpreter}. + */ + static Interpreter instance = new Interpreter(); + + /** + * Python object used to signal interpreter shutdown. + */ + public static PyObject stop = null; + + /** + * Determines if the current operating system is Windows. + * + * @return {@code true} if the operating system is Windows; {@code false} + * otherwise. + */ + private static boolean checkWindows() + { + String osName = System.getProperty("os.name"); + return osName.startsWith("Windows"); + } + + /** + * Returns the backend used to interact with Python. + * + * @return The {@link Backend} instance. + */ + public static Backend getBackend() + { + return backend; + } + + /** + * Sets the backend used to interact with Python. + * + *

+ * This method is called during the initialization process to establish the + * backend connection.

+ * + * @param entry The {@link Backend} instance to set. + */ + public static void setBackend(Backend entry) + { + backend = entry; + stop = backend.object(); + } + + /** + * Returns the singleton instance of the {@code Interpreter}. + * + * @return The singleton {@code Interpreter} instance. + */ + public static Interpreter getInstance() + { + return instance; + } + + /** + * Main entry point for starting the Python interpreter. + * + *

+ * This method initializes the interpreter, starts it with the provided + * arguments, and enters interactive mode.

+ * + * @param args Command-line arguments passed to the interpreter. + */ + public static void main(String[] args) + { + Interpreter interpreter = getInstance(); + interpreter.start(args); + interpreter.interactive(); + System.out.println("done"); + } + + /** + * Parses a version string into an integer array. + * + * @param version The version string to parse (e.g., "3.9.7"). + * @return An integer array representing the version (e.g., [3, 9, 7]). + */ + private static int[] parseVersion(String version) + { + String[] parts = version.split("\\."); + int[] out = new int[3]; + try + { + for (int i = 0; i < parts.length; ++i) + { + if (i == 3) + break; + out[i] = Integer.parseInt(parts[i]); + } + } catch (NumberFormatException ex) + { + } + return out; + } + + /** + * Constructs a new {@code Interpreter}. + * + *

+ * This constructor initializes the interpreter and sets up any module paths + * specified via the Java system property {@code python.module.path}.

+ */ + private Interpreter() + { + String paths = System.getProperty("python.module.path"); + if (paths != null) + this.modulePaths.addAll(Arrays.asList(paths.split(File.pathSeparator))); + } + + /** + * Checks the cache for a previously probed Python installation. + * + * @param key A hash code associated with the Python installation. + * @return {@code true} if the installation is found in the cache; + * {@code false} otherwise. + */ + private boolean checkCache(String key) + { + // Determine the location + String homeDir = System.getProperty("user.home"); + String appHome = isWindows ? "\\AppData\\Roaming\\JPype" : ".jpype"; + Path appPath = Paths.get(homeDir, appHome); + if (!Files.exists(appPath)) + return false; + Properties properties = new Properties(); + + // Load the properties + Path propFile = appPath.resolve("jpype.properties"); + if (!Files.exists(propFile)) + return false; + try (InputStream is = Files.newInputStream(propFile)) + { + properties.load(is); + String parameters = properties.getProperty(key + "-python.lib"); + if (parameters == null) + return false; + this.pythonLibrary = parameters; + parameters = properties.getProperty(key + "-jpype.lib"); + if (parameters == null) + return false; + this.jpypeLibrary = parameters; + parameters = properties.getProperty(key + "-jpype.version"); + if (parameters == null) + return false; + this.jpypeVersion = parameters; + return true; + } catch (IOException ex) + { + return false; + } + } + + /** + * Searches the system PATH for an executable. + * + * @param exec The name of the executable to search for. + * @return The path to the executable, or {@code null} if not found. + */ + private String checkPath(String exec) + { + String path = System.getenv("PATH"); + if (path == null) + return null; + String[] parts = path.split(File.pathSeparator); + for (String part : parts) + { + Path test = Paths.get(part, exec); + if (Files.exists(test) && Files.isExecutable(test)) + return test.toString(); + } + return null; + } + + /** + * Determines the location of the Python executable to use for probing. + * + *

+ * This method uses the following sources to locate the executable:

+ *
    + *
  • Java system property {@code python.executable}
  • + *
  • Environment variable {@code PYTHONHOME}
  • + *
  • First `python3` found in the system PATH
  • + *
+ * + * @return The path to the Python executable. + * @throws RuntimeException If the executable cannot be located. + */ + private String getExecutable() + { + // Was is supplied via Java? + String out = System.getProperty("python.executable"); + + // Was it passed as environment variable? + if (out != null) + return out; + + String suffix = isWindows ? ".exe" : ""; + String home = System.getenv("PYTHONHOME"); + if (home != null) + return Paths.get(home, "python" + suffix).toString(); + + String onPath = checkPath("python" + suffix); + if (onPath != null) + return onPath; + + throw new RuntimeException("Unable to locate Python executable"); + } + + /** + * Returns the list of module paths used by the Python interpreter. + * + *

+ * This list can be used to limit the modules available for embedded + * applications. If the list is not empty, the default module path will not be + * used.

+ * + * @return The list of module paths. + */ + public List getModulePaths() + { + return modulePaths; + } + + /** + * Enters interactive mode with the Python interpreter. + * + *

+ * This method allows the user to interact directly with the Python + * interpreter.

+ */ + public void interactive() + { + Natives.interactive(); + } + + /** + * Get the method used to start the interpreter. + * + * The interpreter may have been started from either Java or Python. If + * started from Java side we clean up resources differently, becuase Python + * shuts down before Java in that case. + * + * @return true if the interpreter was started from Java. + */ + public boolean isJava() + { + return active; + } + + public boolean isStarted() + { + return backend != null; + } + + private String makeHash(String path) + { + // No need to be cryptographic here. We just need a unique key + long hash = 0; + for (int i = 0; i < path.length(); ++i) + { + hash = hash * 0x19185193123l + path.charAt(i); + } + return Long.toHexString(hash); + } + + private void resolveLibraries() + { + // System properties dub compiled in paths + this.pythonLibrary = System.getProperty("python.lib", pythonLibrary); + + // No need to do a probe + if (this.jpypeLibrary != null && this.pythonLibrary != null) + return; + + // Find the Python executable + String pythonExecutable = getExecutable(); + String key = makeHash(pythonExecutable); + if (checkCache(key)) + return; + + // Probe the Python executeable for the values we need to start + try + { + String[] cmd = + { + pythonExecutable, "-c", isWindows ? WINDOWS_PROBE : UNIX_PROBE + }; + ProcessBuilder pb = new ProcessBuilder(cmd); + pb.redirectOutput(ProcessBuilder.Redirect.PIPE); + Process process = pb.start(); + BufferedReader out = new BufferedReader(new InputStreamReader(process.getInputStream())); + BufferedReader err = new BufferedReader(new InputStreamReader(process.getErrorStream())); + int rc = process.waitFor(); + String a = out.readLine(); + String b = out.readLine(); + String c = out.readLine(); + + // Dump stderr out to so users can see problems. + String e = err.readLine(); + while (e != null) + { + System.err.println(e); + e = err.readLine(); + } + out.close(); + err.close(); + + // Failed to run Python + if (rc != 0) + throw new RuntimeException(String.format("Python was unable to be probed. Check stderr for details. (%d)", rc)); + + // Copy over the values from stdout. + if (pythonLibrary == null) + pythonLibrary = a; + if (jpypeLibrary == null) + jpypeLibrary = b; + if (jpypeVersion == null) + jpypeVersion = c; + + // Verify that everything we need was found + if (pythonLibrary == null || !Files.exists(Paths.get(pythonLibrary))) + throw new RuntimeException("Unable to locate Python shared library"); + if (jpypeLibrary == null || !Files.exists(Paths.get(jpypeLibrary))) + throw new RuntimeException("Unable to locate JPype shared library"); + if (jpypeVersion == null) // FIXME check version here + throw new RuntimeException("Incorrect JPype version"); + + // Update the cache + saveCache(key, pythonExecutable); + + // FIXME we need to check to see if JPype is equal to or newer than + // the wrapper version. Else we will fail to operate properly. + } catch (InterruptedException | IOException ex) + { + throw new RuntimeException("Failed to find JPype resources"); + } + } + + /** + * Store the results of the probe in the users home directory so we can skip + * future probes. + * + * @param key is a hash code for this python environment. + * @param exe is the location of the executable. + */ + private void saveCache(String key, String exe) + { + try + { + // Determine where to store it + String homeDir = System.getProperty("user.home"); + String appHome = isWindows ? "\\AppData\\Roaming\\JPype" : ".jpype"; + Path appPath = Paths.get(homeDir, appHome); + + // Create the path if it doesn't exist + if (!Files.exists(appPath)) + Files.createDirectories(appPath); + + // Load the existing configuration + Path propFile = appPath.resolve("jpype.properties"); + Properties prop = new Properties(); + if (Files.exists(propFile)) + { + try (InputStream is = Files.newInputStream(propFile)) + { + prop.load(is); + } + } + + // Store the executable name so the user knows which configuration this is for. + prop.setProperty(key, exe); + + // Store the values we need + prop.setProperty(key + "-python.lib", this.pythonLibrary); + prop.setProperty(key + "-jpype.lib", this.jpypeLibrary); + prop.setProperty(key + "-jpype.version", this.jpypeVersion); + + // Save back to disk + try (OutputStream os = Files.newOutputStream(propFile)) + { + prop.store(os, ""); + } + + } catch (IOException ex) + { + // do nothing if we can't cache our variables + } + } + + /** + * Start the interpreter.Any configuration actions must have been completed + * before the interpreter is started. + * + * Many configuration variables may be adjusted with Java System properties. + * + * @param args + */ + public void start(String... args) + { + // Once builtin is set internally then we can't call create again. + if (Interpreter.backend != null) + return; + active = true; + // Get the _jpype extension library + resolveLibraries(); + + // If we don't find the required libraries then we must fail. + if (jpypeLibrary == null || pythonLibrary == null) + { + throw new RuntimeException("Unable to find _jpype module"); + } + + // Make sure the JPype we found is compatible + int[] version = parseVersion(this.jpypeVersion); + int[] required = parseVersion(REQUIRED_VERSION); + if (version[0] < required[0] + || (version[0] == required[0] && version[1] < required[1])) + throw new RuntimeException("JPype version is too old. Found " + this.jpypeLibrary); + + if (!isWindows) + { + // We need our preload hooks to get started. + // On linux System.load() loads all symbols with RLTD_LOCAL which + // means they are not available for librarys to link against. That + // breaks the Python module loading system. Thus on Linux or any + // system that is similar we will need to load a bootstrap class which + // forces loading Python with global linkage prior to loading the + // first Python module. + String jpypeBootstrapLibrary = jpypeLibrary.replace("jpype.c", "jpypeb.c"); + + // First, load the preload hooks + System.load(jpypeBootstrapLibrary); + + // Next, load libpython as a global library + BootstrapLoader.loadLibrary(pythonLibrary); + } else + { + // If no bootstrap is required we will simply preload the Python library. + System.load(pythonLibrary); + } + + // Finally, load the Python module + System.load(jpypeLibrary); + + String[] paths = null; + if (!this.modulePaths.isEmpty()) + paths = this.modulePaths.toArray(String[]::new); + + // There is a large pile of configuration variables to Python. + // I am not sure what will be important for different modes of operation. + // Best to pass most of them in from system properties. + String program_name = System.getProperty("python.config.program_name"); + String prefix = System.getProperty("python.config.prefix"); + String home = System.getProperty("python.config.home"); + String exec_prefix = System.getProperty("python.config.exec_prefix"); + String executable = System.getProperty("python.config.executable"); + boolean isolated = Boolean.parseBoolean(System.getProperty("python.config.isolated", "false")); + boolean fault_handler = Boolean.parseBoolean(System.getProperty("python.config.fault_handler", "false")); + boolean quiet = Boolean.parseBoolean(System.getProperty("python.config.quiet", "false")); + boolean verbose = Boolean.parseBoolean(System.getProperty("python.config.verbose", "false")); + boolean site_import = Boolean.parseBoolean(System.getProperty("python.config.site_import ", "true")); + boolean user_site = Boolean.parseBoolean(System.getProperty("python.config.user_site_directory ", "true")); + boolean bytecode = Boolean.parseBoolean(System.getProperty("python.config.write_bytecode", "true")); + + // Start interpreter + Natives.start(paths, args, + program_name, prefix, home, exec_prefix, executable, + isolated, fault_handler, quiet, verbose, + site_import, user_site, bytecode); + } +} diff --git a/native/jpype_module/src/main/java/org/jpype/bridge/Natives.java b/native/jpype_module/src/main/java/org/jpype/bridge/Natives.java new file mode 100644 index 000000000..f1f0e468e --- /dev/null +++ b/native/jpype_module/src/main/java/org/jpype/bridge/Natives.java @@ -0,0 +1,36 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package org.jpype.bridge; + +/** + * Internal behaviors used by the binding. + * + * Loaded by the _jpype module. + * + */ +public class Natives +{ + + native static void start(String[] modulePaths, String[] args, + String name, String prefix, String home, String exec_prefix, String executable, + boolean isolated, boolean fault_handler, boolean quiet, boolean verbose, + boolean site_import, boolean user_site, boolean write_bytecode); + + native static void interactive(); + + native static void finish(); + +} diff --git a/native/jpype_module/src/main/java/org/jpype/html/AttrGrammar.java b/native/jpype_module/src/main/java/org/jpype/html/AttrGrammar.java index dc331eb47..463645e78 100644 --- a/native/jpype_module/src/main/java/org/jpype/html/AttrGrammar.java +++ b/native/jpype_module/src/main/java/org/jpype/html/AttrGrammar.java @@ -234,10 +234,10 @@ static class BooleanRule extends Parser.MatchRule } @Override - public void execute(Parser parser) + public void execute(Parser parser) { - Entity e2 = (Entity) parser.stack.removeLast(); - Entity e1 = (Entity) parser.stack.removeLast(); + Entity e2 = parser.stack.removeLast(); + Entity e1 = parser.stack.removeLast(); AttrParser aparser = (AttrParser) parser; Attr attr = aparser.doc.createAttribute((String) e1.value); attr.setNodeValue((String) e1.value); diff --git a/native/jpype_module/src/main/java/org/jpype/html/HtmlGrammar.java b/native/jpype_module/src/main/java/org/jpype/html/HtmlGrammar.java index edefdf664..49f4abc88 100644 --- a/native/jpype_module/src/main/java/org/jpype/html/HtmlGrammar.java +++ b/native/jpype_module/src/main/java/org/jpype/html/HtmlGrammar.java @@ -29,14 +29,14 @@ private HtmlGrammar() } @Override - public void start(Parser p) + public void start(Parser p) { p.state = State.FREE; ((HtmlParser) p).handler.startDocument(); } @Override - public Object end(Parser p) + public Object end(Parser p) { ((HtmlParser) p).handler.endDocument(); return ((HtmlParser) p).handler.getResult(); @@ -50,15 +50,15 @@ static StringBuilder promote(Entity e) StringBuilder sb = new StringBuilder(e.toString()); e.value = sb; e.token = Token.TEXT; - return (StringBuilder) sb; + return sb; } - static HtmlGrammar getGrammar(Parser p) + static HtmlGrammar getGrammar(Parser p) { return ((HtmlGrammar) p.grammar); } - static HtmlHandler getHandler(Parser p) + static HtmlHandler getHandler(Parser p) { return ((HtmlParser) p).handler; } @@ -271,7 +271,7 @@ public Escaped() } @Override - public void execute(Parser parser) + public void execute(Parser parser) { LinkedList stack = parser.stack; Entity e2 = stack.removeLast(); @@ -291,7 +291,7 @@ public MergeText() } @Override - public void execute(Parser parser) + public void execute(Parser parser) { LinkedList stack = parser.stack; Entity t2 = stack.removeLast(); @@ -306,7 +306,7 @@ static class BeginElement implements Rule { @Override - public boolean apply(Parser parser, Entity entity) + public boolean apply(Parser parser, Entity entity) { if (entity.token != Token.LT) return false; @@ -348,7 +348,7 @@ static class StartElement extends Parser.MatchRule } @Override - public void execute(Parser parser) + public void execute(Parser parser) { LinkedList stack = parser.stack; stack.removeLast(); @@ -375,7 +375,7 @@ static class CompleteElement extends Parser.MatchRule } @Override - public void execute(Parser parser) + public void execute(Parser parser) { LinkedList stack = parser.stack; stack.removeLast(); // > @@ -410,7 +410,7 @@ static class EndElement extends Parser.MatchRule } @Override - public void execute(Parser parser) + public void execute(Parser parser) { LinkedList stack = parser.stack; Entity e2 = stack.removeLast(); @@ -432,7 +432,7 @@ static class EndDirective extends Parser.MatchRule } @Override - public void execute(Parser parser) + public void execute(Parser parser) { LinkedList stack = parser.stack; Entity e2 = stack.removeLast(); @@ -452,7 +452,7 @@ static class Directive implements Rule { @Override - public boolean apply(Parser parser, Entity entity) + public boolean apply(Parser parser, Entity entity) { if (entity.token == Token.LSB) { @@ -475,7 +475,7 @@ static class CData implements Rule { @Override - public boolean apply(Parser parser, Entity entity) + public boolean apply(Parser parser, Entity entity) { if (entity.token != Token.TEXT) parser.error("Expected CDATA"); @@ -483,7 +483,7 @@ public boolean apply(Parser parser, Entity entity) return true; } - public boolean next(Parser parser, Parser.Entity entity) + public boolean next(Parser parser, Parser.Entity entity) { if (entity.token != Token.LSB) parser.error("Expected ["); @@ -503,7 +503,7 @@ public EndCData() } @Override - public void execute(Parser parser) + public void execute(Parser parser) { LinkedList stack = parser.stack; stack.removeLast(); // > @@ -580,7 +580,7 @@ public EndComment() } @Override - public void execute(Parser parser) + public void execute(Parser parser) { LinkedList stack = parser.stack; stack.removeLast(); // > diff --git a/native/jpype_module/src/main/java/org/jpype/html/Parser.java b/native/jpype_module/src/main/java/org/jpype/html/Parser.java index 620a28211..583a385af 100644 --- a/native/jpype_module/src/main/java/org/jpype/html/Parser.java +++ b/native/jpype_module/src/main/java/org/jpype/html/Parser.java @@ -42,6 +42,7 @@ public class Parser this.grammar = grammar; } + @SuppressWarnings("unchecked") public T parse(InputStream is) { ByteBuffer incoming = ByteBuffer.allocate(1024); @@ -69,6 +70,7 @@ public T parse(InputStream is) return (T) grammar.end(this); } + @SuppressWarnings("unchecked") public T parse(String str) { byte[] b = str.getBytes(); @@ -220,7 +222,7 @@ public interface Grammar * * @param p */ - public void start(Parser p); + public void start(Parser p); /** * Should check the state of the stack, fail if bad, or return the final @@ -229,7 +231,7 @@ public interface Grammar * @param p * @return */ - public Object end(Parser p); + public Object end(Parser p); } /** @@ -276,7 +278,7 @@ abstract static class MatchRule implements Rule } @Override - public boolean apply(Parser parser, Entity entity) + public boolean apply(Parser parser, Entity entity) { LinkedList stack = parser.stack; int n = stack.size(); @@ -297,7 +299,7 @@ public boolean apply(Parser parser, Entity entity) return true; } - abstract public void execute(Parser parser); + abstract public void execute(Parser parser); } // diff --git a/native/jpype_module/src/main/java/org/jpype/internal/Utility.java b/native/jpype_module/src/main/java/org/jpype/internal/Utility.java new file mode 100644 index 000000000..732a7731c --- /dev/null +++ b/native/jpype_module/src/main/java/org/jpype/internal/Utility.java @@ -0,0 +1,141 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package org.jpype.internal; + +import java.util.Iterator; +import java.util.Map; +import java.util.function.BiFunction; +import java.util.function.Function; + +/** + * Utility class providing support methods and helper implementations. + * + * This class contains generic utility methods and nested classes that simplify + * working with iterators and map entries with custom setter logic. + */ +public class Utility +{ + + /** + * Transforms an iterator by applying a given function to each element. + * + * This method creates a new iterator that maps each element of the input + * iterator to a new value using the specified function. + * + * @param The type of elements in the input iterator. + * @param The type of elements in the resulting iterator. + * @param iterator The input iterator whose elements will be transformed. + * @param function The function to apply to each element of the input + * iterator. + * @return A new iterator that provides transformed elements. + */ + public static Iterator mapIterator(Iterator iterator, Function function) + { + return new Iterator() + { + @Override + public boolean hasNext() + { + return iterator.hasNext(); + } + + @Override + public R next() + { + return function.apply(iterator.next()); + } + }; + } + + /** + * A custom implementation of {@link Map.Entry} that supports a custom setter + * logic. + * + * This class allows the value of a map entry to be updated using a provided + * {@link BiFunction}, which takes the key and the new value as inputs and + * returns the updated value. + * + * @param The type of the key. + * @param The type of the value. + */ + public static class MapEntryWithSet implements Map.Entry + { + + /** + * The key of the map entry. + */ + K key; + + /** + * The custom setter logic for updating the value. + */ + BiFunction setter; + + /** + * The current value of the map entry. + */ + V value; + + /** + * Constructs a new {@link MapEntryWithSet} with the specified key, value, + * and setter logic. + * + * @param key The key of the map entry. + * @param value The initial value of the map entry. + * @param set A {@link BiFunction} that defines the custom setter logic. + */ + public MapEntryWithSet(K key, V value, BiFunction set) + { + this.key = key; + this.value = value; + this.setter = set; + } + + /** + * Returns the key of the map entry. + * + * @return The key of the map entry. + */ + @Override + public K getKey() + { + return key; + } + + /** + * Returns the current value of the map entry. + * + * @return The current value of the map entry. + */ + @Override + public V getValue() + { + return value; + } + + /** + * Updates the value of the map entry using the custom setter logic. + * + * @param newValue The new value to set. + * @return The updated value after applying the custom setter logic. + */ + @Override + public V setValue(V newValue) + { + return setter.apply(key, newValue); + } + } +} diff --git a/native/jpype_module/src/main/java/org/jpype/javadoc/JavadocRenderer.java b/native/jpype_module/src/main/java/org/jpype/javadoc/JavadocRenderer.java index 403b06547..e22537bad 100644 --- a/native/jpype_module/src/main/java/org/jpype/javadoc/JavadocRenderer.java +++ b/native/jpype_module/src/main/java/org/jpype/javadoc/JavadocRenderer.java @@ -23,8 +23,6 @@ /** * Render a node as ReStructured Text. - * - * @author nelson85 */ public class JavadocRenderer { diff --git a/native/jpype_module/src/main/java/org/jpype/javadoc/JavadocTransformer.java b/native/jpype_module/src/main/java/org/jpype/javadoc/JavadocTransformer.java index 98b4059f9..2dfe44379 100644 --- a/native/jpype_module/src/main/java/org/jpype/javadoc/JavadocTransformer.java +++ b/native/jpype_module/src/main/java/org/jpype/javadoc/JavadocTransformer.java @@ -34,7 +34,6 @@ * The goal of this is to convert all inline markup into rst and leave markup by * section, paragraph to be used by the renderer. * - * @author nelson85 */ public class JavadocTransformer { diff --git a/native/jpype_module/src/main/java/org/jpype/manager/MethodResolution.java b/native/jpype_module/src/main/java/org/jpype/manager/MethodResolution.java index 35acbf76b..a175ead61 100644 --- a/native/jpype_module/src/main/java/org/jpype/manager/MethodResolution.java +++ b/native/jpype_module/src/main/java/org/jpype/manager/MethodResolution.java @@ -32,7 +32,6 @@ * are more general. If two or more methods match and one is not more specific * than the other. * - * @author nelson85 */ public class MethodResolution { @@ -147,14 +146,14 @@ static Class[] of(Class... l) of(Boolean.TYPE, Boolean.class)); } - static boolean isAssignableTo(Class c1, Class c2) + static boolean isAssignableTo(Class c1, Class c2) { if (!c1.isPrimitive()) return c2.isAssignableFrom(c1); - Class[] cl = CONVERSION.get(c1); + Class[] cl = CONVERSION.get(c1); if (cl == null) return false; - for (Class c3 : cl) + for (Class c3 : cl) if (c2.equals(c3)) return true; return false; diff --git a/native/jpype_module/src/main/java/org/jpype/manager/ModifierCode.java b/native/jpype_module/src/main/java/org/jpype/manager/ModifierCode.java index aa0c9cb27..0aedb9014 100644 --- a/native/jpype_module/src/main/java/org/jpype/manager/ModifierCode.java +++ b/native/jpype_module/src/main/java/org/jpype/manager/ModifierCode.java @@ -23,7 +23,6 @@ *

* These pretty much match Java plus a few codes we need. * - * @author nelson85 */ public enum ModifierCode { diff --git a/native/jpype_module/src/main/java/org/jpype/manager/TypeAudit.java b/native/jpype_module/src/main/java/org/jpype/manager/TypeAudit.java index 94702949c..cebfabff2 100644 --- a/native/jpype_module/src/main/java/org/jpype/manager/TypeAudit.java +++ b/native/jpype_module/src/main/java/org/jpype/manager/TypeAudit.java @@ -22,7 +22,6 @@ *

* This is not used during operation. * - * @author nelson85 */ public interface TypeAudit { diff --git a/native/jpype_module/src/main/java/org/jpype/manager/TypeFactory.java b/native/jpype_module/src/main/java/org/jpype/manager/TypeFactory.java index b43fd719a..a0ef430c9 100644 --- a/native/jpype_module/src/main/java/org/jpype/manager/TypeFactory.java +++ b/native/jpype_module/src/main/java/org/jpype/manager/TypeFactory.java @@ -19,38 +19,57 @@ import java.lang.reflect.Field; /** - * Interface for creating new resources used by JPype. - *

- * This calls the C++ constructors with all of the required fields for each - * class. This pattern eliminates the need for C++ layer probing Java for - * resources. + * Interface for creating and managing resources used by JPype. + * + * The {@code TypeFactory} interface defines methods for constructing various + * JPype-related types and resources, such as wrapper types, array classes, + * object classes, primitive types, methods, fields, and dispatch mechanisms. It + * serves as a bridge between the Java layer and the C++ layer, enabling + * efficient resource creation and management without requiring direct probing + * of Java from C++. + * + * This interface is also designed to facilitate testing by providing a clear + * contract for resource creation and destruction. + * *

- * This is an interface for testing. + * Key responsibilities: + *

    + *
  • Define and create JPype-specific types and classes
  • + *
  • Assign members (fields and methods) to classes
  • + *
  • Manage method dispatch and field definitions
  • + *
  • Destroy resources when no longer needed
  • + *
* - * @author nelson85 */ public interface TypeFactory { -// + // /** - * Create a new wrapper type for Python. + * Creates a new wrapper type for Python. + * + * This method constructs a wrapper type that can be used to interface with + * Python. * - * @param context - * @param cls is the pointer to the JClass. + * @param context The JPContext object representing the current JPype context. + * @param cls A pointer to the Java class (JClass) to wrap. */ void newWrapper(long context, long cls); /** - * Create a JPArray class. - * - * @param context JPContext object - * @param cls is the class type. - * @param name - * @param superClass - * @param componentPtr - * @param modifiers - * @return the pointer to the JPArrayClass. + * Creates a JPArray class. + * + * This method defines a new array class in JPype, including its type + * information, superclass, component type, and modifiers. + * + * @param context The JPContext object representing the current JPype context. + * @param cls The Java class type for the array. + * @param name The name of the array class. + * @param superClass A pointer to the superclass of the array class. + * @param componentPtr A pointer to the component type of the array. + * @param modifiers The modifiers for the array class (e.g., public, private, + * etc.). + * @return A pointer to the newly created JPArrayClass. */ long defineArrayClass( long context, @@ -61,15 +80,20 @@ long defineArrayClass( int modifiers); /** - * Create a class type. - * - * @param context JPContext object - * @param cls - * @param superClass - * @param interfaces - * @param modifiers - * @param name - * @return the pointer to the JPClass. + * Creates a JPObject class. + * + * This method defines a new object class in JPype, including its type + * information, superclass, interfaces, and modifiers. + * + * @param context The JPContext object representing the current JPype context. + * @param cls The Java class type for the object. + * @param name The name of the object class. + * @param superClass A pointer to the superclass of the object class. + * @param interfaces An array of pointers to the interfaces implemented by the + * object class. + * @param modifiers The modifiers for the object class (e.g., public, private, + * etc.). + * @return A pointer to the newly created JPObjectClass. */ long defineObjectClass( long context, @@ -80,13 +104,17 @@ long defineObjectClass( int modifiers); /** - * Define a primitive types. + * Defines a primitive type. * - * @param context JPContext object - * @param cls is the Java class for this primitive. - * @param boxedPtr is the JPClass for the boxed class. - * @param modifiers - * @return + * This method creates a JPype representation of a Java primitive type, + * including its boxed type and modifiers. + * + * @param context The JPContext object representing the current JPype context. + * @param name The name of the primitive type. + * @param cls The Java class representing the primitive type. + * @param boxedPtr A pointer to the JPClass representing the boxed type. + * @param modifiers The modifiers for the primitive type. + * @return A pointer to the newly defined primitive type. */ long definePrimitive( long context, @@ -95,17 +123,21 @@ long definePrimitive( long boxedPtr, int modifiers); -// -// + // + // /** - * Called after a class is constructed to populate the required fields and - * methods. + * Assigns members (fields and methods) to a class. + * + * This method populates the specified JPClass with its constructor, methods, + * and fields. * - * @param context JPContext object - * @param cls is the JPClass to populate - * @param ctorMethod is the JPMethod for the constructor. - * @param methodList is a list of JPMethod for the method list. - * @param fieldList is a list of JPField for the field list. + * @param context The JPContext object representing the current JPype context. + * @param cls A pointer to the JPClass to populate. + * @param ctorMethod A pointer to the JPMethod representing the constructor. + * @param methodList An array of pointers to JPMethod objects representing the + * methods. + * @param fieldList An array of pointers to JPField objects representing the + * fields. */ void assignMembers( long context, @@ -115,35 +147,39 @@ void assignMembers( long[] fieldList); /** - * Create a Method. - * - * @param context JPContext object - * @param cls is the class holding this. - * @param name - * @param field - * @param fieldType - * @param modifiers - * @return the pointer to the JPMethod. + * Defines a field. + * + * This method creates a JPField for the specified class. + * + * @param context The JPContext object representing the current JPype context. + * @param cls A pointer to the class owning the field. + * @param name The name of the field. + * @param field The Java {@link Field} object representing the field. + * @param fieldType A pointer to the JPClass representing the field's type. + * @param modifiers The modifiers for the field (e.g., public, private, etc.). + * @return A pointer to the newly defined JPField. */ long defineField( long context, long cls, String name, - Field field, // This will convert to a field id + Field field, long fieldType, int modifiers); /** - * Create a Method. - * - * @param context JPContext object - * @param cls is the class holding this. - * @param name - * @param method is the Java method that will be called, converts to a method - * id. - * @param overloadList - * @param modifiers - * @return the pointer to the JPMethod. + * Defines a method. + * + * This method creates a JPMethod for the specified class. + * + * @param context The JPContext object representing the current JPype context. + * @param cls A pointer to the class owning the method. + * @param name The name of the method. + * @param method The Java {@link Executable} object representing the method. + * @param overloadList An array of pointers to overloads for the method. + * @param modifiers The modifiers for the method (e.g., public, private, + * static, etc.). + * @return A pointer to the newly defined JPMethod. */ long defineMethod( long context, @@ -153,6 +189,16 @@ long defineMethod( long[] overloadList, int modifiers); + /** + * Populates a method with its return type and argument types. + * + * @param context The JPContext object representing the current JPype context. + * @param method A pointer to the JPMethod to populate. + * @param returnType A pointer to the JPClass representing the return type of + * the method. + * @param argumentTypes An array of pointers to JPClass objects representing + * the argument types. + */ void populateMethod( long context, long method, @@ -160,14 +206,18 @@ void populateMethod( long[] argumentTypes); /** - * Create a Method dispatch for Python by name. - * - * @param context JPContext object - * @param cls is the class that owns this dispatch. - * @param name is the name of the dispatch. - * @param overloadList is the list of all methods constructed for this class. - * @param modifiers contains if the method is (CTOR, STATIC), - * @return the pointer to the JPMethodDispatch. + * Defines a method dispatch for Python by name. + * + * This method creates a JPMethodDispatch for the specified class and method + * name. + * + * @param context The JPContext object representing the current JPype context. + * @param cls A pointer to the class owning the dispatch. + * @param name The name of the dispatch. + * @param overloadList An array of pointers to overloads for the dispatch. + * @param modifiers The modifiers for the dispatch (e.g., constructor, static, + * etc.). + * @return A pointer to the newly defined JPMethodDispatch. */ long defineMethodDispatch( long context, @@ -176,17 +226,20 @@ long defineMethodDispatch( long[] overloadList, int modifiers); -// -// + // + // /** - * Destroy the resources. + * Destroys resources. + * + * This method releases the specified resources in the JPype context. * - * @param context JPContext object - * @param resources - * @param sz + * @param context The JPContext object representing the current JPype context. + * @param resources An array of pointers to the resources to destroy. + * @param sz The size of the resources array. */ void destroy( long context, long[] resources, int sz); -// + + // } diff --git a/native/jpype_module/src/main/java/org/jpype/pickle/ByteBufferInputStream.java b/native/jpype_module/src/main/java/org/jpype/pickle/ByteBufferInputStream.java index a256bf0a1..3417e7957 100644 --- a/native/jpype_module/src/main/java/org/jpype/pickle/ByteBufferInputStream.java +++ b/native/jpype_module/src/main/java/org/jpype/pickle/ByteBufferInputStream.java @@ -20,20 +20,47 @@ import java.nio.ByteBuffer; import java.util.LinkedList; +/** + * A specialized {@link InputStream} implementation backed by multiple + * {@link ByteBuffer} objects. + * + *

+ * This class allows for efficient reading of data from multiple byte buffers, + * treating them as a single continuous stream. It provides methods to add byte + * arrays to the stream and overrides standard {@link InputStream} methods for + * reading data.

+ */ public class ByteBufferInputStream extends InputStream { private LinkedList buffers = new LinkedList<>(); + /** + * Adds a byte array to the stream by wrapping it in a {@link ByteBuffer}. + * + *

+ * The byte array is wrapped directly without copying, so changes to the array + * after it is added will affect the data in the stream.

+ * + * @param bytes The byte array to add to the stream. + */ public void put(byte[] bytes) { - // We can just wrap the buffer instead of copying it, since the buffer is - // wrapped we don't need to write the bytes to it. Wrapping the bytes relies - // on the array not being changed before being read. ByteBuffer buffer = ByteBuffer.wrap(bytes); buffers.add(buffer); } + /** + * Reads the next byte of data from the stream. + * + *

+ * If the stream is empty or the end of the stream is reached, this method + * returns -1.

+ * + * @return The next byte of data as an integer in the range 0 to 255, or -1 if + * the end of the stream is reached. + * @throws IOException If an I/O error occurs. + */ @Override public int read() throws IOException { @@ -51,12 +78,43 @@ public int read() throws IOException return b.get() & 0xFF; // Mask with 0xFF to convert signed byte to int (range 0-255) } + /** + * Reads up to {@code buffer.length} bytes of data from the stream into the + * specified byte array. + * + *

+ * This method is a convenience wrapper for + * {@link #read(byte[], int, int)}.

+ * + * @param buffer The byte array into which data is read. + * @return The number of bytes read, or -1 if the end of the stream is + * reached. + * @throws IOException If an I/O error occurs. + */ @Override - public int read(byte[] arg0) throws IOException + public int read(byte[] buffer) throws IOException { - return read(arg0, 0, arg0.length); + return read(buffer, 0, buffer.length); } + /** + * Reads up to {@code len} bytes of data from the stream into the specified + * byte array starting at {@code offset}. + * + *

+ * This method reads data from the stream into the provided buffer, starting + * at the specified offset and reading up to the specified length.

+ * + * @param buffer The byte array into which data is read. + * @param offset The starting offset in the byte array. + * @param len The maximum number of bytes to read. + * @return The number of bytes read, or -1 if the end of the stream is + * reached. + * @throws IOException If an I/O error occurs. + * @throws NullPointerException If {@code buffer} is null. + * @throws IndexOutOfBoundsException If {@code offset} or {@code len} are + * invalid. + */ @Override public int read(byte[] buffer, int offset, int len) throws IOException { @@ -87,6 +145,14 @@ public int read(byte[] buffer, int offset, int len) throws IOException return (total == 0) ? -1 : total; } + /** + * Closes the stream and releases all resources. + * + *

+ * This method clears all {@link ByteBuffer} objects from the stream.

+ * + * @throws IOException If an I/O error occurs. + */ @Override public void close() throws IOException { diff --git a/native/jpype_module/src/main/java/org/jpype/pickle/Decoder.java b/native/jpype_module/src/main/java/org/jpype/pickle/Decoder.java index a453a7d0f..b684740c2 100644 --- a/native/jpype_module/src/main/java/org/jpype/pickle/Decoder.java +++ b/native/jpype_module/src/main/java/org/jpype/pickle/Decoder.java @@ -18,6 +18,9 @@ import java.io.IOException; import java.io.ObjectInputStream; +/** + * Support class for the Python JPickler class. + */ public class Decoder { diff --git a/native/jpype_module/src/main/java/org/jpype/pickle/Encoder.java b/native/jpype_module/src/main/java/org/jpype/pickle/Encoder.java index acb255dd6..86cb1889f 100644 --- a/native/jpype_module/src/main/java/org/jpype/pickle/Encoder.java +++ b/native/jpype_module/src/main/java/org/jpype/pickle/Encoder.java @@ -20,8 +20,7 @@ import java.io.ObjectOutputStream; /** - * - * @author Karl Einar Nelson + * Support class for the Python JPickler class. */ public class Encoder { diff --git a/native/jpype_module/src/main/java/org/jpype/pkg/JPypePackage.java b/native/jpype_module/src/main/java/org/jpype/pkg/JPypePackage.java index ee8e1a65c..52878552d 100644 --- a/native/jpype_module/src/main/java/org/jpype/pkg/JPypePackage.java +++ b/native/jpype_module/src/main/java/org/jpype/pkg/JPypePackage.java @@ -30,23 +30,36 @@ import org.jpype.JPypeKeywords; /** - * Representation of a JPackage in Java. + * Represents a Java package in JPype. * - * This provides the dir and attributes for a JPackage and by extension jpype - * imports. Almost all of the actual work happens in the PackageManager which - * acts like the classloader to figure out what resource are available. + *

+ * This class provides the directory structure and attributes for a Java + * package, enabling JPype imports. Most of the heavy lifting is done by the + * {@link JPypePackageManager}, which acts as a class loader to determine + * available resources.

* + *

+ * The {@code JPypePackage} class allows querying the contents of a package and + * retrieving specific objects (e.g., classes or sub-packages) from the + * package.

*/ public class JPypePackage { // Name of the package final String pkg; - // A mapping from Python names into Paths into the module/jar file system. + + // A mapping from Python names to paths in the module/jar file system Map contents; + int code; private final JPypeClassLoader classLoader; + /** + * Constructs a new {@code JPypePackage} for the specified package name. + * + * @param pkg The name of the Java package. + */ public JPypePackage(String pkg) { this.pkg = pkg; @@ -56,42 +69,36 @@ public JPypePackage(String pkg) } /** - * Get an object from the package. + * Retrieves an object from the package by its name. * - * This is used by the importer to create the attributes for `getattro`. The - * type returned is polymorphic. We can potentially support any type of - * resource (package, classes, property files, xml, data, etc). But for now we - * are primarily interested in packages and classes. Packages are returned as - * strings as loading the package info is not guaranteed to work. Classes are - * returned as classes which are immediately converted into Python wrappers. - * We can return other resource types so long as they have either a wrapper - * type to place the instance into an Python object directly or a magic - * wrapper which will load the resource into a Python object type. + *

+ * This method is used by the importer to create attributes for `getattro`. + * The returned object can represent various types of resources, such as + * packages, classes, or other file types. For packages, the name is returned + * as a string. For classes, the class object is returned.

* - * This should match the acceptable types in getContents so that everything in - * the `dir` is also an attribute of JPackage. - * - * @param name is the name of the resource. - * @return the object or null if no resource is found with a matching name. + * @param name The name of the resource to retrieve. + * @return The resource object, or {@code null} if no matching resource is + * found. */ public Object getObject(String name) { - // We can't use the url contents as the contents may be incomplete due - // to bugs in the JVM classloaders. Instead we will have to probe. String basename = pkg + "." + JPypeKeywords.unwrap(name); ClassLoader cl = JPypeContext.getInstance().getClassLoader(); try { - // Check if it a package + // Check if it is a package if (JPypePackageManager.isPackage(basename)) { return basename; } - // Else probe for a class. + // Else probe for a class Class cls = Class.forName(basename, false, JPypeContext.getInstance().getClassLoader()); if (Modifier.isPublic(cls.getModifiers())) + { return Class.forName(basename, true, cl); + } } catch (ClassNotFoundException ex) { // Continue @@ -100,11 +107,13 @@ public Object getObject(String name) } /** - * Get a list of contents from a Java package. + * Retrieves the list of contents from the Java package. * - * This will be used when creating the package `dir` + *

+ * This method is used to create the package `dir`, listing all resources + * available in the package.

* - * @return + * @return An array of resource names contained in the package. */ public String[] getContents() { @@ -113,74 +122,66 @@ public String[] getContents() for (String key : contents.keySet()) { URI uri = contents.get(key); - // If there is anything null, then skip it. + // Skip null entries if (uri == null) continue; Path p = JPypePackageManager.getPath(uri); - // package are acceptable + // Add directories (packages) if (Files.isDirectory(p)) + { out.add(key); - - // classes must be public + } // Add public classes else if (uri.toString().endsWith(".class")) { - // Make sure it is public if (isPublic(p)) + { out.add(key); + } } } return out.toArray(new String[out.size()]); } /** - * Determine if a class is public. + * Determines if a class is public based on its class file. * - * This checks if a class file contains a public class. When importing classes - * we do not want to instantiate a class which is not public as it may result - * in instantiation of static variables or unwanted class resources. The only - * alternative is to read the class file and get the class modifier flags. - * Unfortunately, the developers of Java were rather stingy on their byte - * allocation and thus the field we want is not in the header but rather - * buried after the constant pool. Further as they didn't give the actual size - * of the tables in bytes, but rather in entries, that means we have to parse - * the whole table just to get the access flags after it. + *

+ * This method reads the class file to check its modifier flags and determines + * whether the class is public. Non-public classes are excluded to prevent + * unwanted instantiation of static variables or resources.

* - * @param p - * @return + * @param p The path to the class file. + * @return {@code true} if the class is public, {@code false} otherwise. */ static boolean isPublic(Path p) { try (InputStream is = Files.newInputStream(p)) { - // Allocate a three byte buffer for traversing the constant pool. - // The minumum entry is a byte for the type and 2 data bytes. We - // will read these three bytes and then based on the type advance - // the read pointer to the next entry. ByteBuffer buffer3 = ByteBuffer.allocate(3); - // Check the magic + // Check the magic number ByteBuffer header = ByteBuffer.allocate(4 + 2 + 2 + 2); is.read(header.array()); ((Buffer) header).rewind(); int magic = header.getInt(); if (magic != (int) 0xcafebabe) return false; - header.getShort(); // skip major - header.getShort(); // skip minor - short cpitems = header.getShort(); // get the number of items + header.getShort(); // Skip major version + header.getShort(); // Skip minor version + short cpitems = header.getShort(); // Get number of constant pool items - // Traverse the cp pool + // Traverse the constant pool for (int i = 0; i < cpitems - 1; ++i) { is.read(buffer3.array()); ((Buffer) buffer3).rewind(); byte type = buffer3.get(); // First byte is the type - // Now based on the entry type we will advance the pointer + // Advance pointer based on entry type switch (type) { - case 1: // Strings are variable length + case 1: // Strings are variable length is.skip(buffer3.getShort()); break; case 7: @@ -204,8 +205,8 @@ static boolean isPublic(Path p) break; case 5: case 6: - is.skip(6); // double and long are special as they are double entries - i++; // long and double take two slots + is.skip(6); // Double and long are special as they take two slots + i++; break; default: return false; @@ -216,20 +217,28 @@ static boolean isPublic(Path p) is.read(buffer3.array()); ((Buffer) buffer3).rewind(); short flags = buffer3.getShort(); - return (flags & 1) == 1; // it is public if bit zero is set + return (flags & 1) == 1; // Public if bit zero is set } catch (IOException ex) { - return false; // If anything goes wrong then it won't be considered a public class. + return false; // Treat as non-public if an error occurs } } + /** + * Checks and updates the cache for the package contents. + * + *

+ * If the class loader's state has changed, the cache is refreshed to reflect + * the current package contents.

+ */ void checkCache() { int current = classLoader.getCode(); if (this.code == current) + { return; + } this.code = current; this.contents = JPypePackageManager.getContentMap(pkg); } - } diff --git a/native/jpype_module/src/main/java/org/jpype/pkg/JPypePackageManager.java b/native/jpype_module/src/main/java/org/jpype/pkg/JPypePackageManager.java index f8ecdcee4..ebc8f0c6c 100644 --- a/native/jpype_module/src/main/java/org/jpype/pkg/JPypePackageManager.java +++ b/native/jpype_module/src/main/java/org/jpype/pkg/JPypePackageManager.java @@ -40,22 +40,23 @@ import org.jpype.JPypeKeywords; /** - * Manager for the contents of a package. + * Manages the contents of Java packages for JPype. * - * This class uses a number of tricks to provide a way to determine what - * packages are available in the class loader. It searches the jar path, the - * boot path (Java 8), and the module path (Java 9+). It does not currently work - * with alternative classloaders. This class was rumored to be unobtainium as - * endless posts indicated that it wasn't possible to determine the contents of - * a package in general nor to retrieve the package contents, but this appears - * to be largely incorrect as the jar and jrt file system provide all the - * required methods. + *

+ * This class provides functionality to determine the availability of packages + * in the class loader and retrieve their contents. It searches through the jar + * path, boot path (Java 8), and module path (Java 9+). The class does not + * currently support alternative class loaders.

* + *

+ * Although it was previously believed that determining package contents was + * impossible, this class leverages the jar and jrt file systems to provide the + * required methods for package inspection.

*/ public class JPypePackageManager { - final static List bases = new ArrayList(); + final static List bases = new ArrayList<>(); final static List modules = getModules(); final static FileSystemProvider jfsp = getFileSystemProvider("jar"); final static Map env = new HashMap<>(); @@ -64,24 +65,32 @@ public class JPypePackageManager /** * Checks if a package exists. * - * @param name is the name of the package. - * @return true if this is a Java package either in a jar, module, or in the - * boot path. + *

+ * Determines whether the specified package is available in a jar file, + * module, or boot path.

+ * + * @param name The name of the package. + * @return {@code true} if the package exists; {@code false} otherwise. */ public static boolean isPackage(String name) { if (name.indexOf('.') != -1) + { name = name.replace(".", "/"); - if (isModulePackage(name) || isBasePackage(name) || isJarPackage(name)) - return true; - return false; + } + return isModulePackage(name) || isBasePackage(name) || isJarPackage(name); } /** - * Get the list of the contents of a package. + * Retrieves the contents of a package. + * + *

+ * Searches the jar path, base path, and module path to collect all resources + * within the specified package.

* - * @param packageName - * @return the list of all resources found. + * @param packageName The name of the package. + * @return A map containing the names of resources and their corresponding + * URIs. */ public static Map getContentMap(String packageName) { @@ -95,12 +104,16 @@ public static Map getContentMap(String packageName) } /** - * Convert a URI into a path. + * Converts a URI into a {@link Path}. * - * This has special magic methods to deal with jar file systems. + *

+ * This method handles special cases for jar file systems to ensure proper + * resolution of paths.

* - * @param uri is the location of the resource. - * @return the path to the uri resource. + * @param uri The URI of the resource. + * @return The {@link Path} corresponding to the URI. + * @throws FileSystemNotFoundException If the file system for the URI cannot + * be found. */ static Path getPath(URI uri) { @@ -109,6 +122,7 @@ static Path getPath(URI uri) return Paths.get(uri); } catch (java.nio.file.FileSystemNotFoundException ex) { + // Handle jar file system } if (uri.getScheme().equals("jar")) @@ -118,31 +132,39 @@ static Path getPath(URI uri) // Limit the number of filesystems open at any one time fs.add(jfsp.newFileSystem(uri, env)); if (fs.size() > 8) + { fs.removeFirst().close(); + } return Paths.get(uri); } catch (IOException ex) { + // Handle IO exceptions } } throw new FileSystemNotFoundException("Unknown filesystem for " + uri); } /** - * Retrieve the Jar file system. + * Retrieves the file system provider for the specified scheme. * - * @return + * @param str The scheme name (e.g., "jar"). + * @return The {@link FileSystemProvider} for the specified scheme. + * @throws FileSystemNotFoundException If no provider is found for the scheme. */ private static FileSystemProvider getFileSystemProvider(String str) { for (FileSystemProvider fsp : FileSystemProvider.installedProviders()) { if (fsp.getScheme().equals(str)) + { return fsp; + } } throw new FileSystemNotFoundException("Unable to find filesystem for " + str); } // + //FIXME we are dropping Java 1.8 support so this code should be removed. /** * Older versions of Java do not have a file system for boot packages. Thus * rather working through the classloader, we will instead probe java to get diff --git a/native/jpype_module/src/main/java/org/jpype/proxy/JPypeProxy.java b/native/jpype_module/src/main/java/org/jpype/proxy/JPypeProxy.java index 836deb74a..c910e4ae3 100644 --- a/native/jpype_module/src/main/java/org/jpype/proxy/JPypeProxy.java +++ b/native/jpype_module/src/main/java/org/jpype/proxy/JPypeProxy.java @@ -29,33 +29,63 @@ import org.jpype.ref.JPypeReferenceQueue; /** + * A proxy implementation for JPype that bridges Java and Python. * - * @author Karl Einar Nelson + *

+ * This class acts as an {@link InvocationHandler} for dynamically created proxy + * objects. It allows Java methods to be invoked on Python objects and handles + * default method invocation for Java interfaces. The proxy is tied to a + * specific JPype context and manages cleanup operations for Python objects.

+ * + *

+ * Key features include:

+ *
    + *
  • Dynamic proxy creation for Java interfaces
  • + *
  • Reflection-based invocation of default methods
  • + *
  • Efficient type resolution for method parameters and return types
  • + *
+ * + *

+ * Note: This class relies on Java reflection and native methods for its + * functionality.

*/ public class JPypeProxy implements InvocationHandler { + // Constructor for accessing default methods in interfaces (Java 8+) private final static Constructor constructor; + + // Reference queue for managing cleanup of Python objects private final static JPypeReferenceQueue referenceQueue = JPypeReferenceQueue.getInstance(); + + // JPype context associated with this proxy JPypeContext context; + + // Instance ID of the Python object being proxied public long instance; + + // Cleanup function ID for the Python object public long cleanup; + + // Interfaces implemented by the proxy Class[] interfaces; + + // ClassLoader used for proxy creation ClassLoader cl = ClassLoader.getSystemClassLoader(); + + // Special object used to indicate a missing implementation public static Object missing = new Object(); - // See following link for Java 8 default access implementation - // https://blog.jooq.org/correct-reflective-access-to-interface-default-methods-in-java-8-9-10/ + // Static block to initialize the constructor for default method invocation static { Constructor c = null; if (System.getProperty("java.version").startsWith("1.")) - { + { // Check for Java 8 try { - c = Lookup.class - .getDeclaredConstructor(Class.class); - c.setAccessible(true); + c = Lookup.class.getDeclaredConstructor(Class.class); + c.setAccessible(true); // Make the constructor accessible } catch (NoSuchMethodException | SecurityException ex) { Logger.getLogger(JPypeProxy.class.getName()).log(Level.SEVERE, null, ex); @@ -64,50 +94,70 @@ public class JPypeProxy implements InvocationHandler constructor = c; } - public static JPypeProxy newProxy(JPypeContext context, - long instance, - long cleanup, - Class[] interfaces) + /** + * Creates a new proxy instance. + * + * @param context The JPype context associated with the proxy. + * @param instance The instance ID of the Python object being proxied. + * @param cleanup The cleanup function ID for the Python object. + * @param interfaces The Java interfaces to be implemented by the proxy. + * @return A new {@link JPypeProxy} instance. + */ + public static JPypeProxy newProxy(JPypeContext context, long instance, long cleanup, Class[] interfaces) { JPypeProxy proxy = new JPypeProxy(); proxy.context = context; proxy.instance = instance; proxy.interfaces = interfaces; proxy.cleanup = cleanup; - // Proxies must point to the correct class loader. For most cases the - // system classloader is find. But if the class is in a custom classloader - // we need to use that one instead - for (Class cls : interfaces) + + // Determine the appropriate class loader for the proxy + for (Class cls : interfaces) { ClassLoader icl = cls.getClassLoader(); if (icl != null && icl != proxy.cl) - proxy.cl = icl; + proxy.cl = icl; // Use the custom class loader if necessary } return proxy; } + /** + * Creates a new proxy instance that implements the specified interfaces. + * + *

+ * The proxy is registered with the reference queue for cleanup.

+ * + * @return The dynamically created proxy object. + */ public Object newInstance() { Object out = Proxy.newProxyInstance(cl, interfaces, this); - referenceQueue.registerRef(out, instance, cleanup); + referenceQueue.registerRef(out, instance, cleanup); // Register for cleanup return out; } + /** + * Handles method invocation on the proxy object. + * + * @param proxy The proxy instance on which the method is invoked. + * @param method The method being invoked. + * @param args The arguments passed to the method. + * @return The result of the method invocation. + * @throws Throwable If an error occurs during method invocation. + */ @Override - public Object invoke(Object proxy, Method method, Object[] args) - throws Throwable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - + // Check if the JPype context is shutting down if (context.isShutdown()) throw new RuntimeException("Proxy called during shutdown"); - // We can save a lot of effort on the C++ side by doing all the - // type lookup work here. + // Resolve method parameter and return types TypeManager typeManager = context.getTypeManager(); long returnType; long[] parameterTypes; synchronized (typeManager) - { + { // Synchronize type resolution returnType = typeManager.findClass(method.getReturnType()); Class[] types = method.getParameterTypes(); parameterTypes = new long[types.length]; @@ -117,37 +167,32 @@ public Object invoke(Object proxy, Method method, Object[] args) } } - // Check first to see if Python has implementated it + // Attempt to invoke the method on the Python object Object result = hostInvoke(context.getContext(), method.getName(), instance, returnType, parameterTypes, args, missing); - // If we get a good result than return it + // Return the result if the method is implemented in Python if (result != missing) return result; - // If it is a default method in the interface then we have to invoke it using special reflection. + // Handle default methods in interfaces if (method.isDefault()) { try { Class cls = method.getDeclaringClass(); - // Java 8 + // Use the appropriate reflection mechanism based on Java version if (constructor != null) - { + { // Java 8 return constructor.newInstance(cls) - .findSpecial(cls, - method.getName(), - MethodType.methodType(method.getReturnType()), - cls) + .findSpecial(cls, method.getName(), MethodType.methodType(method.getReturnType()), cls) .bindTo(proxy) .invokeWithArguments(args); } + // Java 9+ return MethodHandles.lookup() - .findSpecial(cls, - method.getName(), - MethodType.methodType(method.getReturnType()), - cls) + .findSpecial(cls, method.getName(), MethodType.methodType(method.getReturnType()), cls) .bindTo(proxy) .invokeWithArguments(args); } catch (java.lang.IllegalAccessException ex) @@ -156,10 +201,22 @@ public Object invoke(Object proxy, Method method, Object[] args) } } - // Else throw... (this should never happen as proxies are checked when created.) + // Throw an exception if no implementation is found throw new NoSuchMethodError(method.getName()); } + /** + * Native method to invoke a method on the Python object. + * + * @param context is the JPype context. + * @param name is the name of the method to invoke. + * @param pyObject is the instance ID of the Python object. + * @param returnType is the return type of the method. + * @param argsTypes is the types of the method parameters. + * @param args is the arguments passed to the method. + * @param bad is the object indicating a missing implementation. + * @return the result of the method invocation. + */ private static native Object hostInvoke(long context, String name, long pyObject, long returnType, long[] argsTypes, Object[] args, Object bad); } diff --git a/native/jpype_module/src/main/java/org/jpype/ref/JPypeReference.java b/native/jpype_module/src/main/java/org/jpype/ref/JPypeReference.java index e75cabb06..48db9aad1 100644 --- a/native/jpype_module/src/main/java/org/jpype/ref/JPypeReference.java +++ b/native/jpype_module/src/main/java/org/jpype/ref/JPypeReference.java @@ -19,37 +19,101 @@ import java.lang.ref.ReferenceQueue; /** - * (internal) Reference to a PyObject*. + * Represents a phantom reference to a Python object in JPype. + * + *

+ * This class is used internally by JPype to manage references to Python objects + * (`PyObject*`) from Java. It extends {@link PhantomReference} to enable + * cleanup operations when the referenced Java object is garbage collected.

+ * + *

+ * Each instance of {@code JPypeReference} holds metadata about the Python + * object, including its host reference and cleanup function ID. These + * references are managed using a {@link ReferenceQueue} to facilitate resource + * cleanup.

+ * + *

+ * Note: This class is intended for internal use and should not be used directly + * by external code.

*/ -class JPypeReference extends PhantomReference +class JPypeReference extends PhantomReference { + /** + * The host reference to the Python object. + *

+ * This is a native pointer to the Python object (`PyObject*`).

+ */ long hostReference; + + /** + * The cleanup function ID for the Python object. + *

+ * This function is invoked to release resources associated with the Python + * object.

+ */ long cleanup; + + /** + * The pool ID associated with this reference. + *

+ * Used internally to manage pooled resources.

+ */ int pool; + + /** + * The index of this reference within the pool. + *

+ * Used internally for efficient resource management.

+ */ int index; - public JPypeReference(ReferenceQueue arg1, Object javaObject, long host, long cleanup) + /** + * Constructs a new {@code JPypeReference}. + * + * @param arg1 The {@link ReferenceQueue} to which this reference will be + * registered. + * @param javaObject The Java object being referenced. + * @param host The host reference to the Python object (`PyObject*`). + * @param cleanup The cleanup function ID for the Python object. + */ + public JPypeReference(ReferenceQueue arg1, Object javaObject, long host, long cleanup) { super(javaObject, arg1); this.hostReference = host; this.cleanup = cleanup; } + /** + * Computes the hash code for this reference. + * + *

+ * The hash code is derived from the {@code hostReference} field to ensure + * consistency with the {@link #equals(Object)} method.

+ * + * @return The hash code for this reference. + */ @Override public int hashCode() { return (int) hostReference; } + /** + * Compares this reference to another object for equality. + * + *

+ * Two {@code JPypeReference} objects are considered equal if their + * {@code hostReference} fields are identical.

+ * + * @param arg0 The object to compare with this reference. + * @return {@code true} if the objects are equal; {@code false} otherwise. + */ @Override public boolean equals(Object arg0) { if (!(arg0 instanceof JPypeReference)) - { return false; - } - return ((JPypeReference) arg0).hostReference == hostReference; } } diff --git a/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceNative.java b/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceNative.java index c7d18f044..11d80e01c 100644 --- a/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceNative.java +++ b/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceNative.java @@ -3,31 +3,70 @@ import java.lang.reflect.Method; /** + * Internal class used to manage object lifetimes in the Python-to-Java bridge. * - * @author nelson85 + * The {@code JPypeReferenceNative} class provides native methods to handle + * memory management and resource cleanup for objects shared between Python and + * Java. It acts as a bridge to the underlying C++ layer, enabling efficient + * management of native resources during garbage collection (GC) and object + * lifecycle events. + * + *

+ * Key responsibilities: + *

    + *
  • Remove references to native resources when they are no longer needed
  • + *
  • Handle initialization of resources for objects
  • + *
  • Respond to garbage collection events
  • + *
+ * + *

+ * This class is primarily intended for internal use within the JPype library + * and should not be used directly by application developers. */ public class JPypeReferenceNative { /** - * Native hook to delete a native resource. + * Removes a reference to a native resource. + * + * This method is used to release native memory associated with a specific + * resource. It calls a cleanup function provided by the native layer to + * deallocate memory or perform other cleanup operations. + * + * @param host The address of the memory in the native layer (C/C++) that + * needs to be cleaned up. + * @param cleanup The address of the native function responsible for cleaning + * up the memory. * - * @param host is the address of memory in C. - * @param cleanup is the address the function to cleanup the memory. */ public static native void removeHostReference(long host, long cleanup); /** - * Triggered by the sentinel when a GC starts. + * Wakes up the native layer during garbage collection. + * + * This method is triggered by a sentinel when the garbage collector starts. + * It ensures that the native layer is aware of GC events and can perform + * necessary operations to manage object lifetimes. + * + *

+ * Usage Example: + *

+   * JPypeReferenceNative.wake();
+   * 
*/ public static native void wake(); /** - * Initialize resources. + * Initializes resources for an object. + * + * This method sets up the necessary native resources for the specified object + * and associates it with a method. It is typically called during the object's + * creation or initialization phase. + * + * @param self The Java object that requires resource initialization. + * @param m The {@link Method} object representing the method to associate + * with the object. * - * @param self - * @param m */ public static native void init(Object self, Method m); - } diff --git a/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceQueue.java b/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceQueue.java index 9b34ed1bd..e55f6d6a4 100644 --- a/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceQueue.java +++ b/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceQueue.java @@ -17,33 +17,83 @@ import java.lang.ref.PhantomReference; import java.lang.ref.ReferenceQueue; +import org.jpype.bridge.Interpreter; /** - * Reference queue holds the life of python objects to be as long as java items. + * A reference queue that binds the lifetime of Python objects to Java objects. + * *

- * Any java class that holds a pointer into python needs to have a reference so - * that python object does not go away if the references in python fall to zero. - * JPype will add an extra reference to the object which is removed when the - * python object dies. + * This class is used internally by JPype to manage the lifecycle of Python + * objects (`PyObject*`) that are referenced by Java objects. It ensures that + * Python objects do not get garbage collected prematurely when their references + * in Python fall to zero, as long as they are still referenced by Java.

* - * @author smenard + *

+ * The {@code JPypeReferenceQueue} maintains a thread that monitors the queue + * for garbage-collected Java objects and performs cleanup operations on the + * associated Python objects. It uses phantom references to track the lifecycle + * of Java objects.

+ * + *

+ * This class is a singleton, and its instance can be accessed via + * {@link #getInstance()}.

+ * + *

+ * Note: This class is intended for internal use and should not be used directly + * by external code.

* + * @author smenard */ -final public class JPypeReferenceQueue extends ReferenceQueue +public final class JPypeReferenceQueue extends ReferenceQueue { + /** + * Singleton instance of the {@code JPypeReferenceQueue}. + */ private final static JPypeReferenceQueue INSTANCE = new JPypeReferenceQueue(); + + /** + * A set of active references to Python objects. + */ private JPypeReferenceSet hostReferences; + + /** + * Indicates whether the reference queue has been stopped. + */ private boolean isStopped = false; + + /** + * The thread responsible for monitoring the reference queue. + */ private Thread queueThread; - private Object queueStopMutex = new Object(); - private PhantomReference sentinel = null; + /** + * Mutex used to synchronize stopping the queue thread. + */ + private final Object queueStopMutex = new Object(); + + /** + * Sentinel reference used to wake up the queue thread periodically. + */ + private PhantomReference sentinel = null; + + /** + * Returns the singleton instance of the {@code JPypeReferenceQueue}. + * + * @return The singleton instance of the reference queue. + */ public static JPypeReferenceQueue getInstance() { return INSTANCE; } + /** + * Private constructor to initialize the reference queue. + * + *

+ * This constructor sets up the reference queue, initializes the native + * bindings, and adds a sentinel reference.

+ */ private JPypeReferenceQueue() { super(); @@ -60,20 +110,24 @@ private JPypeReferenceQueue() } /** - * (internal) Binds the lifetime of a Python object to a Java object. + * Registers a reference to bind the lifetime of a Python object to a Java + * object. + * *

- * JPype adds an extra reference to a PyObject* and then calls this method to - * hold that reference until the Java object is garbage collected. + * This method adds an extra reference to a Python object (`PyObject*`) and + * holds it until the Java object is garbage collected. When the Java object + * is collected, the Python object is cleaned up.

* - * @param javaObject is the object to bind the lifespan to. - * @param host is the pointer to the host object. - * @param cleanup is the pointer to the function to call to delete the - * resource. + * @param javaObject The Java object to bind the lifetime to. + * @param host The pointer to the Python object. + * @param cleanup The pointer to the cleanup function for the Python object. */ public void registerRef(Object javaObject, long host, long cleanup) { if (cleanup == 0) + { return; + } if (isStopped) { JPypeReferenceNative.removeHostReference(host, cleanup); @@ -85,7 +139,11 @@ public void registerRef(Object javaObject, long host, long cleanup) } /** - * Start the threading queue. + * Starts the reference queue thread. + * + *

+ * This thread monitors the queue for garbage-collected Java objects and + * performs cleanup operations on the associated Python objects.

*/ public void start() { @@ -96,9 +154,11 @@ public void start() } /** - * Stops the reference queue. + * Stops the reference queue thread. + * *

- * This is called by jpype when the jvm shuts down. + * This method is called when the JVM shuts down to stop the reference queue + * and perform any remaining cleanup operations.

*/ public void stop() { @@ -112,22 +172,25 @@ public void stop() queueThread.interrupt(); } - // wait for the thread to finish ... + // Wait for the thread to finish queueStopMutex.wait(10000); } } catch (InterruptedException ex) { - // who cares ... + // Ignore interruptions } - // Empty the queue. - hostReferences.flush(); + // Empty the queue + if (!Interpreter.getInstance().isJava()) + { + hostReferences.flush(); + } } /** - * Checks the status of the reference queue. + * Checks whether the reference queue is running. * - * @return true is the queue is running. + * @return {@code true} if the queue is running; {@code false} otherwise. */ public boolean isRunning() { @@ -135,18 +198,30 @@ public boolean isRunning() } /** - * Get the number of items in the reference queue. + * Returns the number of items currently in the reference queue. * - * @return the number of python resources held. + * @return The number of Python resources held by the reference queue. */ public int getQueueSize() { return this.hostReferences.size(); } -// /** - * Thread to monitor the queue and delete resources. + * Adds a sentinel reference to the queue. + * + *

+ * The sentinel reference is used to periodically wake up the queue + * thread.

+ */ + final void addSentinel() + { + sentinel = new JPypeReference(this, new byte[0], 0, 0); + } + + /** + * Thread responsible for monitoring the reference queue and deleting + * resources. */ private class Worker implements Runnable { @@ -158,8 +233,7 @@ public void run() { try { - // Check if a ref has been queued. and check if the thread has been - // stopped every 0.25 seconds + // Check if a reference has been queued JPypeReference ref = (JPypeReference) remove(250); if (ref == sentinel) { @@ -176,7 +250,7 @@ public void run() } } catch (InterruptedException ex) { - // don't know why ... don't really care ... + // Ignore interruptions } } @@ -186,11 +260,4 @@ public void run() } } } - - final void addSentinel() - { - sentinel = new JPypeReference(this, new byte[0], 0, 0); - } - -//
} diff --git a/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceSet.java b/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceSet.java index 3f58f8e5e..83aa5972c 100644 --- a/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceSet.java +++ b/native/jpype_module/src/main/java/org/jpype/ref/JPypeReferenceSet.java @@ -18,37 +18,82 @@ import java.util.ArrayList; /** + * A set that manages references to Python objects used by JPype. * - * @author nelson85 + *

+ * This class is responsible for storing and managing {@link JPypeReference} + * objects efficiently. It uses an internal pooling mechanism to ensure that + * adding and removing references are performed in constant time (O(1)).

+ * + *

+ * The {@code JPypeReferenceSet} is used internally by JPype to track Python + * objects (`PyObject*`) that are referenced by Java objects. It ensures proper + * cleanup of Python resources when references are removed or during JVM + * shutdown.

+ * + *

+ * Note: This class is intended for internal use and should not be used directly + * by external code.

*/ public class JPypeReferenceSet { + /** + * The size of each pool used to store references. + */ static final int SIZE = 256; + + /** + * A list of pools that store references. + */ ArrayList pools = new ArrayList<>(); + + /** + * The current pool being used for adding new references. + */ Pool current; + + /** + * The total number of active references in the set. + */ private int items; + /** + * Constructs a new {@code JPypeReferenceSet}. + * + *

+ * This constructor initializes the reference set but does not create any + * pools until references are added.

+ */ JPypeReferenceSet() { } + /** + * Returns the number of active references in the set. + * + * @return The number of active references. + */ int size() { return items; } /** - * Add a reference to the set. + * Adds a reference to the set. * - * This should be O(1). + *

+ * This operation is performed in constant time (O(1)) by using a pooling + * mechanism.

* - * @param ref + * @param ref The {@link JPypeReference} to add to the set. */ synchronized void add(JPypeReference ref) { if (ref.cleanup == 0) + { return; + } this.items++; if (current == null) @@ -59,10 +104,8 @@ synchronized void add(JPypeReference ref) if (current.add(ref)) { - // It is full + // Current pool is full, find a free pool current = null; - - // Find a free pool for (Pool pool : pools) { if (pool.tail < SIZE) @@ -75,14 +118,20 @@ synchronized void add(JPypeReference ref) } /** - * Remove a reference from the set. + * Removes a reference from the set. * - * @param ref + *

+ * This operation is performed in constant time (O(1)) by directly accessing + * the reference's pool and index.

+ * + * @param ref The {@link JPypeReference} to remove from the set. */ synchronized void remove(JPypeReference ref) { if (ref.cleanup == 0) + { return; + } pools.get(ref.pool).remove(ref); this.items--; ref.cleanup = 0; @@ -90,11 +139,12 @@ synchronized void remove(JPypeReference ref) } /** - * Release all resources. - * - * This is triggered by shutdown to release an current Python references that - * are being held. + * Releases all resources held by the reference set. * + *

+ * This method is triggered during JVM shutdown to release all Python + * references that are still being held. It ensures proper cleanup of Python + * resources.

*/ void flush() { @@ -105,11 +155,12 @@ void flush() JPypeReference ref = pool.entries[i]; long hostRef = ref.hostReference; long cleanup = ref.cleanup; - // This is a sanity check to prevent calling a cleanup with a null - // pointer, it would only occur if we failed to manage a deleted - // item. + + // Sanity check to prevent calling cleanup on a null pointer if (cleanup == 0) + { continue; + } ref.cleanup = 0; JPypeReferenceNative.removeHostReference(hostRef, cleanup); } @@ -117,19 +168,54 @@ void flush() } } -// + // + /** + * A pool that stores references to Python objects. + * + *

+ * Each pool has a fixed size ({@link JPypeReferenceSet#SIZE}) and is + * responsible for managing references efficiently. Pools are used to ensure + * that adding and removing references are performed in constant time + * (O(1)).

+ */ static class Pool { + /** + * The array of references stored in this pool. + */ JPypeReference[] entries = new JPypeReference[SIZE]; + + /** + * The index of the next available slot in the pool. + */ int tail; + + /** + * The unique ID of this pool. + */ int id; + /** + * Constructs a new {@code Pool}. + * + * @param id The unique ID of the pool. + */ Pool(int id) { this.id = id; } + /** + * Adds a reference to the pool. + * + *

+ * This operation is performed in constant time (O(1)).

+ * + * @param ref The {@link JPypeReference} to add to the pool. + * @return {@code true} if the pool is full after adding the reference; + * {@code false} otherwise. + */ boolean add(JPypeReference ref) { ref.pool = id; @@ -138,11 +224,20 @@ boolean add(JPypeReference ref) return (tail == entries.length); } + /** + * Removes a reference from the pool. + * + *

+ * This operation is performed in constant time (O(1)) by swapping the + * reference to be removed with the last reference in the pool.

+ * + * @param ref The {@link JPypeReference} to remove from the pool. + */ void remove(JPypeReference ref) { entries[ref.index] = entries[--tail]; entries[ref.index].index = ref.index; } } -//
+ //
} diff --git a/native/jpype_module/src/main/java/python/exception/PyADirectionError.java b/native/jpype_module/src/main/java/python/exception/PyADirectionError.java new file mode 100644 index 000000000..ecd41bd3f --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyADirectionError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyADirectionError extends PyOSError +{ + + public PyADirectionError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyArithmeticError.java b/native/jpype_module/src/main/java/python/exception/PyArithmeticError.java new file mode 100644 index 000000000..6b6d75a11 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyArithmeticError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyArithmeticError extends PyException +{ + + public PyArithmeticError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyAssertionError.java b/native/jpype_module/src/main/java/python/exception/PyAssertionError.java new file mode 100644 index 000000000..fb10b6e9d --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyAssertionError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyAssertionError extends PyException +{ + + public PyAssertionError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyAttributeError.java b/native/jpype_module/src/main/java/python/exception/PyAttributeError.java new file mode 100644 index 000000000..6b31332b0 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyAttributeError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyAttributeError extends PyException +{ + + public PyAttributeError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyBlockingIOError.java b/native/jpype_module/src/main/java/python/exception/PyBlockingIOError.java new file mode 100644 index 000000000..f3e54241d --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyBlockingIOError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyBlockingIOError extends PyOSError +{ + + public PyBlockingIOError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyBufferError.java b/native/jpype_module/src/main/java/python/exception/PyBufferError.java new file mode 100644 index 000000000..923bceec2 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyBufferError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyBufferError extends PyException +{ + + public PyBufferError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyChildProcessError.java b/native/jpype_module/src/main/java/python/exception/PyChildProcessError.java new file mode 100644 index 000000000..0e8f880b7 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyChildProcessError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyChildProcessError extends PyOSError +{ + + public PyChildProcessError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyConnectionError.java b/native/jpype_module/src/main/java/python/exception/PyConnectionError.java new file mode 100644 index 000000000..11063625f --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyConnectionError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyConnectionError extends PyOSError +{ + + public PyConnectionError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyEOFError.java b/native/jpype_module/src/main/java/python/exception/PyEOFError.java new file mode 100644 index 000000000..dce4885a3 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyEOFError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyEOFError extends PyException +{ + + public PyEOFError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyException.java b/native/jpype_module/src/main/java/python/exception/PyException.java new file mode 100644 index 000000000..dc33cd270 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyException.java @@ -0,0 +1,45 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.exception; + +import java.util.HashMap; +import python.lang.PyExc; + +/** + * + */ +public class PyException extends RuntimeException +{ + + private final PyExc base; + + public PyException(PyExc base) + { + super(base.getMessage()); + this.base = base; + } + + /** + * Fetch the native version of the Exception. + * + * @return + */ + public PyExc get() + { + return base; + } + +} diff --git a/native/jpype_module/src/main/java/python/exception/PyFileExistsError.java b/native/jpype_module/src/main/java/python/exception/PyFileExistsError.java new file mode 100644 index 000000000..fce89748e --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyFileExistsError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyFileExistsError extends PyOSError +{ + + public PyFileExistsError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyFileNotFoundError.java b/native/jpype_module/src/main/java/python/exception/PyFileNotFoundError.java new file mode 100644 index 000000000..cebd998fc --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyFileNotFoundError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyFileNotFoundError extends PyOSError +{ + + public PyFileNotFoundError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyFloatingPointError.java b/native/jpype_module/src/main/java/python/exception/PyFloatingPointError.java new file mode 100644 index 000000000..0176a8ea4 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyFloatingPointError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyFloatingPointError extends PyArithmeticError +{ + + public PyFloatingPointError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyImportError.java b/native/jpype_module/src/main/java/python/exception/PyImportError.java new file mode 100644 index 000000000..a9272e27a --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyImportError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyImportError extends PyException +{ + + public PyImportError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyIndentationError.java b/native/jpype_module/src/main/java/python/exception/PyIndentationError.java new file mode 100644 index 000000000..395cd74ea --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyIndentationError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyIndentationError extends PySyntaxError +{ + + public PyIndentationError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyIndexError.java b/native/jpype_module/src/main/java/python/exception/PyIndexError.java new file mode 100644 index 000000000..81bf5fb79 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyIndexError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyIndexError extends PyLookupError +{ + + public PyIndexError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyInterruptedError.java b/native/jpype_module/src/main/java/python/exception/PyInterruptedError.java new file mode 100644 index 000000000..a8743bd45 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyInterruptedError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyInterruptedError extends PyOSError +{ + + public PyInterruptedError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyKeyError.java b/native/jpype_module/src/main/java/python/exception/PyKeyError.java new file mode 100644 index 000000000..49bb8cb31 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyKeyError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyKeyError extends PyLookupError +{ + + public PyKeyError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyLookupError.java b/native/jpype_module/src/main/java/python/exception/PyLookupError.java new file mode 100644 index 000000000..188ae0785 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyLookupError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyLookupError extends PyException +{ + + public PyLookupError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyModuleNotFoundError.java b/native/jpype_module/src/main/java/python/exception/PyModuleNotFoundError.java new file mode 100644 index 000000000..ac7f7c6fb --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyModuleNotFoundError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyModuleNotFoundError extends PyImportError +{ + + public PyModuleNotFoundError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyNameError.java b/native/jpype_module/src/main/java/python/exception/PyNameError.java new file mode 100644 index 000000000..564880f16 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyNameError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyNameError extends PyException +{ + + public PyNameError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyNotADirectoryError.java b/native/jpype_module/src/main/java/python/exception/PyNotADirectoryError.java new file mode 100644 index 000000000..bc94147f1 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyNotADirectoryError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyNotADirectoryError extends PyOSError +{ + + public PyNotADirectoryError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyNotImplementedError.java b/native/jpype_module/src/main/java/python/exception/PyNotImplementedError.java new file mode 100644 index 000000000..61a282347 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyNotImplementedError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyNotImplementedError extends PyRuntimeError +{ + + public PyNotImplementedError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyOSError.java b/native/jpype_module/src/main/java/python/exception/PyOSError.java new file mode 100644 index 000000000..10936fb93 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyOSError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyOSError extends PyException +{ + + public PyOSError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyOverflowError.java b/native/jpype_module/src/main/java/python/exception/PyOverflowError.java new file mode 100644 index 000000000..5fc0055ab --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyOverflowError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyOverflowError extends PyArithmeticError +{ + + public PyOverflowError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyPermissionError.java b/native/jpype_module/src/main/java/python/exception/PyPermissionError.java new file mode 100644 index 000000000..c0c704361 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyPermissionError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyPermissionError extends PyOSError +{ + + public PyPermissionError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyProcessLookupError.java b/native/jpype_module/src/main/java/python/exception/PyProcessLookupError.java new file mode 100644 index 000000000..4e0491dea --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyProcessLookupError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyProcessLookupError extends PyOSError +{ + + public PyProcessLookupError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyRecursionError.java b/native/jpype_module/src/main/java/python/exception/PyRecursionError.java new file mode 100644 index 000000000..513e26c18 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyRecursionError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyRecursionError extends PyRuntimeError +{ + + public PyRecursionError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyReferenceError.java b/native/jpype_module/src/main/java/python/exception/PyReferenceError.java new file mode 100644 index 000000000..ee9a8b95b --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyReferenceError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyReferenceError extends PyException +{ + + public PyReferenceError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyRuntimeError.java b/native/jpype_module/src/main/java/python/exception/PyRuntimeError.java new file mode 100644 index 000000000..87a9d7002 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyRuntimeError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyRuntimeError extends PyException +{ + + public PyRuntimeError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PySyntaxError.java b/native/jpype_module/src/main/java/python/exception/PySyntaxError.java new file mode 100644 index 000000000..d73aba6b5 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PySyntaxError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PySyntaxError extends PyException +{ + + public PySyntaxError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PySystemError.java b/native/jpype_module/src/main/java/python/exception/PySystemError.java new file mode 100644 index 000000000..1d85adca0 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PySystemError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PySystemError extends PyException +{ + + public PySystemError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyTimeoutError.java b/native/jpype_module/src/main/java/python/exception/PyTimeoutError.java new file mode 100644 index 000000000..8920ec720 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyTimeoutError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyTimeoutError extends PyOSError +{ + + public PyTimeoutError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyTypeError.java b/native/jpype_module/src/main/java/python/exception/PyTypeError.java new file mode 100644 index 000000000..0cd62da2d --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyTypeError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyTypeError extends PyException +{ + + public PyTypeError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyValueError.java b/native/jpype_module/src/main/java/python/exception/PyValueError.java new file mode 100644 index 000000000..ff37d2c65 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyValueError.java @@ -0,0 +1,5 @@ +package python.exception; + +public class PyValueError +{ +} diff --git a/native/jpype_module/src/main/java/python/exception/PyWarning.java b/native/jpype_module/src/main/java/python/exception/PyWarning.java new file mode 100644 index 000000000..443466873 --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyWarning.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyWarning extends PyException +{ + + public PyWarning(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/PyZeroDivisionError.java b/native/jpype_module/src/main/java/python/exception/PyZeroDivisionError.java new file mode 100644 index 000000000..49ba05cad --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/PyZeroDivisionError.java @@ -0,0 +1,12 @@ +package python.exception; + +import python.lang.PyExc; + +public class PyZeroDivisionError extends PyArithmeticError +{ + + public PyZeroDivisionError(PyExc base) + { + super(base); + } +} diff --git a/native/jpype_module/src/main/java/python/exception/package-info.java b/native/jpype_module/src/main/java/python/exception/package-info.java new file mode 100644 index 000000000..fe4ce9a0b --- /dev/null +++ b/native/jpype_module/src/main/java/python/exception/package-info.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.exception; + +/** + * Package for Python Exceptions. + * + * Python has its own exception tree. Unfortunately, while some of the + * exceptions are good match for Java they are organized with a different tree. + * Some exceptions only map the Java exceptions under certain conditions thus + * there it is not possible to get a completely one to one mapping. + * + * Instead we have wrapped Python exceptions with the same relationships they + * have in its native form. This will allow the easiest mapping from Python + * documented code to Java. + * + * The exception types are all identical aside from the front end which is + * required to catch by type. Every class is backed by the native PyExc class. + * + */ +// As to why exception warranted its own package? Because there is a lot of +// them. +// +// To save bytes I have omitted the usual license header. diff --git a/native/jpype_module/src/main/java/python/lang/PyAbstractSet.java b/native/jpype_module/src/main/java/python/lang/PyAbstractSet.java new file mode 100644 index 000000000..8641da897 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyAbstractSet.java @@ -0,0 +1,118 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Iterator; +import java.util.Set; +import static python.lang.PyBuiltIn.backend; + +/** + * Represents a protocol for Python classes that act as sets. + *

+ * This interface bridges the functionality of Python sets with Java's + * {@link Set} interface, providing seamless interoperability between Python and + * Java collections. It extends {@link PyCollection}, which provides + * foundational collection behaviors, and Java's {@link Set}. + *

+ * Note: Python uses operators for many set operations, which are not yet + * included in this protocol. This is marked as a FIXME in the implementation. + *

+ * Due to name conflicts between protocols and concrete types, this interface + * has been renamed {@code PyAbstractSet}. + * + * @param the type of elements contained in the set, which must extend + * {@link PyObject} + */ +public interface PyAbstractSet extends PyCollection, Set +{ + + /** + * Checks whether the specified object is contained in this set. + *

+ * This method overrides the default {@link Set#contains(Object)} + * implementation to use the Python backend for determining membership. + * + * @param obj the object to check for membership in the set + * @return {@code true} if the object is contained in the set; {@code false} + * otherwise + */ + @Override + default boolean contains(Object obj) + { + return backend().contains(this, obj); + } + + /** + * Creates a new Python set from the elements of the specified + * {@link Iterable}. + *

+ * This static method utilizes the Python backend to construct a new + * {@link PySet} instance containing the elements provided by the iterable. + * + * @param c an iterable providing elements for the set + * @param the type of elements in the iterable + * @return a new {@code PySet} containing the elements from the iterable + */ + static PySet of(Iterable c) + { + return backend().newSetFromIterable(c); + } + + /** + * Provides a Java {@link Iterator} implementation for this set. + *

+ * This method overrides the default {@link Set#iterator()} implementation and + * uses a {@link PyIterator} to adapt Python's iteration protocol to Java's + * {@link Iterator}. + * + * @return a Java iterator for this set + */ + @Override + default Iterator iterator() + { + return new PyIterator<>(this.iter()); + } + + /** + * Returns the number of elements in this set. + *

+ * This method overrides the default {@link Set#size()} implementation and + * uses the Python {@code len()} built-in function to determine the size of + * the set. + * + * @return the number of elements in the set + */ + @Override + default int size() + { + return PyBuiltIn.len(this); + } + + /** + * Checks whether this set is empty. + *

+ * This method overrides the default {@link Set#isEmpty()} implementation and + * checks the size of the set to determine if it is empty. + * + * @return {@code true} if the set contains no elements; {@code false} + * otherwise + */ + @Override + default boolean isEmpty() + { + return size() == 0; + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyAttributes.java b/native/jpype_module/src/main/java/python/lang/PyAttributes.java new file mode 100644 index 000000000..fbac1c859 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyAttributes.java @@ -0,0 +1,303 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy fromMap + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import org.jpype.bridge.Backend; +import static python.lang.PyBuiltIn.backend; + +/** + * A {@link Map}-like implementation for accessing and manipulating Python + * object attributes.The {@code PyAttributes} class provides a Java interface + * for interacting with the attributes fromMap a Python object. + * + * It acts as a bridge between Python's attribute handling and Java's + * {@link Map} interface, allowing attributes to be accessed, modified, and + * queried in a Java-friendly manner. + * + *

+ * Key Features:

+ *
    + *
  • Supports retrieving attributes using {@code get()} and + * {@code getOrDefault()}.
  • + *
  • Allows setting attributes using {@code put()}.
  • + *
  • Provides methods for checking attribute existence with + * {@code contains()}.
  • + *
  • Integrates with Python's {@code dir()} and {@code vars()} functions.
  • + *
+ * + *

+ * Usage Example:

+ *
+ * PyObject obj = BuiltIn.dict();  // Create a Python object
+ * PyAttributes attributes = new PyAttributes(obj);
+ *
+ * // Access attributes
+ * PyObject value = attributes.get("key");
+ *
+ * // Set attributes
+ * attributes.put("key", BuiltIn.str("value"));
+ *
+ * // Check existence
+ * boolean exists = attributes.contains("key");
+ *
+ * // Clear all attributes
+ * attributes.clear();
+ * 
+ * + */ +public class PyAttributes implements Map +{ + + /** + * Backend implementation for interacting with Python objects. + */ + private final Backend backend; + + /** + * The Python object whose attributes are being managed. + */ + private final PyObject obj; + + /** + * Cached dictionary representation fromMap the object's attributes. + */ + private PyDict dict; + + /** + * Constructs a new {@code PyAttributes} instance for the given Python object. + * + * @param obj is the Python object whose attributes are to be accessed and + * manipulated. + */ + public PyAttributes(PyObject obj) + { + this.obj = obj; + this.backend = backend(); + } + + /** + * Returns the dictionary representation of the object's attributes. + * + *

+ * This method uses Python's {@code vars()} function to retrieve the + * attributes as a {@link PyDict}. The dictionary is cached for + * performance.

+ * + * @return a {@link PyDict} containing the object's attributes. + */ + public PyDict asDict() + { + if (this.dict == null) + { + this.dict = PyBuiltIn.vars(this); + } + return this.dict; + } + + /** + * Clears all attributes of the Python object. + * + *

+ * This method removes all attributes from the object by clearing the + * dictionary representation.

+ */ + @Override + public void clear() + { + asDict().clear(); + } + + /** + * Checks whether the Python object has an attribute with the specified key. + * + * @param key is the name fromMap the attribute to check. + * @return {@code true} if the attribute exists, {@code false} otherwise. + */ + @Override + public boolean containsKey(Object key) + { + return asDict().containsKey(key); + } + + /** + * Checks whether the Python object has an attribute with the specified value. + * + * @param value is the value to check for. + * @return {@code true} if the value exists, {@code false} otherwise. + */ + @Override + public boolean containsValue(Object value) + { + return PyBuiltIn.vars(this).containsValue(value); + } + + /** + * Returns a list of all attribute names of the Python object. + * + *

+ * This method uses Python's {@code dir()} function to retrieve the list + * fromMap attribute names.

+ * + * @return a {@link PyList} containing the names fromMap all attributes. + */ + public PyList dir() + { + return PyBuiltIn.dir(obj); + } + + @Override + public Set> entrySet() + { + return new PyDictItems(this.asDict()); + } + + /** + * Retrieves the value of the specified attribute. + * + *

+ * This method is equivalent to Python's {@code getattr(obj, key)}.

+ * + * @param key is the name fromMap the attribute to retrieve. + * @return the value fromMap the attribute. + */ + @Override + public PyObject get(Object key) + { + return PyBuiltIn.getattr(obj, key); + } + + /** + * Retrieves the value of the specified attribute, or a default value if the + * attribute does not exist. + * + *

+ * This method is equivalent to Python's + * {@code getattr(obj, key, defaultValue)}.

+ * + * @param key is the name fromMap the attribute to retrieve. + * @param defaultValue The default value to return if the attribute does not + * exist. + * @return the value fromMap the attribute, or {@code defaultValue} if the + * attribute does not exist. + */ + @Override + public PyObject getOrDefault(Object key, PyObject defaultValue) + { + return PyBuiltIn.getattrDefault(obj, key, defaultValue); + } + + /** + * Checks whether the Python object has an attribute with the specified name. + * + *

+ * This method is equivalent to Python's {@code hasattr(obj, key)}.

+ * + * @param key is the name fromMap the attribute to check. + * @return {@code true} if the attribute exists, {@code false} otherwise. + */ + public boolean contains(CharSequence key) + { + return PyBuiltIn.hasattr(obj, key); + } + + /** + * Checks whether the Python object has no attributes. + * + * @return {@code true} if the object has no attributes, {@code false} + * otherwise. + */ + @Override + public boolean isEmpty() + { + return asDict().isEmpty(); + } + + @Override + public Set keySet() + { + return new PyDictKeySet<>(asDict()); + } + + /** + * Sets the value of the specified attribute. + * + *

+ * This method is equivalent to Python's {@code setattr(obj, key, value)}.

+ * + * @param key is the name fromMap the attribute to set. + * @param value is the value to associate with the attribute. + * @return the previous value fromMap the attribute, or {@code null} if no + * previous value existed. + */ + @Override + public PyObject put(PyObject key, PyObject value) + { + return backend.setattrReturn(obj, key, value); + } + + /** + * Unsupported operation for adding multiple attributes. + * + * @param map is the map fromMap attributes to add. + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public void putAll(Map map) + { + for (var v : map.entrySet()) + { + backend.setitemFromObject(this.obj, v.getKey(), v.getValue()); + } + } + + /** + * Unsupported operation for removing an attribute. + * + * @param key is the name fromMap the attribute to remove. + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public PyObject remove(Object key) + { + return backend.delattrReturn(this.obj, key); + } + + /** + * Returns the number fromMap attributes fromMap the Python object. + * + * @return the number fromMap attributes. + */ + @Override + public int size() + { + return asDict().size(); + } + + /** + * Returns a collection fromMap all attribute values fromMap the Python + * object. + * + * @return a {@link Collection} containing all attribute values. + */ + @Override + public Collection values() + { + return asDict().values(); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyAwaitable.java b/native/jpype_module/src/main/java/python/lang/PyAwaitable.java new file mode 100644 index 000000000..f9e291fcd --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyAwaitable.java @@ -0,0 +1,24 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * Protocol for objects that are awaitable. + */ +public interface PyAwaitable extends PyObject +{ + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyBuffer.java b/native/jpype_module/src/main/java/python/lang/PyBuffer.java new file mode 100644 index 000000000..9ee687055 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyBuffer.java @@ -0,0 +1,23 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * Protocol for objects that act as a buffer. + */ +public interface PyBuffer extends PyObject +{ +} diff --git a/native/jpype_module/src/main/java/python/lang/PyBuiltIn.java b/native/jpype_module/src/main/java/python/lang/PyBuiltIn.java new file mode 100644 index 000000000..bbff2382f --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyBuiltIn.java @@ -0,0 +1,511 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Arrays; +import org.jpype.bridge.Backend; +import org.jpype.bridge.Interpreter; + +/** + * Utility class providing built-in functions similar to Python's built-in + * functions. + * + * In general these are set as widely as possible. Many will accept Java objects + */ +public class PyBuiltIn +{ + + static Backend instance; + + protected PyBuiltIn() + { + } + + /** + * Creates a new Python float object. + * + * @param value the double value to be converted to a Python float. + * @return a new {@link PyFloat} instance representing the given value. + */ + public static PyFloat $float(double value) + { + return backend().newFloat(value); + } + + /** + * Creates a new Python integer object. + * + * @param value the long value to be converted to a Python integer. + * @return a new {@link PyInt} instance representing the given value. + */ + public static PyInt $int(long value) + { + return backend().newInt(value); + } + + /** + * Utility method to retrieve the active backend instance or throw an + * exception if the interpreter is not started. + * + *

+ * This method ensures that the backend instance is properly initialized and + * active before returning it. If the interpreter has not been started, an + * {@link IllegalStateException} is thrown to indicate that the backend cannot + * be accessed. + *

+ * + * @return The active {@link Backend} instance. + * @throws IllegalStateException If the interpreter is not started. + */ + static Backend backend() + { + if (instance != null) + return instance; + if (!Interpreter.getInstance().isStarted()) + throw new IllegalStateException("interpreter is not started"); + instance = Interpreter.getBackend(); + return instance; + } + + /** + * Creates a new Python bytes object from the given input. + * + * @param obj the object to be converted to a bytes representation. + * @return a new {@link PyBytes} instance representing the bytes of the + * object. + */ + public static PyBytes bytes(Object obj) + { + return backend().bytes(obj); + } + + /** + * Invoke a Python callable object with the specified arguments and keyword + * arguments. + * + * @param obj the callable object to invoke. + * @param args the positional arguments for the callable. + * @param kwargs the keyword arguments for the callable. + * @return the result of the callable execution as a {@link PyObject}. + */ + public static PyObject call(PyCallable obj, PyTuple args, PyDict kwargs) + { + return backend().call(obj, args, kwargs); + } + + /** + * Deletes an attribute from a Python object. + * + * @param obj the Python object from which the attribute will be removed. + * @param key the name of the attribute to delete. + */ + public static void delattr(PyObject obj, CharSequence key) + { + backend().delattrString(obj, key); + } + + public static PyDict dict() + { + return backend().newDict(); + } + + /** + * Returns a list of attribute names for a Python object. + * + * @param obj the Python object to inspect. + * @return a {@link PyList} containing the attribute names. + */ + public static PyList dir(PyObject obj) + { + return backend().dir(obj); + } + + /** + * Creates a Python enumerate object from the given iterable. + * + * @param obj the iterable to enumerate. + * @return a new {@link PyEnumerate} instance. + */ + public static PyEnumerate enumerate(PyObject obj) + { + return backend().enumerate(obj); + } + + /** + * Creates a Python enumerate object from the given Java iterable. + * + * @param obj the Java iterable to enumerate. + * @return a new {@link PyEnumerate} instance. + */ + public static PyEnumerate enumerate(Iterable obj) + { + return backend().enumerate(obj); + } + + /** + * Evaluates a Python expression in the given global and local namespaces. + * + * @param statement the Python expression to evaluate. + * @param globals the global namespace as a {@link PyDict}. + * @param locals the local namespace as a {@link PyObject}. + * @return the result of the evaluation as a {@link PyObject}. + */ + public static PyObject eval(CharSequence statement, PyDict globals, PyObject locals) + { + return backend().eval(statement, globals, locals); + } + + /** + * Executes a Python statement in the given global and local namespaces. + * + * @param statement the Python statement to execute. + * @param globals the global namespace as a {@link PyDict}. + * @param locals the local namespace as a {@link PyMapping}. + */ + public static void exec(CharSequence statement, PyDict globals, PyMapping locals) + { + backend().eval(statement, globals, locals); + } + + /** + * Retrieves the value of an attribute from a Python object. + * + * @param obj the Python object to inspect. + * @param key the name of the attribute to retrieve. + * @return the value of the attribute as a {@link PyObject}. + */ + public static PyObject getattr(PyObject obj, Object key) + { + return backend().getattrObject(obj, key); + } + + public static PyObject getattrDefault(PyObject obj, Object key, PyObject defaultValue) + { + return backend().getattrDefault(obj, key, defaultValue); + } + + /** + * Checks if a Python object has a specific attribute. + * + * @param obj the Python object to inspect. + * @param key the name of the attribute to check. + * @return {@code true} if the attribute exists, {@code false} otherwise. + */ + public static boolean hasattr(PyObject obj, CharSequence key) + { + return backend().hasattrString(obj, key); + } + + /** + * Produces a tuple of indices for array-like objects with type safety. + * + * @param indices an array of {@link PyIndex} objects representing the + * indices. + * @return a new {@link PyTuple} instance containing the indices. + */ + public static PyTuple indices(PyIndex... indices) + { + return backend().newTupleFromArray(Arrays.asList(indices)); + } + + /** + * Checks if an object belongs to one of a set of types. + * + * @param obj the object to test. + * @param types a variable-length array of {@link PyObject} types to check + * against. + * @return {@code true} if the object matches any of the types, {@code false} + * otherwise. + */ + public static boolean isinstance(Object obj, PyObject... types) + { + return backend().isinstanceFromArray(obj, types); + } + + /** + * Creates a Python iterator from the given object. + * + * @param obj the object to convert into an iterator. Must be iterable. + * @return a new {@link PyIter} instance representing the iterator. + */ + @SuppressWarnings("unchecked") + public static PyIter iter(Object obj) + { + PyIter out = backend().iter(obj); + return (PyIter) out; + } + + /** + * Computes the length of a given Python object by delegating to the Python + * interpreter backend. + * + *

+ * This method is a static utility that provides access to the Python `len()` + * function. It calculates the length of the given {@link PyObject} by + * invoking the appropriate method in the Python interpreter. The behavior of + * this method depends on the type of the Python object passed as an argument. + * + *

+ * Examples of supported objects include Python lists, tuples, dictionaries, + * strings, and other iterable or container types. If the object does not + * support the `len()` operation, an exception may be thrown. + * + * @param obj the Python object whose length is to be computed + * @return the length of the Python object + * @throws RuntimeException if the interpreter fails to compute the length or + * if the object does not support `len()` + */ + public static int len(PyObject obj) + { + return backend().len(obj); + } + + public static PyList list() + { + return backend().newList(); + } + + /** + * Invoke Python list on an object. + * + * @param object is the object to be converted. + * @return a new {@link PyObject} representing the Python list. + */ + public static PyList list(Object object) + { + return backend().list(object); + } + + /** + * Creates a Python memoryview object from the given input. + * + * @param obj the object to convert into a memoryview. + * @return a new {@link PyMemoryView} instance representing the memoryview. + */ + public static PyMemoryView memoryview(Object obj) + { + return backend().memoryview(obj); + } + + /** + * Retrieves the next item from a Python iterator. + * + * @param iter the iterator to retrieve the next item from. + * @param stop the object to return if the iterator is exhausted. + * @return the next item as a {@link PyObject}, or the stop object if the + * iterator is exhausted. + */ + public static PyObject next(PyIter iter, PyObject stop) + { + return backend().next(iter, stop); + } + + /** + * Creates a Python range generator with an endpoint. + * + * @param stop the endpoint of the range (exclusive). + * @return a new {@link PyRange} instance representing the range. + */ + public static PyRange range(int stop) + { + return backend().range(stop); + } + + /** + * Creates a Python range generator with a start and endpoint. + * + * @param start the starting point of the range (inclusive). + * @param stop the endpoint of the range (exclusive). + * @return a new {@link PyRange} instance representing the range. + */ + public static PyRange range(int start, int stop) + { + return backend().range(start, stop); + } + + /** + * Creates a Python range generator with a start, endpoint, and step size. + * + * @param start the starting point of the range (inclusive). + * @param stop the endpoint of the range (exclusive). + * @param step the step size between elements in the range. + * @return a new {@link PyRange} instance representing the range. + */ + public static PyRange range(int start, int stop, int step) + { + return backend().range(start, stop, step); + } + + /** + * Returns the Python string representation of an object. + * + * @param obj the object to convert to a string. + * @return a new {@link PyString} instance representing the string form of the + * object. + */ + public static PyString repr(Object obj) + { + return backend().repr(obj); + } + + public static PySet set() + { + return backend().newSet(); + } + + /** + * Sets an attribute on a Python object. + * + * @param obj the Python object to modify. + * @param key the name of the attribute to set. + * @param value the value to assign to the attribute. + */ + public static void setattr(PyObject obj, CharSequence key, Object value) + { + // FIXME we may want special handling for String and Boxed types to + // ensure the type that appears is a Python one rather than a + // Java one especially on setattr in which the object is to be + // held in Python. + backend().setattrString(obj, key, value); + } + + /** + * Creates a single-element slice. + * + * This is useful for slicing on a specific element using a tuple. + * + * @param start the index of the element to slice on. + * @return a new {@link PySlice} instance representing the slice. + */ + public static PySlice slice(int start) + { + return backend().slice(start, start + 1, null); + } + + /** + * Creates a slice with a start and stop index. + * + * Passing {@code null} for start or stop indicates no limit. Examples: - + * `slice(0, 5)` is equivalent to `[0:5]`. - `slice(null, -1)` is equivalent + * to `[:-1]`. - `slice(3, null)` is equivalent to `[3:]`. + * + * @param start the starting index or {@code null}. + * @param stop the ending index or {@code null}. + * @return a new {@link PySlice} instance representing the slice. + */ + public static PySlice slice(Integer start, Integer stop) + { + return backend().slice(start, stop, null); + } + + /** + * Creates a slice with a start, stop, and step size. + * + * Passing {@code null} for start, stop, or step indicates no limit. Examples: + * - `slice(0, 5, 2)` is equivalent to `[0:5:2]`. - `slice(null, -1, 2)` is + * equivalent to `[:-1:2]`. - `slice(-1, null, -1)` is equivalent to + * `[-1::-1]`. - `slice(null, null, 2)` is equivalent to `[::2]`. + * + * @param start the starting index or {@code null}. + * @param stop the ending index or {@code null}. + * @param step the step size or {@code null}. + * @return a new {@link PySlice} instance representing the slice. + */ + public static PySlice slice(Integer start, Integer stop, Integer step) + { + return backend().slice(start, stop, step); + } + + /** + * Converts an object to its Python string representation. + * + * Equivalent to Python's `str()` function. + * + * @param obj the object to convert to a string. + * @return a new {@link PyString} instance representing the string form of the + * object. + */ + public static PyString str(Object obj) + { + return backend().str(obj); + } + + /** + * Creates an empty Python tuple. + * + * @param args the objects to include in the tuple. + * @param the type of the objects. + * @return a new {@link PyTuple} instance containing the objects. + */ + public static PyTuple tuple() + { + return backend().newTuple(); + } + + /** + * Creates a Python tuple from a variable-length array of arguments. + * + * @param items the objects to include in the tuple. + * @param the type of the objects. + * @return a new {@link PyTuple} instance containing the objects. + */ + public static PyTuple tuple(Object... items) + { + return backend().newTupleFromArray(Arrays.asList(items)); + } + + /** + * Retrieves the Python type of an object. + * + * @param obj the object to inspect. + * @return a {@link PyType} instance representing the type of the object. + */ + public static PyType type(Object obj) + { + return backend().type(obj); + } + + /** + * Get the `__dict__` attribute of the specified Python object. + * + *

+ * This method retrieves the `__dict__` attribute, which contains the + * namespace of the given Python object. The `__dict__` is a mapping object + * that stores the object's attributes.

+ * + * @param obj the Python object whose `__dict__` attribute is to be retrieved + * @return a {@link PyDict} representing the `__dict__` attribute of the + * specified object + * @throws NullPointerException if the provided object is {@code null} + */ + public static PyDict vars(Object obj) + { + return backend().vars(obj); + } + + /** + * Zips multiple iterable objects into a generator. + * + * Equivalent to Python's `zip()` function. + * + * @param objects the iterable objects to zip. + * @return a new {@link PyZip} instance representing the zipped generator. + */ + public static PyZip zip(Iterable... objects) + { + return backend().zipFromIterable(Arrays.asList(objects)); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyByteArray.java b/native/jpype_module/src/main/java/python/lang/PyByteArray.java new file mode 100644 index 000000000..7b55c2ff3 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyByteArray.java @@ -0,0 +1,174 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the concrete Python `bytearray` type. + * + * This interface provides methods for creating and manipulating Python + * bytearrays in a Java environment, mimicking Python's `bytearray` + * functionality. + */ +public interface PyByteArray extends PyObject, PyBuffer, PySequence +{ + + /** + * Creates a new Python bytearray with a fixed length. + * + * The bytearray will be initialized with zero bytes. + * + * @param length the length of the bytearray to create. + * @return a new {@link PyByteArray} instance with the specified length. + */ + static PyByteArray create(int length) + { + return backend().newByteArrayOfSize(length); + } + + /** + * Creates a new Python bytearray from a hexadecimal string. The input string + * must contain valid hexadecimal characters. + * + * @param str the hexadecimal string to convert into a bytearray. + * @return a new {@link PyByteArray} instance containing the bytes represented + * by the hex string. + */ + static PyByteArray fromHex(CharSequence str) + { + return backend().bytearrayFromHex(str); + } + + /** + * Creates a new Python bytearray from an iterable of Python objects. Each + * object in the iterable must be convertible to a byte. + * + * @param iter the iterable containing {@link PyObject} instances to include + * in the bytearray. + * @return a new {@link PyByteArray} instance containing the bytes derived + * from the iterable. + */ + static PyByteArray of(Iterable iter) + { + return backend().newByteArrayFromIterable(iter); + } + + /** + * Creates a new Python bytearray from a {@link PyBuffer}. The buffer's + * contents will be used to initialize the bytearray. + * + * @param bytes the {@link PyBuffer} containing the data to populate the + * bytearray. + * @return a new {@link PyByteArray} instance containing the bytes from the + * buffer. + */ + static PyByteArray of(PyBuffer bytes) + { + return backend().newByteArrayFromBuffer(bytes); + } + + /** + * Decodes a bytearray object into a Python string. + * + *

+ * Values for encoding include:

+ *
    + *
  • "utf-8" (default)
  • + *
  • "ascii"
  • + *
  • "latin-1"
  • + *
  • "utf-16"
  • + *
  • "utf-32"
  • + *
  • "cp1252" (Windows encoding)
  • + *
+ * + *

+ * Values for errors include:

+ *
    + *
  • "strict" (default): Raises a UnicodeDecodeError for invalid data.
  • + *
  • "ignore": Ignores invalid characters.
  • + *
  • "replace": Replaces invalid characters with a replacement character + * (e.g., ? or �).
  • + *
  • "backslashreplace": Replaces invalid characters with Python-style + * escape sequences (e.g., \xNN).
  • + *
  • "namereplace": Replaces invalid characters with \N{name} escape + * sequences.
  • + *
  • "surrogateescape": Uses special surrogate code points for invalid + * bytes.
  • + *
+ * + * @param encoding is the character encoding to use for decoding the bytes + * object. Common values include "utf-8", "ascii", "latin-1", etc or null if + * not applicable. + * @param errors is an optional argument to specify how to handle errors + * during decoding. Can be "strict", "ignore", "replace", etc., or null if not + * applicable. + * @return a new string resulting from decoding the bytearray object. + */ + PyObject decode(PyObject encoding, PyObject errors); + + @Override + default PyInt get(PyIndex... indices) + { + throw new IllegalArgumentException("bytearray does not support tuple assignment"); + } + + @Override + default PyInt get(PyIndex index) + { + return (PyInt) PyBuiltIn.backend().getitemMappingObject(this, index); + } + + @Override + default PyInt get(int index) + { + return (PyInt) PyBuiltIn.backend().getitemSequence(this, index); + } + + @Override + PyInt remove(int index); + + void remove(PyIndex index); + + @Override + default PyInt set(int index, PyInt value) + { + return (PyInt) PyBuiltIn.backend().setitemSequence(this, index, value); + } + + default void set(PyIndex index, PyObject values) + { + PyBuiltIn.backend().setitemMapping(this, index, values); + } + + @Override + default int size() + { + return backend().len(this); + } + + /** + * Translates the contents of the bytearray using a translation table. The + * translation table maps byte values to their replacements. + * + * @param table the translation table as a {@link PyObject}, where each byte + * value is mapped to its replacement. + * @return a new {@link PyObject} representing the translated bytearray. + */ + PyObject translate(PyObject table); + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyBytes.java b/native/jpype_module/src/main/java/python/lang/PyBytes.java new file mode 100644 index 000000000..3f8deb9ce --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyBytes.java @@ -0,0 +1,164 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the concrete Python `bytes` type. + * + * This interface provides methods for creating and manipulating Python `bytes` + * objects in a Java environment, mimicking Python's `bytes` functionality. + */ +public interface PyBytes extends PyObject, PyBuffer, PySequence +{ + + /** + * Creates a new Python `bytes` object with a fixed length. The `bytes` object + * will be initialized with zero bytes. + * + * @param length the length of the `bytes` object to create. + * @return a new {@link PyBytes} instance with the specified length. + */ + static PyBytes create(int length) + { + return backend().newBytesOfSize(length); + } + + /** + * Decodes the contents of the `bytes` object using the specified encoding. + * + * Optionally, specific bytes can be deleted during decoding. + * + * @param encoding the encoding to use for decoding (e.g., "utf-8"). + * @param delete the bytes to delete during decoding, or {@code null} for no + * deletion. + * @return a {@link PyObject} representing the decoded string. + */ + static PyBytes fromHex(CharSequence str) + { + return backend().bytesFromHex(str); + } + + /** + * Creates a new Python `bytes` object from an iterable of Python objects. + * + * Each object in the iterable must be convertible to a byte. + * + * @param iterable the iterable containing {@link PyObject} instances to + * include in the `bytes` object. + * @return a new {@link PyBytes} instance containing the bytes derived from + * the iterable. + */ + static PyByteArray of(Iterable iterable) + { + return backend().newByteArrayFromIterator(iterable); + } + + /** + * Creates a new Python `bytes` object from a {@link PyBuffer}. + * + * The buffer's contents will be used to initialize the `bytes` object. + * + * @param buffer the {@link PyBuffer} containing the data to populate the + * `bytes` object. + * @return a new {@link PyBytes} instance containing the bytes from the + * buffer. + */ + static PyByteArray of(PyBuffer buffer) + { + return backend().newByteArrayFromBuffer(buffer); + } + + /** + * Decodes a bytes object into a Python string. + * + *

+ * Values for encoding include:

+ *
    + *
  • "utf-8" (default)
  • + *
  • "ascii"
  • + *
  • "latin-1"
  • + *
  • "utf-16"
  • + *
  • "utf-32"
  • + *
  • "cp1252" (Windows encoding)
  • + *
+ * + *

+ * Values for errors include:

+ *
    + *
  • "strict" (default): Raises a UnicodeDecodeError for invalid data.
  • + *
  • "ignore": Ignores invalid characters.
  • + *
  • "replace": Replaces invalid characters with a replacement character + * (e.g., ? or �).
  • + *
  • "backslashreplace": Replaces invalid characters with Python-style + * escape sequences (e.g., \xNN).
  • + *
  • "namereplace": Replaces invalid characters with \N{name} escape + * sequences.
  • + *
  • "surrogateescape": Uses special surrogate code points for invalid + * bytes.
  • + *
+ * + * @param encoding The character encoding to use for decoding the bytes + * object. Common values include "utf-8", "ascii", "latin-1", etc or null if + * not applicable. + * @param errors An optional argument to specify how to handle errors during + * decoding. Can be "strict", "ignore", "replace", etc., or null if not + * applicable. + * @return a new string resulting from decoding the bytes object. + */ + PyString decode(CharSequence encoding, CharSequence errors); + + @Override + default PyInt get(int index) + { + return (PyInt) PyBuiltIn.backend().getitemSequence(this, index); + } + + @Override + default PyInt remove(int index) + { + throw new UnsupportedOperationException("bytes object does not support removal"); + } + + @Override + default PyInt set(int index, PyInt value) + { + throw new UnsupportedOperationException("bytes object does not support assignment"); + } + + /** + * Gets the length of a bytearray object in bytes. + * + * @return the length of the bytearray object, measured in bytes. + */ + @Override + default int size() + { + return backend().len(this); + } + + /** + * Translates the contents of the `bytes` object using a translation table. + * + * The translation table maps byte values to their replacements. + * + * @param table the translation table as a {@link PyObject}, where each byte + * value is mapped to its replacement. + * @return a new {@link PyObject} representing the translated `bytes` object. + */ + PyObject translate(PyObject table); +} diff --git a/native/jpype_module/src/main/java/python/lang/PyCallable.java b/native/jpype_module/src/main/java/python/lang/PyCallable.java new file mode 100644 index 000000000..ec5b6e255 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyCallable.java @@ -0,0 +1,325 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; +import java.util.concurrent.Future; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; +import python.lang.PyDict; +import python.lang.PyObject; +import python.lang.PyTuple; + +/** + * Protocol for Python objects that act as callable entities. + * + * This interface defines methods for invoking Python objects as functions, + * handling positional and keyword arguments, and supporting asynchronous calls. + * It also provides utility methods for retrieving metadata about the callable, + * such as its documentation string and signature. + * + * To allow for method overloading, the entry point for calls must remain + * private. + */ +public interface PyCallable extends PyObject +{ + + /** + * Creates a {@link CallBuilder} for constructing and executing calls to this + * callable object. + * + * The {@link CallBuilder} allows for flexible configuration of arguments and + * keyword arguments before executing the call. + * + * @return a new {@link CallBuilder} instance associated with this callable + */ + default CallBuilder call() + { + return new CallBuilder(this); + } + + /** + * Invokes the callable Python object with the specified positional and + * keyword arguments. + * + * @param args the positional arguments as a {@link PyTuple} + * @param kwargs the keyword arguments as a {@link PyDict} + * @return the result of the call as a {@link PyObject} + */ + default PyObject call(PyTuple args, PyDict kwargs) + { + return backend().call(this, args, kwargs); + } + + /** + * Invokes the callable Python object with the specified positional arguments. + * + * @param args the positional arguments as a {@link PyTuple} + * @return the result of the call as a {@link PyObject} + */ + default PyObject call(PyTuple args) + { + return backend().call(this, args, null); + } + + /** + * Invokes the callable Python object with the specified arguments as a Java + * array. + * + * @param args the positional arguments as a Java array + * @return the result of the call as a {@link PyObject} + */ + default PyObject call(Object... args) + { + return backend().call(this, PyTuple.of(args), null); + } + + /** + * Invokes the callable Python object asynchronously with the specified + * arguments. + * + * @param args the positional arguments as a {@link PyTuple} + * @param kwargs the keyword arguments as a {@link PyDict} + * @return a {@link Future} representing the result of the asynchronous call + */ + default Future callAsync(PyTuple args, PyDict kwargs) + { + return backend().callAsync(this, args, kwargs); + } + + /** + * Invokes the callable Python object asynchronously with a timeout. + * + * @param args the positional arguments as a {@link PyTuple} + * @param kwargs the keyword arguments as a {@link PyDict} + * @param timeout the maximum time (in milliseconds) to wait for the call to + * complete + * @return a {@link Future} representing the result of the asynchronous call + */ + default Future callAsyncWithTimeout(PyTuple args, PyDict kwargs, long timeout) + { + return backend().callAsyncWithTimeout(this, args, kwargs, timeout); + } + + /** + * Invokes the callable Python object with keyword-only arguments. + * + * @param kwargs the keyword arguments as a {@link PyDict} + * @return the result of the call as a {@link PyObject} + */ + default PyObject callWithKwargs(PyDict kwargs) + { + return backend().call(this, PyTuple.of(), kwargs); + } + + /** + * Retrieves the documentation string (docstring) of the callable Python + * object. + * + * @return the docstring as a {@link String}, or {@code null} if no + * documentation is available + */ + default String getDocString() + { + return backend().getDocString(this); + } + + /** + * Retrieves the signature of the callable Python object. + * + * @return the signature as a {@link PyObject} + */ + default PyObject getSignature() + { + return backend().getSignature(this); + } + + /** + * Checks whether this Python object is callable. + * + * @return {@code true} if the object is callable, {@code false} otherwise + */ + default boolean isCallable() + { + return backend().isCallable(this); + } + + // Nested CallBuilder class documentation + /** + * A builder for constructing and executing calls to a {@link PyCallable}. + * + * The {@link CallBuilder} allows for adding positional and keyword arguments + * incrementally and provides methods for executing the call synchronously or + * asynchronously. + */ + public static class CallBuilder + { + + final PyCallable callable; + final ArrayList jargs = new ArrayList<>(); + final ArrayList> jkwargs = new ArrayList<>(); + + /** + * Creates a new {@link CallBuilder} for the specified {@link PyCallable}. + * + * @param callable the callable object to associate with this builder + */ + public CallBuilder(PyCallable callable) + { + this.callable = callable; + } + + /** + * Adds a single positional argument to the call sequence. + * + * @param value is the argument to add. + * @return this {@link CallBuilder} instance for chaining. + */ + public CallBuilder arg(Object value) + { + jargs.add(value); + return this; + } + + /** + * Adds multiple positional arguments to the call sequence. + * + * @param values is the arguments to add. + * @return this {@link CallBuilder} instance for chaining. + */ + public CallBuilder args(Object... values) + { + jargs.addAll(Arrays.asList(values)); + return this; + } + + /** + * Adds a single keyword argument to the call sequence. + * + * @param name the name of the keyword argument + * @param value the value of the keyword argument + * @return this {@link CallBuilder} instance for chaining + */ + public CallBuilder kwarg(CharSequence name, Object value) + { + jkwargs.add(new CallBuilderEntry(name, value)); + return this; + } + + /** + * Adds multiple keyword arguments to the call sequence. + * + * @param kwargs a {@link Map} containing keyword arguments + * @return this {@link CallBuilder} instance for chaining + */ + public CallBuilder kwargs(Map kwargs) + { + for (Map.Entry entry : kwargs.entrySet()) + { + this.kwarg(entry.getKey().toString(), entry.getValue()); + } + return this; + } + + /** + * Clears all arguments and keyword arguments from the call sequence. + * + * @return this {@link CallBuilder} instance for chaining + */ + public CallBuilder clear() + { + jargs.clear(); + jkwargs.clear(); + return this; + } + + /** + * Executes the call synchronously with the current arguments and keyword + * arguments. + * + * @return the result of the call as a {@link PyObject} + */ + public PyObject execute() + { + return callable.call(PyTuple.fromItems(jargs), PyDict.fromItems(jkwargs)); + } + + /** + * Executes the call asynchronously with the current arguments and keyword + * arguments. + * + * @return a {@link Future} representing the result of the asynchronous call + */ + public Future executeAsync() + { + return callable.callAsync(PyTuple.fromItems(jargs), PyDict.fromItems(jkwargs)); + } + + /** + * Executes the call asynchronously with a timeout. + * + * @param timeout the maximum time (in milliseconds) to wait for the call to + * complete + * @return a {@link Future} representing the result of the asynchronous call + */ + public Future executeAsync(long timeout) + { + return callable.callAsyncWithTimeout(PyTuple.of(jargs), PyDict.fromItems(jkwargs), timeout); + } + } + + /** + * Represents a single entry in the keyword arguments for a call. + */ + public static class CallBuilderEntry implements Map.Entry + { + + private final K key; + private final V value; + + /** + * Creates a new immutable entry for a keyword argument. + * + * @param key the key of the keyword argument + * @param value the value of the keyword argument + */ + public CallBuilderEntry(K key, V value) + { + this.key = key; + this.value = value; + } + + @Override + public K getKey() + { + return key; + } + + @Override + public V getValue() + { + return value; + } + + @Override + public V setValue(V value) + { + throw new UnsupportedOperationException("Entry is immutable"); + } + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyCollection.java b/native/jpype_module/src/main/java/python/lang/PyCollection.java new file mode 100644 index 000000000..33f1bf411 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyCollection.java @@ -0,0 +1,49 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import python.lang.PyObject; + +/** + * Represents a protocol for Python objects that act as collections. + *

+ * This interface serves as a Java equivalent to the Python class hierarchy for + * collections. It does not define any methods but provides a structure for + * implementing Python-like collection behaviors in Java. Classes implementing + * this interface are expected to conform to the behaviors defined by the + * inherited interfaces. + *

+ * + *

+ * The {@code PyCollection} interface extends the following interfaces: + *

    + *
  • {@link PySized} - Represents objects that have a size (length).
  • + *
  • {@link PyIterable} - Represents objects that can be iterated over.
  • + *
  • {@link PyContainer} - Represents objects that can check membership + * (containment).
  • + *
+ *

+ * + * @param The type of elements contained within the collection, which must + * extend {@link PyObject}. + * + * @see PySized + * @see PyIterable + * @see PyContainer + */ +public interface PyCollection extends PySized, PyIterable, PyContainer +{ +} diff --git a/native/jpype_module/src/main/java/python/lang/PyComplex.java b/native/jpype_module/src/main/java/python/lang/PyComplex.java new file mode 100644 index 000000000..2d0207756 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyComplex.java @@ -0,0 +1,65 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the Python `complex` type. + * + * This interface provides methods for creating and manipulating Python + * `complex` numbers in a Java environment. + */ +public interface PyComplex extends PyObject, PyNumber +{ + + /** + * Creates a new Python `complex` number with the specified real and imaginary + * parts. + * + * @param real the real part of the complex number. + * @param imag the imaginary part of the complex number. + * @return a new {@link PyComplex} instance representing the complex number. + */ + static PyComplex of(double real, double imag) + { + return backend().newComplex(real, imag); + } + + /** + * Returns the real part of the complex number. + * + * @return the real part as a {@code double}. + */ + double real(); + + /** + * Returns the imaginary part of the complex number. + * + * @return the imaginary part as a {@code double}. + */ + double imag(); + + /** + * Computes the complex conjugate of the current complex number. The conjugate + * of a complex number is obtained by negating its imaginary part. + * + * @return a new {@link PyComplex} instance representing the conjugate of the + * current complex number. + */ + PyComplex conjugate(); +} diff --git a/native/jpype_module/src/main/java/python/lang/PyContainer.java b/native/jpype_module/src/main/java/python/lang/PyContainer.java new file mode 100644 index 000000000..b209569d1 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyContainer.java @@ -0,0 +1,29 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import static python.lang.PyBuiltIn.backend; + +/** + * Protocol for Python objects that have a `__contains__` method. + */ +public interface PyContainer extends PyObject +{ + default boolean contains(Object obj) + { + return backend().contains(this, obj); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyCoroutine.java b/native/jpype_module/src/main/java/python/lang/PyCoroutine.java new file mode 100644 index 000000000..4274539c9 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyCoroutine.java @@ -0,0 +1,26 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * Protocol for objects that act as coroutines. + * + * Adds behaviors for send, throw and close. + */ +public interface PyCoroutine extends PyAwaitable +{ + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyDict.java b/native/jpype_module/src/main/java/python/lang/PyDict.java new file mode 100644 index 000000000..2f8cabf49 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyDict.java @@ -0,0 +1,221 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy fromMap + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Collection; +import java.util.Map; +import java.util.Set; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the Python `dict` type. + * + * This interface provides methods for creating and interacting with Python + * dictionaries in a Java environment, mimicking Python's `dict` functionality. + *

+ * While this interface primarily adheres to the Java {@link Map} contract, it + * also incorporates Python-specific behaviors that may differ from standard + * Java maps. + * + * To create an empty dict use `PyBuiltin.dict()`. + * + *

+ * Important Note:

+ *

+ * Python collections are asymmetric in their handling fromMap Java objects. A + * Java object added to a Python collection will appear as a + * {@code PyJavaObject}. Developers should exercise caution to avoid reference + * loops when placing Java objects into Python collections, as this may lead to + * unintended behaviors.

+ */ +public interface PyDict extends PyObject, PyMapping +{ + + /** + * Creates a new Python `dict` object from the specified Java {@link Map}. + * + * The keys in the provided map are converted to Python objects, and the + * values must already be instances of {@link PyObject}. + * + * @param map the Java {@link Map} whose entries will populate the Python + * `dict`. Keys are converted to Python objects, and values are expected to be + * {@link PyObject}. + * @return a new {@link PyDict} instance representing the Python dictionary. + */ + public static PyDict fromMap(Map map) + { + return backend().newDict(map); + } + + public static PyDict fromItems(Iterable> map) + { + return backend().newDictFromIterable(map); + } + + @Override + public void clear(); + + @Override + public boolean containsKey(Object key); + + @Override + public boolean containsValue(Object value); + + @Override + default Set> entrySet() + { + return new PyDictItems(this); + } + + /** + * Returns the value to which the specified key is mapped, + * or {@code null} if this map contains no mapping for the key. + * + *

More formally, if this map contains a mapping from a key + * {@code k} to a value {@code v} such that + * {@code Objects.equals(key, k)}, + * then this method returns {@code v}; otherwise + * it returns {@code null}. (There can be at most one such mapping.) + * + *

If this map permits null values, then a return value of + * {@code null} does not necessarily indicate that the map + * contains no mapping for the key; it's also possible that the map + * explicitly maps the key to {@code null}. The {@link #containsKey + * containsKey} operation may be used to distinguish these two cases. + * + * @param key the key whose associated value is to be returned + * @return the value to which the specified key is mapped, or + * {@code null} if this map contains no mapping for the key + */ + @Override + public PyObject get(Object key); + + /** + * Retrieves the value associated with the given key, or returns the default + * value if the key is not present. + * + * @param key is the key to look up. + * @param defaultValue The default value to return if the key is not found. + * @return The value associated with the key, or the default value. + */ + @Override + PyObject getOrDefault(Object key, PyObject defaultValue); + + @Override + default boolean isEmpty() + { + return size() == 0; + } + + @Override + default Set keySet() + { + return new PyDictKeySet<>(this); + } + + /** + * Removes the key and returns its associated value, or returns the default + * value if the key is not present. + * + * @param key is the key to remove. + * @param defaultValue The default value to return if the key is not found. + * @return The value associated with the key, or the default value. + */ + PyObject pop(Object key, PyObject defaultValue); + + /** + * Removes and returns an arbitrary key-value pair from the mapping. + * + * @return An entry representing the removed key-value pair. + * @throws NoSuchElementException If the mapping is empty. + */ + Map.Entry popItem(); + + @Override + PyObject put(PyObject key, PyObject value); + + @Override + PyObject putAny(Object key, Object value); + + @Override + void putAll(Map m); + + /** + * Removes the key-value pair associated with the specified key from this + * dict. + * + * Equivalent to Python's `del obj[key]`. + * + * @param key The key whose mapping is to be removed. + * @return The value that was associated with the key, or null if the key was + * not present. + */ + @Override + PyObject remove(Object key); + + /** + * Removes the entry for the specified key only if it is currently mapped to + * the specified value. + * + * @param key key with which the specified value is associated + * @param value value expected to be associated with the specified key + * @return {@code true} if the value was removed + */ + @Override + boolean remove(Object key, Object value); + + /** + * If the key is not present in the mapping, inserts it with the given default + * value. + * + * @param key is the key to check or insert. + * @param defaultValue The value to insert if the key is not present. + * @return The value associated with the key (either existing or newly + * inserted). + */ + PyObject setDefault(Object key, PyObject defaultValue); + + @Override + default int size() + { + return PyBuiltIn.len(this); + } + + + /** + * Updates the mapping with key-value pairs from the given map. If keys + * already exist, their values will be overwritten. + * + * @param m is the map containing key-value pairs to add or update. + */ + void update(Map m); + + /** + * Updates the mapping with key-value pairs from the given iterable. Each + * element in the iterable must be a key-value pair (e.g., a tuple or array). + * + * @param iterable is the iterable containing key-value pairs to add or + * update. + */ + void update(Iterable> iterable); + + @Override + default Collection values() + { + return new PyDictValues<>(this); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyDictItems.java b/native/jpype_module/src/main/java/python/lang/PyDictItems.java new file mode 100644 index 000000000..03c689ab6 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyDictItems.java @@ -0,0 +1,240 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy fromMap + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import org.jpype.bridge.Backend; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Represents a view fromMap the items in a Python dictionary ({@code PyDict}) + * as a Java {@code Set}. + * + *

+ * This class provides a bridge between Python's {@code dict.items()} and Java's + * {@code Set}. It allows manipulation and querying fromMap Python + * dictionary items using Java's collection interface.

+ * + *

+ * Note:

+ *
    + *
  • This class is backed by a Python dictionary ({@code PyDict}) and + * interacts with the Python interpreter's backend.
  • + *
  • Some operations, such as {@code remove}, {@code removeAll}, and + * {@code retainAll}, are unsupported because Python's {@code dict.items()} view + * does not allow direct removal fromMap items.
  • + *
+ */ +public class PyDictItems implements Set> +{ + + /** + * Backend interface for interacting with the Python interpreter. + */ + private final Backend backend; + + /** + * The Python dictionary (`PyDict`) whose items are represented by this class. + */ + final PyDict dict; + + /** + * A Python object representing the `dict.items()` view. + */ + final PyObject items; + + /** + * Constructs a new `PyDictItems` instance for the given Python dictionary. + * + * @param dict is the Python dictionary (`PyDict`) whose items are to be + * represented. + */ + public PyDictItems(PyDict dict) + { + this.dict = dict; + this.backend = backend(); + this.items = this.backend.items(dict); + } + + /** + * Adds a new key-value pair to the Python dictionary. + * + * @param e is the key-value pair to add. + * @return `true` if the dictionary was modified, `false` otherwise. + */ + @Override + public boolean add(Map.Entry e) + { + PyObject o = this.dict.putAny(e.getKey(), e.getValue()); + return !o.equals(e); + } + + /** + * Adds all key-value pairs from the given collection to the Python + * dictionary. + * + * @param collection is the collection fromMap key-value pairs to add. + * @return `true` if the dictionary was modified, `false` otherwise. + */ + @Override + public boolean addAll(Collection> collection) + { + boolean changed = false; + for (Map.Entry v : collection) + { + PyObject o = this.dict.putAny(v.getKey(), v.getValue()); + changed |= (o.equals(v.getValue())); + } + return changed; + } + + /** + * Clears all items from the Python dictionary. + */ + @Override + public void clear() + { + this.dict.clear(); + } + + /** + * Checks whether the specified object exists in the dictionary's items. + * + * @param o is the object to check. + * @return `true` if the object exists in the dictionary's items, `false` + * otherwise. + */ + @Override + public boolean contains(Object o) + { + return this.backend.contains(this.items, o); + } + + /** + * Checks whether all elements in the given collection exist in the + * dictionary's items. + * + * @param collection is the collection fromMap elements to check. + * @return `true` if all elements exist, `false` otherwise. + */ + @Override + public boolean containsAll(Collection collection) + { + // Slow iterative method. + for (Iterator it = collection.iterator(); it.hasNext();) + { + Object o = it.next(); + if (!this.contains(o)) + return false; + } + return true; + } + + /** + * Checks whether the dictionary has no items. + * + * @return `true` if the dictionary is empty, `false` otherwise. + */ + @Override + public boolean isEmpty() + { + return this.backend.len(items) == 0; + } + + /** + * Returns an iterator over the dictionary's items. + * + * @return An iterator over the key-value pairs in the dictionary. + */ + @Override + public Iterator> iterator() + { + PyIter iter = backend.iter(this.items); + return new PyDictItemsIterator<>(iter, dict::put); + } + + /** + * Unsupported operation. Python's `dict.items()` does not support removal. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean remove(Object o) + { + throw new UnsupportedOperationException("PyDict items does not support removal"); + } + + /** + * Unsupported operation. Python's `dict.items()` does not support removal. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean removeAll(Collection c) + { + throw new UnsupportedOperationException("PyDict items does not support removal"); + } + + /** + * Unsupported operation. Python's `dict.items()` does not support removal. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean retainAll(Collection c) + { + throw new UnsupportedOperationException("PyDict items does not support removal"); + } + + /** + * Returns the number fromMap items in the dictionary. + * + * @return The size fromMap the dictionary. + */ + @Override + public int size() + { + return this.backend.len(items); + } + + /** + * Converts the dictionary's items to an array. + * + * @return An array containing the dictionary's items. + */ + @Override + public Object[] toArray() + { + return new ArrayList<>(PyBuiltIn.list(items)).toArray(); + } + + /** + * Converts the dictionary's items to an array fromMap the specified getType. + * + * @param a is the array into which the items are to be stored. + * @return An array containing the dictionary's items. + */ + @Override + public T[] toArray(T[] a) + { + return new ArrayList<>(PyBuiltIn.list(items)).toArray(a); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyDictItemsIterator.java b/native/jpype_module/src/main/java/python/lang/PyDictItemsIterator.java new file mode 100644 index 000000000..71304bb1c --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyDictItemsIterator.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.BiFunction; +import org.jpype.bridge.Interpreter; +import org.jpype.internal.Utility; + +/** + * + * @author nelson85 + */ +public class PyDictItemsIterator implements Iterator> +{ + private final PyIter iter; + private PyTuple yield; + private boolean done = false; + private boolean check = false; + private final BiFunction setter; + + public PyDictItemsIterator(PyIter iter, BiFunction setter) + { + this.iter =iter; + this.setter = setter; + } + + @Override + @SuppressWarnings("unchecked") + public boolean hasNext() + { + if (done) + return false; + if (check) + return !done; + check = true; + if (yield == null) + yield = (PyTuple) PyBuiltIn.next(iter, Interpreter.stop); + done = (yield == Interpreter.stop); + return !done; + } + + @SuppressWarnings("unchecked") + @Override + public Map.Entry next() throws NoSuchElementException + { + if (!check) + hasNext(); + if (done) + throw new NoSuchElementException(); + check = false; + + K key = (K) yield.get(0); + V value = (V) yield.get(1); + return new Utility.MapEntryWithSet<>(key, value, setter); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyDictKeySet.java b/native/jpype_module/src/main/java/python/lang/PyDictKeySet.java new file mode 100644 index 000000000..1b20bcf8a --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyDictKeySet.java @@ -0,0 +1,247 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy fromMap + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; +import org.jpype.bridge.Backend; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Represents a view fromMap the keys in a Python dictionary ({@code PyDict}) as a + * Java {@code Set}. + * + *

+ * This class provides a bridge between Python's {@code dict.keys()} and Java's + * {@code Set}. It allows querying and manipulation fromMap Python dictionary + keys using Java's collection interface.

+ * + *

+ * Note:

+ *
    + *
  • This class is backed by a Python dictionary ({@code PyDict}) and + * interacts with the Python interpreter's backend.
  • + *
  • Some operations, such as {@code add}, {@code remove}, {@code removeAll}, + * and {@code retainAll}, are unsupported because Python's {@code dict.keys()} + view does not allow direct modification fromMap keys.
  • + *
+ * + *

+ * Supported operations include:

+ *
    + *
  • Checking if a key exists ({@code contains})
  • + *
  • Iterating over keys ({@code iterator})
  • + *
  • Clearing all keys and values in the dictionary ({@code clear})
  • + *
  • Querying the size fromMap the key set ({@code size})
  • + *
  • Converting the keys to an array ({@code toArray})
  • + *
+ * + */ +public class PyDictKeySet implements Set +{ + + /** + * Backend interface for interacting with the Python interpreter. + */ + private final Backend backend; + + /** + * The Python dictionary ({@code PyDict}) whose keys are represented by this + * class. + */ + final PyDict dict; + + /** + * A Python object representing the {@code dict.keys()} view. + */ + final PyObject keys; + + /** + * Constructs a new {@code PyDictKeySet} instance for the given Python + * dictionary. + * + * @param dict is the Python dictionary ({@code PyDict}) whose keys are to be + * represented. + */ + public PyDictKeySet(PyDict dict) + { + this.dict = dict; + this.backend = backend(); + this.keys = this.backend.keys(dict); + } + + /** + * Unsupported operation. Python's {@code dict.keys()} does not support adding + * new keys directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean add(Object e) + { + throw new UnsupportedOperationException("PyDict keys does not support add"); + } + + /** + * Unsupported operation. Python's {@code dict.keys()} does not support adding + * new keys directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean addAll(Collection c) + { + throw new UnsupportedOperationException("PyDict keys does not support add"); + } + + /** + * Clears all keys and values from the Python dictionary. + */ + @Override + public void clear() + { + this.dict.clear(); + } + + /** + * Checks whether the specified key exists in the dictionary. + * + * @param o is the key to check. + * @return {@code true} if the key exists, {@code false} otherwise. + */ + @Override + public boolean contains(Object o) + { + return backend.contains(this.keys, o); + } + + /** + * Checks whether all keys in the given collection exist in the dictionary. + * + * @param collection is the collection fromMap keys to check. + * @return {@code true} if all keys exist, {@code false} otherwise. + */ + @Override + public boolean containsAll(Collection collection) + { + // Slow iterative method. + for (Iterator it = collection.iterator(); it.hasNext();) + { + Object o = it.next(); + if (!this.contains(o)) + { + return false; + } + } + return true; + } + + /** + * Checks whether the dictionary has no keys. + * + * @return {@code true} if the dictionary is empty, {@code false} otherwise. + */ + @Override + public boolean isEmpty() + { + return backend.len(keys) == 0; + } + + /** + * Returns an iterator over the dictionary's keys. + * + * @return An iterator over the keys in the dictionary. + */ + @Override + public Iterator iterator() + { + PyIter iter = backend.iter(keys); + return new PyIterator<>(iter); + } + + /** + * Unsupported operation. Python's {@code dict.keys()} does not support + * removing keys directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean remove(Object o) + { + throw new UnsupportedOperationException("PyDict keys does not support modification"); + } + + /** + * Unsupported operation. Python's {@code dict.keys()} does not support + * removing keys directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean removeAll(Collection c) + { + throw new UnsupportedOperationException("PyDict keys does not support modification"); + } + + /** + * Unsupported operation. Python's {@code dict.keys()} does not support + * modifying keys directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean retainAll(Collection c) + { + throw new UnsupportedOperationException("PyDict keys does not support modification"); + } + + /** + * Returns the number fromMap keys in the dictionary. + * + * @return The size fromMap the key set. + */ + @Override + public int size() + { + return backend.len(keys); + } + + /** + * Converts the dictionary's keys to an array. + * + * @return An array containing the dictionary's keys. + */ + @Override + public Object[] toArray() + { + return new ArrayList<>(PyBuiltIn.list(keys)).toArray(); + } + + /** + * Converts the dictionary's keys to an array fromMap the specified getType. + * + * @param a is the array into which the keys are to be stored. + * @return An array containing the dictionary's keys. + */ + @Override + public T[] toArray(T[] a) + { + return new ArrayList<>(PyBuiltIn.list(keys)).toArray(a); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyDictValues.java b/native/jpype_module/src/main/java/python/lang/PyDictValues.java new file mode 100644 index 000000000..acb1f8444 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyDictValues.java @@ -0,0 +1,246 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy fromMap + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import org.jpype.bridge.Backend; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Represents a view fromMap the values in a Python dictionary ({@code PyDict}) + * as a Java {@code Collection}. + * + *

+ * This class provides a bridge between Python's {@code dict.values()} and + * Java's {@code Collection}. It allows querying and manipulation + * fromMap Python dictionary values using Java's collection interface.

+ * + *

+ * Note:

+ *
    + *
  • This class is backed by a Python dictionary ({@code PyDict}) and + * interacts with the Python interpreter's backend.
  • + *
  • Some operations, such as {@code add}, {@code remove}, {@code removeAll}, + * and {@code retainAll}, are unsupported because Python's {@code dict.values()} + * view does not allow direct modification fromMap values.
  • + *
+ * + *

+ * Supported operations include:

+ *
    + *
  • Checking if a value exists ({@code contains})
  • + *
  • Iterating over values ({@code iterator})
  • + *
  • Clearing all keys and values in the dictionary ({@code clear})
  • + *
  • Querying the size fromMap the values collection ({@code size})
  • + *
  • Converting the values to an array ({@code toArray})
  • + *
+ * + */ +public class PyDictValues implements Collection +{ + + /** + * Backend interface for interacting with the Python interpreter. + */ + private final Backend backend; + + /** + * The Python dictionary ({@code PyDict}) whose values are represented by this + * class. + */ + private final PyDict dict; + + /** + * A Python object representing the {@code dict.values()} view. + */ + private final PyObject values; + + /** + * Constructs a new {@code PyDictValues} instance for the given Python + * dictionary. + * + * @param dict is the Python dictionary ({@code PyDict}) whose values are to be + * represented. + */ + public PyDictValues(PyDict dict) + { + this.dict = dict; + this.backend = backend(); + this.values = backend.values(dict); + } + + /** + * Unsupported operation. Python's {@code dict.values()} does not support + * adding new values directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean add(PyObject e) + { + throw new UnsupportedOperationException("Values does not support item assignment"); + } + + /** + * Unsupported operation. Python's {@code dict.values()} does not support + * adding new values directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean addAll(Collection c) + { + throw new UnsupportedOperationException("Values does not support item assignment"); + } + + /** + * Clears all keys and values from the Python dictionary. + */ + @Override + public void clear() + { + dict.clear(); + } + + /** + * Checks whether the specified value exists in the dictionary. + * + * @param o is the value to check. + * @return {@code true} if the value exists, {@code false} otherwise. + */ + @Override + public boolean contains(Object o) + { + return backend.contains(this, o); + } + + /** + * Checks whether all values in the given collection exist in the dictionary. + * + * @param collection is the collection fromMap values to check. + * @return {@code true} if all values exist, {@code false} otherwise. + */ + @Override + public boolean containsAll(Collection collection) + { + // For large collections we should create a set first and test for subsets. + // Slow iterative method. + for (Iterator it = collection.iterator(); it.hasNext();) + { + Object o = it.next(); + if (!this.contains(o)) + { + return false; + } + } + return true; + } + + /** + * Checks whether the dictionary has no values. + * + * @return {@code true} if the dictionary is empty, {@code false} otherwise. + */ + @Override + public boolean isEmpty() + { + return backend.len(this) == 0; + } + + /** + * Returns an iterator over the dictionary's values. + * + * @return An iterator over the values in the dictionary. + */ + @Override + public Iterator iterator() + { + return PyBuiltIn.iter(values).iterator(); + } + + /** + * Unsupported operation. Python's {@code dict.values()} does not support + * removing values directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean remove(Object o) + { + throw new UnsupportedOperationException("PyDict values does not support item removal"); + } + + /** + * Unsupported operation. Python's {@code dict.values()} does not support + * removing values directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean removeAll(Collection c) + { + throw new UnsupportedOperationException("PyDict values does not support item removal"); + } + + /** + * Unsupported operation. Python's {@code dict.values()} does not support + * modifying values directly. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public boolean retainAll(Collection c) + { + throw new UnsupportedOperationException("PyDict values does not support item removal"); + } + + /** + * Returns the number fromMap values in the dictionary. + * + * @return The size fromMap the values collection. + */ + @Override + public int size() + { + return backend.len(values); + } + + /** + * Converts the dictionary's values to an array. + * + * @return An array containing the dictionary's values. + */ + @Override + public Object[] toArray() + { + return new ArrayList<>(PyBuiltIn.list(values)).toArray(); + } + + /** + * Converts the dictionary's values to an array fromMap the specified getType. + * + * @param a is the array into which the values are to be stored. + * @return An array containing the dictionary's values. + */ + @Override + public T2[] toArray(T2[] a) + { + return (T2[]) new ArrayList<>(PyBuiltIn.list(values)).toArray(a); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyEnumerate.java b/native/jpype_module/src/main/java/python/lang/PyEnumerate.java new file mode 100644 index 000000000..ad217808d --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyEnumerate.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the Python `enumerate` type. + * + * This interface provides functionality for working with Python's `enumerate` + * objects in a Java environment, mimicking the behavior of Python's built-in + * `enumerate` function. + *

+ * The Python `enumerate` getType is a generator that yields pairs of an index + * and the corresponding element from an iterable. This interface allows Java + * developers to interact with Python `enumerate` objects seamlessly. + */ +public interface PyEnumerate extends PyIter +{ + + /** + * Creates a new Python `enumerate` object from the specified Java + * {@link Iterable}. The resulting `PyEnumerate` object will yield pairs of an + * index (starting from 0) and the corresponding element from the iterable, + * similar to Python's `enumerate` function. + * + * @param iterable the {@link Iterable} whose elements will be enumerated. + * @return a new {@link PyEnumerate} instance representing the Python + * `enumerate` object. + */ + static PyEnumerate of(Iterable iterable) + { + return backend().newEnumerate(iterable); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyExc.java b/native/jpype_module/src/main/java/python/lang/PyExc.java new file mode 100644 index 000000000..50d148393 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyExc.java @@ -0,0 +1,80 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.lang.reflect.InvocationTargetException; +import java.util.logging.Level; +import java.util.logging.Logger; +import python.exception.PyException; +import static python.lang.PyExceptionFactory.LOOKUP; +import static python.lang.PyBuiltIn.*; + +/** + * Native version of a Python exception. + * + * This will be the type that is unwrapped to in Python. + */ +public interface PyExc extends PyObject +{ + + /** + * Wraps a Python exception with the appropriate Java wrapper getType. + * + * @param base + * @return + */ + static Exception of(PyExc base) + { + PyType type = type(base); + String name = type.getName(); + Class cls = LOOKUP.get(name); + if (cls == null) + { + PyTuple mro = type.mro(); + int sz = mro.size(); + for (int i = 0; i < sz; ++i) + { + mro.get(i); // FIXME we have the wrong wrapper getType here until we fix the probe method + } + cls = PyException.class; + } + try + { + var ctor = cls.getDeclaredConstructor(PyExc.class); + return (Exception) ctor.newInstance(base); + } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) + { + Logger.getLogger(PyExc.class.getName()).log(Level.SEVERE, null, ex); + return new RuntimeException("Unable to find Python error type " + name); + } + } + + /** + * Used to pass an exception through the Python stack. + * + * @param th + * @return + */ + static public PyExc unwrap(Throwable th) + { + if (th instanceof PyException) + return ((PyException) th).get(); + return null; + } + + String getMessage(); + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyExceptionFactory.java b/native/jpype_module/src/main/java/python/lang/PyExceptionFactory.java new file mode 100644 index 000000000..58058a804 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyExceptionFactory.java @@ -0,0 +1,106 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.HashMap; +import python.exception.PyADirectionError; +import python.exception.PyArithmeticError; +import python.exception.PyAssertionError; +import python.exception.PyAttributeError; +import python.exception.PyBlockingIOError; +import python.exception.PyBufferError; +import python.exception.PyChildProcessError; +import python.exception.PyConnectionError; +import python.exception.PyEOFError; +import python.exception.PyException; +import python.exception.PyFileExistsError; +import python.exception.PyFileNotFoundError; +import python.exception.PyFloatingPointError; +import python.exception.PyImportError; +import python.exception.PyIndentationError; +import python.exception.PyIndexError; +import python.exception.PyInterruptedError; +import python.exception.PyKeyError; +import python.exception.PyLookupError; +import python.exception.PyModuleNotFoundError; +import python.exception.PyNameError; +import python.exception.PyNotADirectoryError; +import python.exception.PyNotImplementedError; +import python.exception.PyOSError; +import python.exception.PyOverflowError; +import python.exception.PyPermissionError; +import python.exception.PyProcessLookupError; +import python.exception.PyRecursionError; +import python.exception.PyReferenceError; +import python.exception.PyRuntimeError; +import python.exception.PySyntaxError; +import python.exception.PySystemError; +import python.exception.PyTimeoutError; +import python.exception.PyTypeError; +import python.exception.PyValueError; +import python.exception.PyWarning; +import python.exception.PyZeroDivisionError; + +/** + * + */ +class PyExceptionFactory +{ + + final static HashMap LOOKUP = new HashMap<>(); + + static + { + LOOKUP.put("Exception", PyException.class); + LOOKUP.put("ADirectionError", PyADirectionError.class); + LOOKUP.put("ArithmeticError", PyArithmeticError.class); + LOOKUP.put("AssertionError", PyAssertionError.class); + LOOKUP.put("AttributeError", PyAttributeError.class); + LOOKUP.put("BlockingIOError", PyBlockingIOError.class); + LOOKUP.put("BufferError", PyBufferError.class); + LOOKUP.put("ChildProcessError", PyChildProcessError.class); + LOOKUP.put("ConnectionError", PyConnectionError.class); + LOOKUP.put("EOFError", PyEOFError.class); + LOOKUP.put("FileExistsError", PyFileExistsError.class); + LOOKUP.put("FileNotFoundError", PyFileNotFoundError.class); + LOOKUP.put("FloatingPointError", PyFloatingPointError.class); + LOOKUP.put("ImportError", PyImportError.class); + LOOKUP.put("IndentationError", PyIndentationError.class); + LOOKUP.put("IndexError", PyIndexError.class); + LOOKUP.put("InterruptedError", PyInterruptedError.class); + LOOKUP.put("KeyError", PyKeyError.class); + LOOKUP.put("LookupError", PyLookupError.class); + LOOKUP.put("ModuleNotFoundError", PyModuleNotFoundError.class); + LOOKUP.put("NameError", PyNameError.class); + LOOKUP.put("NotADirectoryError", PyNotADirectoryError.class); + LOOKUP.put("NotImplementedError", PyNotImplementedError.class); + LOOKUP.put("OSError", PyOSError.class); + LOOKUP.put("OverflowError", PyOverflowError.class); + LOOKUP.put("PermissionError", PyPermissionError.class); + LOOKUP.put("ProcessLookupError", PyProcessLookupError.class); + LOOKUP.put("RecursionError", PyRecursionError.class); + LOOKUP.put("ReferenceError", PyReferenceError.class); + LOOKUP.put("RuntimeError", PyRuntimeError.class); + LOOKUP.put("SyntaxError", PySyntaxError.class); + LOOKUP.put("SystemError", PySystemError.class); + LOOKUP.put("TimeoutError", PyTimeoutError.class); + LOOKUP.put("TypeError", PyTypeError.class); + LOOKUP.put("ValueError", PyValueError.class); + LOOKUP.put("Warning", PyWarning.class); + LOOKUP.put("ZeroDivisionError", PyZeroDivisionError.class); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyFloat.java b/native/jpype_module/src/main/java/python/lang/PyFloat.java new file mode 100644 index 000000000..4da270214 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyFloat.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the Python `float` type. + * + * This interface provides functionality for creating and interacting with + * Python `float` objects in a Java environment, mimicking Python's built-in + * `float` type. + * + *

+ * The Python `float` type represents floating-point numbers and supports + * operations defined for Python numeric types. This interface allows Java + * developers to work seamlessly with Python `float` objects, bridging the gap + * between Java's {@code double} type and Python's `float`. + */ +public interface PyFloat extends PyObject, PyNumber +{ + + /** + * Creates a new Python `float` object from the specified Java {@code double} + * value. The resulting {@link PyFloat} object represents the Python + * equivalent of the given floating-point number. + * + * @param value the {@code double} value to be converted into a Python + * `float`. + * @return a new {@link PyFloat} instance representing the Python `float` + * object. + */ + static PyFloat of(double value) + { + return backend().newFloat(value); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyFrozenSet.java b/native/jpype_module/src/main/java/python/lang/PyFrozenSet.java new file mode 100644 index 000000000..14382eebe --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyFrozenSet.java @@ -0,0 +1,186 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the Python `frozenset` type. + * + * This interface provides functionality for creating and interacting with + * Python `frozenset` objects in a Java environment, mimicking Python's + * immutable set type.

+ * + * The Python `frozenset` getType represents an immutable, hashable collection + * of unique elements, similar to Java's {@link Set} interface. This interface + * extends {@link PyObject} and {@link Set}, offering methods to perform set + * operations such as union, intersection, difference, and more. + * + *

+ * Note: While this interface mostly adheres to Java's contract for sets, some + * operations (e.g., {@code add}, {@code update}) may behave differently due to + * the immutable nature of Python's `frozenset`. + * + *

+ * Important Note:

+ *

+ * Python collections are asymmetric in their handling of Java objects. A Java + * object added to a Python collection will appear as a {@code PyJavaObject}. + * Developers should exercise caution to avoid reference loops when placing Java + * objects into Python collections, as this may lead to unintended + * behaviors.

+ */ +public interface PyFrozenSet extends PyObject, Set +{ + /** + * Creates a new Python `frozenset` object from the specified + * {@link Iterable}. + * + * @param c the {@link Iterable} whose elements will be included in the + * `frozenset`. + * @return a new {@link PyFrozenSet} instance representing the Python + * `frozenset` object. + */ + static PyFrozenSet of(Iterable c) + { + return backend().newFrozenSet(c); + } + + /** + * Creates a shallow copy of this `frozenset`. + * + * @return a new {@link PyFrozenSet} instance containing the same elements as + * this set. + */ + PyFrozenSet copy(); + + /** + * Computes the difference between this `frozenset` and one or more other + * sets. + * + * @param set one or more {@link Collection} instances to subtract from this + * set. + * @return a new {@link PyFrozenSet} containing elements in this set but not + * in the specified sets. + */ + PyFrozenSet difference(Collection... set); + + /** + * Computes the intersection of this `frozenset` with one or more other sets. + * + * @param set one or more {@link Collection} instances to intersect with this + * set. + * @return a new {@link PyFrozenSet} containing elements common to all sets. + */ + PyFrozenSet intersect(Collection... set); + + /** + * Checks whether this `frozenset` and the specified set are disjoint. Two + * sets are disjoint if they have no elements in common. + * + * @param set the {@link Collection} to compare with. + * @return {@code true} if the sets are disjoint, {@code false} otherwise. + */ + boolean isDisjoint(Collection set); + + /** + * Checks whether this `frozenset` is a subset of the specified set. + * + * @param set the {@link Collection} to compare with. + * @return {@code true} if this set is a subset of the specified set, + * {@code false} otherwise. + */ + boolean isSubset(Collection set); + + /** + * Checks whether this `frozenset` is a superset of the specified set. + * + * @param set the {@link Collection} to compare with. + * @return {@code true} if this set is a superset of the specified set, + * {@code false} otherwise. + */ + boolean isSuperset(Collection set); + + /** + * Removes and returns an arbitrary element from this `frozenset`. + * + * @return the removed {@link PyObject}. + */ + default PyObject pop() + { + throw new UnsupportedOperationException("Frozenset does not support modification"); + } + + /** + * Computes the symmetric difference between this `frozenset` and one or more + * other sets. The symmetric difference contains elements that are in either + * set, but not in both. + * + * @param set one or more {@link Set} instances to compare with. + * @return a new {@link PyFrozenSet} containing the symmetric difference. + */ + PyFrozenSet symmetricDifference(Collection... set); + + /** + * Computes the union of this `frozenset` with one or more other sets. The + * union contains all elements from all sets. + * + * @param set one or more {@link Set} instances to combine with this set. + * @return a new {@link PyFrozenSet} containing the union of all sets. + */ + PyFrozenSet union(Collection... set); + + /** + * Returns an iterator over the elements in this `frozenset`. + * + * @return an {@link Iterator} for the elements in this set. + */ + @Override + default Iterator iterator() + { + return new PyIterator<>(backend().iterSet(this)); + } + + /** + * Converts this `frozenset` into an array. + * + * @return an array containing all elements in this set. + */ + @Override + default Object[] toArray() + { + return new ArrayList<>(this).toArray(); + } + + /** + * Converts this `frozenset` into an array of the specified getType. + * + * @param reference the array into which the elements of this set will be stored. + * @param the getType of the array elements. + * @return an array containing all elements in this set. + */ + @Override + default T[] toArray(T[] reference) + { + return new ArrayList<>(this).toArray(reference); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyGenerator.java b/native/jpype_module/src/main/java/python/lang/PyGenerator.java new file mode 100644 index 000000000..6bbcf25a6 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyGenerator.java @@ -0,0 +1,73 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Iterator; +import python.lang.PyObject; + +/** + * Represents a Python generator in the Java environment. + * + * This interface provides a Java front end for the abstract concept of a Python + * generator, which adheres to the Python {@code collections.abc.Generator} contract. + * Generators are used to produce values lazily and support iteration, making + * them suitable for handling sequences of data efficiently. + * + *

+ * Key features:

+ *
    + *
  • Provides an API for interacting with Python generators.
  • + *
  • Supports Python's iteration protocol via {@code PyIter}.
  • + *
  • Integrates seamlessly with Java's {@link Iterator} interface.
  • + *
+ * + *

+ * This interface assumes that implementing classes will provide the necessary + * functionality to bridge Python generators with Java's iteration + * mechanisms.

+ */ +public interface PyGenerator extends PyIter +{ + + /** + * Returns a Python iterator for this generator. + * + *

+ * This method provides access to the underlying Python iterator associated + * with the generator, allowing iteration over its elements using Python + * semantics.

+ * + * @return a {@link PyIter} representing the Python iterator for this + * generator + */ + PyIter iter(); + + /** + * Returns a Java {@link Iterator} for this generator. + * + *

+ * This method bridges the Python iteration protocol with Java's + * {@link Iterator} interface, enabling iteration over the generator's + * elements using Java semantics.

+ * + * @return a {@link Iterator} for iterating over the elements of the generator + */ + @Override + default Iterator iterator() + { + return iter().iterator(); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyIndex.java b/native/jpype_module/src/main/java/python/lang/PyIndex.java new file mode 100644 index 000000000..cdf59998e --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyIndex.java @@ -0,0 +1,60 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import python.lang.PyObject; + +/** + * Represents objects that can be used to index arrays or sequences in + * Python-like operations. + * + *

+ * This interface defines the contract for objects that can serve as indices in + * array or sequence operations, similar to Python's indexing behavior. It + * provides a foundation for implementing Python-style indexing in Java, + * including support for extended indexing constructs such as slices. + * + *

+ * Examples of objects that can implement this interface include: + *

    + *
  • Integer-based indices for accessing single elements
  • + *
  • Slice objects for accessing ranges of elements
  • + *
  • Custom index types for advanced indexing behavior
  • + *
+ * + *

+ * Implementations of this interface should ensure compatibility with Python's + * indexing semantics, including support for negative indices, slicing, and + * other advanced features where applicable. + * + *

Usage Example

+ *
+ * PyIndex index = BuiltIn.slice(0, 10, 2); // Create a slice object
+ * PyObject result = array.get(index);   // Use the index to retrieve elements
+ * 
+ * + *

+ * Note: This interface does not define any methods directly, as it serves as a + * marker interface for objects that can be used as indices. Specific behaviors + * and operations should be implemented in concrete classes that extend this + * interface. + * + * @see PySlice + * @see PyProtocol + */ +public interface PyIndex extends PyObject +{ +} diff --git a/native/jpype_module/src/main/java/python/lang/PyInt.java b/native/jpype_module/src/main/java/python/lang/PyInt.java new file mode 100644 index 000000000..df289296c --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyInt.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the Python `int` type. + * + * This interface provides functionality for creating and interacting with + * Python `int` objects in a Java environment, mimicking Python's built-in + * integer type. + * + *

+ * The Python `int` type represents arbitrary-precision integers and supports + * operations defined for Python numeric types. This interface allows Java + * developers to work seamlessly with Python `int` objects, bridging the gap + * between Java's {@code long} type and Python's `int`. + */ +public interface PyInt extends PyObject, PyNumber +{ + + /** + * Creates a new Python `int` object from the specified Java {@code long} + * value. The resulting {@link PyInt} object represents the Python equivalent + * of the given integer. + * + * @param value the {@code long} value to be converted into a Python `int`. + * @return a new {@link PyInt} instance representing the Python `int` object. + */ + static PyInt of(long value) + { + return backend().newInt(value); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyIter.java b/native/jpype_module/src/main/java/python/lang/PyIter.java new file mode 100644 index 000000000..647c67932 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyIter.java @@ -0,0 +1,75 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Python concept of an iterator. + * + * This can be converted to a Java iterator by calling iterator(). + */ +public interface PyIter extends PyObject +{ + + PyIter filter(PyCallable callable); + + /** + * Converts the Python iterator into a Java iterator. + * + * @return + */ + default Iterator iterator() + { + // It is not clear if we should tee the iterator here or not. + // return new PyIterator(backend().tee(this)); + return new PyIterator<>(this); + } + + /** + * Get the next item. + * + * FIXME This throws StopIteration, we need to figure out how to convert and + * catch it. + * + * @return the next element in the series. + */ + @SuppressWarnings("unchecked") + default T next() + { + PyObject out = backend().next(this, Interpreter.stop); + if (out.equals(Interpreter.stop)) + throw new NoSuchElementException(); + return (T) out; + } + + /** + * Get the next item. + * + * @param defaults is the element to return if there is no additional + * elements. + * @return the next element in the series. + */ + @SuppressWarnings("unchecked") + default T next(PyObject defaults) + { + return (T) backend().next(this, defaults); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyIterable.java b/native/jpype_module/src/main/java/python/lang/PyIterable.java new file mode 100644 index 000000000..72e399e9c --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyIterable.java @@ -0,0 +1,111 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Iterator; +import python.lang.PyBuiltIn; +import python.lang.PyObject; + +/** + * Java front end for the abstract concept of a Python iterable. + * + *

+ * This interface provides methods that mimic Python's built-in operations for + * iterable objects, enabling seamless integration of Python-like functionality + * into Java code. + * + *

+ * Note: The `reversed` method was removed due to contract conflicts with + * {@link List}. + * + */ +public interface PyIterable extends PyObject, Iterable +{ + + /** + * Checks if all elements in the iterable evaluate to {@code true}. + * + * @return {@code true} if all elements are true, otherwise {@code false} + */ + boolean allMatch(); + + /** + * Checks if any element in the iterable evaluates to {@code true}. + * + * @return {@code true} if at least one element is true, otherwise + * {@code false} + */ + boolean anyMatch(); + + /** + * Returns a Python-style iterator for this iterable. + * + * @return a {@link PyIter} instance for this iterable + */ + default PyIter iter() + { + return PyBuiltIn.iter(this); + } + + /** + * Provides a Java {@link Iterator} implementation for this iterable. + * + * @return a Java iterator for this iterable + */ + @Override + default Iterator iterator() + { + return new PyIterator<>(this.iter()); + } + + /** + * Applies the given callable function to each element in the iterable and + * returns a new iterable containing the results. + * + * @param callable the function to apply to each element + * @return a new iterable containing the results of the function application + */ + PyObject mapElements(PyCallable callable); + + /** + * Returns the maximum element in the iterable. + * + * @return the maximum element + */ + PyObject findMax(); + + /** + * Returns the minimum element in the iterable. + * + * @return the minimum element + */ + PyObject findMin(); + + /** + * Returns a new iterable containing the elements of this iterable in sorted + * order. + * + * @return a new iterable with sorted elements + */ + PyObject getSorted(); + + /** + * Computes the sum of all elements in the iterable. + * + * @return the sum of the elements + */ + PyObject computeSum(); +} diff --git a/native/jpype_module/src/main/java/python/lang/PyIterator.java b/native/jpype_module/src/main/java/python/lang/PyIterator.java new file mode 100644 index 000000000..c0755e0e7 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyIterator.java @@ -0,0 +1,71 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import org.jpype.bridge.Interpreter; +import python.lang.PyBuiltIn; +import python.lang.PyObject; + +/** + * Conversion of a Python iterator to Java. + * + * This is a private class used under the hood. + * + * Python and Java iterators don't share similar design philosophies, so we will + * need to keep some state on the Java side to manage the conversion. + */ +public class PyIterator implements Iterator +{ + + private final PyIter iter; + private T yield; + private boolean done = false; + private boolean check = false; + + public PyIterator(PyIter iter) + { + this.iter = iter; + } + + @Override + @SuppressWarnings("unchecked") + public boolean hasNext() + { + if (done) + return false; + if (check) + return !done; + check = true; + if (yield == null) + yield = (T) PyBuiltIn.next(iter, Interpreter.stop); + done = (yield == Interpreter.stop); + return !done; + } + + @Override + public T next() throws NoSuchElementException + { + if (!check) + hasNext(); + if (done) + throw new NoSuchElementException(); + check = false; + return yield; + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyJavaObject.java b/native/jpype_module/src/main/java/python/lang/PyJavaObject.java new file mode 100644 index 000000000..f4c76ef75 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyJavaObject.java @@ -0,0 +1,64 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Objects; + +/** + * Java front end for a Python wrapped Java object. + */ +public class PyJavaObject implements PyObject +{ + + Object obj_; + + public PyJavaObject(Object obj) + { + this.obj_ = obj; + } + + @Override + public PyAttributes getAttributes() + { + // Java objects don't support Python attributes directly. + throw new UnsupportedOperationException(); + } + + public Object get() + { + return obj_; + } + + @Override + public int hashCode() + { + return obj_.hashCode(); + } + + @Override + public boolean equals(Object obj) + { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + final PyJavaObject other = (PyJavaObject) obj; + return Objects.equals(this.obj_, other.obj_); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyList.java b/native/jpype_module/src/main/java/python/lang/PyList.java new file mode 100644 index 000000000..b6aa7b01a --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyList.java @@ -0,0 +1,423 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Java front-end interface for the Python `list` type. + * + *

+ * This interface represents a concrete Python `list` object and provides + * methods that closely align with both the Python `list` API and the Java + * {@link List} contract. It facilitates seamless integration between Python's + * dynamic, mutable list behavior and Java's getType-safe collections + * framework.

+ * + *

+ * Key Features:

+ *
    + *
  • Extends {@link PyObject}, {@link List}, and {@link PyIterable}, enabling + * it to function as both a Python object and a Java collection.
  • + *
  • Implements standard Java {@link List} methods for compatibility with + * Java's collections.
  • + *
  • Provides additional Python-specific methods such as {@code extend}, + * {@code insert}, and {@code addAny} to support Python's list + * functionality.
  • + *
+ * + *

+ * Python List Characteristics:

+ *
    + *
  • Dynamic: Python lists can grow or shrink in size as needed.
  • + *
  • Mutable: Elements in a Python list can be modified after creation.
  • + *
  • Heterogeneous: Python lists can contain elements of varying types.
  • + *
+ * + *

+ * This interface bridges the gap between Python's flexible data structures and + * Java's statically typed collections, enabling developers to work with Python + * lists directly in a Java environment.

+ * + *

+ * Important Note:

+ *

+ * Python collections are asymmetric in their handling of Java objects. A Java + * object added to a Python collection will appear as a {@code PyJavaObject}. + * Developers should exercise caution to avoid reference loops when placing Java + * objects into Python collections, as this may lead to unintended + * behaviors.

+ */ +public interface PyList extends PySequence +{ + + /** + * Creates a new Python `list` object from the given {@link Iterable}. + * + * The elements of the provided iterable will be added to the Python list. + * + * @param c the {@link Iterable} whose elements will populate the new Python + * list. + * @return a new {@link PyList} instance containing the elements of the + * iterable. + */ + public static PyList fromItems(Iterable c) + { + return backend().newListFromIterable(c); + } + + /** + * Creates a new Python `list` object from the given a list of objects. + * + * @param items is an array whose elements will populate the new Python list. + * @return a new {@link PyList} instance containing the elements. + */ + public static PyList of(Object... items) + { + return backend().newListFromArray(items); + } + + /** + * Adds a Python object to the end of the list. + * + * @param e the {@link PyObject} to add. + * @return {@code true} if the list was modified as a result of this + * operation. + */ + @Override + boolean add(PyObject e); + + /** + * Inserts a Python object at the specified position in the list. + * + * @param index the position at which the object should be inserted. + * @param element the {@link PyObject} to insert. + */ + @Override + void add(int index, PyObject element); + + /** + * Adds all elements from the specified collection to the end of the list. + * + * @param c the collection of {@link PyObject} elements to add. + * @return {@code true} if the list was modified as a result of this + * operation. + */ + @Override + default boolean addAll(Collection c) + { + this.extend(c); + return !c.isEmpty(); + } + + /** + * Inserts all elements from the specified collection at the specified + * position in the list. + * + * @param index the position at which the elements should be inserted. + * @param c the collection of {@link PyObject} elements to insert. + * @return {@code true} if the list was modified as a result of this + * operation. + */ + @Override + default boolean addAll(int index, Collection c) + { + this.insert(index, c); + return !c.isEmpty(); + } + + /** + * Adds an arbitrary object to the list. The object will be converted to a + * Python-compatible type. + * + * @param obj the object to add. + */ + void addAny(Object obj); + + /** + * Removes all elements from the list. + */ + @Override + void clear(); + + /** + * Checks if the list contains the specified object. + * + * @param o the object to check for. + * @return {@code true} if the list contains the object, {@code false} + * otherwise. + */ + @Override + boolean contains(Object o); + + /** + * Checks if the list contains all elements from the specified collection. + * + * @param c the collection of elements to check for. + * @return {@code true} if the list contains all elements, {@code false} + * otherwise. + */ + @Override + default boolean containsAll(Collection c) + { + PySet s1 = PySet.of(this); + PySet s2 = PySet.of(c); + return s2.isSubset(s1); + } + + /** + * Extends the list by appending all elements from the specified collection. + * + * @param c the collection of {@link PyObject} elements to add. + */ + void extend(Collection c); + + /** + * Retrieves the element at the specified position in the list. + * + * @param index the position of the element to retrieve. + * @return the {@link PyObject} at the specified position. + */ + @Override + PyObject get(int index); + + /** + * Finds the index of the first occurrence of the specified object in the + * list. + * + * @param o the object to search for. + * @return the index of the object, or {@code -1} if not found. + */ + @Override + int indexOf(Object o); + + /** + * Inserts a collection of elements at the specified position in the list. + * + * @param index the position at which the elements should be inserted. + * @param c the collection of {@link PyObject} elements to insert. + */ + void insert(int index, Collection c); + + /** + * Checks if the list is empty. + * + * @return {@code true} if the list is empty, {@code false} otherwise. + */ + @Override + default boolean isEmpty() + { + return size() == 0; + } + + /** + * Returns an iterator over the elements in the list. + * + * @return an {@link Iterator} for the list elements. + */ + @Override + default Iterator iterator() + { + return new PyIterator<>(this.iter()); + } + + /** + * Returns a list iterator over the elements in the tuple. + * + * @return a list iterator starting at the beginning of the tuple + */ + @Override + default ListIterator listIterator() + { + return new PyListIterator(this, 0); + } + + /** + * Returns a list iterator over the elements in the tuple, starting at the + * specified index. + * + * @param index the starting index for the iterator + * @return a list iterator starting at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ + @Override + default ListIterator listIterator(int index) + { + if (index < 0 || index > size()) + { + throw new IndexOutOfBoundsException(); + } + return new PyListIterator(this, index); + } + + /** + * Removes the first occurrence of the specified object from the list. + * + * @param o the object to remove. + * @return {@code true} if the object was removed, {@code false} otherwise. + */ + @Override + default boolean remove(Object o) + { + return backend().delattrReturn(this, this.indexOf(o)) != null; + } + + /** + * Removes the element at the specified position in the list. + * + * @param index the position of the element to remove. + * @return the {@link PyObject} that was removed. + */ + @Override + default PyObject remove(int index) + { + PyObject out = this.get(index); + backend().delitemByIndex(this, index); + return out; + } + + /** + * Removes all elements in the list that are also contained in the specified + * collection. + *

+ * This method behaves similarly to {@link List#removeAll(Collection)}, but is + * implemented in the context of a Python list. + *

+ * + * @param c the collection containing elements to be removed from the list. + * @return {@code true} if the list was modified as a result of this + * operation, {@code false} otherwise. + */ + @Override + boolean removeAll(Collection c); + + /** + * Retains only the elements in the list that are contained in the specified + * collection. + *

+ * This method behaves similarly to {@link List#retainAll(Collection)}, but is + * implemented in the context of a Python list. + *

+ * + * @param c the collection containing elements to be retained in the list. + * @return {@code true} if the list was modified as a result of this + * operation, {@code false} otherwise. + */ + @Override + boolean retainAll(Collection c); + + /** + * Replaces the element at the specified position in the list with the + * specified Python object. + * + * @param index the position of the element to replace. + * @param element the {@link PyObject} to be stored at the specified position. + * @return the {@link PyObject} previously at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range. + */ + @Override + PyObject set(int index, PyObject element); + + /** + * Replaces the element at the specified position in the list with an + * arbitrary object. + *

+ * The object will be converted to a Python-compatible getType before being + * stored. + *

+ * + * @param index the position of the element to replace. + * @param obj the object to be stored at the specified position. + * @throws IndexOutOfBoundsException if the index is out of range. + */ + void setAny(int index, Object obj); + + /** + * Returns the number of elements in the list. + *

+ * This method behaves similarly to {@link List#size()}, but is implemented in + * the context of a Python list. + *

+ * + * @return the number of elements in the list. + */ + @Override + int size(); + + /** + * Returns a view of the portion of the list between the specified + * {@code fromIndex}, inclusive, and {@code toIndex}, exclusive. + *

+ * The returned sublist is backed by the original list, so changes to the + * sublist are reflected in the original list and vice versa. + *

+ * + * @param fromIndex the starting index of the sublist (inclusive). + * @param toIndex the ending index of the sublist (exclusive). + * @return a {@link PyList} representing the specified range within the list. + * @throws IndexOutOfBoundsException if {@code fromIndex} or {@code toIndex} + * is out of range. + * @throws IllegalArgumentException if {@code fromIndex > toIndex}. + */ + @Override + PyList subList(int fromIndex, int toIndex); + + /** + * Returns an array containing all elements in the list in proper sequence. + *

+ * This method behaves similarly to {@link List#toArray()}, but is implemented + * in the context of a Python list. + *

+ * + * @return an array containing all elements in the list. + */ + @Override + default Object[] toArray() + { + return new ArrayList<>(this).toArray(); + } + + /** + * Returns an array containing all elements in the list in proper sequence, + * using the runtime type of the specified array. + *

+ * If the list fits in the specified array, it is returned therein. Otherwise, + * a new array is allocated with the runtime getType of the specified array + * and the size of the list. + *

+ * + * @param a the array into which the elements of the list are to be stored, if + * it is large enough; otherwise, a new array of the same runtime getType is + * allocated for this purpose. + * @param the runtime getType of the array. + * @return an array containing all elements in the list. + * @throws ArrayStoreException if the runtime getType of the specified array + * is not a supertype of the runtime getType of every element in the list. + * @throws NullPointerException if the specified array is {@code null}. + */ + @Override + default T[] toArray(T[] a) + { + return (T[]) new ArrayList<>(this).toArray(a); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyListIterator.java b/native/jpype_module/src/main/java/python/lang/PyListIterator.java new file mode 100644 index 000000000..eec666712 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyListIterator.java @@ -0,0 +1,144 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ListIterator; +import java.util.NoSuchElementException; + +/** + * A custom implementation of {@link ListIterator} for iterating over a + * {@link PyList}. + * + * This iterator supports forward and backward traversal, as well as + * modification of the list. + */ +class PyListIterator implements ListIterator +{ + + private final PyList list; // The list being iterated + private int index; // Current position in the list + private boolean forward = false; // Flag indicating if the last operation was forward traversal + private boolean reverse = false; // Flag indicating if the last operation was reverse traversal + + /** + * Constructs a new {@code PyListIterator} for the specified list starting at + * the given index. + * + * @param list The {@link PyList} to iterate over. + * @param index The starting index for iteration. + */ + PyListIterator(PyList list, int index) + { + this.list = list; + this.index = index; + } + + @Override + public void add(PyObject e) + { + list.add(index, e); // Insert the element at the current index + index++; // Increment index to reflect the added element + forward = false; + reverse = false; + } + + @Override + public boolean hasNext() + { + return index < list.size(); + } + + @Override + public boolean hasPrevious() + { + return index > 0; + } + + @Override + public PyObject next() + { + if (!hasNext()) + { + throw new NoSuchElementException(); + } + PyObject out = list.get(index); + index++; + forward = true; + reverse = false; + return out; + } + + @Override + public int nextIndex() + { + return index; + } + + @Override + public PyObject previous() + { + if (!hasPrevious()) + { + throw new NoSuchElementException(); + } + index--; + PyObject out = list.get(index); + forward = false; + reverse = true; + return out; + } + + @Override + public int previousIndex() + { + return index - 1; + } + + @Override + public void remove() + { + if (!forward && !reverse) + { + throw new IllegalStateException("Cannot remove element without a valid traversal."); + } + if (forward) + { + list.remove(index - 1); // Remove the last element traversed in the forward direction + index--; // Adjust index to reflect the removal + } else if (reverse) + { + list.remove(index); // Remove the last element traversed in the reverse direction + } + forward = false; + reverse = false; + } + + @Override + public void set(PyObject e) + { + if (!forward && !reverse) + { + throw new IllegalStateException("Cannot set element without a valid traversal."); + } + if (forward) + { + list.set(index - 1, e); // Replace the last element traversed in the forward direction + } else if (reverse) + { + list.set(index, e); // Replace the last element traversed in the reverse direction + } + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyMapping.java b/native/jpype_module/src/main/java/python/lang/PyMapping.java new file mode 100644 index 000000000..240bef137 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyMapping.java @@ -0,0 +1,224 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import static python.lang.PyBuiltIn.backend; + +/** + * Represents a protocol for classes registered as Python + * `collections.abc.Mapping`. + * + * This interface is designed to model the behavior of Python's `Mapping` + * abstract base class, which defines the minimal interface for mapping objects, + * such as dictionaries, and other custom mapping types. It extends the Java + * `Map` interface to provide additional functionality specific to Python + * mappings. + * + *

+ * Key Features:

+ *
    + *
  • Implements the core methods of Java's `Map` interface.
  • + *
  • Provides default implementations for common mapping operations.
  • + *
  • Supports views for keys, values, and items, similar to Python's `Mapping` + * API.
  • + *
+ * + *

+ * This interface assumes that the underlying Python object implements the + * `collections.abc.Mapping` protocol, which guarantees the presence of methods + * like `keys()`, `values()`, and `items()`.

+ * + *

+ * Example Usage:

+ *
+ * PyMapping pyMapping = ...;  // Obtain an instance of PyMapping
+ * PyObject value = pyMapping.get("key");  // Retrieve a value by key
+ * Set<Object> keys = pyMapping.keySet();  // Get all keys
+ * Collection<PyObject> values = pyMapping.values();  // Get all values
+ * 
+ * + * @see java.util.Map + * @see collections.abc.Mapping + */ +public interface PyMapping extends PyCollection, Map +{ + + @Override + default boolean contains(Object obj) + { + return backend().contains(this, obj); + } + + @Override + default Iterator iterator() + { + return new PyIterator<>(this.iter()); + } + + /** + * Removes all key-value pairs from this mapping. Equivalent to Python's + * `clear()` method. + */ + @Override + default void clear() + { + backend().mappingClear(this); + } + + /** + * Checks if the specified key exists in this mapping. Equivalent to Python's + * `key in mapping` syntax. + * + * @param key is the key to check for existence. + * @return true if the key exists, false otherwise. + */ + @Override + boolean containsKey(Object key); + + /** + * Checks if the specified value exists in this mapping. + * + * Equivalent to Python's `value in mapping.values()` syntax. + * + * @param value is the value to check for existence. + * @return true if the value exists, false otherwise. + */ + @Override + boolean containsValue(Object value); + + /** + * Retrieves the value associated with the given key. Equivalent to Python's + * `mapping[key]`. + * + * @param key is the key to look up. + * @return the value associated with the key, or null if the key is not + * present. + */ + @Override + @SuppressWarnings("unchecked") + default V get(Object key) + { + return (V) backend().getitemMappingObject(this, key); + } + + /** + * Returns a set view of the key-value pairs contained in this mapping. + * Equivalent to Python's `mapping.items()` method, but returns a Java `Set`. + * + * @return a set view of the key-value pairs. + */ + @Override + default Set> entrySet() + { + return new PyMappingEntrySet<>(this, backend().items(this)); + } + + /** + * Checks if this mapping is empty (contains no key-value pairs). Equivalent + * to Python's `len(mapping) == 0`. + * + * @return true if the mapping is empty, false otherwise. + */ + @Override + default boolean isEmpty() + { + return size() == 0; + } + + /** + * Returns a set view of the keys contained in this mapping. Equivalent to + * Python's `mapping.keys()` method, but returns a Java `Set`. + * + * @return a set view of the keys. + */ + @Override + @SuppressWarnings("unchecked") + default Set keySet() + { + return new PyMappingKeySet(this); + } + + /** + * Associates the specified value with the specified key in this mapping. If + * the mapping previously contained a value for the key, the old value is + * replaced. Equivalent to Python's `mapping[key] = value`. + * + * @param key is the key with which the specified value is to be associated. + * @param value is the value to associate with the key. + * @return the previous value associated with the key, or null if there was no + * mapping for the key. + */ + @Override + @SuppressWarnings("unchecked") + default V put(K key, V value) + { + return (V) backend().setitemFromObject(this, key, value); + } + + default PyObject putAny(Object key, Object value) + { + return backend().setitemFromObject(this, key, value); + } + + /** + * Copies all key-value pairs from the specified map to this mapping. + * Equivalent to Python's `mapping.update(other_mapping)`. + * + * @param m is the map containing key-value pairs to copy. + */ + @Override + void putAll(Map m); + + /** + * Removes the key-value pair associated with the specified key from this + * mapping. Equivalent to Python's `del mapping[key]`. + * + * @param key is the key whose mapping is to be removed. + * @return the value that was associated with the key, or null if the key was + * not present. + */ + @Override + V remove(Object key); + + /** + * Returns the number of key-value pairs in this mapping. Equivalent to + * Python's `len(mapping)`. + * + * @return the number of key-value pairs in the mapping. + */ + @Override + default int size() + { + return PyBuiltIn.len(this); + } + + /** + * Returns a collection view of the values contained in this mapping. + * Equivalent to Python's `mapping.values()` method, but returns a Java + * `Collection`. + * + * @return a collection view of the values. + */ + @Override + default Collection values() + { + return new PyMappingValues(this); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyMappingEntrySet.java b/native/jpype_module/src/main/java/python/lang/PyMappingEntrySet.java new file mode 100644 index 000000000..f4f55eb91 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyMappingEntrySet.java @@ -0,0 +1,277 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import python.lang.PyBuiltIn; +import python.lang.PyObject; + +/** + * A representation of the entry set of a Python mapping, implementing the + * {@link Set} interface. + * + *

+ * This class provides a view of the key-value pairs in a Python mapping + * (`PyMapping`) as a Java {@link Set} of {@link Map.Entry}. It allows + * operations such as adding entries, clearing the mapping, and iterating over + * entries. + * + *

+ * Note: This class is a wrapper around a {@link PyMapping} instance and + * delegates most operations to the underlying mapping. It is designed to + * integrate Python-style mappings into Java collections seamlessly. + * + *

Unsupported Operations

+ * Some operations, such as {@code retainAll}, are not supported and will throw + * {@link UnsupportedOperationException}. Additionally, methods such as + * {@code contains} and {@code remove} are currently not implemented and always + * return {@code false}. + * + *

Thread Safety

+ * This class does not guarantee thread safety. If the underlying + * {@link PyMapping} is accessed concurrently, external synchronization is + * required. + * + *

Usage Example

+ *
+ * PyMapping pyMapping = ...; // Initialize a Python mapping
+ * PyObject items = BuiltIn.items(pyMapping); // Get the items view
+ * PyMappingEntrySet entrySet = new PyMappingEntrySet(pyMapping, items);
+ *
+ * // Add a new entry
+ * entrySet.add(new AbstractMap.SimpleEntry<>("key", new PyObject("value")));
+ *
+ * // Iterate over entries
+ * for (Map.Entry entry : entrySet) {
+ *     System.out.println(entry.getKey() + " -> " + entry.getValue());
+ * }
+ *
+ * // Clear all entries
+ * entrySet.clear();
+ * 
+ * + * @see PyMapping + * @see Map.Entry + * @see Set + */ +public class PyMappingEntrySet implements Set> +{ + + /** + * The Python mapping whose entries are represented by this set. + */ + private final PyMapping map; + + /** + * The Python object representing the items view of the mapping. + */ + private final PyObject items; + + /** + * Constructs a {@code PyMappingEntrySet} for the given Python mapping and its + * items view. + * + * @param map the Python mapping whose entries will be represented as a set + * @param items the Python object representing the items view of the mapping + */ + PyMappingEntrySet(PyMapping map, PyObject items) + { + this.map = map; + this.items = items; + } + + /** + * Adds the specified entry to the mapping. + * + * @param e the entry to add + * @return {@code true} if the entry was added successfully, meaning the key + * did not previously exist in the mapping; {@code false} if the key already + * existed and its value was updated. + */ + @Override + public boolean add(Map.Entry e) + { + boolean previous = this.map.containsKey(e.getKey()); + this.map.putAny(e.getKey(), e.getValue()); + return !previous; + } + + /** + * Adds all entries in the specified collection to the mapping. + * + * @param c the collection of entries to add + * @return {@code true} if all entries were added successfully + */ + @Override + public boolean addAll(Collection> c) + { + for (var v : c) + { + this.add(v); + } + return true; + } + + /** + * Clears all entries from the mapping. + */ + @Override + public void clear() + { + this.map.clear(); + } + + /** + * Checks if the specified entry exists in the mapping. + * + *

+ * Note: This method is not implemented and always returns {@code false}. + * + * @param o the entry to check + * @return {@code false} always + */ + @Override + public boolean contains(Object o) + { + return false; + } + + /** + * Checks if the mapping contains all entries in the specified collection. + * + *

+ * Note: This method is not implemented and always returns {@code false}. + * + * @param c the collection of entries to check + * @return {@code false} always + */ + @Override + public boolean containsAll(Collection c) + { + return false; + } + + /** + * Checks if the mapping contains no entries. + * + * @return {@code true} if the mapping is empty, otherwise {@code false} + */ + @Override + public boolean isEmpty() + { + return map.isEmpty(); + } + + /** + * Returns an iterator over the entries in the mapping. + * + * @return an iterator for the entry set + */ + @Override + public Iterator> iterator() + { + return new PyMappingEntrySetIterator<>(map, PyBuiltIn.iter(this.items)); + } + + /** + * Removes the specified entry from the mapping. + * + *

+ * Note: This method is not implemented and always returns {@code false}. + * + * @param o the entry to remove + * @return {@code false} always + */ + @Override + public boolean remove(Object o) + { + return false; + } + + /** + * Removes all entries in the specified collection from the mapping. + * + *

+ * Note: This method is not implemented and always returns {@code false}. + * + * @param c the collection of entries to remove + * @return {@code false} always + */ + @Override + public boolean removeAll(Collection c) + { + return false; + } + + /** + * Retains only the entries in the mapping that are contained in the specified + * collection. + * + *

+ * This operation is unsupported and will throw + * {@link UnsupportedOperationException}. + * + * @param c the collection of entries to retain + * @throws UnsupportedOperationException always thrown + */ + @Override + public boolean retainAll(Collection c) + { + throw new UnsupportedOperationException(); + } + + /** + * Returns the number of entries in the mapping. + * + * @return the size of the entry set + */ + @Override + public int size() + { + return this.map.size(); + } + + /** + * Returns an array containing all entries in the mapping. + * + * @return an array of entries + */ + @Override + public Object[] toArray() + { + return new ArrayList<>(this).toArray(); + } + + /** + * Returns an array containing all entries in the mapping, using the provided + * array type. + * + * @param a the array into which the entries will be stored, if it is large + * enough; otherwise, a new array of the same type will be allocated + * @return an array containing the entries + * @throws ArrayStoreException if the runtime type of the specified array is + * not a supertype of the runtime type of every entry + */ + @Override + public T[] toArray(T[] a) + { + return (T[]) new ArrayList<>(this).toArray(a); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyMappingEntrySetIterator.java b/native/jpype_module/src/main/java/python/lang/PyMappingEntrySetIterator.java new file mode 100644 index 000000000..f74806d86 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyMappingEntrySetIterator.java @@ -0,0 +1,175 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.internal.Utility; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.function.BiFunction; +import org.jpype.bridge.Interpreter; +import python.lang.PyBuiltIn; +import python.lang.PyObject; +import python.lang.PyTuple; + +/** + * Iterator implementation for iterating over the entries of a Python + * mapping.The {@code PyMappingEntrySetIterator} class provides an iterator for + * Python mappings, allowing Java code to traverse the key-value pairs of a + * Python map. + * + * This iterator is designed to work seamlessly with Python's iteration + * protocols and integrates with the JPype library's Python-to-Java bridge. + * + *

+ * Key features: + *

    + *
  • Supports iteration over Python mapping entries as {@link Map.Entry} + * objects
  • + *
  • Provides a mechanism for updating mapping entries via a custom + * setter
  • + *
  • Handles Python iteration semantics, including stop iteration
  • + *
+ * + *

+ * Usage Example: + *

+ * PyMapping map = BuiltIn.dict();  // Python mapping object
+ * Iterator<Object, PyObject> iterator = map.iterator();
+ * while (iterator.hasNext()) {
+ *     Map.Entry<&Object, PyObject> entry = iterator.next();
+ *     System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
+ * }
+ * 
+ * + * @param The getType of keys in the mapping. + * @param The getType of values in the mapping. + */ +class PyMappingEntrySetIterator implements Iterator> +{ + + /** + * The Python mapping object being iterated over. + */ + private final PyMapping map; + + /** + * The Python iterator for the mapping entries. + */ + private final PyIter iter; + + /** + * The current value yielded by the Python iterator. + */ + private PyObject yield; + + /** + * Indicates whether the iteration is complete. + */ + private boolean done = false; + + /** + * Tracks whether the next element has been checked. + */ + private boolean check = false; + + /** + * A custom setter function used to update mapping entries. + */ + private final BiFunction setter; + + /** + * Constructs a new {@code PyMappingEntrySetIterator}. + * + * @param map The Python mapping object to iterate over. + * @param iter The Python iterator for the mapping entries. + */ + public PyMappingEntrySetIterator(PyMapping map, PyIter iter) + { + this.map = map; + this.iter = iter; + this.setter = this::set; + } + + /** + * Updates the value of a mapping entry and returns the previous value. + * + * This method is used as the setter function for + * {@link Utility.MapEntryWithSet}. + * + * @param key The key of the mapping entry to update. + * @param value The new value to associate with the key. + * @return The previous value associated with the key. + */ + private V set(K key, V value) + { + @SuppressWarnings("element-type-mismatch") + var out = map.get(key); + map.putAny(key, value); + return out; + } + + /** + * Checks whether there are more elements to iterate over. + * + * This method determines if the Python iterator has more entries. It handles + * Python's stop iteration semantics and ensures proper integration with + * Java's {@link Iterator} interface. + * + * @return {@code true} if there are more elements to iterate over, + * {@code false} otherwise. + */ + @Override + public boolean hasNext() + { + if (done) + return false; + if (check) + return !done; + check = true; + if (yield == null) + yield = PyBuiltIn.next(iter, Interpreter.stop); + done = (yield == Interpreter.stop); + return !done; + } + + /** + * Returns the next mapping entry in the iteration. + * + * This method retrieves the next key-value pair from the Python iterator and + * wraps it in a {@link Map.Entry} object. If the iteration is complete, it + * throws a {@link NoSuchElementException}. + * + * @return The next mapping entry as a {@link Map.Entry} object. + * @throws NoSuchElementException If there are no more elements to iterate + * over. + */ + @Override + @SuppressWarnings("unchecked") + public Map.Entry next() throws NoSuchElementException + { + if (!check) + hasNext(); + if (done) + throw new NoSuchElementException(); + check = false; + // The yielded object has two members: key and value + PyTuple tuple = (PyTuple) yield; + PyObject key = tuple.get(0); + PyObject value = tuple.get(1); + return new Utility.MapEntryWithSet(key, value, setter); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyMappingKeySet.java b/native/jpype_module/src/main/java/python/lang/PyMappingKeySet.java new file mode 100644 index 000000000..7fd03ef18 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyMappingKeySet.java @@ -0,0 +1,257 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.Set; +import org.jpype.bridge.Backend; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; +import python.lang.PyObject; + +/** + * A representation of the key set of a Python mapping, implementing the + * {@link Set} interface. + * + *

+ * This class provides a view of the keys in a Python mapping (`PyMapping`) as a + * Java {@link Set}. It allows operations such as checking for key existence, + * iterating over keys, and removing keys. However, modification operations such + * as adding or retaining elements are unsupported, as the underlying Python + * mapping does not support such operations. + * + *

+ * Note: This class is a wrapper around a {@link PyMapping} instance and + * delegates most operations to the underlying mapping. It is designed to + * integrate Python-style mappings into Java collections seamlessly. + * + *

Unsupported Operations

+ * Methods that modify the set, such as {@code add}, {@code addAll}, and + * {@code retainAll}, will throw {@link UnsupportedOperationException}, as + * Python mappings do not support these operations. + * + *

Thread Safety

+ * This class does not guarantee thread safety. If the underlying + * {@link PyMapping} is accessed concurrently, external synchronization is + * required. + * + *

Usage Example

+ *
+ * PyMapping pyMapping = ...; // Initialize a Python mapping
+ * PyMappingKeySet keySet = new PyMappingKeySet(pyMapping);
+ *
+ * // Check if a key exists
+ * boolean exists = keySet.contains("someKey");
+ *
+ * // Iterate over keys
+ * for (Object key : keySet) {
+ *     System.out.println(key);
+ * }
+ *
+ * // Remove a key
+ * keySet.remove("someKey");
+ * 
+ * + * @version 1.0 + * @see PyMapping + * @see Set + */ +public class PyMappingKeySet implements Set +{ + + private final Backend backend; + /** + * The underlying Python mapping whose keys are represented by this set. + */ + final PyMapping map; + + /** + * Constructs a {@code PyMappingKeySet} for the given Python mapping. + * + * @param mapping the Python mapping whose keys will be represented as a set + */ + public PyMappingKeySet(PyMapping mapping) + { + this.map = mapping; + this.backend = backend(); + } + + /** + * Unsupported operation. Adding keys is not allowed in this key set. + * + * @param e the element to add + * @throws UnsupportedOperationException always thrown + */ + @Override + public boolean add(PyObject e) + { + throw new UnsupportedOperationException(); + } + + /** + * Unsupported operation. Adding all keys is not allowed in this key set. + * + * @param c the collection of elements to add + * @throws UnsupportedOperationException always thrown + */ + @Override + public boolean addAll(Collection c) + { + throw new UnsupportedOperationException(); + } + + /** + * Clears all keys from the mapping. + */ + @Override + public void clear() + { + map.clear(); + } + + /** + * Checks if the specified key exists in the mapping. + * + * @param o the key to check for existence + * @return {@code true} if the key exists, otherwise {@code false} + */ + @Override + @SuppressWarnings("element-type-mismatch") + public boolean contains(Object o) + { + return map.containsKey(o); + } + + /** + * Checks if the mapping contains all keys in the specified collection. + * + * @param c the collection of keys to check + * @return {@code true} if all keys exist, otherwise {@code false} + */ + @Override + public boolean containsAll(Collection c) + { + for (var x : c) + { + if (!map.containsKey(x)) + { + return false; + } + } + return true; + } + + /** + * Checks if the mapping contains no keys. + * + * @return {@code true} if the key set is empty, otherwise {@code false} + */ + @Override + public boolean isEmpty() + { + return map.isEmpty(); + } + + /** + * Returns an iterator over the keys in the mapping. + * + * @return an iterator for the key set + */ + @Override + @SuppressWarnings("unchecked") + public Iterator iterator() + { + PyIter iter = backend.iterMap(map); + return new PyIterator<>(iter); + } + + /** + * Removes the specified key from the mapping. + * + * @param o the key to remove + * @return {@code true} if the key was removed, otherwise {@code false} + */ + @Override + public boolean remove(Object o) + { + return map.remove(o) != null; + } + + /** + * Removes all keys in the specified collection from the mapping. + * + * @param collection the collection of keys to remove + * @return {@code true} if all keys were removed successfully, otherwise + * {@code false} + */ + @Override + public boolean removeAll(Collection collection) + { + return backend.mappingRemoveAllKeys(map, collection); + } + + /** + * Unsupported operation. Retaining keys is not allowed in this key set. + * + * @param collection the collection of elements to retain + * @throws UnsupportedOperationException always thrown + */ + @Override + public boolean retainAll(Collection collection) + { + return backend.mappingRetainAllKeys(map, collection); + } + + /** + * Returns the number of keys in the mapping. + * + * @return the size of the key set + */ + @Override + public int size() + { + return map.size(); + } + + /** + * Returns an array containing all keys in the mapping. + * + * @return an array of keys + */ + @Override + public Object[] toArray() + { + return new ArrayList<>(this).toArray(); + } + + /** + * Returns an array containing all keys in the mapping, using the provided + * array type. + * + * @param a the array into which the keys will be stored, if it is large + * enough; otherwise, a new array of the same type will be allocated + * @return an array containing the keys + * @throws ArrayStoreException if the runtime type of the specified array is + * not a supertype of the runtime type of every key + */ + @Override + public T[] toArray(T[] a) + { + return (T[]) new ArrayList<>(this).toArray(a); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyMappingValues.java b/native/jpype_module/src/main/java/python/lang/PyMappingValues.java new file mode 100644 index 000000000..2ae1f7b08 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyMappingValues.java @@ -0,0 +1,240 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.internal.Utility; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.function.Function; +import org.jpype.bridge.Backend; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; +import python.lang.PyObject; + +/** + * Provides a Java {@link Collection} interface for the values of a Python + * {@link PyMapping}. + * + * This class is specifically designed to bridge Python's + * {@code collections.abc.Mapping} with Java's {@link Collection} interface, + * allowing Python mappings to expose their values in a way that is compatible + * with Java's collection APIs. + * + *

+ * It assumes that the Python mapping implements {@code __iter__} and + * {@code __getitem__}, which are used to iterate over keys and retrieve + * corresponding values. Complex operations are delegated to a backend + * interface, {@link Backend}, which handles Python-specific operations.

+ * + *

+ * Note: This class does not support adding values directly, as Python mappings + * require key-value pairs for insertion. Unsupported operations will throw + * {@link UnsupportedOperationException}.

+ * + * @see PyMapping + * @see Backend + */ +class PyMappingValues implements Collection +{ + + private final Backend backend; + private final PyMapping map; + + /** + * Constructs a {@code PyMappingValues} instance for the given Python mapping. + * + * @param map the Python mapping whose values are to be exposed as a Java + * {@link Collection} + */ + public PyMappingValues(PyMapping map) + { + this.map = map; + this.backend = backend(); + } + + /** + * Unsupported operation. + * + *

+ * Python mappings require key-value pairs for insertion, so adding values + * directly is not supported.

+ * + * @param e the value to add (unsupported) + * @return never returns (throws {@link UnsupportedOperationException}) + * @throws UnsupportedOperationException always thrown + */ + @Override + public boolean add(V e) + { + throw new UnsupportedOperationException("Adding values directly is not supported."); + } + + /** + * Unsupported operation. + * + *

+ * Python mappings require key-value pairs for insertion, so adding + * collections of values directly is not supported.

+ * + * @param collection the collection of values to add (unsupported) + * @return never returns (throws {@link UnsupportedOperationException}) + * @throws UnsupportedOperationException always thrown + */ + @Override + public boolean addAll(Collection collection) + { + throw new UnsupportedOperationException("Adding collections of values directly is not supported."); + } + + /** + * Removes all key-value pairs from the underlying Python mapping. + */ + @Override + public void clear() + { + this.map.clear(); + } + + /** + * Checks if the specified value exists in the underlying Python mapping. + * + * @param value the value to check for + * @return {@code true} if the value exists, {@code false} otherwise + */ + @Override + public boolean contains(Object value) + { + return backend.mappingContainsValue(map, value); + } + + /** + * Checks if all values in the given collection exist in the underlying Python + * mapping. + * + * @param collection the collection of values to check for + * @return {@code true} if all values exist, {@code false} otherwise + */ + @Override + public boolean containsAll(Collection collection) + { + return backend.mappingContainsAllValues(map, collection); + } + + /** + * Checks if the underlying Python mapping is empty. + * + * @return {@code true} if the mapping is empty, {@code false} otherwise + */ + @Override + public boolean isEmpty() + { + return this.map.isEmpty(); + } + + /** + * Returns an {@link Iterator} over the values of the underlying Python + * mapping. + * + *

+ * The iterator applies a transformation function to map keys to their + * corresponding values using the {@code __getitem__} functionality of the + * Python mapping.

+ * + * @return an iterator over the values of the mapping + */ + @Override + public Iterator iterator() + { + Function function = p -> this.map.get(p); + Iterator iter = this.map.keySet().iterator(); + return Utility.mapIterator(this.map.keySet().iterator(), function); + } + + /** + * Removes the specified value from the underlying Python mapping. + * + * @param value the value to remove + * @return {@code true} if the value was removed, {@code false} otherwise + */ + @Override + public boolean remove(Object value) + { + return backend.mappingRemoveValue(map, value); + } + + /** + * Removes all values in the given collection from the underlying Python + * mapping. + * + * @param collection the collection of values to remove + * @return {@code true} if any values were removed, {@code false} otherwise + */ + @Override + public boolean removeAll(Collection collection) + { + return backend.mappingRemoveAllValue(map, collection); + } + + /** + * Retains only the values in the given collection in the underlying Python + * mapping. + * + * @param collection the collection of values to retain + * @return {@code true} if the mapping was modified, {@code false} otherwise + */ + @Override + public boolean retainAll(Collection collection) + { + return backend.mappingRetainAllValue(map, collection); + } + + /** + * Returns the number of key-value pairs in the underlying Python mapping. + * + * @return the size of the mapping + */ + @Override + public int size() + { + return this.map.size(); + } + + /** + * Returns an array containing all the values in the mapping. + * + * @return an array of values + */ + @Override + public Object[] toArray() + { + return new ArrayList<>(this).toArray(); + } + + /** + * Returns an array containing all the values in the mapping, using the + * provided array's type. + * + * @param a the array into which the values will be stored + * @param the type of the array elements + * @return an array of values + */ + @Override + public T[] toArray(T[] a) + { + return new ArrayList<>(this).toArray(a); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyMemoryView.java b/native/jpype_module/src/main/java/python/lang/PyMemoryView.java new file mode 100644 index 000000000..7712c8ba8 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyMemoryView.java @@ -0,0 +1,143 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * Java front-end interface for the Python `memoryview` type. + *

+ * The {@code PyMemoryView} interface represents a concrete Python `memoryview` + * object, which provides a way to access the memory of other objects (such as + * bytes or bytearrays) without copying the data. This enables efficient + * manipulation of binary data in Python. + *

+ * + *

+ * The {@code PyMemoryView} interface extends {@link PyObject}, allowing it to + * behave as a Python object in a Java environment. It provides access to + * Python's `memoryview` functionality, enabling developers to work with memory + * buffers directly from Java. + *

+ * + *

+ * Python's `memoryview` objects support slicing, indexing, and various + * attributes for inspecting memory buffers. This interface provides methods + * that align with Python's `memoryview` API to enable efficient manipulation of + * memory buffers in Java. + *

+ * + *

+ * Example usage: + *

+ * PyMemoryView memoryView = ...; // Obtain a PyMemoryView instance
+ * int length = memoryView.getLength(); // Get the size of the memory buffer
+ * byte[] slice = memoryView.sublist(0, 10); // Get a slice of the memory buffer
+ * 
+ *

+ */ +public interface PyMemoryView extends PySequence +{ + + /** + * Retrieves the underlying buffer as a {@link PyBuffer}. + *

+ * This method provides access to the raw memory buffer represented by the + * {@code PyMemoryView} object. + *

+ * + * @return the underlying {@link PyBuffer} object. + */ + PyBuffer getBuffer(); + + /** + * Returns the format of the elements stored in the memory buffer. + *

+ * This method retrieves the format string that describes the type of elements + * stored in the memory buffer. Equivalent to Python's + * {@code memoryview.format} attribute. + *

+ * + * @return the format string of the memory buffer elements. + */ + String getFormat(); + + /** + * Returns the shape of the memory buffer. + *

+ * This method retrieves the dimensions of the memory buffer as a tuple. + * Equivalent to Python's {@code memoryview.shape} attribute. + *

+ * + * @return a {@link PyTuple} representing the shape of the memory buffer. + */ + PyTuple getShape(); + + /** + * Returns a slice of the memory buffer between the specified indices as a + * view. + * + * @param start the starting index of the slice. + * @param end the ending index of the slice. + * @return a {@code PyMemoryView} representing slice of the memory buffer. + */ + PyMemoryView getSlice(int start, int end); + + /** + * Returns the strides of the memory buffer. + *

+ * This method retrieves the step sizes to access elements in the memory + * buffer. Equivalent to Python's {@code memoryview.strides} attribute. + *

+ * + * @return a {@link PyTuple} representing the strides of the memory buffer. + */ + PyTuple getStrides(); + + /** + * Returns the sub-offsets of the memory buffer. + *

+ * This method retrieves the sub-offsets of the memory buffer, which are used + * for multi-dimensional arrays. Equivalent to Python's + * {@code memoryview.suboffsets} attribute. + *

+ * + * @return a {@link PyTuple} representing the sub-offsets of the memory + * buffer. + */ + PyTuple getSubOffsets(); + + /** + * Checks if the memory buffer is read-only. + *

+ * This method determines whether the memory buffer is read-only. Equivalent + * to Python's {@code memoryview.readonly} attribute. + *

+ * + * @return {@code true} if the memory buffer is read-only; {@code false} + * otherwise. + */ + boolean isReadOnly(); + + /** + * Releases the memory buffer. + *

+ * This method releases the underlying memory buffer, making the + * {@code PyMemoryView} object unusable. Equivalent to Python's + * {@code memoryview.release()} method. + *

+ */ + void release(); + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyMutableSet.java b/native/jpype_module/src/main/java/python/lang/PyMutableSet.java new file mode 100644 index 000000000..03018d71e --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyMutableSet.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import static python.lang.PyBuiltIn.backend; + +/** + * + */ +public interface PyMutableSet extends PyAbstractSet +{ + + @Override + default boolean contains(Object obj) + { + return backend().contains(this, obj); + } + + @Override + default int size() + { + return PyBuiltIn.len(this); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyNumber.java b/native/jpype_module/src/main/java/python/lang/PyNumber.java new file mode 100644 index 000000000..5235fa220 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyNumber.java @@ -0,0 +1,347 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import python.lang.PyObject; + +/** + * Interface representing Python numeric operations. + * + * This interface provides methods to perform arithmetic operations, type + * conversions, and other behaviors that mimic Python's numeric protocol. It is + * designed to facilitate interoperability between Java and Python numeric + * types, enabling seamless integration of Python-like numeric behavior in Java + * applications. + * + * Each method corresponds to a Python numeric operation (e.g., addition, + * subtraction, multiplication) and supports operations with Python objects, + * primitive types, and in-place modifications. + */ +public interface PyNumber extends PyObject, Comparable +{ + + // Addition operations + /** + * Adds the given PyObject to this object. + * + * @param other The PyObject to add. + * @return A new PyObject representing the result of the addition. + */ + PyObject add(PyObject other); + + /** + * Adds the given long value to this object. + * + * @param value The long value to add. + * @return A new PyObject representing the result of the addition. + */ + PyObject add(long value); + + /** + * Adds the given double value to this object. + * + * @param value The double value to add. + * @return A new PyObject representing the result of the addition. + */ + PyObject add(double value); + + /** + * Performs in-place addition with the given PyObject. + * + * @param other The PyObject to add. + * @return The updated PyObject after in-place addition. + */ + PyObject addInPlace(PyObject other); + + /** + * Performs in-place addition with the given long value. + * + * @param value The long value to add. + * @return The updated PyObject after in-place addition. + */ + PyObject addInPlace(long value); + + /** + * Performs in-place addition with the given double value. + * + * @param value The double value to add. + * @return The updated PyObject after in-place addition. + */ + PyObject addInPlace(double value); + + // Division operations + /** + * Divides this object by the given PyObject. + * + * @param other The PyObject to divide by. + * @return A new PyObject representing the result of the division. + */ + PyObject divide(PyObject other); + + /** + * Divides this object by the given long value. + * + * @param value The long value to divide by. + * @return A new PyObject representing the result of the division. + */ + PyObject divide(long value); + + /** + * Divides this object by the given double value. + * + * @param value The double value to divide by. + * @return A new PyObject representing the result of the division. + */ + PyObject divide(double value); + + /** + * Performs in-place division by the given PyObject. + * + * @param other The PyObject to divide by. + * @return The updated PyObject after in-place division. + */ + PyObject divideInPlace(PyObject other); + + /** + * Performs in-place division by the given long value. + * + * @param value The long value to divide by. + * @return The updated PyObject after in-place division. + */ + PyObject divideInPlace(long value); + + /** + * Performs in-place division by the given double value. + * + * @param value The double value to divide by. + * @return The updated PyObject after in-place division. + */ + PyObject divideInPlace(double value); + + // Division with remainder (modulus) + /** + * Performs division with remainder (divmod) operation with the given + * PyObject. + * + * @param other The PyObject to divide by. + * @return A PyObject representing the result of the divmod operation. + */ + PyObject divideWithRemainder(PyObject other); + + // Matrix multiplication + /** + * Performs matrix multiplication with the given PyObject. + * + * @param other The PyObject to multiply with. + * @return A PyObject representing the result of the matrix multiplication. + */ + PyObject matrixMultiply(PyObject other); + + // Multiplication operations + /** + * Multiplies this object by the given PyObject. + * + * @param other The PyObject to multiply with. + * @return A new PyObject representing the result of the multiplication. + */ + PyObject multiply(PyObject other); + + /** + * Multiplies this object by the given long value. + * + * @param value The long value to multiply with. + * @return A new PyObject representing the result of the multiplication. + */ + PyObject multiply(long value); + + /** + * Multiplies this object by the given double value. + * + * @param value The double value to multiply with. + * @return A new PyObject representing the result of the multiplication. + */ + PyObject multiply(double value); + + /** + * Performs in-place multiplication with the given PyObject. + * + * @param other The PyObject to multiply with. + * @return The updated PyObject after in-place multiplication. + */ + PyObject multiplyInPlace(PyObject other); + + /** + * Performs in-place multiplication with the given long value. + * + * @param value The long value to multiply with. + * @return The updated PyObject after in-place multiplication. + */ + PyObject multiplyInPlace(long value); + + /** + * Performs in-place multiplication with the given double value. + * + * @param value The double value to multiply with. + * @return The updated PyObject after in-place multiplication. + */ + PyObject multiplyInPlace(double value); + + // Logical negation + /** + * Performs logical negation (NOT operation) on this object. + * + * @return A boolean representing the negated value. + */ + boolean negate(); + + // Exponentiation + /** + * Raises this object to the power of the given PyObject. + * + * @param exponent The PyObject representing the exponent. + * @return A PyObject representing the result of the exponentiation. + */ + PyObject power(PyObject exponent); + + // Remainder (modulus) + /** + * Computes the remainder (modulus) of this object divided by the given + * PyObject. + * + * @param other The PyObject to divide by. + * @return A PyObject representing the result of the modulus operation. + */ + PyObject modulus(PyObject other); + + // Subtraction operations + /** + * Subtracts the given PyObject from this object. + * + * @param other The PyObject to subtract. + * @return A new PyObject representing the result of the subtraction. + */ + PyObject subtract(PyObject other); + + /** + * Subtracts the given long value from this object. + * + * @param value The long value to subtract. + * @return A new PyObject representing the result of the subtraction. + */ + PyObject subtract(long value); + + /** + * Subtracts the given double value from this object. + * + * @param value The double value to subtract. + * @return A new PyObject representing the result of the subtraction. + */ + PyObject subtract(double value); + + /** + * Performs in-place subtraction with the given PyObject. + * + * @param other The PyObject to subtract. + * @return The updated PyObject after in-place subtraction. + */ + PyObject subtractInPlace(PyObject other); + + /** + * Performs in-place subtraction with the given long value. + * + * @param value The long value to subtract. + * @return The updated PyObject after in-place subtraction. + */ + PyObject subtractInPlace(long value); + + /** + * Performs in-place subtraction with the given double value. + * + * @param value The double value to subtract. + * @return The updated PyObject after in-place subtraction. + */ + PyObject subtractInPlace(double value); + + // Conversion methods + /** + * Converts this object to a boolean value. + * + * @return A boolean representation of this object. + */ + boolean toBoolean(); + + /** + * Converts this object to a double value. + * + * @return A double representation of this object. + */ + double toDouble(); + + /** + * Converts this object to an integer value. + * + * @return An integer representation of this object. + */ + int toInteger(); + + /** + * Computes the absolute value of this object. + * + * @return A PyObject representing the absolute value. + */ + PyObject abs(); + + /** + * Computes the negation (unary minus) of this object. + * + * @return A PyObject representing the negated value. + */ + PyObject negateValue(); + + /** + * Computes the positive value (unary plus) of this object. + * + * @return A PyObject representing the positive value. + */ + PyObject positive(); + + /** + * Performs floor division with the given PyObject. + * + * @param other The PyObject to divide by. + * @return A PyObject representing the result of the floor division. + */ + PyObject floorDivide(PyObject other); + + /** + * Performs floor division with the given long value. + * + * @param value The long value to divide by. + * @return A PyObject representing the result of the floor division. + */ + PyObject floorDivide(long value); + + /** + * Performs floor division with the given double value. + * + * @param value The double value to divide by. + * @return A PyObject representing the result of the floor division. + */ + PyObject floorDivide(double value); + + @Override + int compareTo(Number o); +} diff --git a/native/jpype_module/src/main/java/python/lang/PyObject.java b/native/jpype_module/src/main/java/python/lang/PyObject.java new file mode 100644 index 000000000..6e3909903 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyObject.java @@ -0,0 +1,62 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * PyObject is a representation of a generic object in Python. + * + * PyObject when created inherit from multiple Java interfaces called protocols + * based on their duck type behavior. Use `instanceof` and casting to access the + * available behaviors. Specific Java like behaviors are implemented on the + * protocols where applicable. + * + */ +public interface PyObject +{ + + /** + * Apply the attributes protocol to this object. + * + * The PyAttributes provides access to getattr, setattr, and delattr. + * + * This method never fails. + * + * @return an attribute protocol. + */ + default PyAttributes getAttributes() + { + return new PyAttributes(this); + } + + @Override + int hashCode(); + + @Override + boolean equals(Object obj); + + /** + * Returns a string representation of the object. + *

+ * This method generates a string representation of the object, equivalent to + * Python's {@code str(object)} operation. + *

+ * + * @return a string representation of the slice. + */ + @Override + String toString(); + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyRange.java b/native/jpype_module/src/main/java/python/lang/PyRange.java new file mode 100644 index 000000000..74d30fbe7 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyRange.java @@ -0,0 +1,167 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * Java front-end interface for the Python `range` type. + *

+ * The {@code PyRange} interface represents a concrete Python `range` object, + * which is an immutable sequence of numbers commonly used for iteration in + * Python. + *

+ * + *

+ * The {@code PyRange} interface extends {@link PyObject} and + * {@link PyGenerator}, allowing it to behave as both a Python object and a + * generator in a Java environment. This enables developers to work with + * Python's `range` functionality directly from Java. + *

+ * + *

+ * Python's `range` objects support iteration, slicing, and various attributes + * for inspecting the sequence. This interface provides methods that align with + * Python's `range` API, enabling efficient manipulation of ranges in Java. + *

+ * + *

+ * Example usage: + *

+ * PyRange range = BuiltIn.range(1,5); // Obtain a PyRange instance
+ * int length = range.getLength(); // Get the number of elements in the range
+ * int first = range.getItem(0); // Get the first item in the range
+ * PyRange slice = range.getSlice(1, 5); // Get a slice of the range
+ * boolean containsValue = range.contains(3); // Check if the range contains a value
+ * PyList rangeAsList = range.toList(); // Convert the range to a list
+ * 
+ *

+ */ +public interface PyRange extends PyIter +{ + + /** + * Returns the starting value of the range. + *

+ * This method retrieves the first value in the range, equivalent to Python's + * {@code range.start} attribute. + *

+ * + * @return the starting value of the range. + */ + int getStart(); + + /** + * Returns the stopping value of the range. + *

+ * This method retrieves the end value of the range, equivalent to Python's + * {@code range.stop} attribute. + *

+ * + * @return the stopping value of the range. + */ + int getStop(); + + /** + * Returns the step value of the range. + *

+ * This method retrieves the step size between consecutive values in the + * range, equivalent to Python's {@code range.step} attribute. + *

+ * + * @return the step value of the range. + */ + int getStep(); + + /** + * Returns the number of elements in the range. + *

+ * This method calculates the total number of values in the range, equivalent + * to Python's {@code len(range)} operation. + *

+ * + * @return the number of elements in the range. + */ + int getLength(); + + /** + * Retrieves the item at the specified index in the range. + *

+ * This method provides access to a specific element in the range using its + * index, equivalent to Python's {@code range[index]} operation. + *

+ * + * @param index the index of the item to retrieve. + * @return the item at the specified index. + * @throws IndexOutOfBoundsException if the index is out of range. + */ + int getItem(int index); + + /** + * Returns a slice of the range between the specified indices. + *

+ * This method creates a new {@code PyRange} object representing a subset of + * the original range, equivalent to Python's {@code range[start:end]} slicing + * operation. + *

+ * + * @param start the starting index of the slice. + * @param end the ending index of the slice. + * @return a {@code PyRange} representing the slice of the range. + * @throws IllegalArgumentException if the indices are invalid. + */ + PyRange getSlice(int start, int end); + + /** + * Checks if the specified value exists in the range. + *

+ * This method determines whether the given value is part of the range, + * equivalent to Python's {@code value in range} operation. + *

+ * + * @param value the value to check for. + * @return {@code true} if the value exists in the range; {@code false} + * otherwise. + */ + boolean contains(int value); + + // FIXME MOVE TO PyIter + /** + * Converts the range object into a list of integers. + *

+ * This method creates a {@link PyList} containing all the elements in the + * range, equivalent to Python's {@code list(range)} operation. + *

+ * + * @return a {@link PyList} containing all the elements in the range. + */ + PyList toList(); + + // FIXME MOVE TO PyIter + /** + * Returns an iterator for the range object. + *

+ * This method provides an iterator to traverse the elements of the range, + * equivalent to Python's {@code iter(range)} operation. + *

+ * + * @return a {@link PyIterator} for the range object. + */ + @Override + default PyIterator iterator() + { + return new PyIterator<>(this); + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PySequence.java b/native/jpype_module/src/main/java/python/lang/PySequence.java new file mode 100644 index 000000000..2affda61e --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PySequence.java @@ -0,0 +1,140 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Iterator; +import java.util.List; +import python.lang.PyBuiltIn; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; +import python.lang.PyObject; + +/** + * Interface for Python-like objects that act as sequences. + * + * This interface extends {@link PyProtocol} and {@link List}, providing methods + * for accessing, modifying, and deleting elements in a sequence. It also + * supports Python-style slicing and indexing operations. + * + * Methods in this interface are designed to mimic Python's sequence protocol, + * including operations such as `getitem`, `setitem`, and `delitem`. + */ +public interface PySequence extends PyCollection, List +{ + + @Override + default boolean contains(Object obj) + { + return backend().contains(this, obj); + } + + /** + * Retrieves an item from the sequence by its index. + * + * This method is equivalent to Python's `getitem(obj, index)` operation. + * + * @param index the index of the item to retrieve + * @return the item at the specified index as a {@link PyObject} + */ + @Override + @SuppressWarnings("unchecked") + default T get(int index) + { + return (T) backend().getitemSequence(this, index); + } + + /** + * Retrieves an item or slice from the sequence using a {@link PyIndex}. + * + * This method is equivalent to Python's `obj[index]` operation, where the + * index may represent a single item or a slice. + * + * @param index the {@link PyIndex} representing the item or slice to retrieve + * @return the item or slice as a {@link PyObject} + */ + default PyObject get(PyIndex index) + { + return backend().getitemMappingObject(this, index); + } + + /** + * Retrieves a slice or multiple slices from the sequence using an array of + * {@link PyIndex}. + * + * This method is equivalent to Python's `obj[slice, slice]` operation, + * supporting multidimensional slicing. + * + * @param indices an array of {@link PyIndex} objects representing the slices + * to retrieve + * @return the resulting slice(s) as a {@link PyObject} + * @throws IllegalArgumentException if the type does not support tuple assignment. + */ + default PyObject get(PyIndex... indices) + { + return backend().getitemMappingObject(this, PyBuiltIn.indices(indices)); + } + + @Override + default boolean isEmpty() + { + return size() == 0; + } + + @Override + default Iterator iterator() + { + return new PyIterator<>(this.iter()); + } + + /** + * Removes an item from the sequence at the specified index. + * + * This method is equivalent to Python's `delitem(obj, index)` operation. + * + * @param index the index of the item to remove + * @return the removed item as a {@link PyObject} + */ + @Override + T remove(int index); + + /** + * Sets an item in the sequence at the specified index. + * + * This method is equivalent to Python's `setitem(obj, index, value)` + * operation. + * + * @param index the index of the item to set + * @param value the value to assign to the specified index + * @return the previous value at the specified index as a {@link PyObject} + */ + @Override + T set(int index, T value); + + void setAny(Object index, Object values); + + /** + * Returns the size of the sequence. + * + * This method is equivalent to Python's `len(obj)` operation. + * + * @return the number of items in the sequence + */ + @Override + default int size() + { + return backend().len(this); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PySet.java b/native/jpype_module/src/main/java/python/lang/PySet.java new file mode 100644 index 000000000..26147f65a --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PySet.java @@ -0,0 +1,415 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Represents a Python set in the Java environment. + * + * This interface provides a Java front end for Python's concrete set type, + * bridging the gap between Python's set operations and Java's {@link Set} + * interface. It mostly adheres to the Java contract for sets while + * incorporating Python-specific behaviors and operations. + * + *

+ * Key features include:

+ *
    + *
  • Support for Python-specific set operations (e.g., union, intersection, + * difference).
  • + *
  • Integration with Java's {@link Collection} framework.
  • + *
  • Ability to create and manipulate sets using Python semantics.
  • + *
+ * + *

+ * All methods in this interface are designed to work seamlessly with Python + * objects ({@link PyObject}) and provide a consistent API for interacting with + * Python sets.

+ * + *

+ * Important Note:

+ *

+ * Python collections are asymmetric in their handling of Java objects. A Java + * object added to a Python collection will appear as a {@code PyJavaObject}. + * Developers should exercise caution to avoid reference loops when placing Java + * objects into Python collections, as this may lead to unintended + * behaviors.

+ */ +public interface PySet extends PyAbstractSet +{ + + /** + * Creates a new Python set from the elements of the given {@link Iterable}. + * + * @param c an iterable providing elements for the set + * @param the getType of elements in the iterable + * @return a new {@code PySet} containing the elements from the iterable + */ + public static PySet of(Iterable c) + { + return backend().newSetFromIterable(c); + } + + /** + * Adds a single element to the set. + * + * @param e the element to add + * @return {@code true} if the set was modified as a result of this operation, + * {@code false} otherwise + */ + @Override + boolean add(PyObject e); + + void addAny(Object o); + + /** + * Adds all elements from the specified collection to the set. + * + * @param collection the collection containing elements to be added + * @return {@code true} if the set was modified as a result of this operation, + * {@code false} otherwise + */ + @Override + default boolean addAll(Collection collection) + { + int l1 = this.size(); + this.update(of(collection)); + int l2 = this.size(); + return l1 != l2; + } + + /** + * Removes all elements from the set. + */ + @Override + void clear(); + + /** + * Checks if the set contains the specified element. + * + * @param o the element to check for + * @return {@code true} if the set contains the element, {@code false} + * otherwise + */ + @Override + boolean contains(Object o); + + /** + * Checks if the set contains all elements from the specified collection. + * + * @param collection the collection of elements to check + * @return {@code true} if the set contains all elements, {@code false} + * otherwise + */ + @Override + default boolean containsAll(Collection collection) + { + PySet set2 = of(collection); + return set2.isSubset(this); + } + + /** + * Creates a shallow copy of the set. + * + * @return a new {@code PySet} containing the same elements as this set + */ + PySet copy(); + + /** + * Returns a new set containing the difference between this set and the + * specified sets. + * + * @param set the sets to subtract from this set + * @return a new {@code PySet} containing the difference + */ + PySet difference(Collection... set); + + /** + * Updates this set to contain the difference between itself and the specified + * sets. + * + * @param set the sets to subtract from this set + */ + void differenceUpdate(Collection... set); + + /** + * Removes the specified element from the set, if it exists. + * + * @param item the element to remove + */ + void discard(Object item); + + /** + * Checks if this set is equal to the specified object. + * + * @param obj the object to compare with + * @return {@code true} if the sets are equal, {@code false} otherwise + */ + @Override + boolean equals(Object obj); + + /** + * Returns the hash code for this set. + * + * @return the hash code of the set + */ + @Override + int hashCode(); + + /** + * Returns a new set containing the intersection of this set and the specified + * sets. + * + * @param set the sets to intersect with this set + * @return a new {@code PySet} containing the intersection + */ + PySet intersect(Collection... set); + + /** + * Updates this set to contain the intersection of itself and the specified + * sets. + * + * @param set the sets to intersect with this set + */ + void intersectionUpdate(Collection... set); + + /** + * Checks if this set is disjoint with the specified set. + * + * @param set the set to compare with + * @return {@code true} if the sets are disjoint, {@code false} otherwise + */ + boolean isDisjoint(Collection set); + + /** + * Checks if the set is empty. + * + * @return {@code true} if the set contains no elements, {@code false} + * otherwise + */ + @Override + default boolean isEmpty() + { + return size() == 0; + } + + /** + * Checks if this set is a subset of the specified set. + * + * @param set the set to compare with + * @return {@code true} if this set is a subset, {@code false} otherwise + */ + boolean isSubset(Collection set); + + /** + * Checks if this set is a superset of the specified set. + * + * @param set the set to compare with + * @return {@code true} if this set is a superset, {@code false} otherwise + */ + boolean isSuperset(Collection set); + + /** + * Returns an iterator over the elements in this set. + * + * @return an iterator for the set + */ + @Override + default Iterator iterator() + { + return new PyIterator<>(backend().iterSet(this)); + } + + /** + * Returns a parallel {@link Stream} of the elements in this set. + * + * @return a parallel stream of the set's elements + */ + @Override + default Stream parallelStream() + { + return StreamSupport.stream(this.spliterator(), true); + } + + /** + * Removes and returns an arbitrary element from the set. + * + * @return the removed element + */ + PyObject pop(); + + /** + * Removes the specified element from the set. + * + * @param o the element to remove + * @return {@code true} if the set was modified, {@code false} otherwise + */ + @Override + default boolean remove(Object o) + { + int initialSize = this.size(); + this.discard(o); + return this.size() != initialSize; + } + + /** + * Removes all elements in the specified collection from this set. + * + * @param collection the collection of elements to remove + * @return {@code true} if the set was modified as a result of this operation, + * {@code false} otherwise + */ + @Override + default boolean removeAll(Collection collection) + { + int initialSize = this.size(); + PySet delta = this.difference(of(collection)); + this.clear(); + this.update(delta); + return this.size() != initialSize; + } + + /** + * Retains only the elements in this set that are contained in the specified + * collection. + * + * @param collection the collection of elements to retain + * @return {@code true} if the set was modified as a result of this operation, + * {@code false} otherwise + */ + @Override + default boolean retainAll(Collection collection) + { + int initialSize = this.size(); + PySet delta = this.intersect(of(collection)); + this.clear(); + this.update(delta); + return this.size() != initialSize; + } + + /** + * Returns the number of elements in this set. + * + * @return the size of the set + */ + @Override + public int size(); + + /** + * Returns a {@link Spliterator} for the elements in this set. + * + * @return a spliterator for the set + */ + @Override + default Spliterator spliterator() + { + return Spliterators.spliterator(this, Spliterator.DISTINCT); + } + + /** + * Returns a sequential {@link Stream} of the elements in this set. + * + * @return a sequential stream of the set's elements + */ + @Override + default Stream stream() + { + return StreamSupport.stream(this.spliterator(), false); + } + + /** + * Returns a new set containing the symmetric difference between this set and + * the specified sets. + * + * @param set the sets to compute the symmetric difference with + * @return a new {@code PySet} containing the symmetric difference + */ + PySet symmetricDifference(Collection... set); + + /** + * Updates this set to contain the symmetric difference between itself and the + * specified set. + * + * @param set the set to compute the symmetric difference with + */ + void symmetricDifferenceUpdate(Collection set); + + /** + * Returns an array containing all elements in this set. + * + * @return an array containing the set's elements + */ + @Override + default Object[] toArray() + { + return new ArrayList<>(this).toArray(); + } + + /** + * Returns an array containing all elements in this set, using the specified + * array as the target. + * + * @param a the array into which the elements of the set are to be stored + * @param the getType of the array elements + * @return an array containing the set's elements + */ + @Override + default T[] toArray(T[] a) + { + return new ArrayList<>(this).toArray(a); + } + + /** + * Returns a {@link List} containing all elements in this set, using Python + * semantics. + * + * @return a Python-style list containing the set's elements + */ + List toList(); + + /** + * Returns a new set containing the union of this set and the specified sets. + * + * @param set the sets to compute the union with + * @return a new {@code PySet} containing the union + */ + PySet union(Collection... set); + + /** + * Updates this set to contain the union of itself and the specified sets. + * + * @param set the sets to compute the union with + */ + void unionUpdate(Collection... set); + + /** + * Updates this set to include all elements from the specified iterable. + * + * @param other the iterable providing elements to add to the set + */ + void update(Iterable other); + +} diff --git a/native/jpype_module/src/main/java/python/lang/PySized.java b/native/jpype_module/src/main/java/python/lang/PySized.java new file mode 100644 index 000000000..a6c7682f4 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PySized.java @@ -0,0 +1,37 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import python.lang.PyBuiltIn; +import python.lang.PyObject; + +/** + * Protocol for Python objects are sized. + */ +public interface PySized extends PyObject +{ + + default int size() + { + return PyBuiltIn.len(this); + } + + default boolean isEmpty() + { + return size() == 0; + } + +} diff --git a/native/jpype_module/src/main/java/python/lang/PySlice.java b/native/jpype_module/src/main/java/python/lang/PySlice.java new file mode 100644 index 000000000..3e6771438 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PySlice.java @@ -0,0 +1,113 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * Java front-end interface for the Python `slice` type. + *

+ * The {@code PySlice} interface represents a concrete Python `slice` object, + * which is used to define a range of indices for slicing sequences in Python. + *

+ * + *

+ * The {@code PySlice} interface extends {@link PyObject} and {@link PyIndex}, + * allowing it to behave as both a Python object and an indexable object in a + * Java environment. + *

+ * + *

+ * Python's `slice` objects support attributes for inspecting the slicing + * parameters and methods for applying the slice to sequences. This interface + * provides methods that align with Python's `slice` API to enable efficient + * manipulation of slices in Java. + *

+ * + *

+ * Example usage: + *

+ * PySlice slice = ...; // Obtain a PySlice instance
+ * Integer start = slice.getStart(); // Get the starting index
+ * Integer stop = slice.getStop(); // Get the stopping index
+ * Integer step = slice.getStep(); // Get the step size
+ * PyTuple indices = slice.indices(10); // Get normalized indices for a sequence of length 10
+ * PySequence result = slice.apply(sequence); // Apply the slice to a sequence
+ * 
+ *

+ */ +public interface PySlice extends PyObject, PyIndex +{ + + /** + * Returns the starting index of the slice. + *

+ * This method retrieves the {@code start} parameter of the slice, equivalent + * to Python's {@code slice.start} attribute. + *

+ * + * @return the starting index of the slice, or {@code null} if not specified. + */ + Integer getStart(); + + /** + * Returns the stopping index of the slice. + *

+ * This method retrieves the {@code stop} parameter of the slice, equivalent + * to Python's {@code slice.stop} attribute. + *

+ * + * @return the stopping index of the slice, or {@code null} if not specified. + */ + Integer getStop(); + + /** + * Returns the step size of the slice. + *

+ * This method retrieves the {@code step} parameter of the slice, equivalent + * to Python's {@code slice.step} attribute. + *

+ * + * @return the step size of the slice, or {@code null} if not specified. + */ + Integer getStep(); + + /** + * Returns normalized indices for a sequence of the specified length. + *

+ * This method calculates the normalized {@code start}, {@code stop}, and + * {@code step} values for slicing a sequence of the given length, equivalent + * to Python's {@code slice.indices(length)} method. + *

+ * + * @param length the length of the sequence to normalize indices for. + * @return a {@link PyTuple} containing the normalized {@code start}, + * {@code stop}, and {@code step} values. + */ + PyTuple indices(int length); + + /** + * Validates the slice parameters. + *

+ * This method checks whether the slice parameters ({@code start}, + * {@code stop}, {@code step}) are logically consistent. For example, + * {@code step} cannot be {@code 0}. + *

+ * + * @return {@code true} if the slice parameters are valid; {@code false} + * otherwise. + */ + boolean isValid(); + +} diff --git a/native/jpype_module/src/main/java/python/lang/PyString.java b/native/jpype_module/src/main/java/python/lang/PyString.java new file mode 100644 index 000000000..5bd5f8e1b --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyString.java @@ -0,0 +1,694 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * Interface for Python objects acting as strings. + * + * This interface provides methods to mimic Python string operations in Java. It + * includes common string manipulation methods, formatting, and encoding + * options, as well as behaviors to facilitate seamless integration with Java + * code. + * + * Method names mimic those in Java String where possible, while maintaining + * Python-like functionality for developers familiar with Python string + * behavior. + * + */ +public interface PyString extends PyObject, CharSequence +{ + + /** + * Creates a PyString instance from a given CharSequence. + * + * @param sequence is the input sequence to convert into a PyString. + * @return a PyString instance representing the given sequence. + */ + public static PyString from(CharSequence sequence) + { + return PyBuiltIn.str(sequence); + } + + /** + * Returns the Python getType object for strings. + * + * @return the Python getType object representing strings. + */ + static PyType getType() + { + return (PyType) PyBuiltIn.eval("str", null, null); + } + + /** + * Returns the character at the specified index. + * + * @param index is the index of the character to retrieve. + * @return the character at the specified index. + */ + @Override + char charAt(int index); + + /** + * Checks if the string contains the specified substring. + * + * @param substring is the substring to search for. + * @return true if the substring is found, false otherwise. + */ + boolean containsSubstring(CharSequence substring); + + /** + * Counts the occurrences of a substring in the string. + * + * @param substring is the substring to count. + * @return the number of occurrences of the substring. + */ + int countOccurrences(CharSequence substring); + + /** + * Counts the occurrences of a substring in the string, starting from a + * specific index. + * + * @param substring is the substring to count. + * @param start is the starting index for the search. + * @return the number of occurrences of the substring. + */ + int countOccurrences(CharSequence substring, int start); + + /** + * Counts the occurrences of a substring in the string within a specific + * range. + * + * @param substring is the substring to count. + * @param start is the starting index for the search. + * @param end is the ending index for the search. + * @return the number of occurrences of the substring. + */ + int countOccurrences(CharSequence substring, int start, int end); + + /** + * Checks if the string ends with the specified suffix. + * + * @param suffix is the suffix to check. + * @return true if the string ends with the suffix, false otherwise. + */ + boolean endsWithSuffix(CharSequence suffix); + + /** + * Checks if the string ends with the specified suffix, starting from a + * specific index. + * + * @param suffix is the suffix to check. + * @param start is the starting index for the search. + * @return true if the string ends with the suffix, false otherwise. + */ + boolean endsWithSuffix(CharSequence suffix, int start); + + /** + * Checks if the string ends with the specified suffix within a specific + * range. + * + * @param suffix is the suffix to check. + * @param start is the starting index for the search. + * @param end is the ending index for the search. + * @return true if the string ends with the suffix, false otherwise. + */ + boolean endsWithSuffix(CharSequence suffix, int start, int end); + + /** + * Expands tabs in the string to spaces. + * + * @param tabSize The number of spaces to replace each tab. + * @return a new string with tabs replaced by spaces. + */ + PyString expandTabs(int tabSize); + + /** + * Finds the index of the last occurrence of a substring in the string. + * + * @param substring is the substring to search for. + * @return the index of the last occurrence, or -1 if not found. + */ + int findLastSubstring(CharSequence substring); + + /** + * Finds the index of the last occurrence of a substring in the string, + * starting from a specific index. + * + * @param substring is the substring to search for. + * @param start is the starting index for the search. + * @return the index of the last occurrence, or -1 if not found. + */ + int findLastSubstring(CharSequence substring, int start); + + /** + * Finds the index of the last occurrence of a substring in the string within + * a specific range. + * + * @param substring is the substring to search for. + * @param start is the starting index for the search. + * @param end is the ending index for the search. + * @return the index of the last occurrence, or -1 if not found. + */ + int findLastSubstring(CharSequence substring, int start, int end); + + /** + * Finds the first occurrence of a substring in the string. + * + * @param substring is the substring to search for. + * @return the index of the first occurrence, or -1 if not found. + */ + int findSubstring(CharSequence substring); + + /** + * Finds the first occurrence of a substring in the string, starting from a + * specific index. + * + * @param substring is the substring to search for. + * @param start is the starting index for the search. + * @return the index of the first occurrence, or -1 if not found. + */ + int findSubstring(CharSequence substring, int start); + + /** + * Finds the first occurrence of a substring in the string within a specific + * range. + * + * @param substring is the substring to search for. + * @param start is the starting index for the search. + * @param end is the ending index for the search. + * @return the index of the first occurrence, or -1 if not found. + */ + int findSubstring(CharSequence substring, int start, int end); + + /** + * Formats the string using a mapping of key-value pairs. + * + * @param mapping A mapping object containing keys and their corresponding + * values. + * @return a formatted PyString instance. + */ + PyString formatUsingMapping(PyMapping mapping); + + /** + * Formats the string using positional and keyword arguments. + * + * @param args A tuple containing positional arguments for formatting. + * @param kwargs A dictionary containing keyword arguments for formatting. + * @return a formatted PyString instance. + */ + PyString formatWith(PyTuple args, PyDict kwargs); +// FIXME conflict +// /** +// * Formats the string using a format string and variable arguments. +// * +// * @param format is the format string. +// * @param args is the arguments to substitute into the format string. +// * @return a formatted PyString instance. +// */ +// PyString formatWith(String format, Object... args); + + /** + * Retrieves the character at the specified index. + * + * @param index is the index of the character to retrieve. + * @return the character at the specified index. + */ + char getCharacterAt(int index); + + /** + * Finds the index of the last occurrence of a substring in the string. Throws + * an exception if the substring is not found. + * + * @param substring is the substring to search for. + * @return the index of the last occurrence. + */ + int indexOfLastSubstring(CharSequence substring); + + /** + * Finds the index of the last occurrence of a substring in the string, + * starting from a specific index. Throws an exception if the substring is not + * found. + * + * @param substring is the substring to search for. + * @param start is the starting index for the search. + * @return the index of the last occurrence. + */ + int indexOfLastSubstring(CharSequence substring, int start); + + /** + * Finds the index of the last occurrence of a substring in the string within + * a specific range. Throws an exception if the substring is not found. + * + * @param substring is the substring to search for. + * @param start is the starting index for the search. + * @param end is the ending index for the search. + * @return the index of the last occurrence. + */ + int indexOfLastSubstring(CharSequence substring, int start, int end); + + /** + * Finds the index of the first occurrence of a substring. + * + * @param substring is the substring to search for. + * @return the index of the first occurrence, or -1 if not found. + */ + int indexOfSubstring(CharSequence substring); + + /** + * Finds the index of the first occurrence of a substring, starting from a + * specific index. + * + * @param substring is the substring to search for. + * @param start is the starting index for the search. + * @return the index of the first occurrence, or -1 if not found. + */ + int indexOfSubstring(CharSequence substring, int start); + + /** + * Finds the index of the first occurrence of a substring within a specific + * range. + * + * @param substring is the substring to search for. + * @param start is the starting index for the search. + * @param end is the ending index for the search. + * @return the index of the first occurrence, or -1 if not found. + */ + int indexOfSubstring(CharSequence substring, int start, int end); + + /** + * Checks if the string contains only alphabetic characters. + * + * @return true if the string is alphabetic, false otherwise. + */ + boolean isAlphabetic(); + + /** + * Checks if the string contains only alphanumeric characters. + * + * @return true if the string is alphanumeric, false otherwise. + */ + boolean isAlphanumeric(); + + /** + * Checks if the string contains only ASCII characters. + * + * @return true if the string contains only ASCII characters, false otherwise. + */ + boolean isAsciiCharacters(); + + /** + * Checks if the string represents a decimal number. + * + * @return true if the string is a decimal number, false otherwise. + */ + boolean isDecimalNumber(); + + /** + * Checks if the string contains only digit characters. + * + * @return true if the string contains only digit characters, false otherwise. + */ + boolean isDigitCharacters(); + + /** + * Checks if the string is in lowercase. + * + * @return true if the string is lowercase, false otherwise. + */ + boolean isLowercase(); + + /** + * Checks if the string contains only numeric characters. + * + * @return true if the string contains only numeric characters, false + * otherwise. + */ + boolean isNumericCharacters(); + + /** + * Checks if the string contains only printable characters. + * + * @return true if the string is printable, false otherwise. + */ + boolean isPrintableCharacters(); + + /** + * Checks if the string is in title case. + * + * @return true if the string is in title case, false otherwise. + */ + boolean isTitleCase(); + + /** + * Checks if the string is in uppercase. + * + * @return true if the string is uppercase, false otherwise. + */ + boolean isUppercase(); + + /** + * Checks if the string is a valid Python identifier. + * + * @return true if the string is a valid identifier, false otherwise. + */ + boolean isValidIdentifier(); + + /** + * Checks if the string contains only whitespace characters. + * + * @return true if the string is whitespace, false otherwise. + */ + boolean isWhitespace(); + + /** + * Joins the elements of an iterable into a single string, separated by the + * current string. + * + * @param iterable is the iterable containing elements to join. + * @return a new PyString instance with the joined elements. + */ + PyString join(PyIterable iterable); + + /** + * Returns the length of the string. + * + * @return the length of the string. + */ + @Override + int length(); + + /** + * Centers the string within a specified width, padded with spaces. + * + * @param width is the total width of the resulting string. + * @return a new string centered within the specified width. + */ + PyString paddedCenter(int width); + + /** + * Centers the string within a specified width, padded with a specified + * character. + * + * @param width is the total width of the resulting string. + * @param fill is the character used for padding. + * @return a new string centered within the specified width. + */ + PyString paddedCenter(int width, char fill); + + /** + * Removes the specified prefix from the string if it exists. + * + * @param prefix is the prefix to remove. + * @return a new PyString instance with the prefix removed. + */ + PyString removePrefix(CharSequence prefix); + + /** + * Removes the specified suffix from the string if it exists. + * + * @param suffix is the suffix to remove. + * @return a new PyString instance with the suffix removed. + */ + PyString removeSuffix(CharSequence suffix); + + /** + * Replaces occurrences of a substring with a replacement string. + * + * @param oldSubstring The substring to replace. + * @param replacement is the replacement string. + * @return a new PyString instance with the replacements applied. + */ + PyString replaceSubstring(CharSequence oldSubstring, CharSequence replacement); + + /** + * Replaces a specified number of occurrences of a substring with a + * replacement string. + * + * @param oldSubstring The substring to replace. + * @param replacement is the replacement string. + * @param count is the maximum number of replacements to perform. + * @return a new PyString instance with the replacements applied. + */ + PyString replaceSubstring(CharSequence oldSubstring, CharSequence replacement, int count); + + /** + * Splits the string into a list of substrings using the specified separator. + * + * @param separator is the separator to split on. + * @return a PyList containing the substrings. + */ + PyList splitInto(CharSequence separator); + + /** + * Splits the string into a list of substrings using the specified separator, + * with a maximum number of splits. + * + * @param separator is the separator to split on. + * @param maxSplit The maximum number of splits to perform. + * @return a PyList containing the substrings. + */ + PyList splitInto(CharSequence separator, int maxSplit); + + /** + * Splits the string into a list of substrings using whitespace as the default + * separator. + * + * @return a PyList containing the substrings. + */ + PyList splitInto(); + + /** + * Splits the string into lines, optionally keeping line endings. + * + * @param keepEnds Whether to keep line endings in the result. + * @return a PyList containing the lines. + */ + PyList splitIntoLines(boolean keepEnds); + + /** + * Splits the string into a tuple of three parts: the part before the + * separator, the separator itself, and the part after the separator. + * + * @param separator is the separator to split on. + * @return a PyTuple containing the three parts. + */ + PyTuple splitIntoPartition(CharSequence separator); + + /** + * Splits the string into a list of substrings, starting from the end of the + * string. + * + * @param separator is the separator to split on. + * @return a PyList containing the substrings. + */ + PyList splitIntoReverse(CharSequence separator); + + /** + * Splits the string into a list of substrings, starting from the end of the + * string, with a maximum number of splits. + * + * @param separator is the separator to split on. + * @param maxSplit The maximum number of splits to perform. + * @return a PyList containing the substrings. + */ + PyList splitIntoReverse(CharSequence separator, int maxSplit); + + /** + * Splits the string into a list of substrings, starting from the end of the + * string. Uses whitespace as the default separator. + * + * @return a PyList containing the substrings. + */ + PyList splitIntoReverse(); + + /** + * Splits the string into three parts: the part before the separator, the + * separator itself, and the part after the separator, searching from the end + * of the string. + * + * @param separator is the separator to split on. + * @return a PyTuple containing the three parts. + */ + PyTuple splitIntoReversePartition(CharSequence separator); + + /** + * Checks if the string starts with the specified prefix. + * + * @param prefix is the prefix to check. + * @return true if the string starts with the prefix, false otherwise. + */ + boolean startsWithPrefix(CharSequence prefix); + + /** + * Checks if the string starts with the specified prefix, starting from a + * specific index. + * + * @param prefix is the prefix to check. + * @param start is the starting index for the check. + * @return true if the string starts with the prefix, false otherwise. + */ + boolean startsWithPrefix(CharSequence prefix, int start); + + /** + * Checks if the string starts with the specified prefix within a specific + * range. + * + * @param prefix is the prefix to check. + * @param start is the starting index for the check. + * @param end is the ending index for the check. + * @return true if the string starts with the prefix, false otherwise. + */ + boolean startsWithPrefix(CharSequence prefix, int start, int end); + + /** + * Removes all leading and trailing occurrences of the specified characters + * from the string. + * + * @param characters is the characters to remove. + * @return a new PyString instance with characters removed. + */ + PyString stripCharacters(CharSequence characters); + + /** + * Removes leading whitespace from the string. + * + * @return a new PyString instance with leading whitespace removed. + */ + PyString stripLeading(); + + /** + * Removes leading occurrences of the specified characters from the string. + * + * @param characters is the characters to remove. + * @return a new PyString instance with leading characters removed. + */ + PyString stripLeading(CharSequence characters); + + /** + * Removes trailing occurrences of the specified characters from the string. + * + * @param characters is the characters to remove. + * @return a new PyString instance with trailing characters removed. + */ + PyString stripTrailing(CharSequence characters); + + /** + * Removes all leading and trailing whitespace from the string. + * + * @return a new PyString instance with whitespace removed. + */ + PyString stripWhitespace(); + + /** + * Returns a subsequence of the string between the specified start and end + * indices. + * + * @param start is the starting index of the subsequence. + * @param end is the ending index of the subsequence. + * @return a PyString representing the subsequence. + */ + @Override + PyString subSequence(int start, int end); + + /** + * Swaps the case of all characters in the string. Uppercase characters are + * converted to lowercase, and vice versa. + * + * @return a new PyString instance with swapped case characters. + */ + PyString swapCaseCharacters(); + + /** + * Converts the string to a capitalized version. The first character is + * converted to uppercase, and the rest to lowercase. + * + * @return a capitalized version of the string. + */ + PyString toCapitalized(); + + /** + * Converts the string to a case-folded version. Case folding is useful for + * caseless matching. + * + * @return a case-folded version of the string. + */ + PyString toCaseFolded(); + + /** + * Encodes the string using the default encoding. + * + * @return a PyBytes object representing the encoded string. + */ + PyBytes toEncoded(); + + /** + * Encodes the string using the specified encoding. + * + * @param encoding is the name of the encoding to use. + * @return a PyBytes object representing the encoded string. + */ + PyBytes toEncoded(CharSequence encoding); + + /** + * Encodes the string using the specified encoding and error handling + * strategy. + * + * @param encoding is the name of the encoding to use. + * @param errorHandling The error handling strategy (e.g., "strict", + * "ignore"). + * @return a PyBytes object representing the encoded string. + */ + PyBytes toEncoded(CharSequence encoding, String errorHandling); + + /** + * Converts the string to title case. Each word's first character is + * capitalized, and the rest are lowercase. + * + * @return a new PyString instance in title case. + */ + PyString toTitleCase(); + + /** + * Converts the string to uppercase. + * + * @return a new PyString instance in uppercase. + */ + PyString toUppercase(); + + /** + * Translates the string using a mapping object. + * + * @param mapping is the mapping object specifying character replacements. + * @return a new PyString instance with translated characters. + */ + PyString translateUsingMapping(PyMapping mapping); + + /** + * Translates the string using a sequence of character replacements. + * + * @param sequence is the sequence specifying character replacements. + * @return a new PyString instance with translated characters. + */ + PyString translateUsingSequence(PySequence sequence); + + /** + * Pads the string with zeros on the left to reach the specified width. + * + * @param width is the total width of the resulting string. + * @return a new PyString instance with zero padding. + */ + PyString zeroFill(int width); +} diff --git a/native/jpype_module/src/main/java/python/lang/PyTuple.java b/native/jpype_module/src/main/java/python/lang/PyTuple.java new file mode 100644 index 000000000..3c76bebaf --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyTuple.java @@ -0,0 +1,318 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Collection; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; +import org.jpype.bridge.Interpreter; +import static python.lang.PyBuiltIn.backend; + +/** + * Represents a Java front-end for a concrete Python tuple. + * + *

+ * A Python tuple is an immutable, ordered collection of elements. This + * interface provides a Java representation of Python tuples, implementing the + * {@link List} contract for interoperability with Java collections. However, + * since Python tuples are immutable, all mutating methods from the {@link List} + * interface throw {@link UnsupportedOperationException}. + * + *

+ * Key features: + *

    + *
  • Immutability: Any attempt to modify the tuple (e.g., add, remove, or + * replace elements) will result in an + * {@link UnsupportedOperationException}.
  • + *
  • Full support for {@link List} methods that do not modify the collection, + * such as {@code get}, {@code contains}, {@code size}, and + * {@code iterator}.
  • + *
  • Integration with Java streams and spliterators for functional + * programming.
  • + *
+ * + *

+ * Example usage: + *

+ * PyTuple tuple = PyTuple.of(1, 2, 3);
+ * System.out.println(tuple.get(0)); // Output: 1
+ * System.out.println(tuple.size()); // Output: 3
+ * 
+ * + *

+ * Note: This interface assumes the existence of supporting classes such as + * {@code BuiltIn}, {@code PyObject}, {@code PyIterable}, {@code PySet}, and + * {@code PyIterator}. + * + *

+ * Important Note:

+ *

+ * Python collections are asymmetric in their handling of Java objects. A Java + * object added to a Python collection will appear as a {@code PyJavaObject}. + * Developers should exercise caution to avoid reference loops when placing Java + * objects into Python collections, as this may lead to unintended + * behaviors.

+ * + */ +public interface PyTuple extends PySequence +{ + + /** + * Creates a new {@code PyTuple} from a variable number of elements. + * + * @param values the elements to include in the tuple + * @return a new {@code PyTuple} containing the specified elements + */ + public static PyTuple of(Object... values) + { + return PyBuiltIn.tuple(values); + } + + /** + * Creates a new {@code PyTuple} from an {@link Iterable}. + * + * @param values an iterator providing the elements to include in the tuple + * @param the getType of elements in the iterator + * @return a new {@code PyTuple} containing the elements from the iterator + */ + public static PyTuple fromItems(Iterable values) + { + return PyBuiltIn.tuple(values); + } + + /** + * Returns the Python getType object for tuples. + * + * @return the Python getType object representing {@code tuple} + */ + static PyType getType() + { + return (PyType) PyBuiltIn.eval("tuple", null, null); + } + + // --- Mutating methods (throw UnsupportedOperationException) --- + /** + * Throws {@link UnsupportedOperationException} because {@code PyTuple} is + * immutable. + */ + @Override + default boolean add(PyObject e) + { + throw new UnsupportedOperationException("PyTuple is immutable."); + } + + /** + * Throws {@link UnsupportedOperationException} because {@code PyTuple} is + * immutable. + */ + @Override + default void add(int index, PyObject element) + { + throw new UnsupportedOperationException("PyTuple is immutable."); + } + + /** + * Throws {@link UnsupportedOperationException} because {@code PyTuple} is + * immutable. + */ + @Override + default boolean addAll(Collection c) + { + throw new UnsupportedOperationException("PyTuple is immutable."); + } + + /** + * Throws {@link UnsupportedOperationException} because {@code PyTuple} is + * immutable. + */ + @Override + default boolean addAll(int index, Collection c) + { + throw new UnsupportedOperationException("PyTuple is immutable."); + } + + /** + * Throws {@link UnsupportedOperationException} because {@code PyTuple} is + * immutable. + */ + @Override + default void clear() + { + throw new UnsupportedOperationException("PyTuple is immutable."); + } + + // --- Non-mutating methods --- + /** + * Checks if the tuple contains the specified object. + * + * @param o the object to check + * @return {@code true} if the tuple contains the object, {@code false} + * otherwise + */ + @Override + default boolean contains(Object obj) + { + return backend().contains(this, obj); + } + + /** + * Checks if the tuple contains all elements in the specified collection. + * + * @param c the collection of elements to check + * @return {@code true} if the tuple contains all elements in the collection, + * {@code false} otherwise + */ + @Override + default boolean containsAll(Collection c) + { + PySet s1 = PySet.of(this); + PySet s2 = PySet.of(c); + return s2.isSubset(s1); + } + + /** + * Returns the element at the specified index. + * + * @param index the index of the element to retrieve. + * @return the element at the specified index. + * @throws IndexOutOfBoundsException if the index is out of range + */ + @Override + PyObject get(int index); + + /** + * Returns the index of the first occurrence of the specified object in the + * tuple. + * + * @param o the object to search for + * @return the index of the first occurrence, or -1 if the object is not found + */ + @Override + public int indexOf(Object o); + + /** + * Returns {@code true} if the tuple is empty. + * + * @return {@code true} if the tuple contains no elements, {@code false} + * otherwise + */ + @Override + default boolean isEmpty() + { + return size() == 0; + } + + /** + * Returns an iterator over the elements in the tuple. + * + * @return an iterator over the elements in the tuple + */ + @Override + default Iterator iterator() + { + return new PyIterator<>(this.iter()); + } + + /** + * Returns a list iterator over the elements in the tuple. + * + * @return a list iterator starting at the beginning of the tuple + */ + @Override + default ListIterator listIterator() + { + return new PyTupleIterator(this, 0); + } + + /** + * Returns a list iterator over the elements in the tuple, starting at the + * specified index. + * + * @param index the starting index for the iterator + * @return a list iterator starting at the specified index + * @throws IndexOutOfBoundsException if the index is out of range + */ + @Override + default ListIterator listIterator(int index) + { + if (index < 0 || index > size()) + { + throw new IndexOutOfBoundsException(); + } + return new PyTupleIterator(this, index); + } + + /** + * Returns the number of elements in the tuple. + * + * @return the number of elements in the tuple + */ + @Override + default int size() + { + return backend().len(this); + } + + /** + * Returns a sublist view of the tuple between the specified indices. + * + * @param fromIndex the starting index (inclusive) + * @param toIndex the ending index (exclusive) + * @return a sublist view of the tuple + * @throws IndexOutOfBoundsException if the indices are out of range + * @throws IllegalArgumentException if {@code fromIndex > toIndex} + */ + @Override + PyTuple subList(int fromIndex, int toIndex); + + /** + * Returns a sequential {@link Stream} over the elements in the tuple. + * + * @return a sequential stream of the tuple elements + */ + @Override + default Stream stream() + { + return StreamSupport.stream(this.spliterator(), false); + } + + /** + * Returns a parallel {@link Stream} over the elements in the tuple. + * + * @return a parallel stream of the tuple elements + */ + @Override + default Stream parallelStream() + { + return StreamSupport.stream(this.spliterator(), true); + } + + /** + * Returns a {@link Spliterator} over the elements in the tuple. + * + * @return a spliterator for the tuple elements + */ + @Override + default Spliterator spliterator() + { + return Spliterators.spliterator(this, Spliterator.ORDERED); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyTupleIterator.java b/native/jpype_module/src/main/java/python/lang/PyTupleIterator.java new file mode 100644 index 000000000..db98dd982 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyTupleIterator.java @@ -0,0 +1,179 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.ListIterator; +import java.util.NoSuchElementException; + +/** + * An iterator for traversing a PyTuple object in both forward and reverse + * directions. + * + * This class implements the {@link ListIterator} interface to provide + * bidirectional iteration over a PyTuple, which is assumed to behave like a + * Python tuple. + * + * Modifications to the PyTuple (e.g., adding or removing elements) are not + * supported by this iterator. + */ +class PyTupleIterator implements ListIterator +{ + + /** + * The PyTuple being iterated over. + */ + private final PyTuple tuple; + + /** + * The current index of the iterator. + */ + private int index; + + /** + * Constructs a PyTupleIterator starting at the specified index. + * + * @param tuple The PyTuple to iterate over. + * @param index The initial index of the iterator. + */ + PyTupleIterator(PyTuple tuple, int index) + { + if (index < 0 || index > tuple.size()) + { + throw new IndexOutOfBoundsException("Index out of bounds for PyTuple."); + } + this.tuple = tuple; + this.index = index; + } + + /** + * Unsupported operation: Adding elements is not allowed for PyTupleIterator. + * + * @param e The element to add (ignored). + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public void add(PyObject e) + { + throw new UnsupportedOperationException("PyTupleIterator does not support add operation."); + } + + /** + * Checks if there are more elements when iterating forward. + * + * @return True if there is a next element, false otherwise. + */ + @Override + public boolean hasNext() + { + return index < tuple.size(); + } + + /** + * Checks if there are more elements when iterating backward. + * + * @return True if there is a previous element, false otherwise. + */ + @Override + public boolean hasPrevious() + { + return index > 0; + } + + /** + * Returns the next element in the iteration and advances the cursor forward. + * + * @return The next PyObject in the tuple. + * @throws NoSuchElementException If there is no next element. + */ + @Override + public PyObject next() + { + if (!hasNext()) + { + throw new NoSuchElementException("No next element."); + } + PyObject out = tuple.get(index); + index++; + return out; + } + + /** + * Returns the index of the element that would be returned by a subsequent + * call to {@link #next()}. + * + * @return The index of the next element. + */ + @Override + public int nextIndex() + { + return index; + } + + /** + * Returns the previous element in the iteration and moves the cursor + * backward. + * + * @return The previous PyObject in the tuple. + * @throws NoSuchElementException If there is no previous element. + */ + @Override + public PyObject previous() + { + if (!hasPrevious()) + { + throw new NoSuchElementException("No previous element in PyTuple."); + } + index--; + PyObject out = tuple.get(index); + return out; + } + + /** + * Returns the index of the element that would be returned by a subsequent + * call to {@link #previous()}. + * + * @return The index of the previous element. + */ + @Override + public int previousIndex() + { + return index - 1; + } + + /** + * Unsupported operation: Removing elements is not allowed for + * PyTupleIterator. + * + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public void remove() + { + throw new UnsupportedOperationException("PyTupleIterator does not support remove operation."); + } + + /** + * Unsupported operation: Setting elements is not allowed for PyTupleIterator. + * + * @param e The element to set (ignored). + * @throws UnsupportedOperationException Always thrown. + */ + @Override + public void set(PyObject e) + { + throw new UnsupportedOperationException("PyTupleIterator does not support set operation."); + } +} diff --git a/native/jpype_module/src/main/java/python/lang/PyType.java b/native/jpype_module/src/main/java/python/lang/PyType.java new file mode 100644 index 000000000..4a5344788 --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyType.java @@ -0,0 +1,135 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * Represents a Java front-end for a concrete Python type. + * + * This interface provides methods to interact with Python types in Java, + * mimicking Python's type object functionality. It includes methods for + * retrieving type information, checking type relationships, and accessing + * attributes and methods defined on the type. + * + */ +public interface PyType extends PyObject, PyCallable +{ + + /** + * Returns the Python type object for the "zip" built-in function. + * + * This is a static utility method to demonstrate how Python types can be + * evaluated and accessed from Java. + * + * @return the PyType object corresponding to the "zip" function. + */ + static PyType getType() + { + return (PyType) PyBuiltIn.eval("zip", null, null); + } + + /** + * Retrieves the name of the type. + * + * This corresponds to the Python `__name__` attribute, which represents the + * name of the type as a string. + * + * @return the name of the type as a String. + */ + String getName(); + + /** + * Retrieves the method resolution order (MRO) of the type.The MRO defines the + * order in which base classes are searched when resolving methods and + * attributes. + * + * This corresponds to the Python `__mro__` attribute. + * + * @return a PyTuple representing the MRO of the getType. + */ + PyTuple mro(); + + /** + * Retrieves the base class of the type. + * + * This corresponds to the Python `__base__` attribute, which represents the + * immediate base class of the type. + * + * @return the base class as a PyType. + */ + PyType getBase(); + + /** + * Retrieves a tuple of all base classes for the type.This corresponds to the + * Python `__bases__` attribute, which contains all base classes of the type. + * + * + * @return a PyTuple containing the base classes of the getType. + */ + PyTuple getBases(); + + /** + * Checks if the current type is a subclass of the specified type.This + * corresponds to Python's `issubclass()` function and allows checking type + * relationships. + * + * + * @param type The getType to check against. + * @return true if this getType is a subclass of the specified getType, false + * otherwise. + */ + boolean isSubclassOf(PyType type); + + /** + * Checks if the given object is an instance of this type.This corresponds to + * Python's `isinstance()` function and allows checking if an object belongs + * to the current type. + * + * + * @param obj The object to check. + * @return true if the object is an instance of this getType, false otherwise. + */ + boolean isInstance(PyObject obj); + + /** + * Retrieves a callable method by name from the type. + * + * This allows accessing methods defined on the type by their name. + * + * @param name The name of the method to retrieve. + * @return the callable method as a PyCallable, or null if the method does not + * exist. + */ + PyCallable getMethod(String name); + + /** + * Checks if the type is abstract.An abstract type is one that contains + * abstract methods and cannot be instantiated directly. + * + * This corresponds to Python's `abc` module behavior. + * + * @return true if the getType is abstract, false otherwise. + */ + boolean isAbstract(); + + /** + * Retrieves a list of subclasses of the type.This corresponds to Python's + * `__subclasses__()` method, which returns all known subclasses of the type. + * + * + * @return a PyList containing the subclasses of the getType. + */ + PyList getSubclasses(); +} diff --git a/native/jpype_module/src/main/java/python/lang/PyZip.java b/native/jpype_module/src/main/java/python/lang/PyZip.java new file mode 100644 index 000000000..9b20092be --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/PyZip.java @@ -0,0 +1,47 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import static python.lang.PyBuiltIn.backend; + +/** + * Java front end for concrete Python zip. + */ +public interface PyZip extends PyIter +{ + + /** + * Creates a new PyZip object by zipping the provided iterables. + * + * @param items is the iterables to zip together. + * @return a PyZip object representing the zipped iterables. + */ + static PyZip of(Iterable... items) + { + return backend().newZip(items); + } + + /** + * Retrieves all remaining items from the zipped iterables as a list. + * + * This method collects all remaining tuples from the iterator and returns + * them in a PyList, similar to Python's `list(zip(...))`. + * + * @return a PyList containing all remaining tuples from the zipped iterables. + */ + PyList toList(); + +} diff --git a/native/jpype_module/src/main/java/python/lang/package-info.java b/native/jpype_module/src/main/java/python/lang/package-info.java new file mode 100644 index 000000000..ef10ceb5f --- /dev/null +++ b/native/jpype_module/src/main/java/python/lang/package-info.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +/** + * This package holds all the concrete classes we support from Python. + * + * We implement a Java collection interface if it does not interfere with the + * Python behaviors. We use Java case conventions or map the the nearest Java + * concept name so long as it the method has the same concept. We use + * CharSequence rather than String so that both PyString and Java String can be + * taken as an argument. + * + * Return types are as tight as can be supported by Python. + * + * Parameter types are as lose as possible to accept a wide range of arguments. + * + * Some methods or combination of arguments may not be available in Java on + * these wrappers but can be accessed using the + * {@link org.jpype.bridge.BuiltIn#eval eval} method. Thus type restrictions can + * generally be avoided when they are burdensome. + * + * Not every protocol will be available on every object as Python objects may + * pick and choose what protocols they want to implement. Exceptions will be + * thrown if the action is not allowed. + * + */ diff --git a/project/jpype_java/test/org/jpype/manager/TestDefault.java b/native/jpype_module/src/test/java/org/jpype/manager/TestDefault.java similarity index 98% rename from project/jpype_java/test/org/jpype/manager/TestDefault.java rename to native/jpype_module/src/test/java/org/jpype/manager/TestDefault.java index 0e3a591c5..19baed556 100644 --- a/project/jpype_java/test/org/jpype/manager/TestDefault.java +++ b/native/jpype_module/src/test/java/org/jpype/manager/TestDefault.java @@ -20,7 +20,6 @@ /** * - * @author nelson85 */ public class TestDefault { diff --git a/project/jpype_java/test/org/jpype/manager/TestMethodResolution.java b/native/jpype_module/src/test/java/org/jpype/manager/TestMethodResolution.java similarity index 100% rename from project/jpype_java/test/org/jpype/manager/TestMethodResolution.java rename to native/jpype_module/src/test/java/org/jpype/manager/TestMethodResolution.java diff --git a/project/jpype_java/test/org/jpype/manager/TestTypeManager.java b/native/jpype_module/src/test/java/org/jpype/manager/TestTypeManager.java similarity index 99% rename from project/jpype_java/test/org/jpype/manager/TestTypeManager.java rename to native/jpype_module/src/test/java/org/jpype/manager/TestTypeManager.java index 388ca935e..a41f87c31 100644 --- a/project/jpype_java/test/org/jpype/manager/TestTypeManager.java +++ b/native/jpype_module/src/test/java/org/jpype/manager/TestTypeManager.java @@ -20,7 +20,6 @@ /** * - * @author nelson85 */ public class TestTypeManager { diff --git a/project/jpype_java/test/org/jpype/manager/TypeFactoryHarness.java b/native/jpype_module/src/test/java/org/jpype/manager/TypeFactoryHarness.java similarity index 99% rename from project/jpype_java/test/org/jpype/manager/TypeFactoryHarness.java rename to native/jpype_module/src/test/java/org/jpype/manager/TypeFactoryHarness.java index 329a5fab8..fa14c533b 100644 --- a/project/jpype_java/test/org/jpype/manager/TypeFactoryHarness.java +++ b/native/jpype_module/src/test/java/org/jpype/manager/TypeFactoryHarness.java @@ -27,8 +27,6 @@ *

* This harness operates like JPype C++ layer with checks for problems and * inconsistencies that may indicate a problem with the TypeManager. - * - * @author nelson85 */ public class TypeFactoryHarness implements TypeFactory, TypeAudit { @@ -54,7 +52,8 @@ T assertResource(long id, Class cls) throw new RuntimeException("Incorrect resource type " + id + " expected: " + cls.getName() + " found: " + resource.getClass().getName()); - return (T) resource; + // Use Class.cast() to safely cast the resource + return cls.cast(resource); } // diff --git a/project/jpype_java/test/org/jpype/pkg/JPypePackageNGTest.java b/native/jpype_module/src/test/java/org/jpype/pkg/JPypePackageNGTest.java similarity index 81% rename from project/jpype_java/test/org/jpype/pkg/JPypePackageNGTest.java rename to native/jpype_module/src/test/java/org/jpype/pkg/JPypePackageNGTest.java index 9c20257b8..b05fe665f 100644 --- a/project/jpype_java/test/org/jpype/pkg/JPypePackageNGTest.java +++ b/native/jpype_module/src/test/java/org/jpype/pkg/JPypePackageNGTest.java @@ -19,12 +19,12 @@ import java.nio.file.Path; import java.util.Arrays; import java.util.Map; +import java.util.Set; import static org.testng.Assert.*; import org.testng.annotations.Test; /** * - * @author nelson85 */ public class JPypePackageNGTest { @@ -50,21 +50,16 @@ public void testBase() { assertTrue(JPypePackageManager.isPackage("java")); JPypePackage pkg = new JPypePackage("java"); - for (Map.Entry e : pkg.contents.entrySet()) - { - System.out.println(e.getKey()); - } + Set entries = pkg.contents.keySet(); + assertTrue(entries.contains("rmi")); } @Test public void testOrg() { - assertTrue(JPypePackageManager.isPackage("org")); JPypePackage pkg = new JPypePackage("org"); - for (Map.Entry e : pkg.contents.entrySet()) - { - System.out.println(e.getKey()); - } + Set entries = pkg.contents.keySet(); + assertTrue(entries.contains("jpype")); } /** @@ -73,7 +68,6 @@ public void testOrg() @Test public void testGetObject() { - System.out.println("getObject"); JPypePackage instance = new JPypePackage("java.lang"); Object expResult = Object.class; Object result = instance.getObject("Object"); @@ -86,13 +80,14 @@ public void testGetObject() @Test public void testGetContents() { - System.out.println("getContents"); JPypePackage instance = new JPypePackage("java.lang"); String[] expResult = new String[] { - "Enum", "ClassValue", "String" + "AbstractMethodError", "Appendable" }; - String[] result = Arrays.copyOfRange(instance.getContents(), 0, 3); + var contents = instance.getContents(); + Arrays.sort(contents); + String[] result = Arrays.copyOfRange(contents, 0, 2); assertEquals(result, expResult); } diff --git a/project/jpype_java/test/org/jpype/pkg/ListPackage.java b/native/jpype_module/src/test/java/org/jpype/pkg/ListPackage.java similarity index 98% rename from project/jpype_java/test/org/jpype/pkg/ListPackage.java rename to native/jpype_module/src/test/java/org/jpype/pkg/ListPackage.java index b78ee683f..515141bb6 100644 --- a/project/jpype_java/test/org/jpype/pkg/ListPackage.java +++ b/native/jpype_module/src/test/java/org/jpype/pkg/ListPackage.java @@ -17,7 +17,6 @@ /** * - * @author nelson85 */ public class ListPackage { diff --git a/native/jpype_module/src/test/java/python/lang/LangSuite.xml b/native/jpype_module/src/test/java/python/lang/LangSuite.xml new file mode 100644 index 000000000..15cafb87a --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/LangSuite.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + diff --git a/native/jpype_module/src/test/java/python/lang/PyByteArrayNGTest.java b/native/jpype_module/src/test/java/python/lang/PyByteArrayNGTest.java new file mode 100644 index 000000000..d9fd1e52d --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyByteArrayNGTest.java @@ -0,0 +1,138 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.List; +import org.jpype.bridge.Interpreter; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; +import python.lang.PyBuiltIn; + +/** + * + * @author nelson85 + */ +public class PyByteArrayNGTest +{ + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + // Helper method to create a new instance of PyByteArray for testing + private PyByteArray newInstance() + { + // Assume this method provides a concrete implementation of PyByteArray + return PyByteArray.create(0); // Replace with actual instantiation logic + } + + @Test + public void testCreateWithLength() + { + int length = 10; + PyByteArray instance = PyByteArray.create(length); + + // Assert that the created PyByteArray object is not null + assertNotNull(instance, "PyByteArray object should not be null"); + + // Optionally, verify the size of the bytearray (if accessible) + int len2 = PyBuiltIn.len(instance); + assertEquals(len2, length, "PyByteArray size should match the specified length"); + } + + @Test + public void testCreateFromIterable() + { + Iterable iterable = List.of(newInstance(), newInstance()); + PyByteArray instance = PyByteArray.of(iterable); + + // Assert that the created PyByteArray object is not null + assertNotNull(instance, "PyByteArray object should not be null"); + + // Optionally, verify the contents of the PyByteArray (if accessible) + // Example: assertTrue(instance.containsAll(iterable)); + } + + @Test + public void testCreateFromBuffer() + { + PyBuffer buffer = (PyBuffer) PyBytes.fromHex("48656c6c6f"); + PyByteArray instance = PyByteArray.of(buffer); + + // Assert that the created PyByteArray object is not null + assertNotNull(instance, "PyByteArray object should not be null"); + + // Optionally, verify the contents of the PyByteArray (if accessible) + // Example: assertEquals(instance.getBuffer(), buffer); + } + + @Test + public void testDecode() + { +// PyByteArray instance = newInstance(); +// PyObject encoding = new PyObject(); // Assume PyObject is properly instantiated +// PyObject delete = null; // No bytes to delete +// +// PyObject decoded = instance.decode(encoding, delete); +// +// // Assert that the decoded PyObject is not null +// assertNotNull(decoded, "Decoded PyObject should not be null"); +// +// // Optionally, verify the decoded content (if accessible) +// // Example: assertEquals(decoded.toString(), "expectedString"); + } + + @Test + public void testTranslate() + { +// PyByteArray instance = newInstance(); +// PyObject translationTable = new PyObject(); // Assume PyObject is properly instantiated +// PyObject translated = instance.translate(translationTable); +// +// // Assert that the translated PyObject is not null +// assertNotNull(translated, "Translated PyObject should not be null"); +// +// // Optionally, verify the translated content (if accessible) +// // Example: assertEquals(translated.toString(), "expectedTranslatedString"); + } + + @Test + public void testFromHex() + { + CharSequence hexString = "48656c6c6f"; // Hex for "Hello" + PyByteArray instance = PyByteArray.fromHex(hexString); + + // Assert that the created PyByteArray object is not null + assertNotNull(instance, "PyByteArray object should not be null"); + + // Optionally, verify the decoded content (if accessible) + // Example: assertEquals(instance.toString(), "Hello"); + } + + @Test + public void testSize() + { + PyByteArray instance = PyByteArray.create(10); + + // Assert that the size of the PyByteArray matches the specified length + int len2 = PyBuiltIn.len(instance); + assertEquals(len2, 10, "PyByteArray size should match the specified length"); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyBytesNGTest.java b/native/jpype_module/src/test/java/python/lang/PyBytesNGTest.java new file mode 100644 index 000000000..f7223abc3 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyBytesNGTest.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyBytesNGTest +{ + + public PyBytesNGTest() + { + } + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyComplexNGTest.java b/native/jpype_module/src/test/java/python/lang/PyComplexNGTest.java new file mode 100644 index 000000000..9db780c61 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyComplexNGTest.java @@ -0,0 +1,92 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * + */ +public class PyComplexNGTest +{ + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + @Test + public void testCreateComplexNumber() + { + double real = 3.0; + double imag = 4.0; + PyComplex pyComplex = PyComplex.of(real, imag); + + // Assert that the created PyComplex object is not null + assertNotNull(pyComplex, "PyComplex object should not be null"); + + // Assert that the real and imaginary parts match the input values + assertEquals(pyComplex.real(), real, "Real part should match the input value"); + assertEquals(pyComplex.imag(), imag, "Imaginary part should match the input value"); + } + + + @Test + public void testRealPart() + { + double real = 5.0; + double imag = 2.0; + PyComplex pyComplex = PyComplex.of(real, imag); + + // Assert that the real part matches the expected value + assertEquals(pyComplex.real(), real, "Real part should match the expected value"); + } + + @Test + public void testImaginaryPart() + { + double real = 1.0; + double imag = 6.0; + PyComplex pyComplex = PyComplex.of(real, imag); + + // Assert that the imaginary part matches the expected value + assertEquals(pyComplex.imag(), imag, "Imaginary part should match the expected value"); + } + + @Test + public void testConjugate() + { + double real = 3.0; + double imag = 4.0; + PyComplex pyComplex = PyComplex.of(real, imag); + + PyComplex conjugate = pyComplex.conjugate(); + + // Assert that the conjugate is not null + assertNotNull(conjugate, "Conjugate PyComplex object should not be null"); + + // Assert that the real part remains the same + assertEquals(conjugate.real(), real, "Real part of conjugate should match the original real part"); + + // Assert that the imaginary part is negated + assertEquals(conjugate.imag(), -imag, "Imaginary part of conjugate should be negated"); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyDictItemsNGTest.java b/native/jpype_module/src/test/java/python/lang/PyDictItemsNGTest.java new file mode 100644 index 000000000..b5af07b72 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyDictItemsNGTest.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyDictItemsNGTest +{ + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyDictKeySetNGTest.java b/native/jpype_module/src/test/java/python/lang/PyDictKeySetNGTest.java new file mode 100644 index 000000000..a91e02e88 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyDictKeySetNGTest.java @@ -0,0 +1,33 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyDictKeySetNGTest +{ + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } +} diff --git a/native/jpype_module/src/test/java/python/lang/PyDictNGTest.java b/native/jpype_module/src/test/java/python/lang/PyDictNGTest.java new file mode 100644 index 000000000..2038de12e --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyDictNGTest.java @@ -0,0 +1,145 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy fromMap + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; +import org.jpype.bridge.Interpreter; +import org.testng.annotations.Test; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import python.lang.PyBuiltIn; + +/** + * + * @author nelson85 + */ +public class PyDictNGTest +{ + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + @Test + public void testPutAndGet() + { + PyDict obj = PyBuiltIn.dict(); + PyObject value = PyString.from("value1"); + obj.putAny("key1", value); + assertEquals(obj.get("key1"), value); + } + + @Test + public void testGetOrDefault() + { + PyDict dict = PyBuiltIn.dict(); + PyObject defaultValue = PyString.from("default"); + + assertEquals(dict.getOrDefault("missingKey", defaultValue), defaultValue); + } + + @Test + public void testPop() + { + PyDict dict = PyBuiltIn.dict(); + PyObject value = PyString.from("value1"); + PyObject defaultValue = PyString.from("default"); + dict.putAny("key1", value); + assertEquals(dict.pop("key1", defaultValue), value); + assertEquals(dict.pop("missingKey", defaultValue), defaultValue); + } + + @Test + public void testPopItem() + { + PyDict dict = PyBuiltIn.dict(); + PyObject value = PyString.from("value1"); + dict.putAny("key1", value); + Map.Entry entry = dict.popItem(); + assertEquals(entry.getKey(), "key1"); + assertEquals(entry.getValue(), value); + assertTrue(dict.isEmpty()); + } + + @Test(expectedExceptions = NoSuchElementException.class) + public void testPopItemEmptyDict() + { + PyDict dict = PyBuiltIn.dict(); + dict.popItem(); // Should throw NoSuchElementException + } + + @Test + public void testSize() + { + PyDict dict = PyBuiltIn.dict(); + assertEquals(dict.size(), 0); + dict.putAny("key1", PyString.from("value1")); + assertEquals(dict.size(), 1); + } + + @Test + public void testIsEmpty() + { + PyDict dict = PyBuiltIn.dict(); + assertTrue(dict.isEmpty()); + dict.putAny("key1", PyString.from("value1")); + assertFalse(dict.isEmpty()); + } + + @Test + public void testClear() + { + PyDict dict = PyBuiltIn.dict(); + dict.putAny("key1", PyString.from("value1")); + dict.putAny("key2", PyString.from("value2")); + dict.clear(); + assertTrue(dict.isEmpty()); + } + + @Test + public void testUpdateWithMap() + { + PyDict dict = PyBuiltIn.dict(); + Map updateMap = new HashMap<>(); + updateMap.put("key1", PyString.from("value1")); + updateMap.put("key2", PyString.from("value2")); + dict.update(updateMap); + assertEquals(dict.size(), 2); + assertEquals(dict.get("key1").toString(), "value1"); + assertEquals(dict.get("key2").toString(), "value2"); + } + + @Test + public void testUpdateWithIterable() + { + PyDict dict = PyBuiltIn.dict(); + List> updateList = new ArrayList<>(); + updateList.add(new AbstractMap.SimpleEntry<>("key1", PyString.from("value1"))); + updateList.add(new AbstractMap.SimpleEntry<>("key2", PyString.from("value2"))); + dict.update(updateList); + assertEquals(dict.size(), 2); + assertEquals(dict.get("key1").toString(), "value1"); + assertEquals(dict.get("key2").toString(), "value2"); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyDictValuesNGTest.java b/native/jpype_module/src/test/java/python/lang/PyDictValuesNGTest.java new file mode 100644 index 000000000..8c23ebeb6 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyDictValuesNGTest.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyDictValuesNGTest +{ + + public PyDictValuesNGTest() + { + } + + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyEnumerateNGTest.java b/native/jpype_module/src/test/java/python/lang/PyEnumerateNGTest.java new file mode 100644 index 000000000..e76324c69 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyEnumerateNGTest.java @@ -0,0 +1,52 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * + * @author nelson85 + */ +public class PyEnumerateNGTest +{ + + public PyEnumerateNGTest() + { + } + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + + @Test + public void testOf() + { + System.out.println("of"); + Iterable iterable = null; + PyEnumerate expResult = null; + PyEnumerate result = PyEnumerate.of(iterable); + assertEquals(result, expResult); + fail("The test case is a prototype."); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyExcNGTest.java b/native/jpype_module/src/test/java/python/lang/PyExcNGTest.java new file mode 100644 index 000000000..c4dccb97c --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyExcNGTest.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyExcNGTest +{ + + public PyExcNGTest() + { + } + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyExceptionFactoryNGTest.java b/native/jpype_module/src/test/java/python/lang/PyExceptionFactoryNGTest.java new file mode 100644 index 000000000..4f035e88c --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyExceptionFactoryNGTest.java @@ -0,0 +1,48 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * + * @author nelson85 + */ +public class PyExceptionFactoryNGTest +{ + + public PyExceptionFactoryNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + + @Test + public void testSomeMethod() + { + fail("The test case is a prototype."); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyFloatNGTest.java b/native/jpype_module/src/test/java/python/lang/PyFloatNGTest.java new file mode 100644 index 000000000..a0b6d316a --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyFloatNGTest.java @@ -0,0 +1,50 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * + * @author nelson85 + */ +public class PyFloatNGTest +{ + + public PyFloatNGTest() + { + } + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + @Test + public void testOf() + { + // Arrange + double value = 42.5; + PyFloat pyFloat = PyFloat.of(value); + assertNotNull(pyFloat, "The PyFloat instance should not be null"); + assertEquals(pyFloat.toString(), "42.5", "The PyFloat instance should represent the correct value"); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyFrozenSetNGTest.java b/native/jpype_module/src/test/java/python/lang/PyFrozenSetNGTest.java new file mode 100644 index 000000000..8ae5a9b2f --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyFrozenSetNGTest.java @@ -0,0 +1,39 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyFrozenSetNGTest +{ + + public PyFrozenSetNGTest() + { + } + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyIntNGTest.java b/native/jpype_module/src/test/java/python/lang/PyIntNGTest.java new file mode 100644 index 000000000..c980baf02 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyIntNGTest.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyIntNGTest +{ + + public PyIntNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyJavaObjectNGTest.java b/native/jpype_module/src/test/java/python/lang/PyJavaObjectNGTest.java new file mode 100644 index 000000000..638bb8294 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyJavaObjectNGTest.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyJavaObjectNGTest +{ + + public PyJavaObjectNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyListIteratorNGTest.java b/native/jpype_module/src/test/java/python/lang/PyListIteratorNGTest.java new file mode 100644 index 000000000..1551c4406 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyListIteratorNGTest.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * + * @author nelson85 + */ +public class PyListIteratorNGTest +{ + + public PyListIteratorNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyListNGTest.java b/native/jpype_module/src/test/java/python/lang/PyListNGTest.java new file mode 100644 index 000000000..ececec31d --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyListNGTest.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyListNGTest +{ + + public PyListNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } +} diff --git a/native/jpype_module/src/test/java/python/lang/PyMemoryViewNGTest.java b/native/jpype_module/src/test/java/python/lang/PyMemoryViewNGTest.java new file mode 100644 index 000000000..207dfcc91 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyMemoryViewNGTest.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyMemoryViewNGTest +{ + + public PyMemoryViewNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyObjectNGTest.java b/native/jpype_module/src/test/java/python/lang/PyObjectNGTest.java new file mode 100644 index 000000000..e8efaaa6d --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyObjectNGTest.java @@ -0,0 +1,40 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyObjectNGTest +{ + + public PyObjectNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyRangeNGTest.java b/native/jpype_module/src/test/java/python/lang/PyRangeNGTest.java new file mode 100644 index 000000000..884c256de --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyRangeNGTest.java @@ -0,0 +1,38 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyRangeNGTest +{ + + public PyRangeNGTest() + { + } + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PySetNGTest.java b/native/jpype_module/src/test/java/python/lang/PySetNGTest.java new file mode 100644 index 000000000..d11775e05 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PySetNGTest.java @@ -0,0 +1,39 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PySetNGTest +{ + + public PySetNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PySliceNGTest.java b/native/jpype_module/src/test/java/python/lang/PySliceNGTest.java new file mode 100644 index 000000000..fe04af083 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PySliceNGTest.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PySliceNGTest +{ + + public PySliceNGTest() + { + } + + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyStringNGTest.java b/native/jpype_module/src/test/java/python/lang/PyStringNGTest.java new file mode 100644 index 000000000..a890ba13a --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyStringNGTest.java @@ -0,0 +1,41 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Interpreter; +import org.testng.annotations.BeforeClass; + +/** + * + * @author nelson85 + */ +public class PyStringNGTest +{ + + public PyStringNGTest() + { + } + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyTupleIteratorNGTest.java b/native/jpype_module/src/test/java/python/lang/PyTupleIteratorNGTest.java new file mode 100644 index 000000000..b29d520a3 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyTupleIteratorNGTest.java @@ -0,0 +1,148 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.NoSuchElementException; +import org.jpype.bridge.Interpreter; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * + * @author nelson85 + */ +public class PyTupleIteratorNGTest +{ + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + @Test + public void testForwardIteration() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTupleIterator iterator = (PyTupleIterator) tuple.listIterator(0); + + // Act & Assert + assertTrue(iterator.hasNext(), "Iterator should have a next element."); + assertEquals(iterator.next().toString(), "a", "First element should be 'a'."); + assertTrue(iterator.hasNext(), "Iterator should have a next element."); + assertEquals(iterator.next().toString(), "b", "Second element should be 'b'."); + assertTrue(iterator.hasNext(), "Iterator should have a next element."); + assertEquals(iterator.next().toString(), "c", "Third element should be 'c'."); + assertFalse(iterator.hasNext(), "Iterator should not have a next element."); + } + + @Test + public void testBackwardIteration() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTupleIterator iterator = new PyTupleIterator(tuple, tuple.size()); + + // Act & Assert + assertTrue(iterator.hasPrevious(), "Iterator should have a previous element."); + assertEquals(iterator.previous().toString(), "c", "Last element should be 'c'."); + assertTrue(iterator.hasPrevious(), "Iterator should have a previous element."); + assertEquals(iterator.previous().toString(), "b", "Second-to-last element should be 'b'."); + assertTrue(iterator.hasPrevious(), "Iterator should have a previous element."); + assertEquals(iterator.previous().toString(), "a", "First element should be 'a'."); + assertFalse(iterator.hasPrevious(), "Iterator should not have a previous element."); + } + + @Test(expectedExceptions = NoSuchElementException.class) + public void testNextThrowsExceptionWhenNoNextElement() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTupleIterator iterator = new PyTupleIterator(tuple, tuple.size()); + + // Act + iterator.next(); // Should throw NoSuchElementException + } + + @Test(expectedExceptions = NoSuchElementException.class) + public void testPreviousThrowsExceptionWhenNoPreviousElement() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTupleIterator iterator = (PyTupleIterator) tuple.listIterator(0); + + // Act + iterator.previous(); // Should throw NoSuchElementException + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testAddOperationThrowsException() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTupleIterator iterator = (PyTupleIterator) tuple.listIterator(0); + + // Act + iterator.add(null); // Should throw UnsupportedOperationException + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testRemoveOperationThrowsException() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTupleIterator iterator = (PyTupleIterator) tuple.listIterator(0); + + // Act + iterator.remove(); // Should throw UnsupportedOperationException + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testSetOperationThrowsException() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTupleIterator iterator = (PyTupleIterator) tuple.listIterator(0); + + // Act + iterator.set(null); // Should throw UnsupportedOperationException + } + + @Test(expectedExceptions = IndexOutOfBoundsException.class) + public void testConstructorThrowsExceptionForInvalidIndex() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + + // Act + new PyTupleIterator(tuple, -1); // Should throw IndexOutOfBoundsException + } + + @Test + public void testNextIndexAndPreviousIndex() + { + // Arrange + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTupleIterator iterator = (PyTupleIterator) tuple.listIterator(1); + + // Act & Assert + assertEquals(iterator.nextIndex(), 1, "Next index should be 1."); + assertEquals(iterator.previousIndex(), 0, "Previous index should be 0."); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyTupleNGTest.java b/native/jpype_module/src/test/java/python/lang/PyTupleNGTest.java new file mode 100644 index 000000000..5320be7bb --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyTupleNGTest.java @@ -0,0 +1,129 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.jpype.bridge.Interpreter; +import org.testng.annotations.Test; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; + +public class PyTupleNGTest +{ + + + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + @Test + public void testEmpty() + { + PyTuple emptyTuple = PyTuple.of(); + assertNotNull(emptyTuple, "Empty tuple should not be null."); + assertTrue(emptyTuple.isEmpty(), "Empty tuple should have no elements."); + assertEquals(emptyTuple.size(), 0, "Empty tuple size should be 0."); + } + + @Test + public void testOfVarArgs() + { + PyTuple tuple = PyTuple.of("a", "b", "c"); + assertNotNull(tuple, "Tuple should not be null."); + assertEquals(tuple.size(), 3, "Tuple size should be 3."); + assertEquals(tuple.get(0), "a", "First element should be 'a'."); + assertEquals(tuple.get(1), "b", "Second element should be 'b'."); + assertEquals(tuple.get(2), "c", "Third element should be 'c'."); + } + + @Test + public void testOfIterator() + { + List items = Arrays.asList("x", "y"); + PyTuple tuple = PyTuple.fromItems(items); + assertNotNull(tuple, "Tuple should not be null."); + assertEquals(tuple.size(), 2, "Tuple size should be 2."); + assertEquals(tuple.get(0).toString(), "x", "First element should be 'x'."); + assertEquals(tuple.get(1).toString(), "y", "Second element should be 'y'."); + } + + @Test + @SuppressWarnings("element-type-mismatch") + public void testContains() + { + PyTuple tuple = PyTuple.of("a", "b"); + assertTrue(tuple.contains("a"), "Tuple should contain 'a'."); + assertFalse(tuple.contains("z"), "Tuple should not contain 'z'."); + } + + @Test + public void testIsEmpty() + { + PyTuple emptyTuple = PyTuple.of(); + assertTrue(emptyTuple.isEmpty(), "Empty tuple should be empty."); + + PyTuple nonEmptyTuple = PyTuple.of("a"); + assertFalse(nonEmptyTuple.isEmpty(), "Non-empty tuple should not be empty."); + } + + @Test + public void testSize() + { + PyTuple tuple = PyTuple.of("a", "b"); + assertEquals(tuple.size(), 2, "Tuple size should be 2."); + } + + @Test + public void testSubList() + { + PyTuple tuple = PyTuple.of("a", "b", "c"); + PyTuple subTuple = tuple.subList(0, 2); + assertEquals(subTuple.size(), 2, "Sublist size should be 2."); + assertEquals(subTuple.get(0).toString(), "a", "First element of sublist should be 'a'."); + assertEquals(subTuple.get(1).toString(), "b", "Second element of sublist should be 'b'."); + } + + @Test + public void testStream() + { + PyTuple tuple = PyTuple.of("a", "b"); + Stream stream = tuple.stream(); + List elements = stream.map(PyObject::toString).collect(Collectors.toList()); + assertEquals(elements.size(), 2, "Stream should contain 2 elements."); + assertEquals(elements.get(0), "a", "First stream element should be 'a'."); + assertEquals(elements.get(1), "b", "Second stream element should be 'b'."); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testAddThrowsException() + { + PyTuple tuple = PyTuple.of("a", "b"); + tuple.add(PyString.from("a")); + } + + @Test(expectedExceptions = UnsupportedOperationException.class) + public void testAddThrowsException2() + { + PyTuple tuple = PyTuple.of("a", "b"); + tuple.add(1, PyString.from("a")); + } +} diff --git a/native/jpype_module/src/test/java/python/lang/PyTypeNGTest.java b/native/jpype_module/src/test/java/python/lang/PyTypeNGTest.java new file mode 100644 index 000000000..af0404f85 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyTypeNGTest.java @@ -0,0 +1,112 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import org.jpype.bridge.Context; +import org.jpype.bridge.Interpreter; +import org.testng.annotations.Test; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import python.lang.PyBuiltIn; + +/** + * + * @author nelson85 + */ +public class PyTypeNGTest +{ + static PyType objectType; + static PyType dictType; + static PyType rangeType; + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter interpreter = Interpreter.getInstance(); + interpreter.start(new String[0]); + Context context = new Context(); + objectType = (PyType) context.eval("type(object)"); + dictType = (PyType) context.eval("type(dict)"); + rangeType = (PyType) context.eval("type(range)"); + } + + @Test + public void testGetName() + { + PyType type = dictType; + assertEquals(type.getName(), "dict"); + } + + @Test + public void testGetBase() + { + PyType type = dictType; + + assertEquals(type.getBase().getName(), "object"); + } + + @Test + public void testGetBases() + { + PyType baseType1 = objectType; + PyType type = dictType; + assertEquals(type.getBases().size(), 1); + assertTrue(type.getBases().contains(baseType1)); + } + + @Test + public void testIsSubclassOf() + { + PyType type = dictType; + assertTrue(type.isSubclassOf(objectType)); + assertFalse(type.isSubclassOf(rangeType)); + } + + @Test + public void testIsInstance() + { + PyObject obj = PyString.from("test"); + PyType type = PyBuiltIn.type(obj); + assertTrue(type.isInstance(obj)); + assertFalse(type.isInstance(dictType)); + } + + @Test + public void testGetMethod() + { + PyType type = dictType; + assertNotNull(type.getMethod("keys")); + } + + @Test + public void testIsAbstract() + { + Context context = new Context(); + context.importModule("collections"); + PyObject obj = context.eval("collections.abc.Mapping"); + PyType type = PyBuiltIn.type(obj); + assertTrue(type.isAbstract()); + PyType concreteType = dictType; + assertFalse(concreteType.isAbstract()); + } + + @Test + public void testGetSubclasses() + { + PyType type = dictType; + assertEquals(((PyType) type.getSubclasses().get(0)).getName(), ""); + } + +} diff --git a/native/jpype_module/src/test/java/python/lang/PyZipNGTest.java b/native/jpype_module/src/test/java/python/lang/PyZipNGTest.java new file mode 100644 index 000000000..b84cf4a05 --- /dev/null +++ b/native/jpype_module/src/test/java/python/lang/PyZipNGTest.java @@ -0,0 +1,94 @@ +/* + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy of + * the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * See NOTICE file for details. + */ +package python.lang; + +import java.util.Arrays; +import java.util.List; +import org.jpype.bridge.Interpreter; +import static org.testng.Assert.*; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.Test; + +/** + * + * @author nelson85 + */ +public class PyZipNGTest +{ + + @BeforeClass + public static void setUpClass() throws Exception + { + Interpreter.getInstance().start(new String[0]); + } + + @Test + public void testStaticOfMethod() + { + // Create test iterables + List list1 = Arrays.asList(1, 2, 3); + List list2 = Arrays.asList("a", "b", "c"); + + // Create PyZip using the static `of` method + PyZip pyZip = PyBuiltIn.zip(list1, list2); + + // Convert to list and verify zipped items + PyList result = pyZip.toList(); + + assertEquals(result.size(), 3); + assertEquals(result.get(0).toString(), "[1, a]"); + assertEquals(result.get(1).toString(), "[2, b]"); + assertEquals(result.get(2).toString(), "[3, c]"); + } + + @Test + public void testStaticOfMethodUnevenLengths() + { + // Create test iterables with uneven lengths + List list1 = Arrays.asList(1, 2); + List list2 = Arrays.asList("a", "b", "c"); + + // Create PyZip using the static `of` method + PyZip pyZip = PyBuiltIn.zip(list1, list2); + + // Convert to list and verify zipped items + PyList result = pyZip.toList(); + + assertEquals(result.size(), 3); + assertEquals(result.get(0).toString(), "[1, a]"); + assertEquals(result.get(1).toString(), "[2, b]"); + assertEquals(result.get(2).toString(), "[null, c]"); + } + + @Test + public void testToListMethod() + { + // Create test iterables + List list1 = Arrays.asList(1, 2, 3); + List list2 = Arrays.asList("a", "b", "c"); + + // Create PyZip instance + PyZip pyZip = PyBuiltIn.zip(list1, list2); + + // Convert to list and verify zipped items + PyList result = pyZip.toList(); + + assertEquals(result.size(), 3); + assertEquals(result.get(0).toString(), "[1, a]"); + assertEquals(result.get(1).toString(), "[2, b]"); + assertEquals(result.get(2).toString(), "[3, c]"); + } +} diff --git a/native/python/include/jp_pythontypes.h b/native/python/include/jp_pythontypes.h index b69ebd690..9df2873ee 100755 --- a/native/python/include/jp_pythontypes.h +++ b/native/python/include/jp_pythontypes.h @@ -74,45 +74,72 @@ typedef _object PyObject; */ class JPPyObject { - /** Create a new reference to a Python object. - * - * @param obj is the python object. - */ +/** + * Creates a new reference to a Python object. + * + * This constructor takes ownership of the provided Python object reference. + * If the object is not null, the reference count is incremented to ensure + * the object remains valid for the lifetime of the `JPPyObject` instance. + * + * @param obj A pointer to the Python object (`PyObject*`). May be null. + */ explicit JPPyObject(PyObject* obj); public: - /** - * This policy is used if we need to hold a reference to an existing - * object for some duration. The object may be null. + * Creates a strong reference to an existing Python object for a limited duration. + * + * This policy is used when the caller needs to temporarily manage an existing + * Python object. It can be applied to both existing references and "borrowed" + * references returned by Python methods. + * + * If the object is not null, its reference count is incremented upon creation + * and decremented when the `JPPyObject` instance is destroyed. * - * Increment reference count if not null, and decrement when done. + * @param obj A pointer to the Python object (`PyObject*`). Must not be null. + * @return A `JPPyObject` instance managing the provided reference. + * @throws std::runtime_error if the object is null. */ static JPPyObject use(PyObject* obj); /** - * This policy is used when we are given a new reference that we must - * destroy. This will steal a reference. + * Unconditionally takes ownership of a new Python object reference. * - * claim reference, and decremented when done. Clears errors if NULL. + * This policy is used when the caller is responsible for managing a new + * Python object reference and transfers ownership to the `JPPyObject` instance. + * If the object is null, any existing Python errors are cleared. The caller + * must ensure the reference is valid before using this method. + * + * @param obj A pointer to the Python object (`PyObject*`). May be null. + * @return A `JPPyObject` instance managing the provided reference. */ static JPPyObject accept(PyObject* obj); /** - * This policy is used when we are given a new reference that we must - * destroy. This will steal a reference. + * Takes ownership of a new Python object reference, ensuring it is not null. + * + * This policy is used when the caller is responsible for managing a new + * Python object reference and transfers ownership to the `JPPyObject` instance. + * If the object is null, an exception is thrown. * - * Assert not null, claim reference, and decremented when done. - * Will throw an exception in the object is null. + * @param obj A pointer to the Python object (`PyObject*`). Must not be null. + * @return A `JPPyObject` instance managing the provided reference. + * @throws std::runtime_error if the object is null. */ static JPPyObject claim(PyObject* obj); /** - * This policy is used when we are capturing an object returned from a python - * call that we are responsible for. This will steal a reference. + * Captures and takes ownership of a Python object returned from a Python call. + * + * This policy is used when the caller is responsible for managing a Python + * object returned from a Python call. Ownership of the reference is transferred + * to the `JPPyObject` instance. Before claiming the reference, this method + * first checks for Python errors and then ensures the object is not null. + * If an error occurs or the object is null, an exception is thrown. * - * Check for errors, assert not null, then claim. - * Will throw an exception an error occurs. + * @param obj A pointer to the Python object (`PyObject*`). Must not be null. + * @return A `JPPyObject` instance managing the provided reference. + * @throws std::runtime_error if the object is null or if a Python error occurs. */ static JPPyObject call(PyObject* obj); @@ -127,13 +154,23 @@ class JPPyObject JPPyObject& operator=(const JPPyObject& o); /** - * Keep an object by creating a reference. + * Transfers ownership of the Python object reference. * - * This should only appear in the return statement in the cpython module. - * The reference must not be null. Keep invalidates this handle from any - * further use as you were supposed to have called return. + * This method is used to transfer control of an existing reference, such as: + * - Returning a reference from a function. + * - Passing a reference to a Python method that steals ownership of the reference. * - * @return the pointer to the Python object. + * Important Notes: + * - The reference must not be null. If the reference is null, this method will raise + * a SystemError exception. + * - Calling `keep()` invalidates this handle, meaning the `JPPyObject` instance + * should no longer be used after calling this method. + * - This method does NOT increment the reference counter. Instead, it transfers + * ownership of the existing reference to the caller. The caller is responsible + * for managing the reference lifecycle (e.g., calling `Py_DECREF` when appropriate). + * + * @return A pointer to the Python object (`PyObject*`). + * @throws PyExc_SystemError if the reference is null. */ PyObject* keep(); @@ -153,6 +190,15 @@ class JPPyObject return m_PyObject; } + /** Determine if this python reference is valid. + * + * @returns true if is a valid reference. + */ + bool isValid() const + { + return m_PyObject != nullptr; + } + /** Determine if this python reference is null. * * @returns true if null. diff --git a/native/python/pyjp_module.cpp b/native/python/pyjp_module.cpp index 32e0bedb8..305831940 100644 --- a/native/python/pyjp_module.cpp +++ b/native/python/pyjp_module.cpp @@ -1,17 +1,17 @@ /***************************************************************************** - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. - See NOTICE file for details. + See NOTICE file for details. *****************************************************************************/ #include "jpype.h" #include "pyjp.h" @@ -77,17 +77,27 @@ PyObject* _JMethodCode = nullptr; PyObject* _JObjectKey = nullptr; PyObject* _JVMNotRunning = nullptr; +// Lookup tables +static PyObject *_concreteDict = nullptr; +static PyObject *_protocolDict = nullptr; +static PyObject *_methodsDict = nullptr; + +// Speed caches +static PyObject *_cacheDict = nullptr; +static PyObject *_cacheInterfacesDict = nullptr; +static PyObject *_cacheMethodsDict = nullptr; + void PyJPModule_loadResources(PyObject* module) { // Note that if any resource is missing the user will get // the message: // - // AttributeError: module '_jpype' has no attribute 'SomeResource' - // The above exception was the direct cause of the following exception: + // AttributeError: module '_jpype' has no attribute 'SomeResource' + // The above exception was the direct cause of the following exception: // - // Traceback (most recent call last): - // File ... - // RuntimeError: JPype resource is missing + // Traceback (most recent call last): + // File ... + // RuntimeError: JPype resource is missing try { // Complete the initialization here @@ -125,6 +135,30 @@ void PyJPModule_loadResources(PyObject* module) JP_PY_CHECK(); Py_INCREF(_JMethodCode); + _concreteDict= PyObject_GetAttrString(module, "_concrete"); + JP_PY_CHECK(); + Py_INCREF(_concreteDict); + + _protocolDict = PyObject_GetAttrString(module, "_protocol"); + JP_PY_CHECK(); + Py_INCREF(_protocolDict); + + _cacheDict = PyObject_GetAttrString(module, "_cache"); + JP_PY_CHECK(); + Py_INCREF(_cacheDict); + + _cacheInterfacesDict = PyObject_GetAttrString(module, "_cache_interfaces"); + JP_PY_CHECK(); + Py_INCREF(_cacheInterfacesDict); + + _cacheMethodsDict = PyObject_GetAttrString(module, "_cache_methods"); + JP_PY_CHECK(); + Py_INCREF(_cacheMethodsDict); + + _methodsDict = PyObject_GetAttrString(module, "_methods"); + JP_PY_CHECK(); + Py_INCREF(_methodsDict); + _JObjectKey = PyCapsule_New(module, "constructor key", nullptr); } catch (JPypeException&) // GCOVR_EXCL_LINE @@ -600,6 +634,261 @@ static PyObject* PyJPModule_isPackage(PyObject *module, PyObject *pkg) JP_PY_CATCH(nullptr); // GCOVR_EXCL_LINE } +/** Code to determine what interfaces are required based on the + * dunder methods. + */ +PyObject* PyJPModule_probe(PyObject *module, PyObject *other) +{ + JP_PY_TRY("probe"); + PyTypeObject *type; + if (PyType_Check(other)) + type = (PyTypeObject*) other; + else + type = Py_TYPE(other); + + // We would start by checking the cache here + JPPyObject cached = JPPyObject::use(PyObject_GetItem(_cacheDict, (PyObject*) type)); + if (cached.isValid()) + return cached.keep(); + PyErr_Clear(); + + printf("======\n"); + printf(" Concrete: %s\n", type->tp_name); + PyObject *mro = type->tp_mro; + Py_ssize_t sz = PyTuple_Size(mro); + + JPPyObject abc = JPPyObject::call(PyImport_ImportModule("collections.abc")); // Call returns a new reference + JPPyObject abc_sequence = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Sequence")); + JPPyObject abc_mapping = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Mapping")); + JPPyObject abc_generator = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Generator")); + JPPyObject abc_iterator = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Iterator")); + JPPyObject abc_iterable = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Iterable")); + JPPyObject abc_coroutine = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Coroutine")); + JPPyObject abc_awaitable = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Awaitable")); + JPPyObject abc_set = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Set")); + JPPyObject abc_collection = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Collection")); + JPPyObject abc_container = JPPyObject::use(PyObject_GetAttrString(abc.get(), "Container")); + JPPyObject typing = JPPyObject::call(PyImport_ImportModule("typing")); // Call returns a new reference + // Buffer appears in 3.12 so will probe dunder instead + + printf(" Protocol: %s\n", type->tp_name); + bool is_callable = (type->tp_call != nullptr); + bool is_buffer = (type->tp_as_buffer != nullptr); + + bool seq_get = (type->tp_as_sequence != nullptr) && (type->tp_as_sequence->sq_item != nullptr); + bool seq_set = (type->tp_as_sequence != nullptr) && (type->tp_as_sequence->sq_ass_item != nullptr); + bool map_get = (type->tp_as_mapping != nullptr) && (type->tp_as_mapping->mp_subscript != nullptr); + bool map_set = (type->tp_as_mapping != nullptr) && (type->tp_as_mapping->mp_ass_subscript != nullptr); + + bool as_float = (type->tp_as_number != nullptr) && (type->tp_as_number->nb_float != nullptr); + bool as_int = (type->tp_as_number != nullptr) && (type->tp_as_number->nb_int != nullptr); + bool logical = (type->tp_as_number != nullptr) && ((type->tp_as_number->nb_and != nullptr) + || (type->tp_as_number->nb_or != nullptr) + || (type->tp_as_number->nb_xor != nullptr)); + bool as_matrix = (type->tp_as_number != nullptr) && (type->tp_as_number->nb_int != nullptr); + + bool as_resource = (PyObject_HasAttrString((PyObject*)type, "__enter__")!=0); + bool as_index = (PyObject_HasAttrString((PyObject*)type, "__index__")!=0); + + bool is_collection = (PyObject_IsSubclass((PyObject*)type, abc_collection.get())!=0); + bool is_container = (PyObject_IsSubclass((PyObject*)type, abc_container.get())!=0); + bool is_sequence = (PyObject_IsSubclass((PyObject*)type, abc_sequence.get())!=0); + bool is_mapping = (PyObject_IsSubclass((PyObject*)type, abc_mapping.get())!=0); + bool is_set = (PyObject_IsSubclass((PyObject*)type, abc_set.get())!=0); + bool is_generator = (PyObject_IsSubclass((PyObject*)type, abc_generator.get())!=0); + bool is_iterable = (PyObject_IsSubclass((PyObject*)type, abc_iterable.get())!=0); + bool is_iterator = (PyObject_IsSubclass((PyObject*)type, abc_iterator.get())!=0); + bool is_awaitable = (PyObject_IsSubclass((PyObject*)type, abc_awaitable.get())!=0); + bool is_coroutine = (PyObject_IsSubclass((PyObject*)type, abc_coroutine.get())!=0); + + printf(" is_callable=%d is_buffer=%d resource=%d\n", is_callable, is_buffer, as_resource); + printf(" is_generator=%d is_iterable=%d is_iterator=%d\n", is_generator, is_iterable, is_iterator); + printf(" is_seq=%d seq_get=%d seq_set=%d\n", is_sequence, seq_get, seq_set); + printf(" is_map=%d map_get=%d map_set=%d\n", is_mapping, map_get, map_set); + printf(" is_set=%d is_collection=%d is_container=%d\n", is_set, is_collection, is_container); + printf(" index=%d int=%d float=%d logical=%d matrix=%d\n", as_index, as_int, as_float, logical, as_matrix); + printf(" is_awaitable=%d is_coroutine=%d\n", is_awaitable, is_coroutine); + + JPPyObject cls; + JPPyObject interfaces = JPPyObject::accept(PyList_New(0)); + // We always add PyObject methods + PyObject *object = PyDict_GetItem(_concreteDict, (PyObject*) &PyBaseObject_Type); // borrowed + if (object != nullptr) + PyList_Append(interfaces.get(), object); + + // Add all the protocols + // For this section we do not care if a prototocol addition fails, so + // we must only clean up the extra references rather than produce an error. + if (is_callable) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "callable")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_buffer) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "buffer")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_sequence) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "sequence")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_mapping) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "mapping")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_iterable) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "iterable")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_iterator) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "iter")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_generator) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "generator")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_coroutine) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "coroutine")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_awaitable) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "awaitable")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_set) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "abstract_set")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_collection) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "collection")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (is_container) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "container")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (as_index) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "index")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + if (as_int || as_float || logical) + { + cls = JPPyObject::use(PyDict_GetItemString(_protocolDict, "number")); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + + // We look to see if there is a concrete method + if (sz > 1) + { + PyObject *primary = PyTuple_GetItem(mro, sz-2); // borrowed + cls = JPPyObject::use(PyDict_GetItem(_concreteDict, primary)); + if (cls.isValid()) + PyList_Append(interfaces.get(), cls.get()); + } + + // Convert the list of interfaces into a tuple + // ::call will throw so no need to check isValid + JPPyObject interfaces_tuple = JPPyObject::call(PyList_AsTuple(interfaces.get())); // Convert list to tuple + + // Perform lookup in _cacheInterfacesDict + PyObject* item = PyDict_GetItem(_cacheInterfacesDict, interfaces_tuple.get()); // Borrowed reference + JPPyObject existing_interfaces; + if (item == nullptr) + { + // If the tuple does not exist, insert it as both the key and value + if (PyDict_SetItem(_cacheInterfacesDict, interfaces_tuple.get(), interfaces_tuple.get()) == -1) + return nullptr; + + // Wrap the newly inserted item in JPPyObject + existing_interfaces = JPPyObject::use(interfaces_tuple.get()); + } + else + { + // Wrap the borrowed reference + existing_interfaces = JPPyObject::use(item); + } + + + // First consult the methods cache for the tuple + PyObject* methods_item = PyDict_GetItem(_cacheMethodsDict, interfaces_tuple.get()); // Borrowed reference + JPPyObject existing_methods; + if (methods_item == nullptr) + { + // Create a new dictionary for methods + JPPyObject new_methods = JPPyObject::call(PyDict_New()); + + // Iterate through the tuple and update methods + Py_ssize_t tuple_size = PyTuple_Size(interfaces_tuple.get()); + for (Py_ssize_t i = 0; i < tuple_size; ++i) + { + PyObject* interface = PyTuple_GetItem(interfaces_tuple.get(), i); // Borrowed reference + if (interface == nullptr) + return nullptr; + + PyObject* methods_item = PyDict_GetItem(_methodsDict, interface); // Borrowed reference + if (methods_item != nullptr) + { + JPPyObject methods = JPPyObject::use(methods_item); + if (PyDict_Update(new_methods.get(), methods.get()) == -1) + return nullptr; + } + } + + // Insert the new methods dictionary into _cacheMethodsDict + if (PyDict_SetItem(_cacheMethodsDict, interfaces_tuple.get(), new_methods.get()) == -1) + return nullptr; + + existing_methods = new_methods; + } + else + { + // Wrap the borrowed reference + existing_methods = JPPyObject::use(methods_item); + } + + // Create the result tuple + JPPyObject result = JPPyObject::call(PyTuple_New(2)); + if (!result.isValid()) + return nullptr; + + // Set the items in the result tuple + PyTuple_SetItem(result.get(), 0, existing_interfaces.keep()); // Steals reference + PyTuple_SetItem(result.get(), 1, existing_methods.keep()); // Steals reference + + // Insert the result into _cacheDict + if (PyObject_SetItem(_cacheDict, (PyObject*) type, result.get()) == -1) + return nullptr; + + return result.keep(); + JP_PY_CATCH(nullptr); +} #if 1 // GCOVR_EXCL_START @@ -620,28 +909,28 @@ PyObject* examine(PyObject *module, PyObject *other) { offset = PyJPValue_getJavaSlotOffset(other); printf(" Object:\n"); - printf(" size: %d\n", (int) Py_SIZE(other)); - printf(" dictoffset: %d\n", (int) ((long long) _PyObject_GetDictPtr(other)-(long long) other)); - printf(" javaoffset: %d\n", offset); + printf(" size: %d\n", (int) Py_SIZE(other)); + printf(" dictoffset: %d\n", (int) ((long long) _PyObject_GetDictPtr(other)-(long long) other)); + printf(" javaoffset: %d\n", offset); } printf(" Type: %p\n", type); - printf(" name: %s\n", type->tp_name); - printf(" typename: %s\n", Py_TYPE(type)->tp_name); - printf(" gc: %d\n", (type->tp_flags & Py_TPFLAGS_HAVE_GC) == Py_TPFLAGS_HAVE_GC); - printf(" basicsize: %d\n", (int) type->tp_basicsize); - printf(" itemsize: %d\n", (int) type->tp_itemsize); - printf(" dictoffset: %d\n", (int) type->tp_dictoffset); - printf(" weaklistoffset: %d\n", (int) type->tp_weaklistoffset); - printf(" hasJavaSlot: %d\n", PyJPValue_hasJavaSlot(type)); - printf(" getattro: %p\n", type->tp_getattro); - printf(" setattro: %p\n", type->tp_setattro); - printf(" getattr: %p\n", type->tp_getattr); - printf(" setattr: %p\n", type->tp_setattr); - printf(" alloc: %p\n", type->tp_alloc); - printf(" free: %p\n", type->tp_free); - printf(" finalize: %p\n", type->tp_finalize); + printf(" name: %s\n", type->tp_name); + printf(" typename: %s\n", Py_TYPE(type)->tp_name); + printf(" gc: %d\n", (type->tp_flags & Py_TPFLAGS_HAVE_GC) == Py_TPFLAGS_HAVE_GC); + printf(" basicsize: %d\n", (int) type->tp_basicsize); + printf(" itemsize: %d\n", (int) type->tp_itemsize); + printf(" dictoffset: %d\n", (int) type->tp_dictoffset); + printf(" weaklistoffset: %d\n", (int) type->tp_weaklistoffset); + printf(" hasJavaSlot: %d\n", PyJPValue_hasJavaSlot(type)); + printf(" getattro: %p\n", type->tp_getattro); + printf(" setattro: %p\n", type->tp_setattro); + printf(" getattr: %p\n", type->tp_getattr); + printf(" setattr: %p\n", type->tp_setattr); + printf(" alloc: %p\n", type->tp_alloc); + printf(" free: %p\n", type->tp_free); + printf(" finalize: %p\n", type->tp_finalize); long v = _PyObject_VAR_SIZE(type, 1)+(PyJPValue_hasJavaSlot(type)?sizeof (JPValue):0); - printf(" size?: %ld\n",v); + printf(" size?: %ld\n",v); printf("======\n"); return PyBool_FromLong(ret); @@ -728,6 +1017,7 @@ static PyMethodDef moduleMethods[] = { {"fault", (PyCFunction) PyJPModule_fault, METH_O, ""}, #endif {"examine", (PyCFunction) examine, METH_O, ""}, + {"probe", (PyCFunction) PyJPModule_probe, METH_O, ""}, // sentinel {nullptr} @@ -755,7 +1045,7 @@ PyMODINIT_FUNC PyInit__jpype() Py_INCREF(module); PyJPModule = module; #ifdef Py_GIL_DISABLED - PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); + PyUnstable_Module_SetGIL(module, Py_MOD_GIL_NOT_USED); #endif PyModule_AddStringConstant(module, "__version__", "1.6.0.dev0"); @@ -922,6 +1212,7 @@ static PyObject *PyJPModule_convertBuffer(JPPyBuffer& buffer, PyObject *dtype) return pcls->newMultiArray(frame, buffer, subs, base, (jobject) jdims); } + #ifdef JP_INSTRUMENTATION int PyJPModuleFault_check(uint32_t code) diff --git a/project/jpype_java/build.xml b/project/jpype_java/build.xml deleted file mode 100755 index ccb6cba85..000000000 --- a/project/jpype_java/build.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - Builds, tests, and runs the project jpype_java. - - - - diff --git a/project/jpype_java/nbproject/license.txt b/project/jpype_java/nbproject/license.txt deleted file mode 100644 index 5ea4d91ff..000000000 --- a/project/jpype_java/nbproject/license.txt +++ /dev/null @@ -1,19 +0,0 @@ -<#if licenseFirst??> -${licenseFirst}**************************************************************************** - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - - See NOTICE file for details. -<#if licenseLast??> -****************************************************************************${licenseLast} - diff --git a/project/jpype_java/nbproject/project.properties b/project/jpype_java/nbproject/project.properties deleted file mode 100755 index 3ec6b403a..000000000 --- a/project/jpype_java/nbproject/project.properties +++ /dev/null @@ -1,116 +0,0 @@ -annotation.processing.enabled=true -annotation.processing.enabled.in.editor=false -annotation.processing.processors.list= -annotation.processing.run.all.processors=true -annotation.processing.source.output=${build.generated.sources.dir}/ap-source-output -application.title=jpype_java -application.vendor= -auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.expand-tabs=true -auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.indent-shift-width=4 -auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.spaces-per-tab=4 -auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.tab-size=8 -auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-limit-width=80 -auxiliary.org-netbeans-modules-editor-indent.CodeStyle.project.text-line-wrap=none -auxiliary.org-netbeans-modules-editor-indent.CodeStyle.usedProfile=project -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.classDeclBracePlacement=NEW_LINE -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.enable-indent=true -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.expand-tabs=true -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.indent-shift-width=2 -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.methodDeclBracePlacement=NEW_LINE -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.moduleDeclBracePlacement=NEW_LINE -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.otherBracePlacement=NEW_LINE -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantForBraces=LEAVE_ALONE -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantIfBraces=LEAVE_ALONE -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.redundantWhileBraces=LEAVE_ALONE -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.spaces-per-tab=2 -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.tab-size=8 -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.text-limit-width=80 -auxiliary.org-netbeans-modules-editor-indent.text.x-java.CodeStyle.project.text-line-wrap=none -build.classes.dir=${build.dir}/classes -build.classes.excludes=**/*.java,**/*.form -# This directory is removed when the project is cleaned: -build.dir=build -build.generated.dir=${build.dir}/generated -build.generated.sources.dir=${build.dir}/generated-sources -# Only compile against the classpath explicitly listed here: -build.sysclasspath=ignore -build.test.classes.dir=${build.dir}/test/classes -build.test.results.dir=${build.dir}/test/results -# Uncomment to specify the preferred debugger connection transport: -#debug.transport=dt_socket -debug.classpath=\ - ${run.classpath} -debug.modulepath=\ - ${run.modulepath} -debug.test.classpath=\ - ${run.test.classpath} -debug.test.modulepath=\ - ${run.test.modulepath} -# Files in build.classes.dir which should be excluded from distribution jar -dist.archive.excludes= -# This directory is removed when the project is cleaned: -dist.dir=dist -dist.jar=${dist.dir}/org.jpype.jar -dist.javadoc.dir=${dist.dir}/javadoc -endorsed.classpath= -excludes= -file.reference.native-java=../../native/java -file.reference.test-harness=../../test/harness -includes=** -jar.compress=false -javac.classpath= -# Space-separated list of extra javac options -javac.compilerargs= -javac.deprecation=false -javac.external.vm=true -javac.modulepath= -javac.processormodulepath= -javac.processorpath=\ - ${javac.classpath} -javac.source=1.8 -javac.target=1.8 -javac.test.classpath=\ - ${javac.classpath}:\ - ${build.classes.dir}:\ - ${libs.testng.classpath} -javac.test.modulepath=\ - ${javac.modulepath} -javac.test.processorpath=\ - ${javac.test.classpath} -javadoc.additionalparam= -javadoc.author=false -javadoc.encoding=${source.encoding} -javadoc.html5=false -javadoc.noindex=false -javadoc.nonavbar=false -javadoc.notree=false -javadoc.private=false -javadoc.splitindex=true -javadoc.use=true -javadoc.version=false -javadoc.windowtitle= -jlink.launcher=false -jlink.launcher.name=jpype_java -meta.inf.dir=${src.dir}/META-INF -mkdist.disabled=true -platform.active=default_platform -project.licensePath=./nbproject/license.txt -run.classpath=\ - ${javac.classpath}:\ - ${build.classes.dir} -# Space-separated list of JVM arguments used when running the project. -# You may also define separate properties like run-sys-prop.name=value instead of -Dname=value. -# To set system properties for unit tests define test-sys-prop.name=value: -run.jvmargs= -run.modulepath=\ - ${javac.modulepath} -run.test.classpath=\ - ${javac.test.classpath}:\ - ${build.test.classes.dir} -run.test.modulepath=\ - ${javac.test.modulepath} -source.encoding=UTF-8 -src.java.dir=${file.reference.native-java} -test.harness.dir=${file.reference.test-harness} -test.src.dir=test -manifest.file=../../native/java/manifest.txt diff --git a/project/jpype_java/nbproject/project.xml b/project/jpype_java/nbproject/project.xml deleted file mode 100755 index 74ce17fc9..000000000 --- a/project/jpype_java/nbproject/project.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - org.netbeans.modules.java.j2seproject - - - jpype_java - - - - - - - - - - diff --git a/pyproject.toml b/pyproject.toml index d5ebbfcf6..fe8953704 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,6 +17,8 @@ maintainers = [ description = "A Python to Java bridge" readme = "README.rst" requires-python = ">=3.8" +# Not currently possible to support both 3.8 and versions going forward with deprecation warning +# https://github.com/pypa/setuptools/issues/4903 license = {text = "License :: OSI Approved :: Apache Software License"} classifiers = [ 'Programming Language :: Python :: 3', @@ -58,6 +60,7 @@ homepage = "https://github.com/jpype-project/jpype" [[tool.mypy.overrides]] module = [ "_jpype", + "_jpyne", "commonx", "brokenx", "java.*", diff --git a/setup.py b/setup.py index 1b4d02214..4a86b2964 100644 --- a/setup.py +++ b/setup.py @@ -43,6 +43,13 @@ ), platform=platform, )) +jpypeBootstrapLib = Extension(name='_jpypeb', **setupext.platform.Platform( + include_dirs=[], + sources=sorted( + list(Path('native', 'bootstrap').glob('*.cpp')) + ), + platform=platform, +)) p = [ i for i in Path("native", "jpype_module", "src", "main", "java").glob("**/*.java")] javaSrc = [i for i in p if not "exclude" in str(i)] @@ -73,5 +80,5 @@ 'sdist': setupext.sdist.BuildSourceDistribution, }, zip_safe=False, - ext_modules=[jpypeLib, jpypeJar], + ext_modules=[jpypeBootstrapLib, jpypeLib, jpypeJar], ) diff --git a/test/jpypetest/test_bridge.py b/test/jpypetest/test_bridge.py new file mode 100644 index 000000000..215f474ae --- /dev/null +++ b/test/jpypetest/test_bridge.py @@ -0,0 +1,211 @@ +# ***************************************************************************** +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# See NOTICE file for details. +# +# ***************************************************************************** +from jpype.types import * +from jpype import java +import common + +class MyTest(): + def __init__(self): + pass + +class JBridgeTestCase(common.JPypeTestCase): + """ Test for methods of java.lang.Map + """ + + def setUp(self): + common.JPypeTestCase.setUp(self) + self.fixture = JClass("jpype.common.Fixture")() + self.Bridge = JClass("org.jpype.bridge.Interpreter") + self.Backend = JClass("org.jpype.bridge.Backend") + + # Concrete + self.PyByteArray = JClass("python.lang.PyByteArray") + self.PyBytes = JClass("python.lang.PyBytes") + self.PyComplex = JClass("python.lang.PyComplex") + self.PyDict = JClass("python.lang.PyDict") + self.PyEnumerate = JClass("python.lang.PyEnumerate") + self.PyJavaObject = JClass("python.lang.PyJavaObject") + self.PyList = JClass("python.lang.PyList") + self.PyMemoryView = JClass("python.lang.PyMemoryView") + self.PyObject = JClass("python.lang.PyObject") + self.PyRange = JClass("python.lang.PyRange") + self.PySet = JClass("python.lang.PySet") + self.PySlice = JClass("python.lang.PySlice") + self.PyString = JClass("python.lang.PyString") + self.PyTuple = JClass("python.lang.PyTuple") + self.PyType = JClass("python.lang.PyType") + self.PyZip = JClass("python.lang.PyZip") + + # Protocols + self.PyIter = JClass("python.protocol.PyIter") + self.PyIterable = JClass("python.protocol.PyIterable") + self.PyAttributes = JClass("python.protocol.PyAttributes") + self.PyMapping = JClass("python.protocol.PyMapping") + self.PyNumber = JClass("python.protocol.PyNumber") + self.PySequence = JClass("python.protocol.PySequence") + + def testByteArray(self): + obj = bytearray() + self.assertEqual(self.PyByteArray._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyByteArray@obj, self.PyByteArray) + + def testBytes(self): + obj = bytes() + self.assertEqual(self.PyBytes._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyBytes@obj, self.PyBytes) + + def testComplex(self): + obj = complex(1,2) + self.assertEqual(self.PyComplex._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyComplex@obj, self.PyComplex) + + def testDict(self): + obj = {"a":1, "b":2} + self.assertEqual(self.PyDict._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyDict@obj, self.PyDict) + + def testEnumerate(self): + l = list([1,2,3]) + obj = enumerate(l) + self.assertEqual(self.PyEnumerate._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyEnumerate@obj, self.PyEnumerate) + + def testIterable(self): + obj = list() + self.assertEqual(self.PyIterable._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyIterable@obj, self.PyIterable) + + def testIter(self): + obj = iter([1,2,3,4]) + self.assertEqual(self.PyIter._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyIter@obj, self.PyIter) + + def even(s): + return s%2==0 + + def keep(s): + return True + + jobj = self.PyIter@iter([1,2,3,4]) + self.assertEqual(list(jobj.filter(even)), [2,4]) + + jobj = self.PyIter@iter([1,2,3,4]) + self.assertEqual(jobj.next(), 1) + self.assertEqual(jobj.next(), 2) + self.assertEqual(list(jobj.filter(keep)), [3,4]) + + jobj = self.PyIter@iter([1,2,3,4]) + jiter1 = jobj.iterator() +# FIXME return type issue +# l = [] +# while jiter1.hasNext(): +# jiter1.append(jiter1.next()) +# self.assertEqual(l, [1,2,3,4]) + + + + def testList(self): + obj = list() + self.assertEqual(self.PyList._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyList@obj, self.PyList) + + def testMemoryView(self): + obj = memoryview(bytes()) + self.assertEqual(self.PyMemoryView._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyMemoryView@obj, self.PyMemoryView) + + def testObject(self): + obj = object() + self.assertEqual(self.PyObject._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyObject@obj, self.PyObject) + + def testRange(self): + obj = range(5) + self.assertEqual(self.PyRange._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyRange@obj, self.PyRange) + + def testSet(self): + self.assertEqual(self.PySet._canConvertToJava(set()), "implicit") + self.assertIsInstance(self.PySet@set(), self.PySet) + + def testSlice(self): + obj = slice(1,5,None) + self.assertEqual(self.PySlice._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PySlice@obj, self.PySlice) + + def testString(self): + obj = "hello" + self.assertEqual(self.PyString._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyString@obj, self.PyString) + + def testTuple(self): + obj = (1,2,3) + self.assertEqual(self.PyTuple._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyTuple@obj, self.PyTuple) + + def testType(self): + obj = type(dict()) + self.assertEqual(self.PyType._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyType@obj, self.PyType) + + def testZip(self): + l1 = ('a','b','c') + l2 = (1,2,3) + obj = zip(l1, l2) + self.assertEqual(self.PyZip._canConvertToJava(obj), "implicit") + self.assertIsInstance(self.PyZip@obj, self.PyZip) + + def testBackend(self): + built = JClass("org.jpype.bridge.BuiltIn") + + self.assertIsInstance(built.list(java.util.Arrays.asList("A","B","C")), list) + self.assertIsInstance(built.list("A","B","C"), list) + self.assertIsInstance(built.str(self.PyString@"hello"), str) + + o = MyTest() + setattr(o, "field", "AA") + self.assertTrue(built.hasattr(o, "field")) + self.assertEqual(built.getattr(o, "field"), "AA") + built.setattr(o, "field", "BB") + self.assertEqual(o.field, "BB") + built.delattr(o, "field") + self.assertFalse(hasattr(o, "field")) + self.assertTrue(built.isinstance(int(), self.PyObject[:]((int, float)))) + self.assertFalse(built.isinstance(str(), self.PyObject[:]((int, float)))) + + #========================================================== + # Test the object behavior + + def testAttributes(self): + o = MyTest() + setattr(o, "A", 1) + setattr(o, "B", 2) + j = self.PyObject@o + a = self.PyAttributes@(j.asAttributes()) + #print(dir(a)) + self.assertEqual(a.get("A"), 1) + self.assertEqual(a.get("B"), 2) + a.set("B",3) # Note we end up with a Java Integer here + self.assertTrue(a.has("B")) + self.assertEqual(a.get("B").get(), 3) # Use get() to unwrap the Integer + self.assertFalse(a.has("C")) + self.assertEqual(a.dict(), {"A":1, "B":3}) + a.remove("B") + self.assertFalse(a.has("B")) + + diff --git a/test_probe.py b/test_probe.py new file mode 100644 index 000000000..eec0319ce --- /dev/null +++ b/test_probe.py @@ -0,0 +1,878 @@ +import jpype +import _jpype +import numpy as np +import weakref +import inspect +from jpype import JClass + +jpype.startJVM() + +############################################################################# +# Define all the Java types using JClass + +PyByteArray = JClass("python.lang.PyByteArray") +PyBytes = JClass("python.lang.PyBytes") +PyComplex = JClass("python.lang.PyComplex") +PyDict = JClass("python.lang.PyDict") +PyEnumerate = JClass("python.lang.PyEnumerate") +PyFloat = JClass("python.lang.PyFloat") +PyFrozenSet = JClass("python.lang.PyFrozenSet") +PyExc = JClass("python.lang.PyExc") +PyInt = JClass("python.lang.PyInt") +PyList = JClass("python.lang.PyList") +PyMemoryView = JClass("python.lang.PyMemoryView") +PyObject = JClass("python.lang.PyObject") +PyRange = JClass("python.lang.PyList") +PySet = JClass("python.lang.PySet") +PySlice = JClass("python.lang.PySlice") +PyString = JClass("python.lang.PyString") +PyTuple = JClass("python.lang.PyTuple") +PyType = JClass("python.lang.PyType") +PyZip = JClass("python.lang.PyZip") + +PyAbstractSet = JClass("python.lang.PyAbstractSet") +PyAwaitable = JClass("python.lang.PyAwaitable") +PyBuffer = JClass("python.lang.PyBuffer") +PyCallable = JClass("python.lang.PyCallable") +PyCollection = JClass("python.lang.PyCollection") +PyContainer = JClass("python.lang.PyContainer") +PyCoroutine = JClass("python.lang.PyCoroutine") +PyGenerator = JClass("python.lang.PyGenerator") +PyIndex = JClass("python.lang.PyIndex") +PyIter = JClass("python.lang.PyIter") +PyIterable = JClass("python.lang.PyIterable") +PyMapping = JClass("python.lang.PyMapping") +PyMutableSet = JClass("python.lang.PyMutableSet") +PyNumber = JClass("python.lang.PyNumber") +PySequence = JClass("python.lang.PySequence") +PySized = JClass("python.lang.PySized") + + +############################################################################# +# Add all of the concrete types to the _concrete interfaces list. +_jpype._concrete[bytearray] = PyByteArray +_jpype._concrete[bytes] = PyBytes +_jpype._concrete[complex] = PyComplex +_jpype._concrete[dict] = PyDict +_jpype._concrete[enumerate] = PyEnumerate +_jpype._concrete[float] = PyFloat +_jpype._concrete[frozenset] = PyFrozenSet +_jpype._concrete[BaseException] = PyExc +_jpype._concrete[int] = PyInt +_jpype._concrete[list] = PyList +_jpype._concrete[memoryview] = PyMemoryView +_jpype._concrete[object] = PyObject +_jpype._concrete[range] = PyList +_jpype._concrete[set] = PySet +_jpype._concrete[slice] = PySlice +_jpype._concrete[str] = PyString +_jpype._concrete[tuple] = PyTuple +_jpype._concrete[type] = PyType +_jpype._concrete[zip] = PyZip + +############################################################################# +# Add all of the abstract types to the _protocol interfaces list +# The key must be a string and the value a Java class +_jpype._protocol["abstract_set"] = PyAbstractSet +_jpype._protocol["awaitable"] = PyAwaitable +_jpype._protocol["buffer"] = PyBuffer +_jpype._protocol["callable"] = PyCallable +_jpype._protocol["collection"] = PyCollection +_jpype._protocol["container"] = PyContainer +_jpype._protocol["coroutine"] = PyCoroutine +_jpype._protocol["generator"] = PyGenerator +_jpype._protocol["index"] = PyIndex +_jpype._protocol["iter"] = PyIter +_jpype._protocol["iterable"] = PyIterable +_jpype._protocol["mapping"] = PyMapping +_jpype._protocol["mutable_set"] = PyMutableSet +_jpype._protocol["number"] = PyNumber +_jpype._protocol["sequence"] = PySequence +_jpype._protocol["sized"] = PySized + +############################################################################# +# This section defines all of the functions required to implement the Java +# interfaces when a builtin or class method is not sufficient. + +# Specific object for missing fields +missing = object() + +# Generic methods that aply to many Python objects +def _contains(obj, value): + return value in obj + +def _equals(x,y): + return x == y + +def _getitem(self, index): + return self[index] + +def _getitem_range(self, start, end): + return self[start:end] + +def _setitem(self, index, obj): + self[index] = obj + +def _setitem_return(self, index, obj): + old_value = self[index] + self[index] = value + return old_value + +def _delitem(self, index): + del self[index] + +def _delitem_return(self, index): + item = self[index] + del self[index] + return item + + + +# Specific methods by type +def complex_real(self): + return self.real + +def complex_imag(self): + return self.imag + +def complex_conjugate(self): + return self.imag + +def dict_pop(self, key, default_value=None): + return self.pop(key, default_value) + +def dict_pop_item(self): + if not self: + raise KeyError("popitem(): dictionary is empty") + return self.popitem() + +def dict_put(self, key, value): + rc = self.pop(key, None) + self[key] = value + return rc + +def dict_remove(self, key, value=missing): + if value is missing: + if self.get(key) == value: + del self[key] + return True + return False + return self.pop(key, None) + +def dict_update_iterable(self, iterable): + for key, value in iterable: + self[key] = value + +def frozenset_difference(self, *sets): + return self.difference(*sets) + +def frozenset_intersect(self, *sets): + return self.intersection(*sets) + +def frozenset_symmetric_difference(self, *sets): + return self.symmetric_difference(*sets) + +def iterable_map_elements(self, callable): + return map(callable, self) + +def iter_filter(self, callable): + return filter(callable, self) + +def list_add(self, e, v=missing): + if v is missing: + self.append(e) + return True + self.insert(index, element) + +def seq_index_of(self, o): + try: + return self.index(o) + except ValueError: + return -1 + +def list_insert(self, index, c): + for i, element in enumerate(c): + self.insert(index + i, element) + +def list_remove_all(self, c): + c = set(c) + original_size = len(self) + self[:] = [item for item in self if item not in c] + return len(self) != original_size + +def list_retain_all(self, c): + c = set(c) + original_size = len(self) + self[:] = [item for item in self if item in c] + return len(self) != original_size + +def list_set(self, index, element): + old_value = self[index] + self[index] = element + return old_value + +def list_sublist(self, from_index, to_index): + if from_index > to_index: + raise ValueError("fromIndex must be less than or equal to toIndex") + return self[from_index:to_index] + +def frozenset_union(self, *sets): + return self.union(*sets) + +def mapping_contains_value(self, value): + return value in self.values() + +def mapping_get(self, key, default=None): + return self.get(key, default) + +def mapping_put(self, key, value): + previous_value = self.get(key, None) + self[key] = value + return previous_value + +def mapping_put_all(self, other_mapping): + self.update(other_mapping) + +def mapping_remove(self, key): + return self.pop(key, None) + +def mapping_clear(self): + self.clear() + +def memoryview_is_read_only(self): + return self.readonly + +def memoryview_get_format(self): + return self.format + +def memoryview_get_shape(self): + return self.shape + +def memoryview_get_strides(self): + return self.strides + +def memoryview_get_sub_offsets(self): + return self.suboffsets + +def memoryview_get_buffer(self): + return self.obj # Access the underlying buffer object + +def number_add(self, other): + return self + other + +def number_add_in_place(self, other): + self += other + return self + +def number_subtract(self, other): + return self - other + +def number_subtract_in_place(self, other): + self -= other + return self + +def number_multiply(self, other): + return self * other + +def number_multiply_in_place(self, other): + self *= other + return self + +def number_divide(self, other): + return self / other + +def number_divide_in_place(self, other): + self /= other + return self + +def number_floor_divide(self, other): + return self // other + +def number_modulus(self, other): + return self % other + +def number_power(self, exponent): + return self ** exponent + +def number_negate(self): + return -self + +def number_positive(self): + return +self + +def number_compare_to(self, other): + return (self > other) - (self < other) + +def range_get_start(obj): + return obj.start + +def range_get_stop(obj): + return obj.stop + +def range_get_step(obj): + return obj.step + +def range_get_slice(obj, start, end): + return range(obj.start + start * obj.step, + obj.start + end * obj.step, + obj.step) + +def set_add(self, element): + rc = element in self + self.add(element) + return rc + +def set_difference(self, *sets): + return self.difference(*sets) + +def set_difference_update(self, *sets): + self.difference_update(*sets) + +def set_equals(self, obj): + return self == obj + +def set_intersect(self, *sets): + return self.intersection(*sets) + +def set_intersection_update(self, *sets): + self.intersection_update(*sets) + +def set_is_disjoint(self, other_set): + return self.isdisjoint(other_set) + +def set_is_superset(self, other_set): + return self.issuperset(other_set) + +def set_symmetric_difference(self, *sets): + return self.symmetric_difference(*sets) + +def set_symmetric_difference_update(self, other_set): + self.symmetric_difference_update(other_set) + +def set_union(self, *sets): + return self.union(*sets) + +def set_union_update(self, *sets): + self.update(*sets) + +def sequence_get_slice(self, *indices): + return self[tuple(*indices)] + +def slice_get_start(self): + return self.start + +def slice_get_stop(self): + return self.stop + +def slice_get_step(self): + return self.step + +def slice_indices(self, length): + return self.indices(length) + +def slice_is_valid(self): + if self.step == 0: + return False + return True + +def str_count_occurrences(self, substring, start=None, end=None): + if start is None and end is None: + return self.count(substring) + elif end is None: + return self.count(substring, start) + else: + return self.count(substring, start, end) + +def str_ends_with_suffix(self, suffix, start=None, end=None): + if start is None and end is None: + return self.endswith(suffix) + elif end is None: + return self[start:].endswith(suffix) + else: + return self[start:end].endswith(suffix) + +def str_expand_tabs_to_spaces(self, tab_size): + return self.expandtabs(tab_size) + +def str_find_last_substring(self, substring, start=None, end=None): + if start is None and end is None: + return self.rfind(substring) + elif end is None: + return self.rfind(substring, start) + else: + return self.rfind(substring, start, end) + +def str_find_substring(self, substring, start=None, end=None): + if start is None and end is None: + return self.find(substring) + elif end is None: + return self.find(substring, start) + else: + return self.find(substring, start, end) + +def str_format_using_mapping(self, mapping): + return self.format_map(mapping) + +def str_format_with(self, args, kwargs): + return self.format(*args, **kwargs) + +def str_index_of_last_substring(self, substring, start=None, end=None): + if start is None and end is None: + return self.rindex(substring) + elif end is None: + return self.rindex(substring, start) + else: + return self.rindex(substring, start, end) + +def str_index_of_substring(self, substring, start=None, end=None): + if start is None and end is None: + return self.index(substring) + elif end is None: + return self.index(substring, start) + else: + return self.index(substring, start, end) + +def str_padded_center(self, width, fill=' '): + return self.center(width, fill) + +def str_remove_leading_prefix(self, prefix): + return self.removeprefix(prefix) + +def str_remove_trailing_suffix(self, suffix): + return self.removesuffix(suffix) + +def str_replace_substring(self, old_substring, replacement, count=None): + if count is None: + return self.replace(old_substring, replacement) + else: + return self.replace(old_substring, replacement, count) + +def str_split_into(self, separator=None, max_split=-1): + return self.split(separator, max_split) + +def str_split_into_lines(self, keep_ends=False): + return self.splitlines(keep_ends) + +def str_split_into_reverse(self, separator=None, max_split=-1): + return self.rsplit(separator, max_split) + +def str_starts_with_prefix(self, prefix, start=None, end=None): + if start is None and end is None: + return self.startswith(prefix) + elif end is None: + return self.startswith(prefix, start) + else: + return self.startswith(prefix, start, end) + +def str_strip_characters(self, characters=None): + return self.strip(characters) + +def str_strip_leading(self, characters=None): + return self.lstrip(characters) + +def str_strip_trailing(self, characters=None): + return self.rstrip(characters) + +def str_strip_whitespace(self): + return self.strip() + +def str_to_encoded(self, encoding='utf-8', error_handling='strict'): + return self.encode(encoding, error_handling) + +def tuple_index_of(self, obj): + try: + return self.index(obj) + except ValueError: + return -1 + +def type_get_name(self): + return self.__name__ + +def type_mro(self): + return self.__mro__ + +def type_get_base(self): + return self.__base__ + +def type_get_bases(self): + return self.__bases__ + +def type_is_subclass_of(self, type): + return issubclass(self, type) + +def type_is_instance(self, obj): + return isinstance(obj, self) + +def type_get_method(self, name): + return getattr(self, name, None) + +def type_is_abstract(self): + from abc import ABCMeta + return isinstance(self, ABCMeta) + +def type_get_subclasses(self): + return self.__subclasses__() + +def _call(obj, args, kwargs): + if kwargs is not None: + return obj(*args, **kwargs) + return obj(*args) + +def _delattr_return(obj, key): + return var(obj).pop(key, None) + +def _delattr(obj, key): + delattr(obj, key) + +def dict_keys(obj): + return obj.keys() + +def dict_values(obj): + return obj.keys() + +def dict_iterable(obj, iterable): + out = dict() + out.update(iterable) + return out + +def object_doc(obj): + return __doc__ + +def callable_signature(obj): + return inspect.signature(obj) + +def type_isinstance(obj, types): + return isinstance(obj, tuple(types)) + +def dict_items(self): + return self.items() + +backend_methods = { + "bytearray": bytearray, + "bytearrayFromHex": bytearray.fromhex, + "bytes": bytes, + "bytesFromHex": bytes.fromhex, + "call": _call, + "contains": _contains, + "delitemByIndex": _delitem, + "delitemByObject": _delitem, + "delattrReturn": _delattr_return, + "delattrString": delattr, + "dir": dir, + "enumerate": enumerate, + "eval": eval, + "exec": exec, + "getDict": vars, + "getDocString": object_doc, + "getSignature": callable_signature, + "getattrDefault": getattr, + "getattrObject": getattr, + "getattrString": getattr, + "getitemMappingObject": _getitem, + "getitemMappingString": _getitem, + "getitemSequence": _getitem, + "hasattrString": hasattr, + "isCallable": callable, + "isinstanceFromArray": type_isinstance, + "items": dict_items, + "keys": dict_keys, + "len": len, + "list": list, + "memoryview": memoryview, + "newByteArray": bytearray, + "newByteArrayFromBuffer": bytes, + "newByteArrayFromIterable": bytearray, + "newByteArrayOfSize": bytearray, + "newBytesOfSize": bytes, + "newComplex": complex, + "newDict": dict, + "newDictFromIterable": dict, + "newEnumerate": enumerate, + "newFloat": float, + "newFrozenSet": frozenset, + "newInt": int, + "newList": list, + "newListFromIterable": list, + "newSet": set, + "newSetFromIterable": set, + "newTuple": tuple, + "newTupleFromIterator": tuple, + "newZip": zip, +} + +############################################################################# +# This section builds dictionaries to map from the Java interfaces to the +# Python implementation. If the behavior of the Java method matches that in +# Python both for arguments and for the return value, then a reference to the +# class method is used. If the arguments and return match a Python builtin +# function then the Python builtin is used. If either the arguments or the +# return do not match then a Python implementation of the function must be +# implemented. +# +# Only non-default methods appear in the methods dictionaries. + +# Define method dictionaries +_jpype._methods[PyByteArray] = { + "decode": bytearray.decode, + "translate": bytearray.translate, +} +_jpype._methods[PyBytes] = { + "decode": bytes.decode, + "translate": bytes.translate, +} +_jpype._methods[PyComplex] = { + "real": complex_real, + "imag": complex_imag, + "conjugate": complex_conjugate, +} +# Define the dictionary for PyDict methods +_jpype._methods[PyDict] = { + "clear": dict.clear, + "containsKey": _contains, + "containsValue": mapping_contains_value, + "entrySet": dict_items, + "get": mapping_get, + "getOrDefault": mapping_get, + "keySet": dict_keys, + "pop": dict_pop, + "popItem": dict_pop_item, + "put": dict_put, + "putAny": dict_put, + "putAll": dict.update, + "remove": dict_remove, + "setDefault": dict.setdefault, + "update": dict.update, + "updateIterable": dict_update_iterable, +} +_jpype._methods[PyEnumerate] = {} # No java methods +_jpype._methods[PyFloat] = {} # No Java methods +_jpype._methods[PyFrozenSet] = { + "copy": frozenset.copy, + "difference": frozenset_difference, + "intersect": frozenset_intersect, + "isDisjoint": frozenset.isdisjoint, + "isSubset": frozenset.issubset, + "isSuperset": frozenset.issuperset, + "symmetricDifference": frozenset_symmetric_difference, + "union": frozenset_union, +} +_jpype._methods[PyExc] = {} # No java methods +_jpype._methods[PyInt] = {} # No java methods +_jpype._methods[PyList] = { + "add": list_add, + "addAny": list_add, + "clear": list.clear, + "contains": _contains, + "extend": list.extend, + "get": _getitem, + "indexOf": seq_index_of, + "insert": list_insert, + "removeAll": list_remove_all, + "retainAll": list_retain_all, + "set": list_set, + "setAny": _setitem, + "subList": list_sublist, +} +_jpype._methods[PyMemoryView] = { + "getSlice": _getitem_range, + "release": memoryview.release, + "isReadOnly": memoryview_is_read_only, + "getFormat": memoryview_get_format, + "getShape": memoryview_get_shape, + "getStrides": memoryview_get_strides, + "getSubOffsets": memoryview_get_sub_offsets, + "getBuffer": memoryview_get_buffer, +} +_jpype._methods[PyMemoryView] = {} +_jpype._methods[PyObject] = { + "hashCode": hash, + "equals": _equals, + "toString": str, +} +_jpype._methods[PyRange] = { + "getStart": range_get_start, + "getStop": range_get_stop, + "getStep": range_get_step, + "getLength": len, + "getItem": _getitem, + "getSlice": range_get_slice, + "contains": _contains, + "toList": list, +} +_jpype._methods[PySet] = { + "add": set_add, + "addAny": set.add, + "clear": set.clear, + "contains": _contains, + "copy": set.copy, + "difference": set_difference, + "differenceUpdate": set_difference_update, + "discard": set.discard, + "equals": _equals, + "hashCode": hash, + "intersect": set_intersect, + "intersectionUpdate": set_intersection_update, + "isDisjoint": set.isdisjoint, + "isSubset": set.issubset, + "isSuperset": set.issuperset, + "pop": set.pop, + "size": len, +# "symmetricDifference": set_symmetric_difference, +# "symmetricDifferenceUpdate": set.symmetric_difference, + "toList": list, + "union": set_union, + "unionUpdate": set_union_update, + "update": set.update, +} +_jpype._methods[PySlice] = { + "getStart": slice_get_start, + "getStop": slice_get_stop, + "getStep": slice_get_step, + "indices": slice_indices, + "isValid": slice_is_valid, +} +_jpype._methods[PyString] = { + "charAt": _getitem, + "containsSubstring": _contains, + "countOccurrences": str_count_occurrences, + "endsWithSuffix": str_ends_with_suffix, + "expandTabs": str.expandtabs, + "findLastSubstring": str_find_last_substring, + "findSubstring": str_find_substring, + "formatUsingMapping": str.format_map, + "formatWith": str_format_with, + "getCharacterAt": _getitem, + "indexOfLastSubstring": str_index_of_last_substring, + "indexOfSubstring": str_index_of_substring, + "isAlphabetic": str.isalpha, + "isAlphanumeric": str.isalnum, + "isAsciiCharacters": str.isascii, + "isDecimalNumber": str.isdecimal, + "isDigitCharacters": str.isdigit, + "isLowercase": str.islower, + "isNumericCharacters": str.isnumeric, + "isPrintableCharacters": str.isprintable, + "isTitleCase": str.istitle, + "isUppercase": str.isupper, + "isValidIdentifier": str.isidentifier, + "isWhitespace": str.isspace, + "join": str.join, + "length": len, + "removePrefix": str.removeprefix, + "removeSuffix": str.removesuffix, + "replaceSubstring": str_replace_substring, + "splitInto": str_split_into, + "splitIntoLines": str_split_into_lines, + "splitIntoPartition": str.partition, + "splitIntoReverse": str_split_into_reverse, + "splitIntoReversePartition": str.rpartition, + "startsWithPrefix": str_starts_with_prefix, + "stripCharacters": str_strip_characters, + "stripLeading": str_strip_leading, + "stripTrailing": str_strip_trailing, + "stripWhitespace": str.strip, + "subSequence": _getitem_range, + "swapCaseCharacters": str.swapcase, + "toCapitalized": str.capitalize, + "toCaseFolded": str.casefold, + "toEncoded": str_to_encoded, + "toTitleCase": str.title, + "toUppercase": str.upper, + "translateUsingMapping": str.translate, + "zeroFill": str.zfill, +} +_jpype._methods[PyTuple] = { + "get": _getitem, + "indexOf": seq_index_of, + "subList": _getitem_range, +} +_jpype._methods[PyTuple] = {} +_jpype._methods[PyType] = { + "getName": type_get_name, + "mro": type_mro, + "getBase": type_get_base, + "getBases": type_get_bases, + "isSubclassOf": type_is_subclass_of, + "isInstance": type_is_instance, + "getMethod": type_get_method, + "isAbstract": type_is_abstract, + "getSubclasses": type_get_subclasses, +} +_jpype._methods[PyZip] = { + "toList": list, +} + +_jpype._methods[PyAbstractSet] ={} # No Java methods +_jpype._methods[PyAwaitable] ={} # No Java methods +_jpype._methods[PyBuffer] ={} # No Java methods +_jpype._methods[PyCallable] ={} # No Java methods +_jpype._methods[PyCollection] ={} # No Java methods +_jpype._methods[PyContainer] ={} # No Java methods +_jpype._methods[PyCoroutine] ={} # No Java methods +_jpype._methods[PyGenerator] ={ + "iter": iter, +} +_jpype._methods[PyIndex] ={} # No Java methods +_jpype._methods[PyIter] = { + "filter": iter_filter, +} +_jpype._methods[PyIterable] = { + "allMatch": all, + "anyMatch": any, + "mapElements": iterable_map_elements, + "findMax": max, + "findMin": min, + "getSorted": sorted, + "computeSum": sum, + "iter": iter, +} +_jpype._methods[PyMapping] = { + "containsValue": mapping_contains_value, + "containsKey": _contains, + "putAll": mapping_put_all, + "remove": mapping_remove, +} +_jpype._methods[PyMutableSet] ={} # No Java methods +_jpype._methods[PyNumber] = { + "add": number_add, + "addInPlace": number_add_in_place, + "subtract": number_subtract, + "subtractInPlace": number_subtract_in_place, + "multiply": number_multiply, + "multiplyInPlace": number_multiply_in_place, + "divide": number_divide, + "divideInPlace": number_divide_in_place, + "floorDivide": number_floor_divide, + "modulus": number_modulus, + "power": number_power, + "negate": number_negate, + "abs": abs, + "positive": number_positive, + "toBoolean": bool, + "toDouble": float, + "toInteger": int, + "compareTo": number_compare_to, +} +_jpype._methods[PySequence] = { + "remove": _delitem_return, + "set": _setitem_return, + "setAny": _setitem, +} +_jpype._methods[PySized] ={} # No Java methods + +################################### +# Testing probe API +#_jpype.probe(object) +#_jpype.probe(slice) +print(_jpype.probe(list)) +_jpype.probe(tuple) +_jpype.probe(set) +_jpype.probe({}) +_jpype.probe(weakref.WeakKeyDictionary) +#_jpype.probe(iter([])) +#_jpype.probe(enumerate) +_jpype.probe(np.array([])) +_jpype.probe(int) +_jpype.probe(np.int32) +_jpype.probe(float) +_jpype.probe(np.float32)