Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
KOLANICH committed Oct 13, 2023
0 parents commit 8e36e85
Show file tree
Hide file tree
Showing 21 changed files with 565 additions and 0 deletions.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
indent_style = tab
indent_size = 4
insert_final_newline = true
end_of_line = lf

[*.{yml,yaml}]
indent_style = space
indent_size = 2
1 change: 1 addition & 0 deletions .github/.templateMarker
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
KOLANICH/python_project_boilerplate.py
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
version: 2
updates:
- package-ecosystem: "pip"
directory: "/"
schedule:
interval: "daily"
allow:
- dependency-type: "all"
15 changes: 15 additions & 0 deletions .github/workflows/CI.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]

jobs:
build:
runs-on: ubuntu-22.04
steps:
- name: typical python workflow
uses: KOLANICH-GHActions/typical-python-workflow@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__pycache__
*.pyc
*.pyo
*.egg-info
build
dist
.eggs
/monkeytype.sqlite3
/*.srctrldb
/*.srctrlbm
/*.srctrlprj
14 changes: 14 additions & 0 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#image: pypy:latest
image: registry.gitlab.com/kolanich-subgroups/docker-images/fixed_python:latest

variables:
DOCKER_DRIVER: overlay2
SAST_ANALYZER_IMAGE_TAG: latest
SAST_DISABLE_DIND: "true"
SAST_CONFIDENCE_LEVEL: 5
CODECLIMATE_VERSION: latest

include:
- template: SAST.gitlab-ci.yml
- template: Code-Quality.gitlab-ci.yml
- template: License-Management.gitlab-ci.yml
1 change: 1 addition & 0 deletions Code_Of_Conduct.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
No codes of conduct! Just do what you feel is right and say what you feel is right using the language you feel is right. If you feel that it is right to [make an own fork with a CoCs and SJWs](https://en.wikipedia.org/wiki/Bender_Rodriguez), just do that. We here are doing the work, not accusing each other in violating codes of conduct.
126 changes: 126 additions & 0 deletions JAbs/JVMInitializer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import typing
from abc import ABC, abstractmethod
from pathlib import Path

from .utils.pathListTools import ClassesImportSpecT, ClassPathT, appendPathsList, dedupPreservingOrder, getTupleFromPathProperty, pathsList2String


class JVMInitializer(ABC):
#__slots__ = ("sys", ) # we inject loaded classes right into this class

classPathPropertyName = "sys.class.path"
classPathPropertyName = "java.class.path" # new
libPathPropertyName = "java.library.path"

def __init__(self, classPaths: ClassPathT, classes2import: ClassesImportSpecT, *, libPaths=None) -> None:
self.prepareJVM()
self.sys = self.loadClass("java.lang.System")
classPaths = list(classPaths)
self.appendClassPath(classPaths)
self.loadClasses(classes2import)

class _Implements(type):
"""Used as a metaclass to wrap python classes implementing interfaces defined in JVM code"""

__slots__ = ()

def _Override(self, meth: typing.Callable) -> typing.Callable:
"""Used as a decorator to wrap python methods overriding methods defined in JVM code"""
return meth

@abstractmethod
def selectJVM(self) -> Path:
"""Returns Path to libjvm.so"""
raise NotImplementedError()

@abstractmethod
def prepareJVM(self):
"""Starts JVM and sets its settings"""
raise NotImplementedError()

def reflClass2Class(self, cls) -> typing.Any: # pylint: disable=no-self-use
"""Transforms a reflection object for a class into an object usable by python"""
return cls

def reflectClass(self, cls) -> typing.Any:
"""Transforms a a class into a reflection object for a class"""
raise NotImplementedError

@abstractmethod
def loadClass(self, name: str) -> typing.Any:
"""Returns a class"""
raise NotImplementedError()

def getSysPropsDict(self):
return {str(k): str(self.sys.getProperty(k)) for k in self.sys.getProperties()}

@property
def libPathStr(self) -> str:
"""libpath string"""
return str(self.sys.getProperty(self.__class__.libPathPropertyName))

@libPathStr.setter
def libPathStr(self, classPath: str) -> None:
self.sys.setProperty(self.__class__.libPathPropertyName, classPath)

@property
def classPathStr(self) -> str:
"""classpath string"""
return str(self.sys.getProperty(self.__class__.classPathPropertyName))

@classPathStr.setter
def classPathStr(self, classPath: str) -> None:
self.sys.setProperty(self.__class__.classPathPropertyName, classPath)

@property
def classPath(self) -> typing.Iterable[str]:
"""classpath string separated into paths"""
return getTupleFromPathProperty(self.classPathStr)

@classPath.setter
def classPath(self, classPaths: ClassPathT) -> None:
self.classPathStr = pathsList2String(classPaths)

def appendClassPath(self, classPaths: ClassPathT) -> None:
"""Adds a jar into classpath"""
self.classPath = appendPathsList(classPaths, self.classPath)

@property
def libPath(self) -> typing.Iterable[str]:
"""classpath string separated into paths"""
return getTupleFromPathProperty(self.libPathStr)

@libPath.setter
def libPath(self, libPaths: ClassPathT) -> None:
self.libPathStr = pathsList2String(libPaths)

def appendLibPath(self, libPaths: ClassPathT) -> None:
"""Adds a lib into libpath"""
self.libPath = appendPathsList(libPaths, self.libPath)

def loadClasses(self, classes2import: ClassesImportSpecT) -> None:
"""Loads the classes that are required to be loaded and injects them into `self`, using the last component of a path as a property. Processes single `$` within paths correctly."""
if isinstance(classes2import, (list, tuple)):
newSpec = {}
for el in classes2import:
if isinstance(el, tuple):
name = el[1]
path = el[0]
else:
name = el.split(".")[-1]
path = el

nameDollarSplitted = name.split("$")
if len(nameDollarSplitted) == 2:
name = nameDollarSplitted[1]
elif len(nameDollarSplitted) > 2:
raise ValueError(name)

newSpec[name] = path

classes2import = newSpec

for name, className in newSpec.items():
setattr(self, name, self.loadClass(className))
else:
raise ValueError("`classes2import` have wrong type", classes2import)
11 changes: 11 additions & 0 deletions JAbs/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
__all__ = ("SelectedJVMInitializer", "ClassPathT", "ClassesImportSpecT")
import sys
from importlib import import_module

from .utils.pathListTools import ClassesImportSpecT, ClassPathT

implPkgNameMapping = {"cpython": "JPype", "graalpython": "GraalVM"}

implPkgName = implPkgNameMapping[sys.implementation.name]
pkg = import_module(".impls." + implPkgName, __package__)
SelectedJVMInitializer = pkg.SelectedJVMInitializer
22 changes: 22 additions & 0 deletions JAbs/impls/GraalVM.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from pathlib import Path

import java # pylint: disable=import-error

from ..JVMInitializer import JVMInitializer


class GraalVMInitializer(JVMInitializer):
__slots__ = ("ClassLoader", "_systemClassLoader")

def prepareJVM(self):
self.ClassLoader = java.type("java.lang.ClassLoader")
self._systemClassLoader = self.ClassLoader.getSystemClassLoader()

def selectJVM(self) -> Path: # pylint: disable=no-self-use
return None

def loadClass(self, name: str):
return self._systemClassLoader.loadClass(name)


SelectedJVMInitializer = GraalVMInitializer
139 changes: 139 additions & 0 deletions JAbs/impls/JPype.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import typing
import warnings
from pathlib import Path

import _jpype
import jpype
import jpype.beans

from ..JVMInitializer import JVMInitializer
from ..utils.pathListTools import ClassesImportSpecT, ClassPathT, appendPathsList, getTupleFromPathProperty, pathsList2String

ji = None


class RootClassLoaderWrapper:
__slots__ = ("cl", "children")

def __init__(self, cl):
self.cl = cl
self.children = {}

def free(self):
del self.cl


class ClassLoaderWrapper(RootClassLoaderWrapper):
__slots__ = ("cl", "children", "parent")

def __init__(self, cl, parent):
super().__init__(cl)
self.parent = parent
parent.children[id(cl)] = self

def free(self):
if self.children:
raise ValueError("Cannot free a loader with children")

del self.parent[id(self.cl)]
super().free()


class _JPypeInitializer(JVMInitializer):
__slots__ = ("_allowShutdown", "_libPaths")
classPathPropertyName = "java.class.path"

_defaultLibsPaths = None

def __init__(self, classPaths: ClassPathT, classes2import: ClassesImportSpecT, *, libPaths=None, _allowShutdown: bool = False) -> None:
self._allowShutdown = _allowShutdown
self._libPaths = libPaths
if _allowShutdown:
warnings.warn("`_allowShutdown` was used to allow `jpype.shutdownJVM`. See https://jpype.readthedocs.io/en/latest/userguide.html#unloading-the-jvm and https://github.com/jpype-project/jpype/blob/master/native/common/jp_context.cpp#L290")

# these ones are defered. Before JVM is initialized they are accumulated by JPipe itself. They are not the same as loaded in runtime. Ones loaded in runtime may have various conflicts because of different classloaders.
for cp in classPaths:
jpype._classpath.addClassPath(cp.absolute())

# because JPype accumulates them itself, we put here nothing
super().__init__([], classes2import)

def selectJVM(self) -> Path:
return Path(jpype.getDefaultJVMPath())

@property
def classPath(self, classPath: ClassPathT) -> None:
return super().classPath

@classPath.setter
def classPath(self, classPath: ClassPathT) -> None:
raise NotImplementedError("For this backend redefining classpath is not supported, use `appendClassPath`")

def appendClassPath(self, classPaths: ClassPathT) -> None:
for cp in classPaths:
jpype._classpath.addClassPath(cp.absolute())

def appendLibPath(self, libPaths: ClassPathT) -> None:
raise NotImplementedError("Unavailable in JPype. Restart the JVM.")

def loadClass(self, name: str):
res = jpype.JClass(name)

assert isinstance(res, jpype._jpype._JClass), "Class `" + repr(name) + "` is not loaded (res.__class__ == " + repr(res.__class__) + "), it's JPype drawback that when something is missing it returns a `jpype._jpackage.JPackage`, that errors only when one tries to instantiate it as a class" # pylint: disable=c-extension-no-member,protected-access
return res

def reflClass2Class(self, cls) -> typing.Any: # pylint: disable=no-self-use
return jpype.types.JClass(cls)

def prepareJVM(self) -> None:
if jpype.isJVMStarted():
warnings.warn("JPype disallows starting multiple JVMs or restarting it. Assuming that JVM is already started with needed arguments, such as classpath.")
if not self._allowShutdown:
return
jpype.shutdownJVM()

# WARNING
# 1. sys.class.path doesn't work
# 2. only classpath set via "-Djava.class.path=" takes effect, https://github.com/jpype-project/jpype/issues/177
# 3. only libpath set at start up via "-Djava.library.path=" takes effect
args = [jpype.getDefaultJVMPath(), "-ea"]
if self._libPaths:
if self.__class__._defaultLibsPaths is None:
from ..utils.javaPropsInASeparateProcess import getDefaultJavaPropsFromSubprocess

defaultProps = getDefaultJavaPropsFromSubprocess(self)
self.__class__._defaultLibsPaths = getTupleFromPathProperty(defaultProps.get(self.__class__.libPathPropertyName, ""))

libPaths = appendPathsList(self.__class__._defaultLibsPaths, self._libPaths)
args.append("-D" + self.__class__.libPathPropertyName + "=" + pathsList2String(libPaths))

jpype.startJVM(*args, convertStrings=False, ignoreUnrecognized=False)

def reflectClass(self, cls) -> typing.Any:
return cls.class_

@staticmethod
def _Implements(className: str, parents: typing.Tuple[typing.Type, ...], attrs: typing.Dict[str, typing.Any]):
interface = parents[0]
res = type(className, (), attrs)
dec = jpype.JImplements(interface)
return dec(res)

_Override = staticmethod(jpype.JOverride)


class JPypeInitializer(_JPypeInitializer):
__slots__ = ()

def __new__(cls, classPaths: ClassPathT, classes2import: ClassesImportSpecT, *args, **kwargs):
global ji
if ji is None:
ji = _JPypeInitializer(classPaths, classes2import, *args, **kwargs)
else:
ji.appendClassPath(classPaths)
ji.loadClasses(classes2import)

return ji


SelectedJVMInitializer = JPypeInitializer
Empty file added JAbs/impls/__init__.py
Empty file.
Empty file added JAbs/py.typed
Empty file.
Empty file added JAbs/utils/__init__.py
Empty file.
18 changes: 18 additions & 0 deletions JAbs/utils/getClasses.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
__all__ = ("extractClassesFromAJar",)

import typing
import zipfile
from pathlib import Path, PurePath


def _extractClassesFromAJar(jarPath: Path) -> typing.Iterator[typing.Tuple[str, ...]]:
classExt = ".class"
with zipfile.ZipFile(jarPath) as z:
for f in z.infolist():
if f.filename.endswith(classExt):
path = PurePath(f.filename)
yield path.parts[:-1] + (path.stem,)


def extractClassesFromAJar(jarPath: Path) -> typing.Any:
return tuple(sorted(_extractClassesFromAJar(jarPath)))
Loading

0 comments on commit 8e36e85

Please sign in to comment.