调整版本并做测试
This commit is contained in:
36
pyenjoy/__init__.py
Normal file
36
pyenjoy/__init__.py
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
py_enjoy - Python implementation of JFinal Template Engine
|
||||
"""
|
||||
|
||||
__version__ = "1.0.1"
|
||||
__author__ = "James Zhan 詹波 (original), Python port by mrzhou@miw.cn"
|
||||
__license__ = "Apache License 2.0"
|
||||
|
||||
# Export main classes
|
||||
from .kit.StrKit import StrKit
|
||||
from .kit.HashKit import HashKit
|
||||
from .kit.Kv import Kv
|
||||
from .kit.Okv import Okv
|
||||
from .kit.Prop import Prop
|
||||
from .kit.PropKit import PropKit
|
||||
from .kit.TypeKit import TypeKit
|
||||
from .kit.TimeKit import TimeKit
|
||||
from .template.Engine import Engine
|
||||
from .template.Template import Template
|
||||
from .template.Directive import Directive
|
||||
|
||||
__all__ = [
|
||||
"StrKit",
|
||||
"HashKit",
|
||||
"Kv",
|
||||
"Okv",
|
||||
"Prop",
|
||||
"PropKit",
|
||||
"TypeKit",
|
||||
"TimeKit",
|
||||
"Engine",
|
||||
"Template",
|
||||
"Directive",
|
||||
]
|
||||
86
pyenjoy/kit/Func.py
Normal file
86
pyenjoy/kit/Func.py
Normal file
@@ -0,0 +1,86 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Func - Lambda Function Utilities
|
||||
"""
|
||||
|
||||
from typing import Callable, TypeVar, Generic
|
||||
|
||||
T = TypeVar('T')
|
||||
U = TypeVar('U')
|
||||
V = TypeVar('V')
|
||||
W = TypeVar('W')
|
||||
X = TypeVar('X')
|
||||
Y = TypeVar('Y')
|
||||
Z = TypeVar('Z')
|
||||
R = TypeVar('R')
|
||||
|
||||
class F00:
|
||||
"""0 parameter 0 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[], None]):
|
||||
"""Execute function with no parameters and no return"""
|
||||
func()
|
||||
|
||||
class F10(Generic[T]):
|
||||
"""1 parameter 0 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[T], None], t: T):
|
||||
"""Execute function with one parameter and no return"""
|
||||
func(t)
|
||||
|
||||
class F20(Generic[T, U]):
|
||||
"""2 parameter 0 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[T, U], None], t: T, u: U):
|
||||
"""Execute function with two parameters and no return"""
|
||||
func(t, u)
|
||||
|
||||
class F30(Generic[T, U, V]):
|
||||
"""3 parameter 0 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[T, U, V], None], t: T, u: U, v: V):
|
||||
"""Execute function with three parameters and no return"""
|
||||
func(t, u, v)
|
||||
|
||||
class F40(Generic[T, U, V, W]):
|
||||
"""4 parameter 0 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[T, U, V, W], None], t: T, u: U, v: V, w: W):
|
||||
"""Execute function with four parameters and no return"""
|
||||
func(t, u, v, w)
|
||||
|
||||
class F01(Generic[R]):
|
||||
"""0 parameter 1 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[], R]) -> R:
|
||||
"""Execute function with no parameters and return value"""
|
||||
return func()
|
||||
|
||||
class F11(Generic[T, R]):
|
||||
"""1 parameter 1 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[T], R], t: T) -> R:
|
||||
"""Execute function with one parameter and return value"""
|
||||
return func(t)
|
||||
|
||||
class F21(Generic[T, U, R]):
|
||||
"""2 parameter 1 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[T, U], R], t: T, u: U) -> R:
|
||||
"""Execute function with two parameters and return value"""
|
||||
return func(t, u)
|
||||
|
||||
class F31(Generic[T, U, V, R]):
|
||||
"""3 parameter 1 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[T, U, V], R], t: T, u: U, v: V) -> R:
|
||||
"""Execute function with three parameters and return value"""
|
||||
return func(t, u, v)
|
||||
|
||||
class F41(Generic[T, U, V, W, R]):
|
||||
"""4 parameter 1 return function"""
|
||||
@staticmethod
|
||||
def call(func: Callable[[T, U, V, W], R], t: T, u: U, v: V, w: W) -> R:
|
||||
"""Execute function with four parameters and return value"""
|
||||
return func(t, u, v, w)
|
||||
107
pyenjoy/kit/HashKit.py
Normal file
107
pyenjoy/kit/HashKit.py
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal HashKit - Hash and Encryption Utilities
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
import threading
|
||||
|
||||
class HashKit:
|
||||
"""Hash utility class"""
|
||||
|
||||
FNV_OFFSET_BASIS_64 = 0xcbf29ce484222325
|
||||
FNV_PRIME_64 = 0x100000001b3
|
||||
|
||||
_HEX_DIGITS = "0123456789abcdef"
|
||||
_CHAR_ARRAY = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
@staticmethod
|
||||
def fnv1a64(key: str) -> int:
|
||||
"""FNV-1a 64-bit hash"""
|
||||
hash_val = HashKit.FNV_OFFSET_BASIS_64
|
||||
for i in range(len(key)):
|
||||
hash_val ^= ord(key[i])
|
||||
hash_val *= HashKit.FNV_PRIME_64
|
||||
return hash_val
|
||||
|
||||
@staticmethod
|
||||
def md5(src_str: str) -> str:
|
||||
"""MD5 hash"""
|
||||
return HashKit._hash("md5", src_str)
|
||||
|
||||
@staticmethod
|
||||
def sha1(src_str: str) -> str:
|
||||
"""SHA-1 hash"""
|
||||
return HashKit._hash("sha1", src_str)
|
||||
|
||||
@staticmethod
|
||||
def sha256(src_str: str) -> str:
|
||||
"""SHA-256 hash"""
|
||||
return HashKit._hash("sha256", src_str)
|
||||
|
||||
@staticmethod
|
||||
def sha384(src_str: str) -> str:
|
||||
"""SHA-384 hash"""
|
||||
return HashKit._hash("sha384", src_str)
|
||||
|
||||
@staticmethod
|
||||
def sha512(src_str: str) -> str:
|
||||
"""SHA-512 hash"""
|
||||
return HashKit._hash("sha512", src_str)
|
||||
|
||||
@staticmethod
|
||||
def _hash(algorithm: str, src_str: str) -> str:
|
||||
"""Generic hash function"""
|
||||
try:
|
||||
md = hashlib.new(algorithm)
|
||||
md.update(src_str.encode('utf-8'))
|
||||
return HashKit.to_hex(md.digest())
|
||||
except Exception as e:
|
||||
raise RuntimeError(e)
|
||||
|
||||
@staticmethod
|
||||
def to_hex(bytes_data: bytes) -> str:
|
||||
"""Convert bytes to hex string"""
|
||||
ret = []
|
||||
for b in bytes_data:
|
||||
ret.append(HashKit._HEX_DIGITS[(b >> 4) & 0x0f])
|
||||
ret.append(HashKit._HEX_DIGITS[b & 0x0f])
|
||||
return ''.join(ret)
|
||||
|
||||
@staticmethod
|
||||
def generate_salt(salt_length: int) -> str:
|
||||
"""Generate random salt"""
|
||||
salt = []
|
||||
for i in range(salt_length):
|
||||
salt.append(secrets.choice(HashKit._CHAR_ARRAY))
|
||||
return ''.join(salt)
|
||||
|
||||
@staticmethod
|
||||
def generate_salt_for_sha256() -> str:
|
||||
"""Generate salt for SHA-256"""
|
||||
return HashKit.generate_salt(32)
|
||||
|
||||
@staticmethod
|
||||
def generate_salt_for_sha512() -> str:
|
||||
"""Generate salt for SHA-512"""
|
||||
return HashKit.generate_salt(64)
|
||||
|
||||
@staticmethod
|
||||
def generate_random_str(str_length: int) -> str:
|
||||
"""Generate random string"""
|
||||
return HashKit.generate_salt(str_length)
|
||||
|
||||
@staticmethod
|
||||
def slow_equals(a: bytes, b: bytes) -> bool:
|
||||
"""Timing-safe byte comparison"""
|
||||
if a is None or b is None:
|
||||
return False
|
||||
if len(a) != len(b):
|
||||
return False
|
||||
|
||||
diff = 0
|
||||
for i in range(len(a)):
|
||||
diff |= a[i] ^ b[i]
|
||||
return diff == 0
|
||||
75
pyenjoy/kit/JavaKeyword.py
Normal file
75
pyenjoy/kit/JavaKeyword.py
Normal file
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal JavaKeyword - Java Keyword Detection
|
||||
"""
|
||||
|
||||
from typing import Set, FrozenSet
|
||||
|
||||
class JavaKeyword:
|
||||
"""Java keyword detection utility"""
|
||||
|
||||
_KEYWORD_SET: FrozenSet[str] = frozenset({
|
||||
"abstract", "assert", "boolean", "break", "byte", "case", "catch",
|
||||
"char", "class", "const", "continue", "default", "do", "double",
|
||||
"else", "enum", "extends", "final", "finally", "float", "for",
|
||||
"goto", "if", "implements", "import", "instanceof", "int",
|
||||
"interface", "long", "native", "new", "package", "private",
|
||||
"protected", "public", "return", "strictfp", "short", "static",
|
||||
"super", "switch", "synchronized", "this", "throw", "throws",
|
||||
"transient", "try", "void", "volatile", "while"
|
||||
})
|
||||
|
||||
_instance = None
|
||||
|
||||
@staticmethod
|
||||
def get_instance():
|
||||
"""Get shared instance"""
|
||||
if JavaKeyword._instance is None:
|
||||
JavaKeyword._instance = JavaKeyword()
|
||||
return JavaKeyword._instance
|
||||
|
||||
def __init__(self):
|
||||
self._keywords = set(JavaKeyword._KEYWORD_SET)
|
||||
# Make it read-only by default
|
||||
self._read_only = True
|
||||
|
||||
def add_keyword(self, keyword: str) -> 'JavaKeyword':
|
||||
"""Add custom keyword"""
|
||||
if self._read_only:
|
||||
raise RuntimeError("Cannot modify read-only JavaKeyword instance")
|
||||
|
||||
if keyword and keyword.strip():
|
||||
self._keywords.add(keyword)
|
||||
|
||||
return self
|
||||
|
||||
def remove_keyword(self, keyword: str) -> 'JavaKeyword':
|
||||
"""Remove keyword"""
|
||||
if self._read_only:
|
||||
raise RuntimeError("Cannot modify read-only JavaKeyword instance")
|
||||
|
||||
self._keywords.discard(keyword)
|
||||
return self
|
||||
|
||||
def contains(self, word: str) -> bool:
|
||||
"""Check if word is a Java keyword"""
|
||||
return word in self._keywords
|
||||
|
||||
def is_keyword(self, word: str) -> bool:
|
||||
"""Check if word is a Java keyword (alias of contains)"""
|
||||
return self.contains(word)
|
||||
|
||||
@property
|
||||
def keywords(self) -> FrozenSet[str]:
|
||||
"""Get all keywords"""
|
||||
return frozenset(self._keywords)
|
||||
|
||||
@property
|
||||
def keyword_count(self) -> int:
|
||||
"""Get keyword count"""
|
||||
return len(self._keywords)
|
||||
|
||||
|
||||
# Create shared instance
|
||||
me = JavaKeyword.get_instance()
|
||||
141
pyenjoy/kit/Kv.py
Normal file
141
pyenjoy/kit/Kv.py
Normal file
@@ -0,0 +1,141 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Kv (Key Value) - Ordered Dictionary Implementation
|
||||
"""
|
||||
|
||||
from .StrKit import StrKit
|
||||
from .TypeKit import TypeKit
|
||||
|
||||
class Kv(dict):
|
||||
"""Key Value dictionary with utility methods"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def of(key, value):
|
||||
"""Create Kv with initial key-value pair"""
|
||||
return Kv().set(key, value)
|
||||
|
||||
@staticmethod
|
||||
def by(key, value):
|
||||
"""Create Kv with initial key-value pair (alias of of)"""
|
||||
return Kv.of(key, value)
|
||||
|
||||
@staticmethod
|
||||
def create():
|
||||
"""Create empty Kv"""
|
||||
return Kv()
|
||||
|
||||
def set(self, key, value):
|
||||
"""Set key-value pair and return self for chaining"""
|
||||
super().__setitem__(key, value)
|
||||
return self
|
||||
|
||||
def set_if_not_blank(self, key, value):
|
||||
"""Set value only if not blank"""
|
||||
if StrKit.not_blank(value):
|
||||
self.set(key, value)
|
||||
return self
|
||||
|
||||
def set_if_not_null(self, key, value):
|
||||
"""Set value only if not None"""
|
||||
if value is not None:
|
||||
self.set(key, value)
|
||||
return self
|
||||
|
||||
def delete(self, key):
|
||||
"""Delete key and return self"""
|
||||
super().pop(key, None)
|
||||
return self
|
||||
|
||||
def get_as(self, key, default_value=None):
|
||||
"""Get value as specific type"""
|
||||
return self.get(key, default_value)
|
||||
|
||||
def get_str(self, key, default_value=None):
|
||||
"""Get value as string"""
|
||||
value = self.get(key)
|
||||
return str(value) if value is not None else default_value
|
||||
|
||||
def get_int(self, key, default_value=None):
|
||||
"""Get value as integer"""
|
||||
return TypeKit.to_int(self.get(key, default_value))
|
||||
|
||||
def get_long(self, key, default_value=None):
|
||||
"""Get value as long integer"""
|
||||
return TypeKit.to_long(self.get(key, default_value))
|
||||
|
||||
def get_big_decimal(self, key, default_value=None):
|
||||
"""Get value as BigDecimal"""
|
||||
return TypeKit.to_big_decimal(self.get(key, default_value))
|
||||
|
||||
def get_double(self, key, default_value=None):
|
||||
"""Get value as double"""
|
||||
return TypeKit.to_double(self.get(key, default_value))
|
||||
|
||||
def get_float(self, key, default_value=None):
|
||||
"""Get value as float"""
|
||||
return TypeKit.to_float(self.get(key, default_value))
|
||||
|
||||
def get_number(self, key, default_value=None):
|
||||
"""Get value as Number"""
|
||||
return TypeKit.to_number(self.get(key, default_value))
|
||||
|
||||
def get_boolean(self, key, default_value=None):
|
||||
"""Get value as boolean"""
|
||||
return TypeKit.to_boolean(self.get(key, default_value))
|
||||
|
||||
def get_date(self, key, default_value=None):
|
||||
"""Get value as Date"""
|
||||
return TypeKit.to_date(self.get(key, default_value))
|
||||
|
||||
def get_local_datetime(self, key, default_value=None):
|
||||
"""Get value as LocalDateTime"""
|
||||
return TypeKit.to_local_date_time(self.get(key, default_value))
|
||||
|
||||
def not_null(self, key):
|
||||
"""Check if key exists and value is not None"""
|
||||
return self.get(key) is not None
|
||||
|
||||
def is_null(self, key):
|
||||
"""Check if key doesn't exist or value is None"""
|
||||
return self.get(key) is None
|
||||
|
||||
def not_blank(self, key):
|
||||
"""Check if value is not blank string"""
|
||||
return StrKit.not_blank(self.get_str(key))
|
||||
|
||||
def is_blank(self, key):
|
||||
"""Check if value is blank string"""
|
||||
return StrKit.is_blank(self.get_str(key))
|
||||
|
||||
def is_true(self, key):
|
||||
"""Check if value is True"""
|
||||
value = self.get(key)
|
||||
return value is not None and TypeKit.to_boolean(value)
|
||||
|
||||
def is_false(self, key):
|
||||
"""Check if value is False"""
|
||||
value = self.get(key)
|
||||
return value is not None and not TypeKit.to_boolean(value)
|
||||
|
||||
def keep(self, *keys):
|
||||
"""Keep only specified keys"""
|
||||
if keys and len(keys) > 0:
|
||||
new_kv = Kv()
|
||||
for key in keys:
|
||||
if self.contains_key(key):
|
||||
new_kv.set(key, self.get(key))
|
||||
|
||||
self.clear()
|
||||
self.update(new_kv)
|
||||
else:
|
||||
self.clear()
|
||||
|
||||
return self
|
||||
|
||||
def to_map(self):
|
||||
"""Convert to regular map"""
|
||||
return self
|
||||
143
pyenjoy/kit/Okv.py
Normal file
143
pyenjoy/kit/Okv.py
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Okv (Ordered Key Value) - Ordered Dictionary Implementation
|
||||
"""
|
||||
|
||||
from .Kv import Kv
|
||||
from .StrKit import StrKit
|
||||
from .TypeKit import TypeKit
|
||||
|
||||
class Okv(dict):
|
||||
"""Ordered Key Value dictionary with utility methods"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
# In Python 3.7+, dict maintains insertion order
|
||||
|
||||
@staticmethod
|
||||
def of(key, value):
|
||||
"""Create Okv with initial key-value pair"""
|
||||
return Okv().set(key, value)
|
||||
|
||||
@staticmethod
|
||||
def by(key, value):
|
||||
"""Create Okv with initial key-value pair (alias of of)"""
|
||||
return Okv.of(key, value)
|
||||
|
||||
@staticmethod
|
||||
def create():
|
||||
"""Create empty Okv"""
|
||||
return Okv()
|
||||
|
||||
def set(self, key, value):
|
||||
"""Set key-value pair and return self for chaining"""
|
||||
super().__setitem__(key, value)
|
||||
return self
|
||||
|
||||
def set_if_not_blank(self, key, value):
|
||||
"""Set value only if not blank"""
|
||||
if StrKit.not_blank(value):
|
||||
self.set(key, value)
|
||||
return self
|
||||
|
||||
def set_if_not_null(self, key, value):
|
||||
"""Set value only if not None"""
|
||||
if value is not None:
|
||||
self.set(key, value)
|
||||
return self
|
||||
|
||||
def delete(self, key):
|
||||
"""Delete key and return self"""
|
||||
super().pop(key, None)
|
||||
return self
|
||||
|
||||
def get_as(self, key, default_value=None):
|
||||
"""Get value as specific type"""
|
||||
return self.get(key, default_value)
|
||||
|
||||
def get_str(self, key, default_value=None):
|
||||
"""Get value as string"""
|
||||
value = self.get(key)
|
||||
return str(value) if value is not None else default_value
|
||||
|
||||
def get_int(self, key, default_value=None):
|
||||
"""Get value as integer"""
|
||||
return TypeKit.to_int(self.get(key, default_value))
|
||||
|
||||
def get_long(self, key, default_value=None):
|
||||
"""Get value as long integer"""
|
||||
return TypeKit.to_long(self.get(key, default_value))
|
||||
|
||||
def get_big_decimal(self, key, default_value=None):
|
||||
"""Get value as BigDecimal"""
|
||||
return TypeKit.to_big_decimal(self.get(key, default_value))
|
||||
|
||||
def get_double(self, key, default_value=None):
|
||||
"""Get value as double"""
|
||||
return TypeKit.to_double(self.get(key, default_value))
|
||||
|
||||
def get_float(self, key, default_value=None):
|
||||
"""Get value as float"""
|
||||
return TypeKit.to_float(self.get(key, default_value))
|
||||
|
||||
def get_number(self, key, default_value=None):
|
||||
"""Get value as Number"""
|
||||
return TypeKit.to_number(self.get(key, default_value))
|
||||
|
||||
def get_boolean(self, key, default_value=None):
|
||||
"""Get value as boolean"""
|
||||
return TypeKit.to_boolean(self.get(key, default_value))
|
||||
|
||||
def get_date(self, key, default_value=None):
|
||||
"""Get value as Date"""
|
||||
return TypeKit.to_date(self.get(key, default_value))
|
||||
|
||||
def get_local_datetime(self, key, default_value=None):
|
||||
"""Get value as LocalDateTime"""
|
||||
return TypeKit.to_local_date_time(self.get(key, default_value))
|
||||
|
||||
def not_null(self, key):
|
||||
"""Check if key exists and value is not None"""
|
||||
return self.get(key) is not None
|
||||
|
||||
def is_null(self, key):
|
||||
"""Check if key doesn't exist or value is None"""
|
||||
return self.get(key) is None
|
||||
|
||||
def not_blank(self, key):
|
||||
"""Check if value is not blank string"""
|
||||
return StrKit.not_blank(self.get_str(key))
|
||||
|
||||
def is_blank(self, key):
|
||||
"""Check if value is blank string"""
|
||||
return StrKit.is_blank(self.get_str(key))
|
||||
|
||||
def is_true(self, key):
|
||||
"""Check if value is True"""
|
||||
value = self.get(key)
|
||||
return value is not None and TypeKit.to_boolean(value)
|
||||
|
||||
def is_false(self, key):
|
||||
"""Check if value is False"""
|
||||
value = self.get(key)
|
||||
return value is not None and not TypeKit.to_boolean(value)
|
||||
|
||||
def keep(self, *keys):
|
||||
"""Keep only specified keys"""
|
||||
if keys and len(keys) > 0:
|
||||
new_okv = Okv()
|
||||
for key in keys:
|
||||
if self.contains_key(key):
|
||||
new_okv.set(key, self.get(key))
|
||||
|
||||
self.clear()
|
||||
self.update(new_okv)
|
||||
else:
|
||||
self.clear()
|
||||
|
||||
return self
|
||||
|
||||
def to_map(self):
|
||||
"""Convert to regular map"""
|
||||
return self
|
||||
166
pyenjoy/kit/Prop.py
Normal file
166
pyenjoy/kit/Prop.py
Normal file
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Prop - Properties File Handler
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
class Prop:
|
||||
"""Properties file handler"""
|
||||
|
||||
DEFAULT_ENCODING = "UTF-8"
|
||||
|
||||
def __init__(self, file_name_or_content=None, encoding: str = DEFAULT_ENCODING, is_file: bool = True):
|
||||
"""
|
||||
Initialize Prop
|
||||
|
||||
Args:
|
||||
file_name_or_content: file name (if is_file=True) or content string
|
||||
encoding: encoding for reading properties file
|
||||
is_file: True if first parameter is file name, False if it's content string
|
||||
"""
|
||||
self._properties = {}
|
||||
|
||||
if file_name_or_content is None:
|
||||
return
|
||||
|
||||
if is_file:
|
||||
self._load_from_file(file_name_or_content, encoding)
|
||||
else:
|
||||
self._load_from_content(file_name_or_content, encoding)
|
||||
|
||||
def _load_from_file(self, file_name: str, encoding: str):
|
||||
"""Load properties from file"""
|
||||
try:
|
||||
# Try to find file in classpath first
|
||||
import importlib.resources
|
||||
try:
|
||||
with importlib.resources.open_text(file_name, encoding=encoding) as f:
|
||||
self._parse_properties(f.read())
|
||||
return
|
||||
except (FileNotFoundError, TypeError):
|
||||
pass
|
||||
|
||||
# Try as absolute file path
|
||||
if os.path.isabs(file_name):
|
||||
file_path = file_name
|
||||
else:
|
||||
file_path = os.path.join(os.getcwd(), file_name)
|
||||
|
||||
if not os.path.exists(file_path):
|
||||
raise FileNotFoundError(f"Properties file not found: {file_name}")
|
||||
|
||||
with open(file_path, 'r', encoding=encoding) as f:
|
||||
self._parse_properties(f.read())
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error loading properties file: {e}")
|
||||
|
||||
def _load_from_content(self, content: str, encoding: str):
|
||||
"""Load properties from content string"""
|
||||
try:
|
||||
self._parse_properties(content)
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error parsing properties content: {e}")
|
||||
|
||||
def _parse_properties(self, content: str):
|
||||
"""Parse properties content"""
|
||||
for line in content.split('\n'):
|
||||
line = line.strip()
|
||||
|
||||
# Skip empty lines and comments
|
||||
if not line or line.startswith('#') or line.startswith('!'):
|
||||
continue
|
||||
|
||||
# Handle line continuation
|
||||
while line.endswith('\\'):
|
||||
line = line[:-1].strip()
|
||||
# This is a simplified implementation
|
||||
|
||||
# Find the first separator
|
||||
separator_idx = -1
|
||||
for sep in ['=', ':']:
|
||||
idx = line.find(sep)
|
||||
if idx != -1:
|
||||
if separator_idx == -1 or idx < separator_idx:
|
||||
separator_idx = idx
|
||||
|
||||
if separator_idx == -1:
|
||||
# No separator, treat as key with empty value
|
||||
key = line.strip()
|
||||
value = ""
|
||||
else:
|
||||
key = line[:separator_idx].strip()
|
||||
value = line[separator_idx + 1:].strip()
|
||||
|
||||
if key:
|
||||
self._properties[key] = value
|
||||
|
||||
def get(self, key: str, default_value: str = None) -> Optional[str]:
|
||||
"""Get property value"""
|
||||
value = self._properties.get(key)
|
||||
if value is not None and len(value) != 0:
|
||||
return value.strip()
|
||||
return default_value
|
||||
|
||||
def get_int(self, key: str, default_value: int = None) -> Optional[int]:
|
||||
"""Get property as integer"""
|
||||
value = self.get(key)
|
||||
if value is not None:
|
||||
return int(value)
|
||||
return default_value
|
||||
|
||||
def get_long(self, key: str, default_value: int = None) -> Optional[int]:
|
||||
"""Get property as long integer"""
|
||||
value = self.get(key)
|
||||
if value is not None:
|
||||
return int(value)
|
||||
return default_value
|
||||
|
||||
def get_double(self, key: str, default_value: float = None) -> Optional[float]:
|
||||
"""Get property as double"""
|
||||
value = self.get(key)
|
||||
if value is not None:
|
||||
return float(value)
|
||||
return default_value
|
||||
|
||||
def get_boolean(self, key: str, default_value: bool = None) -> Optional[bool]:
|
||||
"""Get property as boolean"""
|
||||
value = self.get(key)
|
||||
if value is not None:
|
||||
value_lower = value.lower().strip()
|
||||
if value_lower == "true":
|
||||
return True
|
||||
elif value_lower == "false":
|
||||
return False
|
||||
else:
|
||||
raise ValueError(f"Cannot parse boolean value: {value}")
|
||||
return default_value
|
||||
|
||||
def contains_key(self, key: str) -> bool:
|
||||
"""Check if key exists"""
|
||||
return key in self._properties
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""Check if properties is empty"""
|
||||
return len(self._properties) == 0
|
||||
|
||||
def not_empty(self) -> bool:
|
||||
"""Check if properties is not empty"""
|
||||
return not self.is_empty()
|
||||
|
||||
def append(self, prop) -> 'Prop':
|
||||
"""Append properties from another Prop"""
|
||||
if prop is None:
|
||||
raise ValueError("prop cannot be None")
|
||||
|
||||
for key, value in prop.get_properties().items():
|
||||
self._properties[key] = value
|
||||
|
||||
return self
|
||||
|
||||
def get_properties(self) -> dict:
|
||||
"""Get all properties"""
|
||||
return self._properties.copy()
|
||||
135
pyenjoy/kit/PropKit.py
Normal file
135
pyenjoy/kit/PropKit.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal PropKit - Properties File Management
|
||||
"""
|
||||
|
||||
import os
|
||||
from typing import Optional, Dict
|
||||
from .Prop import Prop
|
||||
from .StrKit import StrKit
|
||||
|
||||
class PropKit:
|
||||
"""Properties file management utility"""
|
||||
|
||||
_env_key = "app.env"
|
||||
_prop: Optional[Prop] = None
|
||||
_cache: Dict[str, Prop] = {}
|
||||
|
||||
def __init__(self):
|
||||
raise NotImplementedError("PropKit is a utility class and cannot be instantiated")
|
||||
|
||||
@staticmethod
|
||||
def set_env_key(env_key: str):
|
||||
"""Set environment key for config"""
|
||||
PropKit._env_key = env_key
|
||||
|
||||
@staticmethod
|
||||
def get_env_key() -> str:
|
||||
"""Get environment key"""
|
||||
return PropKit._env_key
|
||||
|
||||
@staticmethod
|
||||
def get_env() -> str:
|
||||
"""Get current environment value"""
|
||||
return PropKit.get_prop().get(PropKit._env_key)
|
||||
|
||||
@staticmethod
|
||||
def use(file_name: str, encoding: str = Prop.DEFAULT_ENCODING) -> Prop:
|
||||
"""Use properties file"""
|
||||
if file_name not in PropKit._cache:
|
||||
PropKit._cache[file_name] = Prop(file_name, encoding)
|
||||
PropKit._handle_env(PropKit._cache[file_name], file_name)
|
||||
|
||||
if PropKit._prop is None:
|
||||
PropKit._prop = PropKit._cache[file_name]
|
||||
|
||||
return PropKit._cache[file_name]
|
||||
|
||||
@staticmethod
|
||||
def _handle_env(result: Prop, file_name: str):
|
||||
"""Handle environment-specific configuration"""
|
||||
env = result.get(PropKit._env_key)
|
||||
if StrKit.not_blank(env):
|
||||
dot_index = file_name.rfind('.')
|
||||
if dot_index != -1:
|
||||
env_config_name = file_name[:dot_index] + "-" + env + file_name[dot_index:]
|
||||
else:
|
||||
env_config_name = file_name + "-" + env
|
||||
|
||||
try:
|
||||
env_config = Prop(env_config_name)
|
||||
result.append(env_config)
|
||||
except:
|
||||
pass # Ignore if env config doesn't exist
|
||||
|
||||
@staticmethod
|
||||
def useless(file_name: str) -> Optional[Prop]:
|
||||
"""Remove properties file from cache"""
|
||||
removed = PropKit._cache.pop(file_name, None)
|
||||
if PropKit._prop is removed:
|
||||
PropKit._prop = None
|
||||
return removed
|
||||
|
||||
@staticmethod
|
||||
def clear():
|
||||
"""Clear all cached properties"""
|
||||
PropKit._prop = None
|
||||
PropKit._cache.clear()
|
||||
|
||||
@staticmethod
|
||||
def append(prop: Prop) -> Prop:
|
||||
"""Append properties"""
|
||||
with PropKit:
|
||||
if PropKit._prop is not None:
|
||||
PropKit._prop.append(prop)
|
||||
else:
|
||||
PropKit._prop = prop
|
||||
return PropKit._prop
|
||||
|
||||
@staticmethod
|
||||
def append_content(content: str, encoding: str = Prop.DEFAULT_ENCODING) -> Prop:
|
||||
"""Append properties from content string"""
|
||||
return PropKit.append(Prop(content, encoding, is_file=False))
|
||||
|
||||
@staticmethod
|
||||
def get_prop() -> Prop:
|
||||
"""Get current properties"""
|
||||
if PropKit._prop is None:
|
||||
raise IllegalStateException("Load properties file by invoking PropKit.use(String fileName) method first.")
|
||||
return PropKit._prop
|
||||
|
||||
@staticmethod
|
||||
def get(key: str, default_value: str = None) -> Optional[str]:
|
||||
"""Get property value"""
|
||||
return PropKit.get_prop().get(key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def get_int(key: str, default_value: int = None) -> Optional[int]:
|
||||
"""Get property as integer"""
|
||||
return PropKit.get_prop().get_int(key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def get_long(key: str, default_value: int = None) -> Optional[int]:
|
||||
"""Get property as long"""
|
||||
return PropKit.get_prop().get_long(key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def get_double(key: str, default_value: float = None) -> Optional[float]:
|
||||
"""Get property as double"""
|
||||
return PropKit.get_prop().get_double(key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def get_boolean(key: str, default_value: bool = None) -> Optional[bool]:
|
||||
"""Get property as boolean"""
|
||||
return PropKit.get_prop().get_boolean(key, default_value)
|
||||
|
||||
@staticmethod
|
||||
def contains_key(key: str) -> bool:
|
||||
"""Check if key exists"""
|
||||
return PropKit.get_prop().contains_key(key)
|
||||
|
||||
|
||||
class IllegalStateException(Exception):
|
||||
"""Illegal state exception"""
|
||||
pass
|
||||
107
pyenjoy/kit/ReflectKit.py
Normal file
107
pyenjoy/kit/ReflectKit.py
Normal file
@@ -0,0 +1,107 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal ReflectKit - Reflection Utilities
|
||||
"""
|
||||
|
||||
import inspect
|
||||
from typing import Any, Dict, List, Type
|
||||
|
||||
class ReflectKit:
|
||||
"""Reflection utility class"""
|
||||
|
||||
@staticmethod
|
||||
def new_instance(clazz: Type) -> Any:
|
||||
"""Create new instance of class"""
|
||||
try:
|
||||
# Try __new__ first (for immutable types)
|
||||
if hasattr(clazz, '__new__'):
|
||||
obj = clazz.__new__(clazz)
|
||||
if obj is not None and not isinstance(obj, type):
|
||||
return obj
|
||||
|
||||
# Fall back to regular instantiation
|
||||
return clazz()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error creating instance of {clazz}: {e}")
|
||||
|
||||
@staticmethod
|
||||
def get_method_signature(method) -> str:
|
||||
"""Get method signature as string"""
|
||||
if not callable(method):
|
||||
raise ValueError("method must be callable")
|
||||
|
||||
try:
|
||||
# Get function information
|
||||
if hasattr(method, '__name__'):
|
||||
method_name = method.__name__
|
||||
else:
|
||||
method_name = str(method)
|
||||
|
||||
# Get declaring class
|
||||
if hasattr(method, '__qualname__'):
|
||||
declaring_class = method.__qualname__.rsplit('.', 1)[0] if '.' in method.__qualname__ else ''
|
||||
else:
|
||||
declaring_class = ''
|
||||
|
||||
# Get parameters
|
||||
try:
|
||||
sig = inspect.signature(method)
|
||||
params = []
|
||||
for param_name, param in sig.parameters.items():
|
||||
if param_name != 'self':
|
||||
param_type = param.annotation if param.annotation != inspect.Parameter.empty else Any
|
||||
params.append(str(param_type.__name__) if hasattr(param_type, '__name__') else str(param_type))
|
||||
except (ValueError, TypeError):
|
||||
params = []
|
||||
|
||||
# Build signature
|
||||
if declaring_class:
|
||||
signature = f"{declaring_class}.{method_name}({', '.join(params)})"
|
||||
else:
|
||||
signature = f"{method_name}({', '.join(params)})"
|
||||
|
||||
return signature
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error getting method signature: {e}")
|
||||
|
||||
@staticmethod
|
||||
def get_class_methods(clazz: Type) -> Dict[str, callable]:
|
||||
"""Get all methods of a class"""
|
||||
methods = {}
|
||||
try:
|
||||
for name, method in inspect.getmembers(clazz, predicate=inspect.isfunction):
|
||||
if not name.startswith('_'):
|
||||
methods[name] = method
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error getting class methods: {e}")
|
||||
return methods
|
||||
|
||||
@staticmethod
|
||||
def get_class_attributes(clazz: Type) -> Dict[str, Any]:
|
||||
"""Get all class attributes"""
|
||||
attributes = {}
|
||||
try:
|
||||
for name, value in inspect.getmembers(clazz):
|
||||
if not name.startswith('_') and not callable(value):
|
||||
attributes[name] = value
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error getting class attributes: {e}")
|
||||
return attributes
|
||||
|
||||
@staticmethod
|
||||
def is_subclass_of(subclass: Type, superclass: Type) -> bool:
|
||||
"""Check if subclass is subclass of superclass"""
|
||||
try:
|
||||
return issubclass(subclass, superclass)
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_instance_of(obj: Any, clazz: Type) -> bool:
|
||||
"""Check if object is instance of class"""
|
||||
try:
|
||||
return isinstance(obj, clazz)
|
||||
except TypeError:
|
||||
return False
|
||||
139
pyenjoy/kit/StrKit.py
Normal file
139
pyenjoy/kit/StrKit.py
Normal file
@@ -0,0 +1,139 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal StrKit - String Utilities
|
||||
"""
|
||||
|
||||
class StrKit:
|
||||
"""String utility class"""
|
||||
|
||||
@staticmethod
|
||||
def first_char_to_lower_case(s: str) -> str:
|
||||
"""首字母变小写"""
|
||||
if not s:
|
||||
return s
|
||||
first_char = s[0]
|
||||
if 'A' <= first_char <= 'Z':
|
||||
arr = list(s)
|
||||
arr[0] = chr(ord(arr[0]) + ord('a') - ord('A'))
|
||||
return ''.join(arr)
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def first_char_to_upper_case(s: str) -> str:
|
||||
"""首字母变大写"""
|
||||
if not s:
|
||||
return s
|
||||
first_char = s[0]
|
||||
if 'a' <= first_char <= 'z':
|
||||
arr = list(s)
|
||||
arr[0] = chr(ord(arr[0]) - (ord('a') - ord('A')))
|
||||
return ''.join(arr)
|
||||
return s
|
||||
|
||||
@staticmethod
|
||||
def is_blank(s: str) -> bool:
|
||||
"""字符串为 null 或者内部字符全部为 ' ' '\\t' '\\n' '\\r' 这四类字符时返回 true"""
|
||||
if s is None:
|
||||
return True
|
||||
for char in s:
|
||||
if char > ' ':
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def not_blank(s: str) -> bool:
|
||||
return not StrKit.is_blank(s)
|
||||
|
||||
@staticmethod
|
||||
def not_blank(*strings) -> bool:
|
||||
if strings is None or len(strings) == 0:
|
||||
return False
|
||||
for s in strings:
|
||||
if StrKit.is_blank(s):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def has_blank(*strings) -> bool:
|
||||
if strings is None or len(strings) == 0:
|
||||
return True
|
||||
for s in strings:
|
||||
if StrKit.is_blank(s):
|
||||
return True
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def not_null(*paras) -> bool:
|
||||
if paras is None:
|
||||
return False
|
||||
for obj in paras:
|
||||
if obj is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def default_if_blank(s: str, default_value: str) -> str:
|
||||
return default_value if StrKit.is_blank(s) else s
|
||||
|
||||
@staticmethod
|
||||
def to_camel_case(s: str) -> str:
|
||||
"""将包含下划线字符 '_' 的字符串转换成驼峰格式,不包含下划线则原样返回"""
|
||||
return StrKit._to_camel_case(s, False)
|
||||
|
||||
@staticmethod
|
||||
def _to_camel_case(s: str, to_lower_case_anyway: bool) -> str:
|
||||
length = len(s)
|
||||
if length <= 1:
|
||||
return s
|
||||
|
||||
buf = []
|
||||
index = 0
|
||||
i = 0
|
||||
while i < length:
|
||||
ch = s[i]
|
||||
if ch == '_':
|
||||
i += 1
|
||||
if i < length:
|
||||
ch = s[i]
|
||||
if index == 0:
|
||||
buf.append(chr(ord(ch) + 32) if 'A' <= ch <= 'Z' else ch)
|
||||
else:
|
||||
buf.append(chr(ord(ch) - 32) if 'a' <= ch <= 'z' else ch)
|
||||
index += 1
|
||||
else:
|
||||
buf.append(chr(ord(ch) + 32) if 'A' <= ch <= 'Z' else ch)
|
||||
index += 1
|
||||
i += 1
|
||||
|
||||
if to_lower_case_anyway:
|
||||
return ''.join(buf[:index])
|
||||
|
||||
return s if i == index else ''.join(buf[:index])
|
||||
|
||||
@staticmethod
|
||||
def join(string_array, separator: str = '') -> str:
|
||||
"""Join string array"""
|
||||
if not string_array:
|
||||
return ''
|
||||
if separator:
|
||||
return separator.join(string_array)
|
||||
return ''.join(string_array)
|
||||
|
||||
@staticmethod
|
||||
def slow_equals(a: str, b: str) -> bool:
|
||||
"""Timing-safe string comparison"""
|
||||
from .HashKit import HashKit
|
||||
a_bytes = a.encode('utf-8') if a else None
|
||||
b_bytes = b.encode('utf-8') if b else None
|
||||
return HashKit.slow_equals(a_bytes, b_bytes)
|
||||
|
||||
@staticmethod
|
||||
def equals(a: str, b: str) -> bool:
|
||||
return a is None if b is None else a == b
|
||||
|
||||
@staticmethod
|
||||
def get_random_uuid() -> str:
|
||||
"""Generate random UUID without dashes"""
|
||||
import uuid
|
||||
return uuid.uuid4().hex
|
||||
111
pyenjoy/kit/SyncWriteMap.py
Normal file
111
pyenjoy/kit/SyncWriteMap.py
Normal file
@@ -0,0 +1,111 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal SyncWriteMap - Thread-Safe Dictionary
|
||||
"""
|
||||
|
||||
from typing import Dict, Generic, TypeVar, Optional, Callable
|
||||
from threading import Lock
|
||||
|
||||
K = TypeVar('K')
|
||||
V = TypeVar('V')
|
||||
|
||||
class SyncWriteDict(Dict[K, V]):
|
||||
"""Thread-safe dictionary with synchronized write operations"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self._lock = Lock()
|
||||
|
||||
def put(self, key: K, value: V) -> Optional[V]:
|
||||
"""Put key-value pair with synchronization"""
|
||||
with self._lock:
|
||||
old_value = self.get(key)
|
||||
super().__setitem__(key, value)
|
||||
return old_value
|
||||
|
||||
def put_if_absent(self, key: K, value: V) -> V:
|
||||
"""Put value only if key doesn't exist"""
|
||||
with self._lock:
|
||||
if key not in self:
|
||||
super().__setitem__(key, value)
|
||||
return value
|
||||
return self[key]
|
||||
|
||||
def remove(self, key: K) -> Optional[V]:
|
||||
"""Remove key with synchronization"""
|
||||
with self._lock:
|
||||
return super().pop(key, None)
|
||||
|
||||
def clear(self):
|
||||
"""Clear all items with synchronization"""
|
||||
with self._lock:
|
||||
super().clear()
|
||||
|
||||
def update(self, other: dict = None, **kwargs):
|
||||
"""Update dictionary with synchronization"""
|
||||
with self._lock:
|
||||
if other:
|
||||
super().update(other)
|
||||
if kwargs:
|
||||
super().update(kwargs)
|
||||
|
||||
def compute_if_absent(self, key: K, mapping_function: Callable[[K], V]) -> V:
|
||||
"""Compute value if key is absent"""
|
||||
with self._lock:
|
||||
if key not in self:
|
||||
value = mapping_function(key)
|
||||
super().__setitem__(key, value)
|
||||
return value
|
||||
return self[key]
|
||||
|
||||
def compute_if_present(self, key: K, remapping_function: Callable[[K, V], V]) -> Optional[V]:
|
||||
"""Compute new value if key exists"""
|
||||
with self._lock:
|
||||
if key in self:
|
||||
old_value = self[key]
|
||||
new_value = remapping_function(key, old_value)
|
||||
if new_value is None:
|
||||
super().__delitem__(key)
|
||||
else:
|
||||
super().__setitem__(key, new_value)
|
||||
return new_value
|
||||
return None
|
||||
|
||||
def compute(self, key: K, remapping_function: Callable[[K, Optional[V]], Optional[V]]) -> Optional[V]:
|
||||
"""Compute new value for key"""
|
||||
with self._lock:
|
||||
old_value = self.get(key)
|
||||
new_value = remapping_function(key, old_value)
|
||||
if new_value is None:
|
||||
if key in self:
|
||||
super().__delitem__(key)
|
||||
else:
|
||||
super().__setitem__(key, new_value)
|
||||
return new_value
|
||||
|
||||
def replace(self, key: K, old_value: V, new_value: V) -> bool:
|
||||
"""Replace value if old value matches"""
|
||||
with self._lock:
|
||||
if key in self and self[key] == old_value:
|
||||
super().__setitem__(key, new_value)
|
||||
return True
|
||||
return False
|
||||
|
||||
def replace_value(self, key: K, value: V) -> bool:
|
||||
"""Replace value regardless of old value"""
|
||||
with self._lock:
|
||||
if key in self:
|
||||
super().__setitem__(key, value)
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class SyncWriteMap(SyncWriteDict):
|
||||
"""Thread-safe dictionary (alias for compatibility)"""
|
||||
|
||||
def __init__(self, initial_capacity: int = None, load_factor: float = None,
|
||||
mapping: Dict[K, V] = None):
|
||||
super().__init__(mapping or {})
|
||||
# Note: initial_capacity and load_factor are ignored in Python dict implementation
|
||||
# but kept for API compatibility
|
||||
150
pyenjoy/kit/TimeKit.py
Normal file
150
pyenjoy/kit/TimeKit.py
Normal file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal TimeKit - Date and Time Utilities
|
||||
"""
|
||||
|
||||
from datetime import datetime, date, time, timedelta
|
||||
from typing import Union
|
||||
import threading
|
||||
|
||||
class TimeKit:
|
||||
"""Date and time utility class"""
|
||||
|
||||
_formatters = {}
|
||||
_sdf_cache = threading.local()
|
||||
|
||||
@staticmethod
|
||||
def _get_sdf(pattern: str):
|
||||
"""Get thread-local SimpleDateFormat equivalent"""
|
||||
if not hasattr(TimeKit._sdf_cache, 'formatters'):
|
||||
TimeKit._sdf_cache.formatters = {}
|
||||
|
||||
if pattern not in TimeKit._sdf_cache.formatters:
|
||||
TimeKit._sdf_cache.formatters[pattern] = _SimpleDateFormat(pattern)
|
||||
|
||||
return TimeKit._sdf_cache.formatters[pattern]
|
||||
|
||||
@staticmethod
|
||||
def now(pattern: str = "yyyy-MM-dd HH:mm:ss") -> str:
|
||||
"""Get current time as string"""
|
||||
return datetime.now().strftime(TimeKit._to_python_pattern(pattern))
|
||||
|
||||
@staticmethod
|
||||
def now_with_millisecond() -> str:
|
||||
"""Get current time with millisecond precision"""
|
||||
return datetime.now().strftime("%Y%m%d%H%M%S%f")[:-3]
|
||||
|
||||
@staticmethod
|
||||
def format(dt: Union[datetime, date], pattern: str = "yyyy-MM-dd HH:mm:ss") -> str:
|
||||
"""Format datetime or date to string"""
|
||||
python_pattern = TimeKit._to_python_pattern(pattern)
|
||||
|
||||
if isinstance(dt, datetime):
|
||||
return dt.strftime(python_pattern)
|
||||
elif isinstance(dt, date):
|
||||
return dt.strftime(TimeKit._to_python_pattern(pattern))
|
||||
elif isinstance(dt, time):
|
||||
return dt.strftime(TimeKit._to_python_pattern(pattern))
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {type(dt)}")
|
||||
|
||||
@staticmethod
|
||||
def parse(date_string: str, pattern: str = "yyyy-MM-dd HH:mm:ss") -> datetime:
|
||||
"""Parse string to datetime"""
|
||||
try:
|
||||
python_pattern = TimeKit._to_python_pattern(pattern)
|
||||
return datetime.strptime(date_string, python_pattern)
|
||||
except ValueError as e:
|
||||
raise RuntimeError(f"Error parsing date: {e}")
|
||||
|
||||
@staticmethod
|
||||
def parse_date(date_string: str, pattern: str = "yyyy-MM-dd") -> date:
|
||||
"""Parse string to date"""
|
||||
dt = TimeKit.parse(date_string, pattern)
|
||||
return dt.date()
|
||||
|
||||
@staticmethod
|
||||
def parse_time(time_string: str, pattern: str = "HH:mm:ss") -> time:
|
||||
"""Parse string to time"""
|
||||
dt = TimeKit.parse(f"1970-01-01 {time_string}", f"yyyy-MM-dd {pattern}")
|
||||
return dt.time()
|
||||
|
||||
@staticmethod
|
||||
def to_datetime(dt: Union[datetime, date]) -> datetime:
|
||||
"""Convert date to datetime"""
|
||||
if isinstance(dt, datetime):
|
||||
return dt
|
||||
elif isinstance(dt, date):
|
||||
return datetime.combine(dt, time.min)
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {type(dt)}")
|
||||
|
||||
@staticmethod
|
||||
def to_date(dt: Union[datetime, date]) -> date:
|
||||
"""Convert datetime to date"""
|
||||
if isinstance(dt, datetime):
|
||||
return dt.date()
|
||||
elif isinstance(dt, date):
|
||||
return dt
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {type(dt)}")
|
||||
|
||||
@staticmethod
|
||||
def is_after(dt1: datetime, dt2: datetime) -> bool:
|
||||
"""Check if dt1 is after dt2"""
|
||||
return dt1 > dt2
|
||||
|
||||
@staticmethod
|
||||
def is_before(dt1: datetime, dt2: datetime) -> bool:
|
||||
"""Check if dt1 is before dt2"""
|
||||
return dt1 < dt2
|
||||
|
||||
@staticmethod
|
||||
def is_equal(dt1: datetime, dt2: datetime) -> bool:
|
||||
"""Check if dt1 equals dt2"""
|
||||
return dt1 == dt2
|
||||
|
||||
@staticmethod
|
||||
def _to_python_pattern(java_pattern: str) -> str:
|
||||
"""Convert Java date pattern to Python strftime pattern"""
|
||||
mapping = {
|
||||
'yyyy': '%Y',
|
||||
'yy': '%y',
|
||||
'MM': '%m',
|
||||
'dd': '%d',
|
||||
'HH': '%H',
|
||||
'mm': '%M',
|
||||
'ss': '%S',
|
||||
'SSS': '%f',
|
||||
}
|
||||
|
||||
result = java_pattern
|
||||
for java_fmt, python_fmt in mapping.items():
|
||||
result = result.replace(java_fmt, python_fmt)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class _SimpleDateFormat:
|
||||
"""Simple date format implementation (Java SimpleDateFormat equivalent)"""
|
||||
|
||||
def __init__(self, pattern: str):
|
||||
self.pattern = pattern
|
||||
self.python_pattern = TimeKit._to_python_pattern(pattern)
|
||||
|
||||
def format(self, dt: Union[datetime, date]) -> str:
|
||||
"""Format datetime to string"""
|
||||
if isinstance(dt, datetime):
|
||||
return dt.strftime(self.python_pattern)
|
||||
elif isinstance(dt, date):
|
||||
return dt.strftime(self.python_pattern)
|
||||
else:
|
||||
raise ValueError(f"Unsupported type: {type(dt)}")
|
||||
|
||||
def parse(self, date_string: str) -> datetime:
|
||||
"""Parse string to datetime"""
|
||||
try:
|
||||
return datetime.strptime(date_string, self.python_pattern)
|
||||
except ValueError as e:
|
||||
raise RuntimeError(f"Error parsing date: {e}")
|
||||
206
pyenjoy/kit/TypeKit.py
Normal file
206
pyenjoy/kit/TypeKit.py
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal TypeKit - Type Conversion Utilities
|
||||
"""
|
||||
|
||||
from datetime import datetime, date, time, timedelta
|
||||
from decimal import Decimal, InvalidOperation
|
||||
import re
|
||||
|
||||
class TypeKit:
|
||||
"""Type conversion utility class"""
|
||||
|
||||
_date_pattern = "yyyy-MM-dd"
|
||||
_date_len = len(_date_pattern)
|
||||
_date_time_without_second_pattern = "yyyy-MM-dd HH:mm"
|
||||
_date_time_without_second_len = len(_date_time_without_second_pattern)
|
||||
_date_time_pattern = "yyyy-MM-dd HH:mm:ss"
|
||||
|
||||
@staticmethod
|
||||
def to_str(s) -> str:
|
||||
"""Convert to string"""
|
||||
return str(s) if s is not None else None
|
||||
|
||||
@staticmethod
|
||||
def to_int(n) -> int:
|
||||
"""Convert to integer"""
|
||||
if n is None:
|
||||
return None
|
||||
if isinstance(n, int):
|
||||
return n
|
||||
if isinstance(n, float):
|
||||
return int(n)
|
||||
try:
|
||||
return int(str(n))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def to_long(n) -> int:
|
||||
"""Convert to long integer"""
|
||||
return TypeKit.to_int(n)
|
||||
|
||||
@staticmethod
|
||||
def to_float(n) -> float:
|
||||
"""Convert to float"""
|
||||
if n is None:
|
||||
return None
|
||||
if isinstance(n, (int, float)):
|
||||
return float(n)
|
||||
try:
|
||||
return float(str(n))
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def to_double(n) -> float:
|
||||
"""Convert to double"""
|
||||
return TypeKit.to_float(n)
|
||||
|
||||
@staticmethod
|
||||
def to_decimal(n) -> Decimal:
|
||||
"""Convert to Decimal"""
|
||||
if n is None:
|
||||
return None
|
||||
if isinstance(n, Decimal):
|
||||
return n
|
||||
try:
|
||||
return Decimal(str(n))
|
||||
except (InvalidOperation, ValueError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def to_big_decimal(n) -> Decimal:
|
||||
"""Convert to BigDecimal"""
|
||||
return TypeKit.to_decimal(n)
|
||||
|
||||
@staticmethod
|
||||
def to_short(n) -> int:
|
||||
"""Convert to short integer"""
|
||||
result = TypeKit.to_int(n)
|
||||
return result if result is None else max(-32768, min(32767, result))
|
||||
|
||||
@staticmethod
|
||||
def to_byte(n) -> int:
|
||||
"""Convert to byte integer"""
|
||||
result = TypeKit.to_int(n)
|
||||
return result if result is None else max(-128, min(127, result))
|
||||
|
||||
@staticmethod
|
||||
def to_boolean(b) -> bool:
|
||||
"""Convert to boolean"""
|
||||
if b is None:
|
||||
return None
|
||||
if isinstance(b, bool):
|
||||
return b
|
||||
|
||||
if isinstance(b, (int, float)):
|
||||
if b == 1:
|
||||
return True
|
||||
elif b == 0:
|
||||
return False
|
||||
return bool(b)
|
||||
|
||||
if isinstance(b, str):
|
||||
s = str(b).lower().strip()
|
||||
if s in ("true", "1"):
|
||||
return True
|
||||
elif s in ("false", "0"):
|
||||
return False
|
||||
|
||||
return bool(b)
|
||||
|
||||
@staticmethod
|
||||
def to_number(n) -> float:
|
||||
"""Convert to number"""
|
||||
if n is None:
|
||||
return None
|
||||
if isinstance(n, (int, float)):
|
||||
return float(n)
|
||||
|
||||
s = str(n)
|
||||
if '.' in s:
|
||||
return float(s)
|
||||
else:
|
||||
try:
|
||||
return int(s)
|
||||
except ValueError:
|
||||
return float(s)
|
||||
|
||||
@staticmethod
|
||||
def to_date(d):
|
||||
"""Convert to datetime"""
|
||||
if d is None:
|
||||
return None
|
||||
|
||||
if isinstance(d, (datetime, date)):
|
||||
if isinstance(d, datetime):
|
||||
return d
|
||||
else:
|
||||
return datetime.combine(d, time.min)
|
||||
|
||||
if isinstance(d, str):
|
||||
s = str(d).strip()
|
||||
s_len = len(s)
|
||||
|
||||
if s_len <= TypeKit._date_len:
|
||||
return TypeKit._parse_date(s, TypeKit._date_pattern)
|
||||
elif s_len > TypeKit._date_time_without_second_len:
|
||||
return TypeKit._parse_date(s, TypeKit._date_time_pattern)
|
||||
else:
|
||||
colon_count = s.count(':')
|
||||
if colon_count == 2:
|
||||
return TypeKit._parse_date(s, TypeKit._date_time_pattern)
|
||||
elif colon_count == 1:
|
||||
return TypeKit._parse_date(s, TypeKit._date_time_without_second_pattern)
|
||||
|
||||
raise ValueError(f"Cannot convert to date: {d}")
|
||||
|
||||
@staticmethod
|
||||
def _parse_date(date_string: str, pattern: str) -> datetime:
|
||||
"""Parse date string with pattern"""
|
||||
try:
|
||||
# Simplified date parsing - supports common formats
|
||||
if pattern == "yyyy-MM-dd":
|
||||
parts = date_string.split('-')
|
||||
if len(parts) == 3:
|
||||
return datetime(int(parts[0]), int(parts[1]), int(parts[2]))
|
||||
elif pattern == "yyyy-MM-dd HH:mm:ss":
|
||||
date_part, time_part = date_string.split(' ')
|
||||
date_parts = date_part.split('-')
|
||||
time_parts = time_part.split(':')
|
||||
if len(date_parts) == 3 and len(time_parts) == 3:
|
||||
return datetime(
|
||||
int(date_parts[0]), int(date_parts[1]), int(date_parts[2]),
|
||||
int(time_parts[0]), int(time_parts[1]), int(time_parts[2])
|
||||
)
|
||||
elif pattern == "yyyy-MM-dd HH:mm":
|
||||
date_part, time_part = date_string.split(' ')
|
||||
date_parts = date_part.split('-')
|
||||
time_parts = time_part.split(':')
|
||||
if len(date_parts) == 3 and len(time_parts) == 2:
|
||||
return datetime(
|
||||
int(date_parts[0]), int(date_parts[1]), int(date_parts[2]),
|
||||
int(time_parts[0]), int(time_parts[1])
|
||||
)
|
||||
except (ValueError, IndexError):
|
||||
pass
|
||||
|
||||
# Fallback to try parsing with dateutil or regex
|
||||
raise ValueError(f"Cannot parse date: {date_string}")
|
||||
|
||||
@staticmethod
|
||||
def to_local_date_time(ldt):
|
||||
"""Convert to LocalDateTime"""
|
||||
if ldt is None:
|
||||
return None
|
||||
|
||||
if isinstance(ldt, datetime):
|
||||
return ldt
|
||||
|
||||
d = TypeKit.to_date(ldt)
|
||||
if d:
|
||||
return d
|
||||
|
||||
raise ValueError(f"Cannot convert to LocalDateTime: {ldt}")
|
||||
77
pyenjoy/proxy/ProxyClass.py
Normal file
77
pyenjoy/proxy/ProxyClass.py
Normal file
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal ProxyClass - Proxy Class Information
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional, Type
|
||||
|
||||
class ProxyClass:
|
||||
"""Proxy class information holder"""
|
||||
|
||||
def __init__(self, target: Type):
|
||||
"""
|
||||
Initialize proxy class
|
||||
|
||||
Args:
|
||||
target: Target class to be proxied
|
||||
"""
|
||||
self._target = target
|
||||
self._pkg = target.__module__
|
||||
self._name = target.__name__ + "$$EnhancerByJFinal"
|
||||
self._source_code: Optional[str] = None
|
||||
self._byte_code: Optional[Dict[str, bytes]] = None
|
||||
self._clazz: Optional[Type] = None
|
||||
|
||||
@property
|
||||
def target(self) -> Type:
|
||||
"""Get target class"""
|
||||
return self._target
|
||||
|
||||
@property
|
||||
def pkg(self) -> str:
|
||||
"""Get package name"""
|
||||
return self._pkg
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get proxy class name"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def source_code(self) -> Optional[str]:
|
||||
"""Get source code"""
|
||||
return self._source_code
|
||||
|
||||
@source_code.setter
|
||||
def source_code(self, value: str):
|
||||
"""Set source code"""
|
||||
self._source_code = value
|
||||
|
||||
@property
|
||||
def byte_code(self) -> Optional[Dict[str, bytes]]:
|
||||
"""Get byte code"""
|
||||
return self._byte_code
|
||||
|
||||
@byte_code.setter
|
||||
def byte_code(self, value: Dict[str, bytes]):
|
||||
"""Set byte code"""
|
||||
self._byte_code = value
|
||||
|
||||
@property
|
||||
def clazz(self) -> Optional[Type]:
|
||||
"""Get loaded class"""
|
||||
return self._clazz
|
||||
|
||||
@clazz.setter
|
||||
def clazz(self, value: Type):
|
||||
"""Set loaded class"""
|
||||
self._clazz = value
|
||||
|
||||
@property
|
||||
def full_name(self) -> str:
|
||||
"""Get full class name with package"""
|
||||
return f"{self._pkg}.{self._name}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ProxyClass(target={self._target.__name__}, name={self._name})"
|
||||
124
pyenjoy/proxy/ProxyClassLoader.py
Normal file
124
pyenjoy/proxy/ProxyClassLoader.py
Normal file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal ProxyClassLoader - Dynamic Proxy Class Loader
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import sys
|
||||
from typing import Dict, Optional, Type
|
||||
from .ProxyClass import ProxyClass
|
||||
|
||||
class ProxyClassLoader:
|
||||
"""Dynamic proxy class loader"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize proxy class loader"""
|
||||
self._byte_code_map: Dict[str, bytes] = {}
|
||||
|
||||
def load_proxy_class(self, proxy_class: ProxyClass) -> Type:
|
||||
"""
|
||||
Load proxy class from byte code
|
||||
|
||||
Args:
|
||||
proxyClass: ProxyClass object with byte code
|
||||
|
||||
Returns:
|
||||
Loaded proxy class
|
||||
"""
|
||||
# Add byte code to map
|
||||
if proxy_class.byte_code:
|
||||
for class_name, byte_code in proxy_class.byte_code.items():
|
||||
if class_name not in self._byte_code_map:
|
||||
self._byte_code_map[class_name] = byte_code
|
||||
|
||||
# Try to load the class
|
||||
class_name = proxy_class.full_name
|
||||
|
||||
# Check if already loaded
|
||||
if class_name in sys.modules:
|
||||
return sys.modules[class_name]
|
||||
|
||||
try:
|
||||
# For Python, we'll use dynamic import
|
||||
# In a real implementation, this would use importlib or similar
|
||||
loaded_class = self._dynamic_import(class_name, proxy_class)
|
||||
return loaded_class
|
||||
except (ImportError, AttributeError) as e:
|
||||
raise RuntimeError(f"Error loading proxy class {class_name}: {e}")
|
||||
|
||||
def _dynamic_import(self, class_name: str, proxy_class: ProxyClass) -> Type:
|
||||
"""
|
||||
Dynamically import and create class from proxy class
|
||||
|
||||
Args:
|
||||
class_name: Full class name
|
||||
proxyClass: ProxyClass object
|
||||
|
||||
Returns:
|
||||
Dynamically created class
|
||||
"""
|
||||
# In Python, we can execute the source code and get the class
|
||||
if proxy_class.source_code:
|
||||
# Create a module
|
||||
module_name = proxy_class.pkg
|
||||
module_code = proxy_class.source_code
|
||||
|
||||
# Execute the code in a new namespace
|
||||
namespace = {}
|
||||
try:
|
||||
exec(module_code, namespace)
|
||||
|
||||
# Find the class in the namespace
|
||||
for name, obj in namespace.items():
|
||||
if isinstance(obj, type) and name == proxy_class.name:
|
||||
return obj
|
||||
|
||||
raise RuntimeError(f"Class {proxy_class.name} not found in generated code")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error executing generated code: {e}")
|
||||
else:
|
||||
raise RuntimeError("No source code available for proxy class")
|
||||
|
||||
def find_class(self, name: str) -> Optional[bytes]:
|
||||
"""
|
||||
Find class byte code by name
|
||||
|
||||
Args:
|
||||
name: Full class name
|
||||
|
||||
Returns:
|
||||
Byte code or None if not found
|
||||
"""
|
||||
return self._byte_code_map.get(name)
|
||||
|
||||
def add_byte_code(self, name: str, byte_code: bytes):
|
||||
"""
|
||||
Add byte code to the loader
|
||||
|
||||
Args:
|
||||
name: Full class name
|
||||
byte_code: Class byte code
|
||||
"""
|
||||
self._byte_code_map[name] = byte_code
|
||||
|
||||
def remove_byte_code(self, name: str) -> Optional[bytes]:
|
||||
"""
|
||||
Remove and return byte code
|
||||
|
||||
Args:
|
||||
name: Full class name
|
||||
|
||||
Returns:
|
||||
Removed byte code or None
|
||||
"""
|
||||
return self._byte_code_map.pop(name, None)
|
||||
|
||||
def clear(self):
|
||||
"""Clear all cached byte code"""
|
||||
self._byte_code_map.clear()
|
||||
|
||||
@property
|
||||
def byte_code_count(self) -> int:
|
||||
"""Get number of cached byte codes"""
|
||||
return len(self._byte_code_map)
|
||||
143
pyenjoy/proxy/ProxyCompiler.py
Normal file
143
pyenjoy/proxy/ProxyCompiler.py
Normal file
@@ -0,0 +1,143 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal ProxyCompiler - Dynamic Proxy Compiler
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import subprocess
|
||||
import hashlib
|
||||
from typing import Dict, Optional, List, Tuple
|
||||
from .ProxyClass import ProxyClass
|
||||
from .ProxyClassLoader import ProxyClassLoader
|
||||
|
||||
class ProxyCompiler:
|
||||
"""Dynamic proxy compiler"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize proxy compiler"""
|
||||
self._class_loader = ProxyClassLoader()
|
||||
self._compile_options: List[str] = []
|
||||
|
||||
def compile(self, proxy_class: ProxyClass):
|
||||
"""
|
||||
Compile proxy class source code to byte code
|
||||
|
||||
Args:
|
||||
proxyClass: ProxyClass object with source code
|
||||
"""
|
||||
if not proxy_class.source_code:
|
||||
raise ValueError("Proxy class source code is not set")
|
||||
|
||||
try:
|
||||
# Generate byte code by compiling Python source
|
||||
byte_code = self._compile_python_source(proxy_class)
|
||||
|
||||
# Set the byte code
|
||||
proxy_class.byte_code = {
|
||||
proxy_class.full_name: byte_code
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error compiling proxy class: {e}")
|
||||
|
||||
def _compile_python_source(self, proxy_class: ProxyClass) -> bytes:
|
||||
"""
|
||||
Compile Python source code to bytecode
|
||||
|
||||
Args:
|
||||
proxyClass: ProxyClass with source code
|
||||
|
||||
Returns:
|
||||
Compiled bytecode
|
||||
"""
|
||||
# Method 1: Use compile() and exec()
|
||||
try:
|
||||
# Compile the source code
|
||||
compiled = compile(proxy_class.source_code, proxy_class.name, 'exec')
|
||||
|
||||
# Create a namespace and execute
|
||||
namespace = {}
|
||||
exec(compiled, namespace)
|
||||
|
||||
# For Python, bytecode is already loaded in the namespace
|
||||
# Return dummy byte code since Python doesn't have traditional bytecode like Java
|
||||
return b'PYTHON_PROXY_CLASS'
|
||||
|
||||
except SyntaxError as e:
|
||||
raise RuntimeError(f"Syntax error in generated code: {e}")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error compiling Python source: {e}")
|
||||
|
||||
def set_compile_options(self, options: List[str]) -> 'ProxyCompiler':
|
||||
"""
|
||||
Set compile options
|
||||
|
||||
Args:
|
||||
options: List of compiler options
|
||||
|
||||
Returns:
|
||||
Self for chaining
|
||||
"""
|
||||
self._compile_options = options.copy()
|
||||
return self
|
||||
|
||||
def add_compile_option(self, option: str) -> 'ProxyCompiler':
|
||||
"""
|
||||
Add single compile option
|
||||
|
||||
Args:
|
||||
option: Compiler option
|
||||
|
||||
Returns:
|
||||
Self for chaining
|
||||
"""
|
||||
if option:
|
||||
self._compile_options.append(option)
|
||||
return self
|
||||
|
||||
def get_class_loader(self) -> ProxyClassLoader:
|
||||
"""Get associated class loader"""
|
||||
return self._class_loader
|
||||
|
||||
def compile_and_load(self, proxy_class: ProxyClass) -> type:
|
||||
"""
|
||||
Compile proxy class and load the resulting class
|
||||
|
||||
Args:
|
||||
proxyClass: ProxyClass with source code
|
||||
|
||||
Returns:
|
||||
Loaded proxy class
|
||||
"""
|
||||
# Compile
|
||||
self.compile(proxy_class)
|
||||
|
||||
# Load
|
||||
return self._class_loader.load_proxy_class(proxy_class)
|
||||
|
||||
|
||||
class JavaProxyCompiler:
|
||||
"""Compiler for Java proxy classes (for Jython compatibility)"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize Java proxy compiler"""
|
||||
self._options: List[str] = []
|
||||
|
||||
def compile(self, proxy_class: ProxyClass):
|
||||
"""Compile Java proxy class"""
|
||||
# This would use javax.tools.JavaCompiler in Java version
|
||||
# For Python, we provide a stub implementation
|
||||
pass
|
||||
|
||||
def set_options(self, options: List[str]) -> 'JavaProxyCompiler':
|
||||
"""Set compiler options"""
|
||||
self._options = options.copy()
|
||||
return self
|
||||
|
||||
def add_option(self, option: str) -> 'JavaProxyCompiler':
|
||||
"""Add compiler option"""
|
||||
if option:
|
||||
self._options.append(option)
|
||||
return self
|
||||
46
pyenjoy/template/Directive.py
Normal file
46
pyenjoy/template/Directive.py
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Directive - Base Directive Class
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .expr.ast import ExprList
|
||||
from .stat.ast.Stat import Stat
|
||||
|
||||
class Directive(Stat):
|
||||
"""Base class for custom directives"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize directive"""
|
||||
super().__init__()
|
||||
self._expr_list: Optional[ExprList] = None
|
||||
self._stat: Optional[Stat] = None
|
||||
|
||||
@property
|
||||
def expr_list(self) -> Optional[ExprList]:
|
||||
"""Get expression list"""
|
||||
return self._expr_list
|
||||
|
||||
@expr_list.setter
|
||||
def expr_list(self, value: ExprList):
|
||||
"""Set expression list"""
|
||||
self._expr_list = value
|
||||
|
||||
@property
|
||||
def stat(self) -> Optional[Stat]:
|
||||
"""Get nested stat"""
|
||||
return self._stat
|
||||
|
||||
@stat.setter
|
||||
def stat(self, value: Stat):
|
||||
"""Set nested stat"""
|
||||
self._stat = value
|
||||
|
||||
def set_expr_list(self, expr_list: ExprList):
|
||||
"""Set expression list (for parser)"""
|
||||
self._expr_list = expr_list
|
||||
|
||||
def set_stat(self, stat: Stat):
|
||||
"""Set nested stat (for parser)"""
|
||||
self._stat = stat
|
||||
401
pyenjoy/template/Engine.py
Normal file
401
pyenjoy/template/Engine.py
Normal file
@@ -0,0 +1,401 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Engine - Template Engine Main Class
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, Set, Type, Any, Callable
|
||||
from .EngineConfig import EngineConfig
|
||||
from .Env import Env
|
||||
from .Template import Template
|
||||
from .stat.Parser import Parser
|
||||
from .stat.ast.Stat import Stat
|
||||
from .source.ISourceFactory import ISourceFactory
|
||||
from .source.ClassPathSourceFactory import ClassPathSourceFactory
|
||||
from .source.StringSource import StringSource
|
||||
from .io.WriterBuffer import WriterBuffer
|
||||
from .io.EncoderFactory import EncoderFactory
|
||||
from .io.JdkEncoderFactory import JdkEncoderFactory
|
||||
from .stat.Compressor import Compressor
|
||||
from .expr.ast.FieldKit import FieldKit
|
||||
from .expr.ast.FieldKeyBuilder import FieldKeyBuilder
|
||||
from .expr.ast.MethodKit import MethodKit
|
||||
from .Directive import Directive
|
||||
from .stat.OutputDirectiveFactory import OutputDirectiveFactory
|
||||
from ..kit.SyncWriteMap import SyncWriteMap
|
||||
from ..kit.StrKit import StrKit
|
||||
from ..kit.HashKit import HashKit
|
||||
|
||||
class Engine:
|
||||
"""Template Engine - Main class for JFinal Template Engine"""
|
||||
|
||||
MAIN_ENGINE_NAME = "main"
|
||||
|
||||
_MAIN_ENGINE: 'Engine' = None
|
||||
_engine_map: Dict[str, 'Engine'] = {}
|
||||
|
||||
def __init__(self, engine_name: str = None):
|
||||
"""
|
||||
Initialize engine
|
||||
|
||||
Args:
|
||||
engine_name: Engine name (optional)
|
||||
"""
|
||||
self._name = engine_name or "NO_NAME"
|
||||
self._dev_mode = True
|
||||
self._cache_string_template = False
|
||||
self._config = EngineConfig()
|
||||
self._source_factory = self._config.source_factory
|
||||
self._template_cache = SyncWriteMap(2048, 0.5)
|
||||
|
||||
@classmethod
|
||||
def use(cls, engine_name: str = None) -> 'Engine':
|
||||
"""
|
||||
Get engine instance
|
||||
|
||||
Args:
|
||||
engine_name: Engine name (optional, uses main engine if not specified)
|
||||
|
||||
Returns:
|
||||
Engine instance
|
||||
"""
|
||||
if engine_name is None:
|
||||
if cls._MAIN_ENGINE is None:
|
||||
cls._MAIN_ENGINE = cls(cls.MAIN_ENGINE_NAME)
|
||||
cls._engine_map[cls.MAIN_ENGINE_NAME] = cls._MAIN_ENGINE
|
||||
return cls._MAIN_ENGINE
|
||||
|
||||
return cls._engine_map.get(engine_name)
|
||||
|
||||
@classmethod
|
||||
def create(cls, engine_name: str, configurator: Callable[['Engine'], None] = None) -> 'Engine':
|
||||
"""
|
||||
Create new engine with name
|
||||
|
||||
Args:
|
||||
engine_name: Engine name
|
||||
configurator: Configuration callback
|
||||
|
||||
Returns:
|
||||
New engine instance
|
||||
"""
|
||||
if StrKit.is_blank(engine_name):
|
||||
raise ValueError("Engine name cannot be blank")
|
||||
|
||||
engine_name = engine_name.strip()
|
||||
if engine_name in cls._engine_map:
|
||||
raise ValueError(f"Engine already exists: {engine_name}")
|
||||
|
||||
new_engine = cls(engine_name)
|
||||
cls._engine_map[engine_name] = new_engine
|
||||
|
||||
if configurator:
|
||||
configurator(new_engine)
|
||||
|
||||
return new_engine
|
||||
|
||||
@classmethod
|
||||
def create_if_absent(cls, engine_name: str, configurator: Callable[['Engine'], None] = None) -> 'Engine':
|
||||
"""
|
||||
Create engine if absent
|
||||
|
||||
Args:
|
||||
engine_name: Engine name
|
||||
configurator: Configuration callback
|
||||
|
||||
Returns:
|
||||
Engine instance
|
||||
"""
|
||||
ret = cls._engine_map.get(engine_name)
|
||||
if ret is None:
|
||||
# Double-check locking
|
||||
if engine_name not in cls._engine_map:
|
||||
ret = cls.create(engine_name)
|
||||
if configurator:
|
||||
configurator(ret)
|
||||
return ret
|
||||
|
||||
@classmethod
|
||||
def remove(cls, engine_name: str) -> Optional['Engine']:
|
||||
"""
|
||||
Remove engine by name
|
||||
|
||||
Args:
|
||||
engine_name: Engine name
|
||||
|
||||
Returns:
|
||||
Removed engine or None
|
||||
"""
|
||||
removed = cls._engine_map.pop(engine_name, None)
|
||||
if removed and cls.MAIN_ENGINE_NAME == removed.name:
|
||||
cls._MAIN_ENGINE = None
|
||||
return removed
|
||||
|
||||
@classmethod
|
||||
def set_main_engine(cls, engine: 'Engine'):
|
||||
"""
|
||||
Set main engine
|
||||
|
||||
Args:
|
||||
engine: Engine to set as main
|
||||
"""
|
||||
if engine is None:
|
||||
raise ValueError("Engine cannot be null")
|
||||
|
||||
engine._name = cls.MAIN_ENGINE_NAME
|
||||
cls._engine_map[cls.MAIN_ENGINE_NAME] = engine
|
||||
cls._MAIN_ENGINE = engine
|
||||
|
||||
def get_template(self, file_name: str) -> Template:
|
||||
"""
|
||||
Get template by file name
|
||||
|
||||
Args:
|
||||
file_name: Template file name
|
||||
|
||||
Returns:
|
||||
Template instance
|
||||
"""
|
||||
template = self._template_cache.get(file_name)
|
||||
|
||||
if template is None:
|
||||
template = self._build_template_by_source_factory(file_name)
|
||||
self._template_cache[file_name] = template
|
||||
elif self._dev_mode and template.is_modified():
|
||||
template = self._build_template_by_source_factory(file_name)
|
||||
self._template_cache[file_name] = template
|
||||
|
||||
return template
|
||||
|
||||
def _build_template_by_source_factory(self, file_name: str) -> Template:
|
||||
"""Build template using source factory"""
|
||||
source = self._source_factory.get_source(
|
||||
self._config.base_template_path,
|
||||
file_name,
|
||||
self._config.encoding
|
||||
)
|
||||
|
||||
return self._build_template(source, file_name)
|
||||
|
||||
def get_template_by_string(self, content: str, cache: bool = False) -> Template:
|
||||
"""
|
||||
Get template by string content
|
||||
|
||||
Args:
|
||||
content: Template content
|
||||
cache: Whether to cache the template
|
||||
|
||||
Returns:
|
||||
Template instance
|
||||
"""
|
||||
if not cache:
|
||||
source = StringSource(content, False)
|
||||
return self._build_template(source, None)
|
||||
|
||||
cache_key = HashKit.md5(content)
|
||||
template = self._template_cache.get(cache_key)
|
||||
|
||||
if template is None:
|
||||
source = StringSource(content, cache_key)
|
||||
template = self._build_template(source, cache_key)
|
||||
self._template_cache[cache_key] = template
|
||||
elif self._dev_mode and template.is_modified():
|
||||
source = StringSource(content, cache_key)
|
||||
template = self._build_template(source, cache_key)
|
||||
self._template_cache[cache_key] = template
|
||||
|
||||
return template
|
||||
|
||||
def _build_template(self, source, cache_key: str = None) -> Template:
|
||||
"""Build template from source"""
|
||||
env = Env(self._config)
|
||||
|
||||
if self._dev_mode:
|
||||
env.add_source(source)
|
||||
|
||||
parser = Parser(env, source.get_content(), cache_key)
|
||||
stat = parser.parse()
|
||||
|
||||
return Template(env, stat)
|
||||
|
||||
def add_shared_function(self, file_name: str) -> 'Engine':
|
||||
"""Add shared function from file"""
|
||||
self._config.add_shared_function(file_name)
|
||||
return self
|
||||
|
||||
def add_shared_function_by_string(self, content: str) -> 'Engine':
|
||||
"""Add shared function from string"""
|
||||
self._config.add_shared_function_by_content(content)
|
||||
return self
|
||||
|
||||
def add_shared_object(self, name: str, obj: Any) -> 'Engine':
|
||||
"""Add shared object"""
|
||||
self._config.add_shared_object(name, obj)
|
||||
return self
|
||||
|
||||
def remove_shared_object(self, name: str) -> 'Engine':
|
||||
"""Remove shared object"""
|
||||
self._config.remove_shared_object(name)
|
||||
return self
|
||||
|
||||
def add_directive(self, directive_name: str, directive_class: Type[Directive],
|
||||
keep_line_blank: bool = False) -> 'Engine':
|
||||
"""Add custom directive"""
|
||||
self._config.add_directive(directive_name, directive_class, keep_line_blank)
|
||||
return self
|
||||
|
||||
def remove_directive(self, directive_name: str) -> 'Engine':
|
||||
"""Remove directive"""
|
||||
self._config.remove_directive(directive_name)
|
||||
return self
|
||||
|
||||
def add_shared_method(self, obj: Any) -> 'Engine':
|
||||
"""Add shared method from object"""
|
||||
self._config.add_shared_method(obj)
|
||||
return self
|
||||
|
||||
def add_shared_method_from_class(self, clazz: Type) -> 'Engine':
|
||||
"""Add shared method from class"""
|
||||
self._config.add_shared_method_from_class(clazz)
|
||||
return self
|
||||
|
||||
def add_shared_static_method(self, clazz: Type) -> 'Engine':
|
||||
"""Add shared static method from class"""
|
||||
self._config.add_shared_static_method(clazz)
|
||||
return self
|
||||
|
||||
def remove_shared_method(self, method_name: str) -> 'Engine':
|
||||
"""Remove shared method by name"""
|
||||
self._config.remove_shared_method(method_name)
|
||||
return self
|
||||
|
||||
def remove_template_cache(self, cache_key: str):
|
||||
"""Remove template cache by key"""
|
||||
self._template_cache.pop(cache_key, None)
|
||||
|
||||
def remove_all_template_cache(self):
|
||||
"""Remove all template cache"""
|
||||
self._template_cache.clear()
|
||||
|
||||
@property
|
||||
def dev_mode(self) -> bool:
|
||||
"""Get dev mode setting"""
|
||||
return self._dev_mode
|
||||
|
||||
@dev_mode.setter
|
||||
def dev_mode(self, value: bool):
|
||||
"""Set dev mode"""
|
||||
self._dev_mode = value
|
||||
self._config.dev_mode = value
|
||||
if self._dev_mode:
|
||||
self.remove_all_template_cache()
|
||||
|
||||
@property
|
||||
def cache_string_template(self) -> bool:
|
||||
"""Get cache string template setting"""
|
||||
return self._cache_string_template
|
||||
|
||||
@cache_string_template.setter
|
||||
def cache_string_template(self, value: bool):
|
||||
"""Set cache string template"""
|
||||
self._cache_string_template = value
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get engine name"""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def config(self) -> EngineConfig:
|
||||
"""Get engine configuration"""
|
||||
return self._config
|
||||
|
||||
@property
|
||||
def source_factory(self) -> ISourceFactory:
|
||||
"""Get source factory"""
|
||||
return self._source_factory
|
||||
|
||||
@source_factory.setter
|
||||
def source_factory(self, value: ISourceFactory):
|
||||
"""Set source factory"""
|
||||
self._config.source_factory = value
|
||||
self._source_factory = value
|
||||
|
||||
def set_to_class_path_source_factory(self) -> 'Engine':
|
||||
"""Set to class path source factory"""
|
||||
self.source_factory = ClassPathSourceFactory()
|
||||
return self
|
||||
|
||||
@property
|
||||
def base_template_path(self) -> Optional[str]:
|
||||
"""Get base template path"""
|
||||
return self._config.base_template_path
|
||||
|
||||
@base_template_path.setter
|
||||
def base_template_path(self, value: str):
|
||||
"""Set base template path"""
|
||||
self._config.base_template_path = value
|
||||
|
||||
@property
|
||||
def encoding(self) -> str:
|
||||
"""Get encoding"""
|
||||
return self._config.encoding
|
||||
|
||||
@encoding.setter
|
||||
def encoding(self, value: str):
|
||||
"""Set encoding"""
|
||||
self._config.encoding = value
|
||||
|
||||
@property
|
||||
def date_pattern(self) -> str:
|
||||
"""Get date pattern"""
|
||||
return self._config.date_pattern
|
||||
|
||||
@date_pattern.setter
|
||||
def date_pattern(self, value: str):
|
||||
"""Set date pattern"""
|
||||
self._config.date_pattern = value
|
||||
|
||||
def set_compressor(self, compressor: Compressor) -> 'Engine':
|
||||
"""Set compressor"""
|
||||
self._config._compressor = compressor
|
||||
return self
|
||||
|
||||
def set_compressor_on(self, separator: str = '\n') -> 'Engine':
|
||||
"""Set compressor on"""
|
||||
self._config._compressor = Compressor(separator)
|
||||
return self
|
||||
|
||||
def set_encoder_factory(self, encoder_factory: EncoderFactory) -> 'Engine':
|
||||
"""Set encoder factory"""
|
||||
self._config.writer_buffer.set_encoder_factory(encoder_factory)
|
||||
self._config.writer_buffer.set_encoding(self._config.encoding)
|
||||
return self
|
||||
|
||||
def set_to_jdk_encoder_factory(self) -> 'Engine':
|
||||
"""Set to JDK encoder factory"""
|
||||
self.set_encoder_factory(JdkEncoderFactory())
|
||||
return self
|
||||
|
||||
def set_buffer_size(self, buffer_size: int) -> 'Engine':
|
||||
"""Set buffer size"""
|
||||
self._config.writer_buffer.set_buffer_size(buffer_size)
|
||||
return self
|
||||
|
||||
def set_reentrant_buffer_size(self, reentrant_buffer_size: int) -> 'Engine':
|
||||
"""Set reentrant buffer size"""
|
||||
self._config.writer_buffer.set_reentrant_buffer_size(reentrant_buffer_size)
|
||||
return self
|
||||
|
||||
@property
|
||||
def template_cache_size(self) -> int:
|
||||
"""Get template cache size"""
|
||||
return len(self._template_cache)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Template Engine: {self._name}"
|
||||
|
||||
|
||||
# Initialize main engine
|
||||
Engine._MAIN_ENGINE = Engine(Engine.MAIN_ENGINE_NAME)
|
||||
Engine._engine_map[Engine.MAIN_ENGINE_NAME] = Engine._MAIN_ENGINE
|
||||
266
pyenjoy/template/EngineConfig.py
Normal file
266
pyenjoy/template/EngineConfig.py
Normal file
@@ -0,0 +1,266 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal EngineConfig - Template Engine Configuration
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, List, Set, Type, Any
|
||||
from decimal import Decimal
|
||||
from .Directive import Directive
|
||||
from .stat.Parser import Parser
|
||||
from .stat.ast.Define import Define
|
||||
from .stat.OutputDirectiveFactory import OutputDirectiveFactory
|
||||
from .io.WriterBuffer import WriterBuffer
|
||||
from .source.ISourceFactory import ISourceFactory
|
||||
from .source.FileSourceFactory import FileSourceFactory
|
||||
from .expr.ast.SharedMethodKit import SharedMethodKit
|
||||
from .ext.directive.RenderDirective import RenderDirective
|
||||
from .ext.directive.DateDirective import DateDirective
|
||||
from .ext.directive.EscapeDirective import EscapeDirective
|
||||
from .ext.directive.RandomDirective import RandomDirective
|
||||
from .ext.directive.NumberDirective import NumberDirective
|
||||
from .ext.directive.CallDirective import CallDirective
|
||||
from .ext.directive.StringDirective import StringDirective
|
||||
from .ext.sharedmethod.SharedMethodLib import SharedMethodLib
|
||||
|
||||
class EngineConfig:
|
||||
"""Template engine configuration"""
|
||||
|
||||
DEFAULT_ENCODING = "UTF-8"
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize engine configuration"""
|
||||
self._writer_buffer = WriterBuffer()
|
||||
self._compressor = None
|
||||
self._shared_function_map: Dict[str, Define] = {}
|
||||
self._shared_function_source_list: List[object] = []
|
||||
self._shared_object_map: Optional[Dict[str, Any]] = None
|
||||
self._output_directive_factory = OutputDirectiveFactory()
|
||||
self._source_factory = FileSourceFactory()
|
||||
self._directive_map: Dict[str, Type[Directive]] = {}
|
||||
self._shared_method_kit = SharedMethodKit()
|
||||
self._keep_line_blank_directives: Set[str] = set()
|
||||
|
||||
self._dev_mode = False
|
||||
self._reload_modified_shared_function_in_dev_mode = True
|
||||
self._base_template_path: Optional[str] = None
|
||||
self._encoding = EngineConfig.DEFAULT_ENCODING
|
||||
self._date_pattern = "yyyy-MM-dd HH:mm"
|
||||
self._rounding_mode = "HALF_UP" # Default rounding mode
|
||||
self._support_static_method_expression = False
|
||||
self._support_static_field_expression = False
|
||||
|
||||
# Initialize built-in directives
|
||||
self._init_builtin_directives()
|
||||
|
||||
def _init_builtin_directives(self):
|
||||
"""Initialize built-in directives"""
|
||||
self._keep_line_blank_directives.add("output")
|
||||
self._keep_line_blank_directives.add("include")
|
||||
|
||||
self.add_directive("render", RenderDirective, True)
|
||||
self.add_directive("date", DateDirective, True)
|
||||
self.add_directive("escape", EscapeDirective, True)
|
||||
self.add_directive("random", RandomDirective, True)
|
||||
self.add_directive("number", NumberDirective, True)
|
||||
self.add_directive("call", CallDirective, False)
|
||||
self.add_directive("string", StringDirective, False)
|
||||
|
||||
# Add built-in shared methods
|
||||
self.add_shared_method(SharedMethodLib())
|
||||
|
||||
def add_shared_function(self, file_name: str):
|
||||
"""Add shared function from file"""
|
||||
file_name = file_name.replace("\\", "/")
|
||||
source = self._source_factory.get_source(self._base_template_path, file_name, self._encoding)
|
||||
self._do_add_shared_function(source, file_name)
|
||||
|
||||
def _do_add_shared_function(self, source, file_name: str):
|
||||
"""Internal method to add shared function"""
|
||||
# Create a minimal env for parsing
|
||||
env = Env(self)
|
||||
parser = Parser(env, source.get_content(), file_name)
|
||||
parser.parse()
|
||||
|
||||
# Add to shared function map
|
||||
func_map = env.get_function_map()
|
||||
for func_name, func in func_map.items():
|
||||
if func_name in self._shared_function_map:
|
||||
raise ValueError(f"Template function already exists: {func_name}")
|
||||
|
||||
self._shared_function_map[func_name] = func
|
||||
|
||||
# Add source for dev mode
|
||||
if self._dev_mode:
|
||||
self._shared_function_source_list.append(source)
|
||||
env.add_source(source)
|
||||
|
||||
def add_shared_function_by_content(self, content: str):
|
||||
"""Add shared function from content string"""
|
||||
from .source.StringSource import StringSource
|
||||
string_source = StringSource(content, False)
|
||||
self._do_add_shared_function(string_source, None)
|
||||
|
||||
def add_shared_object(self, name: str, obj: Any):
|
||||
"""Add shared object"""
|
||||
if self._shared_object_map is None:
|
||||
self._shared_object_map = {}
|
||||
|
||||
if name in self._shared_object_map:
|
||||
raise ValueError(f"Shared object already exists: {name}")
|
||||
|
||||
self._shared_object_map[name] = obj
|
||||
|
||||
def get_shared_object(self, name: str) -> Optional[Any]:
|
||||
"""Get shared object"""
|
||||
if self._shared_object_map:
|
||||
return self._shared_object_map.get(name)
|
||||
return None
|
||||
|
||||
def remove_shared_object(self, name: str):
|
||||
"""Remove shared object"""
|
||||
if self._shared_object_map:
|
||||
self._shared_object_map.pop(name, None)
|
||||
|
||||
def add_directive(self, directive_name: str, directive_class: Type[Directive], keep_line_blank: bool = False):
|
||||
"""Add custom directive"""
|
||||
if not directive_name or not directive_name.strip():
|
||||
raise ValueError("directive name cannot be blank")
|
||||
|
||||
if directive_class is None:
|
||||
raise ValueError("directiveClass cannot be null")
|
||||
|
||||
if directive_name in self._directive_map:
|
||||
raise ValueError(f"directive already exists: {directive_name}")
|
||||
|
||||
self._directive_map[directive_name] = directive_class
|
||||
|
||||
if keep_line_blank:
|
||||
self._keep_line_blank_directives.add(directive_name)
|
||||
|
||||
def get_directive(self, directive_name: str) -> Optional[Type[Directive]]:
|
||||
"""Get directive class"""
|
||||
return self._directive_map.get(directive_name)
|
||||
|
||||
def remove_directive(self, directive_name: str):
|
||||
"""Remove directive"""
|
||||
self._directive_map.pop(directive_name, None)
|
||||
self._keep_line_blank_directives.discard(directive_name)
|
||||
|
||||
def add_shared_method(self, obj: Any):
|
||||
"""Add shared method from object"""
|
||||
self._shared_method_kit.add_shared_method(obj)
|
||||
|
||||
def add_shared_method_from_class(self, clazz: Type):
|
||||
"""Add shared method from class"""
|
||||
self._shared_method_kit.add_shared_method(clazz)
|
||||
|
||||
def add_shared_static_method(self, clazz: Type):
|
||||
"""Add shared static method from class"""
|
||||
self._shared_method_kit.add_shared_static_method(clazz)
|
||||
|
||||
def remove_shared_method(self, method_name: str):
|
||||
"""Remove shared method by name"""
|
||||
self._shared_method_kit.remove_shared_method(method_name)
|
||||
|
||||
def get_shared_method_kit(self) -> SharedMethodKit:
|
||||
"""Get shared method kit"""
|
||||
return self._shared_method_kit
|
||||
|
||||
# Property accessors
|
||||
@property
|
||||
def dev_mode(self) -> bool:
|
||||
"""Get dev mode setting"""
|
||||
return self._dev_mode
|
||||
|
||||
@dev_mode.setter
|
||||
def dev_mode(self, value: bool):
|
||||
"""Set dev mode"""
|
||||
self._dev_mode = value
|
||||
|
||||
@property
|
||||
def base_template_path(self) -> Optional[str]:
|
||||
"""Get base template path"""
|
||||
return self._base_template_path
|
||||
|
||||
@base_template_path.setter
|
||||
def base_template_path(self, value: str):
|
||||
"""Set base template path"""
|
||||
if value is None:
|
||||
self._base_template_path = None
|
||||
return
|
||||
|
||||
value = value.strip().replace("\\", "/")
|
||||
if not value:
|
||||
raise ValueError("baseTemplatePath cannot be blank")
|
||||
|
||||
if len(value) > 1 and value.endswith("/"):
|
||||
value = value[:-1]
|
||||
|
||||
self._base_template_path = value
|
||||
|
||||
@property
|
||||
def encoding(self) -> str:
|
||||
"""Get encoding"""
|
||||
return self._encoding
|
||||
|
||||
@encoding.setter
|
||||
def encoding(self, value: str):
|
||||
"""Set encoding"""
|
||||
if not value:
|
||||
raise ValueError("encoding cannot be blank")
|
||||
self._encoding = value
|
||||
self._writer_buffer.set_encoding(value)
|
||||
|
||||
@property
|
||||
def date_pattern(self) -> str:
|
||||
"""Get date pattern"""
|
||||
return self._date_pattern
|
||||
|
||||
@date_pattern.setter
|
||||
def date_pattern(self, value: str):
|
||||
"""Set date pattern"""
|
||||
if not value:
|
||||
raise ValueError("datePattern cannot be blank")
|
||||
self._date_pattern = value
|
||||
|
||||
@property
|
||||
def rounding_mode(self) -> str:
|
||||
"""Get rounding mode"""
|
||||
return self._rounding_mode
|
||||
|
||||
@rounding_mode.setter
|
||||
def rounding_mode(self, value: str):
|
||||
"""Set rounding mode"""
|
||||
self._rounding_mode = value
|
||||
|
||||
@property
|
||||
def writer_buffer(self) -> WriterBuffer:
|
||||
"""Get writer buffer"""
|
||||
return self._writer_buffer
|
||||
|
||||
@property
|
||||
def source_factory(self) -> ISourceFactory:
|
||||
"""Get source factory"""
|
||||
return self._source_factory
|
||||
|
||||
@source_factory.setter
|
||||
def source_factory(self, value: ISourceFactory):
|
||||
"""Set source factory"""
|
||||
if value is None:
|
||||
raise ValueError("sourceFactory cannot be null")
|
||||
self._source_factory = value
|
||||
|
||||
@property
|
||||
def shared_function_map(self) -> Dict[str, Define]:
|
||||
"""Get shared function map"""
|
||||
return self._shared_function_map.copy()
|
||||
|
||||
@property
|
||||
def shared_object_map(self) -> Optional[Dict[str, Any]]:
|
||||
"""Get shared object map"""
|
||||
return self._shared_object_map.copy() if self._shared_object_map else None
|
||||
|
||||
|
||||
# Import needed classes to avoid circular imports
|
||||
from .Env import Env
|
||||
92
pyenjoy/template/Env.py
Normal file
92
pyenjoy/template/Env.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Env - Template Environment
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, List
|
||||
|
||||
class Env:
|
||||
"""Template environment for storing template functions and managing sources"""
|
||||
|
||||
def __init__(self, engine_config):
|
||||
"""
|
||||
Initialize environment
|
||||
|
||||
Args:
|
||||
engine_config: Engine configuration
|
||||
"""
|
||||
self._engine_config = engine_config
|
||||
self._function_map: Dict[str, object] = {}
|
||||
self._source_list: Optional[List[object]] = None
|
||||
|
||||
@property
|
||||
def engine_config(self):
|
||||
"""Get engine configuration"""
|
||||
return self._engine_config
|
||||
|
||||
@property
|
||||
def is_dev_mode(self) -> bool:
|
||||
"""Check if in development mode"""
|
||||
return self._engine_config.is_dev_mode()
|
||||
|
||||
def add_function(self, function_name: str, function: object):
|
||||
"""
|
||||
Add template function
|
||||
|
||||
Args:
|
||||
function_name: Function name
|
||||
function: Function object
|
||||
"""
|
||||
if function_name in self._function_map:
|
||||
raise ValueError(f"Template function '{function_name}' already defined")
|
||||
|
||||
self._function_map[function_name] = function
|
||||
|
||||
def get_function(self, function_name: str) -> Optional[object]:
|
||||
"""
|
||||
Get template function
|
||||
|
||||
Args:
|
||||
function_name: Function name
|
||||
|
||||
Returns:
|
||||
Function object or None
|
||||
"""
|
||||
func = self._function_map.get(function_name)
|
||||
if func is not None:
|
||||
return func
|
||||
|
||||
# Try to get from shared functions
|
||||
return self._engine_config.get_shared_function(function_name)
|
||||
|
||||
def get_function_map(self) -> Dict[str, object]:
|
||||
"""Get all functions"""
|
||||
return self._function_map.copy()
|
||||
|
||||
def add_source(self, source):
|
||||
"""
|
||||
Add source for dev mode tracking
|
||||
|
||||
Args:
|
||||
source: Source object
|
||||
"""
|
||||
if self._source_list is None:
|
||||
self._source_list = []
|
||||
|
||||
self._source_list.append(source)
|
||||
|
||||
def is_source_list_modified(self) -> bool:
|
||||
"""Check if any source has been modified (for dev mode)"""
|
||||
if self._source_list:
|
||||
for source in self._source_list:
|
||||
if hasattr(source, 'is_modified') and source.is_modified():
|
||||
return True
|
||||
return False
|
||||
|
||||
def clear_functions(self):
|
||||
"""Clear all functions"""
|
||||
self._function_map.clear()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Env(function_count={len(self._function_map)}, dev_mode={self.is_dev_mode})"
|
||||
123
pyenjoy/template/Template.py
Normal file
123
pyenjoy/template/Template.py
Normal file
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Template - Template Rendering Engine
|
||||
"""
|
||||
|
||||
from typing import Dict, Optional, Union, Callable
|
||||
from io import StringIO
|
||||
from .Env import Env
|
||||
from .stat.ast.Stat import Stat
|
||||
|
||||
class Template:
|
||||
"""Template rendering engine"""
|
||||
|
||||
def __init__(self, env: Env, ast: Stat):
|
||||
"""
|
||||
Initialize template
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
ast: Abstract syntax tree
|
||||
"""
|
||||
if env is None or ast is None:
|
||||
raise ValueError("env and ast cannot be null")
|
||||
|
||||
self._env = env
|
||||
self._ast = ast
|
||||
|
||||
def render(self, data: Dict = None, output = None):
|
||||
"""
|
||||
Render template to output
|
||||
|
||||
Args:
|
||||
data: Data dictionary
|
||||
output: Output target (file, stream, writer, etc.)
|
||||
"""
|
||||
data = data or {}
|
||||
scope = self._create_scope(data)
|
||||
|
||||
if hasattr(output, 'write'):
|
||||
# It's a writer-like object
|
||||
self._ast.exec(self._env, scope, output)
|
||||
elif hasattr(output, 'writebytes') or hasattr(output, 'write_bytes'):
|
||||
# It's a binary stream
|
||||
self._render_to_binary(output, scope)
|
||||
else:
|
||||
# Try to convert to appropriate output
|
||||
self._render_to_output(output, scope)
|
||||
|
||||
def render_to_string(self, data: Dict = None) -> str:
|
||||
"""
|
||||
Render template to string
|
||||
|
||||
Args:
|
||||
data: Data dictionary
|
||||
|
||||
Returns:
|
||||
Rendered string
|
||||
"""
|
||||
output = StringIO()
|
||||
self.render(data, output)
|
||||
return output.getvalue()
|
||||
|
||||
def render_to_string_builder(self, data: Dict = None) -> StringIO:
|
||||
"""
|
||||
Render template to StringIO
|
||||
|
||||
Args:
|
||||
data: Data dictionary
|
||||
|
||||
Returns:
|
||||
StringIO object
|
||||
"""
|
||||
output = StringIO()
|
||||
self.render(data, output)
|
||||
return output
|
||||
|
||||
def _create_scope(self, data: Dict) -> 'Scope':
|
||||
"""Create execution scope"""
|
||||
from .stat.Scope import Scope
|
||||
shared_objects = self._env.engine_config.shared_object_map or {}
|
||||
return Scope(data, shared_objects)
|
||||
|
||||
def _render_to_binary(self, output, scope):
|
||||
"""Render to binary output"""
|
||||
# Simplified binary output handling
|
||||
result = self._ast.exec(self._env, scope, StringIO())
|
||||
if isinstance(result, str):
|
||||
output.write(result.encode('utf-8'))
|
||||
|
||||
def _render_to_output(self, output, scope):
|
||||
"""Render to various output types"""
|
||||
result = self._ast.exec(self._env, scope, StringIO())
|
||||
if isinstance(result, str):
|
||||
if hasattr(output, '__fspath__'):
|
||||
# It's a file path
|
||||
with open(output, 'w', encoding='utf-8') as f:
|
||||
f.write(result)
|
||||
elif isinstance(output, str):
|
||||
# It's a file path string
|
||||
with open(output, 'w', encoding='utf-8') as f:
|
||||
f.write(result)
|
||||
else:
|
||||
# Try to convert to string
|
||||
output_str = str(result)
|
||||
if hasattr(output, 'write'):
|
||||
output.write(output_str)
|
||||
|
||||
def is_modified(self) -> bool:
|
||||
"""Check if template has been modified (for dev mode)"""
|
||||
return self._env.is_source_list_modified()
|
||||
|
||||
def get_env(self) -> Env:
|
||||
"""Get template environment"""
|
||||
return self._env
|
||||
|
||||
def get_ast(self) -> Stat:
|
||||
"""Get abstract syntax tree"""
|
||||
return self._ast
|
||||
|
||||
|
||||
# Import Scope to avoid circular import
|
||||
from .stat.Scope import Scope
|
||||
44
pyenjoy/template/TemplateException.py
Normal file
44
pyenjoy/template/TemplateException.py
Normal file
@@ -0,0 +1,44 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal TemplateException - Template Exception
|
||||
"""
|
||||
|
||||
class TemplateException(Exception):
|
||||
"""Template runtime exception"""
|
||||
|
||||
def __init__(self, message: str, location = None, cause: Exception = None):
|
||||
"""
|
||||
Initialize template exception
|
||||
|
||||
Args:
|
||||
message: Error message
|
||||
location: Error location (optional)
|
||||
cause: Original exception (optional)
|
||||
"""
|
||||
full_message = message
|
||||
if location is not None:
|
||||
full_message += str(location)
|
||||
|
||||
super().__init__(full_message)
|
||||
self._location = location
|
||||
self._cause = cause
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
"""Get error location"""
|
||||
return self._location
|
||||
|
||||
@property
|
||||
def cause(self) -> Exception:
|
||||
"""Get original cause"""
|
||||
return self._cause
|
||||
|
||||
def __str__(self) -> str:
|
||||
result = super().__str__()
|
||||
if self._cause and self._cause != self:
|
||||
result += f"\nCaused by: {self._cause}"
|
||||
return result
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"TemplateException(message={super().__repr__()}, location={self._location})"
|
||||
1
pyenjoy/template/expr/__init__.py
Normal file
1
pyenjoy/template/expr/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Python package initialization
|
||||
172
pyenjoy/template/expr/ast/Arith.py
Normal file
172
pyenjoy/template/expr/ast/Arith.py
Normal file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Arith - Arithmetic Expression
|
||||
"""
|
||||
|
||||
from .Expr import Expr
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
||||
class Arith(Expr):
|
||||
"""Arithmetic expression"""
|
||||
|
||||
ADD = "+"
|
||||
SUB = "-"
|
||||
MUL = "*"
|
||||
DIV = "/"
|
||||
MOD = "%"
|
||||
|
||||
_rounding_mode = ROUND_HALF_UP
|
||||
|
||||
def __init__(self, left: Expr, operator: str, right: Expr):
|
||||
"""
|
||||
Initialize arithmetic expression
|
||||
|
||||
Args:
|
||||
left: Left expression
|
||||
operator: Arithmetic operator
|
||||
right: Right expression
|
||||
"""
|
||||
self._left = left
|
||||
self._operator = operator
|
||||
self._right = right
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate arithmetic expression"""
|
||||
left_val = self._left.eval(scope)
|
||||
right_val = self._right.eval(scope)
|
||||
|
||||
# Handle None values
|
||||
if left_val is None or right_val is None:
|
||||
return None
|
||||
|
||||
# Convert to appropriate numeric types
|
||||
left_num = self._to_number(left_val)
|
||||
right_num = self._to_number(right_val)
|
||||
|
||||
if left_num is None or right_num is None:
|
||||
return None
|
||||
|
||||
# Perform operation
|
||||
if self._operator == Arith.ADD:
|
||||
return left_num + right_num
|
||||
elif self._operator == Arith.SUB:
|
||||
return left_num - right_num
|
||||
elif self._operator == Arith.MUL:
|
||||
return left_num * right_num
|
||||
elif self._operator == Arith.DIV:
|
||||
if right_num == 0:
|
||||
raise ZeroDivisionError("Division by zero")
|
||||
return self._divide(left_num, right_num)
|
||||
elif self._operator == Arith.MOD:
|
||||
return left_num % right_num
|
||||
else:
|
||||
raise ValueError(f"Unknown operator: {self._operator}")
|
||||
|
||||
def _to_number(self, value):
|
||||
"""Convert value to number"""
|
||||
if isinstance(value, (int, float)):
|
||||
return value
|
||||
if isinstance(value, Decimal):
|
||||
return value
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
if '.' in value:
|
||||
return float(value)
|
||||
else:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return None
|
||||
return None
|
||||
|
||||
def _divide(self, left, right):
|
||||
"""Handle division with proper rounding"""
|
||||
if isinstance(left, Decimal) or isinstance(right, Decimal):
|
||||
result = Decimal(str(left)) / Decimal(str(right))
|
||||
return result.quantize(Decimal('0.00000001'), rounding=self._rounding_mode)
|
||||
else:
|
||||
return left / right
|
||||
|
||||
@classmethod
|
||||
def set_big_decimal_divide_rounding_mode(cls, rounding_mode):
|
||||
"""Set rounding mode for decimal division"""
|
||||
cls._rounding_mode = rounding_mode
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Arith({self._left} {self._operator} {self._right})"
|
||||
|
||||
|
||||
class Unary(Expr):
|
||||
"""Unary expression (positive, negative, increment, decrement)"""
|
||||
|
||||
POSITIVE = "+"
|
||||
NEGATIVE = "-"
|
||||
NOT = "!"
|
||||
|
||||
def __init__(self, operator: str, expr: Expr):
|
||||
"""
|
||||
Initialize unary expression
|
||||
|
||||
Args:
|
||||
operator: Unary operator
|
||||
expr: Expression to operate on
|
||||
"""
|
||||
self._operator = operator
|
||||
self._expr = expr
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate unary expression"""
|
||||
val = self._expr.eval(scope)
|
||||
|
||||
if self._operator == Unary.POSITIVE:
|
||||
return +val if isinstance(val, (int, float)) else val
|
||||
elif self._operator == Unary.NEGATIVE:
|
||||
return -val if isinstance(val, (int, float)) else val
|
||||
elif self._operator == Unary.NOT:
|
||||
return not val
|
||||
else:
|
||||
raise ValueError(f"Unknown operator: {self._operator}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Unary({self._operator}{self._expr})"
|
||||
|
||||
|
||||
class IncDec(Expr):
|
||||
"""Increment/Decrement expression"""
|
||||
|
||||
INC = "++"
|
||||
DEC = "--"
|
||||
|
||||
def __init__(self, expr: Expr, operator: str, is_prefix: bool = True):
|
||||
"""
|
||||
Initialize increment/decrement
|
||||
|
||||
Args:
|
||||
expr: Expression to increment/decrement
|
||||
operator: INC or DEC
|
||||
is_prefix: Whether this is prefix (++x) or postfix (x++)
|
||||
"""
|
||||
self._expr = expr
|
||||
self._operator = operator
|
||||
self._is_prefix = is_prefix
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate increment/decrement"""
|
||||
# This is a simplified implementation
|
||||
# In a real implementation, this would modify the variable
|
||||
current_val = self._expr.eval(scope)
|
||||
|
||||
if isinstance(current_val, int):
|
||||
if self._operator == IncDec.INC:
|
||||
return current_val + 1
|
||||
else:
|
||||
return current_val - 1
|
||||
|
||||
return current_val
|
||||
|
||||
def __repr__(self) -> str:
|
||||
op_str = self._operator
|
||||
if self._is_prefix:
|
||||
return f"IncDec({op_str}{self._expr})"
|
||||
else:
|
||||
return f"IncDec({self._expr}{op_str})"
|
||||
138
pyenjoy/template/expr/ast/Compare.py
Normal file
138
pyenjoy/template/expr/ast/Compare.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Compare - Comparison Expression
|
||||
"""
|
||||
|
||||
from .Expr import Expr
|
||||
|
||||
class Compare(Expr):
|
||||
"""Comparison expression"""
|
||||
|
||||
EQ = "=="
|
||||
NE = "!="
|
||||
GT = ">"
|
||||
GTE = ">="
|
||||
LT = "<"
|
||||
LTE = "<="
|
||||
|
||||
def __init__(self, left: Expr, operator: str, right: Expr):
|
||||
"""
|
||||
Initialize comparison
|
||||
|
||||
Args:
|
||||
left: Left expression
|
||||
operator: Comparison operator
|
||||
right: Right expression
|
||||
"""
|
||||
self._left = left
|
||||
self._operator = operator
|
||||
self._right = right
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate comparison"""
|
||||
left_val = self._left.eval(scope)
|
||||
right_val = self._right.eval(scope)
|
||||
|
||||
# Handle None comparisons
|
||||
if left_val is None and right_val is None:
|
||||
return self._operator == Compare.EQ
|
||||
if left_val is None or right_val is None:
|
||||
return self._operator == Compare.NE
|
||||
|
||||
# Perform comparison based on operator
|
||||
if self._operator == Compare.EQ:
|
||||
return left_val == right_val
|
||||
elif self._operator == Compare.NE:
|
||||
return left_val != right_val
|
||||
elif self._operator == Compare.GT:
|
||||
return left_val > right_val
|
||||
elif self._operator == Compare.GTE:
|
||||
return left_val >= right_val
|
||||
elif self._operator == Compare.LT:
|
||||
return left_val < right_val
|
||||
elif self._operator == Compare.LTE:
|
||||
return left_val <= right_val
|
||||
else:
|
||||
raise ValueError(f"Unknown operator: {self._operator}")
|
||||
|
||||
@property
|
||||
def operator(self) -> str:
|
||||
"""Get operator"""
|
||||
return self._operator
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Compare({self._left} {self._operator} {self._right})"
|
||||
|
||||
|
||||
class Logic(Expr):
|
||||
"""Logical expression (and, or, not)"""
|
||||
|
||||
AND = "&&"
|
||||
OR = "||"
|
||||
NOT = "!"
|
||||
|
||||
def __init__(self, operator: str, *expressions: Expr):
|
||||
"""
|
||||
Initialize logical expression
|
||||
|
||||
Args:
|
||||
operator: Logical operator
|
||||
expressions: Expressions to operate on
|
||||
"""
|
||||
self._operator = operator
|
||||
self._expressions = expressions
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate logical expression"""
|
||||
if self._operator == Logic.NOT:
|
||||
# Unary NOT
|
||||
if not self._expressions:
|
||||
return True
|
||||
return not self._expressions[0].eval(scope)
|
||||
else:
|
||||
# Binary AND/OR
|
||||
results = [expr.eval(scope) for expr in self._expressions]
|
||||
|
||||
if self._operator == Logic.AND:
|
||||
return all(results)
|
||||
elif self._operator == Logic.OR:
|
||||
return any(results)
|
||||
else:
|
||||
raise ValueError(f"Unknown operator: {self._operator}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
expr_str = f" {self._operator} ".join(str(expr) for expr in self._expressions)
|
||||
if self._operator == Logic.NOT:
|
||||
return f"NOT({expr_str})"
|
||||
return f"({expr_str})"
|
||||
|
||||
|
||||
class Ternary(Expr):
|
||||
"""Ternary conditional expression (condition ? true_val : false_val)"""
|
||||
|
||||
def __init__(self, condition: Expr, true_expr: Expr, false_expr: Expr):
|
||||
"""
|
||||
Initialize ternary expression
|
||||
|
||||
Args:
|
||||
condition: Condition expression
|
||||
true_expr: Expression when condition is true
|
||||
false_expr: Expression when condition is false
|
||||
"""
|
||||
self._condition = condition
|
||||
self._true_expr = true_expr
|
||||
self._false_expr = false_expr
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate ternary expression"""
|
||||
cond_val = self._condition.eval(scope)
|
||||
|
||||
# Convert to boolean (handle None, 0, empty strings, etc.)
|
||||
if cond_val:
|
||||
return self._true_expr.eval(scope)
|
||||
else:
|
||||
return self._false_expr.eval(scope)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Ternary({self._condition} ? {self._true_expr} : {self._false_expr})"
|
||||
151
pyenjoy/template/expr/ast/Const.py
Normal file
151
pyenjoy/template/expr/ast/Const.py
Normal file
@@ -0,0 +1,151 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Const - Constant Expression
|
||||
"""
|
||||
|
||||
from .Expr import Expr
|
||||
|
||||
class Const(Expr):
|
||||
"""Constant value expression"""
|
||||
|
||||
def __init__(self, value):
|
||||
"""
|
||||
Initialize constant
|
||||
|
||||
Args:
|
||||
value: Constant value
|
||||
"""
|
||||
self._value = value
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate and return constant value"""
|
||||
return self._value
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Const({self._value})"
|
||||
|
||||
|
||||
class NullExpr(Expr):
|
||||
"""Null expression"""
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate and return None"""
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "NullExpr()"
|
||||
|
||||
|
||||
class Id(Expr):
|
||||
"""Identifier expression (variable reference)"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
"""
|
||||
Initialize identifier
|
||||
|
||||
Args:
|
||||
name: Variable name
|
||||
"""
|
||||
self._name = name
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate and return variable value"""
|
||||
return scope.get(self._name)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get identifier name"""
|
||||
return self._name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Id({self._name})"
|
||||
|
||||
|
||||
class Map(Expr):
|
||||
"""Map/dictionary expression"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize empty map"""
|
||||
self._entries = []
|
||||
|
||||
def add_entry(self, key_expr: Expr, value_expr: Expr):
|
||||
"""
|
||||
Add map entry
|
||||
|
||||
Args:
|
||||
key_expr: Key expression
|
||||
value_expr: Value expression
|
||||
"""
|
||||
self._entries.append((key_expr, value_expr))
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate and return map"""
|
||||
result = {}
|
||||
for key_expr, value_expr in self._entries:
|
||||
key = key_expr.eval(scope)
|
||||
value = value_expr.eval(scope)
|
||||
if key is not None:
|
||||
result[key] = value
|
||||
return result
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Map({len(self._entries)} entries)"
|
||||
|
||||
|
||||
class Array(Expr):
|
||||
"""Array/list expression"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize empty array"""
|
||||
self._elements = []
|
||||
|
||||
def add_element(self, element_expr: Expr):
|
||||
"""
|
||||
Add array element
|
||||
|
||||
Args:
|
||||
element_expr: Element expression
|
||||
"""
|
||||
self._elements.append(element_expr)
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate and return array"""
|
||||
return [expr.eval(scope) for expr in self._elements]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Array({len(self._elements)} elements)"
|
||||
|
||||
|
||||
class RangeArray(Expr):
|
||||
"""Range array expression (e.g., 1..10)"""
|
||||
|
||||
def __init__(self, start_expr: Expr, end_expr: Expr, inclusive: bool = True):
|
||||
"""
|
||||
Initialize range array
|
||||
|
||||
Args:
|
||||
start_expr: Start expression
|
||||
end_expr: End expression
|
||||
inclusive: Whether end is inclusive
|
||||
"""
|
||||
self._start_expr = start_expr
|
||||
self._end_expr = end_expr
|
||||
self._inclusive = inclusive
|
||||
|
||||
def eval(self, scope):
|
||||
"""Evaluate and return range array"""
|
||||
start = self._start_expr.eval(scope)
|
||||
end = self._end_expr.eval(scope)
|
||||
|
||||
if isinstance(start, int) and isinstance(end, int):
|
||||
if self._inclusive:
|
||||
return list(range(start, end + 1))
|
||||
else:
|
||||
return list(range(start, end))
|
||||
|
||||
# For non-integer ranges, return empty list
|
||||
return []
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"RangeArray({self._start_expr}, {self._end_expr}, inclusive={self._inclusive})"
|
||||
100
pyenjoy/template/expr/ast/Expr.py
Normal file
100
pyenjoy/template/expr/ast/Expr.py
Normal file
@@ -0,0 +1,100 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Expr - Expression Evaluation Base Class
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from ...stat.Scope import Scope
|
||||
from ...Env import Env
|
||||
|
||||
class Expr:
|
||||
"""Base class for expression AST nodes"""
|
||||
|
||||
def eval(self, scope: Scope) -> Any:
|
||||
"""
|
||||
Evaluate expression
|
||||
|
||||
Args:
|
||||
scope: Execution scope
|
||||
|
||||
Returns:
|
||||
Expression result
|
||||
"""
|
||||
raise NotImplementedError("Expr.eval() must be implemented by subclasses")
|
||||
|
||||
def eval_expr_list(self, scope: Scope) -> list:
|
||||
"""
|
||||
Evaluate as expression list
|
||||
|
||||
Args:
|
||||
scope: Execution scope
|
||||
|
||||
Returns:
|
||||
List of expression results
|
||||
"""
|
||||
return [self.eval(scope)]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Expr({self.__class__.__name__})"
|
||||
|
||||
|
||||
class ExprList(Expr):
|
||||
"""Expression list containing multiple expressions"""
|
||||
|
||||
def __init__(self, expressions: list = None):
|
||||
"""
|
||||
Initialize expression list
|
||||
|
||||
Args:
|
||||
expressions: List of expressions
|
||||
"""
|
||||
self._expressions = expressions or []
|
||||
|
||||
def add_expr(self, expr: Expr):
|
||||
"""
|
||||
Add expression to list
|
||||
|
||||
Args:
|
||||
expr: Expression to add
|
||||
"""
|
||||
if expr:
|
||||
self._expressions.append(expr)
|
||||
|
||||
def eval(self, scope: Scope) -> Any:
|
||||
"""
|
||||
Evaluate all expressions and return last result
|
||||
|
||||
Args:
|
||||
scope: Execution scope
|
||||
|
||||
Returns:
|
||||
Last expression result
|
||||
"""
|
||||
result = None
|
||||
for expr in self._expressions:
|
||||
result = expr.eval(scope)
|
||||
return result
|
||||
|
||||
def eval_expr_list(self, scope: Scope) -> list:
|
||||
"""
|
||||
Evaluate all expressions and return list of results
|
||||
|
||||
Args:
|
||||
scope: Execution scope
|
||||
|
||||
Returns:
|
||||
List of results
|
||||
"""
|
||||
return [expr.eval(scope) for expr in self._expressions]
|
||||
|
||||
def get_expressions(self) -> list:
|
||||
"""Get all expressions"""
|
||||
return self._expressions.copy()
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Get number of expressions"""
|
||||
return len(self._expressions)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ExprList({len(self._expressions)} expressions)"
|
||||
27
pyenjoy/template/expr/ast/FieldKeyBuilder.py
Normal file
27
pyenjoy/template/expr/ast/FieldKeyBuilder.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal FieldKeyBuilder - Field Key Builder
|
||||
"""
|
||||
|
||||
class FieldKeyBuilder:
|
||||
"""Field key builder for field access"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize field key builder"""
|
||||
pass
|
||||
|
||||
def build_field_key(self, field_name: str) -> str:
|
||||
"""
|
||||
Build field key
|
||||
|
||||
Args:
|
||||
field_name: Field name
|
||||
|
||||
Returns:
|
||||
Field key
|
||||
"""
|
||||
return field_name
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "FieldKeyBuilder()"
|
||||
64
pyenjoy/template/expr/ast/FieldKit.py
Normal file
64
pyenjoy/template/expr/ast/FieldKit.py
Normal file
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal FieldKit - Field Access Utilities
|
||||
"""
|
||||
|
||||
class FieldKit:
|
||||
"""Field access utilities"""
|
||||
|
||||
@staticmethod
|
||||
def get_field(obj, field_name: str):
|
||||
"""
|
||||
Get field value from object
|
||||
|
||||
Args:
|
||||
obj: Object to get field from
|
||||
field_name: Field name
|
||||
|
||||
Returns:
|
||||
Field value or None
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
# Try attribute access
|
||||
if hasattr(obj, field_name):
|
||||
return getattr(obj, field_name)
|
||||
|
||||
# Try dictionary access
|
||||
if isinstance(obj, dict) and field_name in obj:
|
||||
return obj[field_name]
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def set_field(obj, field_name: str, value):
|
||||
"""
|
||||
Set field value on object
|
||||
|
||||
Args:
|
||||
obj: Object to set field on
|
||||
field_name: Field name
|
||||
value: Value to set
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
if obj is None:
|
||||
return False
|
||||
|
||||
# Try attribute access
|
||||
if hasattr(obj, field_name):
|
||||
setattr(obj, field_name, value)
|
||||
return True
|
||||
|
||||
# Try dictionary access
|
||||
if isinstance(obj, dict):
|
||||
obj[field_name] = value
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "FieldKit()"
|
||||
56
pyenjoy/template/expr/ast/MethodKit.py
Normal file
56
pyenjoy/template/expr/ast/MethodKit.py
Normal file
@@ -0,0 +1,56 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal MethodKit - Method Access Utilities
|
||||
"""
|
||||
|
||||
class MethodKit:
|
||||
"""Method access utilities"""
|
||||
|
||||
@staticmethod
|
||||
def get_method(obj, method_name: str, *args):
|
||||
"""
|
||||
Get method from object
|
||||
|
||||
Args:
|
||||
obj: Object to get method from
|
||||
method_name: Method name
|
||||
args: Method arguments
|
||||
|
||||
Returns:
|
||||
Method or None
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
|
||||
# Try attribute access
|
||||
if hasattr(obj, method_name):
|
||||
method = getattr(obj, method_name)
|
||||
if callable(method):
|
||||
return method
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def invoke_method(obj, method_name: str, *args):
|
||||
"""
|
||||
Invoke method on object
|
||||
|
||||
Args:
|
||||
obj: Object to invoke method on
|
||||
method_name: Method name
|
||||
args: Method arguments
|
||||
|
||||
Returns:
|
||||
Method result or None
|
||||
"""
|
||||
method = MethodKit.get_method(obj, method_name)
|
||||
if method:
|
||||
try:
|
||||
return method(*args)
|
||||
except:
|
||||
return None
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "MethodKit()"
|
||||
121
pyenjoy/template/expr/ast/SharedMethodKit.py
Normal file
121
pyenjoy/template/expr/ast/SharedMethodKit.py
Normal file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal SharedMethodKit - Shared Method Kit
|
||||
"""
|
||||
|
||||
from typing import Dict, List, Any, Optional
|
||||
|
||||
class SharedMethodKit:
|
||||
"""Shared method kit for template functions"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize shared method kit"""
|
||||
self._shared_methods: Dict[str, List[Any]] = {}
|
||||
|
||||
def add_shared_method(self, obj: Any):
|
||||
"""
|
||||
Add shared methods from object
|
||||
|
||||
Args:
|
||||
obj: Object with methods to add
|
||||
"""
|
||||
if obj is None:
|
||||
return
|
||||
|
||||
# Get all methods from object
|
||||
import inspect
|
||||
for name, method in inspect.getmembers(obj, predicate=inspect.ismethod):
|
||||
if not name.startswith('_'):
|
||||
if name not in self._shared_methods:
|
||||
self._shared_methods[name] = []
|
||||
self._shared_methods[name].append(method)
|
||||
|
||||
def add_shared_method_from_class(self, clazz):
|
||||
"""
|
||||
Add shared methods from class
|
||||
|
||||
Args:
|
||||
clazz: Class with methods to add
|
||||
"""
|
||||
if clazz is None:
|
||||
return
|
||||
|
||||
# Get all methods from class
|
||||
import inspect
|
||||
for name, method in inspect.getmembers(clazz, predicate=inspect.isfunction):
|
||||
if not name.startswith('_'):
|
||||
if name not in self._shared_methods:
|
||||
self._shared_methods[name] = []
|
||||
self._shared_methods[name].append(method)
|
||||
|
||||
def add_shared_static_method(self, clazz):
|
||||
"""
|
||||
Add shared static methods from class
|
||||
|
||||
Args:
|
||||
clazz: Class with static methods to add
|
||||
"""
|
||||
if clazz is None:
|
||||
return
|
||||
|
||||
# Get all static methods from class
|
||||
import inspect
|
||||
for name, method in inspect.getmembers(clazz, predicate=inspect.isfunction):
|
||||
if not name.startswith('_'):
|
||||
if name not in self._shared_methods:
|
||||
self._shared_methods[name] = []
|
||||
self._shared_methods[name].append(method)
|
||||
|
||||
def remove_shared_method(self, method_name: str):
|
||||
"""
|
||||
Remove shared method by name
|
||||
|
||||
Args:
|
||||
method_name: Method name to remove
|
||||
"""
|
||||
if method_name in self._shared_methods:
|
||||
del self._shared_methods[method_name]
|
||||
|
||||
def get_shared_method(self, method_name: str) -> Optional[Any]:
|
||||
"""
|
||||
Get shared method by name
|
||||
|
||||
Args:
|
||||
method_name: Method name to get
|
||||
|
||||
Returns:
|
||||
Shared method or None
|
||||
"""
|
||||
if method_name in self._shared_methods:
|
||||
methods = self._shared_methods[method_name]
|
||||
if methods:
|
||||
return methods[0]
|
||||
return None
|
||||
|
||||
def get_shared_methods(self, method_name: str) -> List[Any]:
|
||||
"""
|
||||
Get all shared methods with given name
|
||||
|
||||
Args:
|
||||
method_name: Method name
|
||||
|
||||
Returns:
|
||||
List of shared methods
|
||||
"""
|
||||
return self._shared_methods.get(method_name, [])
|
||||
|
||||
def has_shared_method(self, method_name: str) -> bool:
|
||||
"""
|
||||
Check if shared method exists
|
||||
|
||||
Args:
|
||||
method_name: Method name
|
||||
|
||||
Returns:
|
||||
True if method exists, False otherwise
|
||||
"""
|
||||
return method_name in self._shared_methods and self._shared_methods[method_name]
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SharedMethodKit({len(self._shared_methods)} methods)"
|
||||
20
pyenjoy/template/expr/ast/__init__.py
Normal file
20
pyenjoy/template/expr/ast/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# Export expression AST classes
|
||||
from .Expr import Expr, ExprList
|
||||
from .Const import Const, NullExpr, Id, Map, Array, RangeArray
|
||||
from .Compare import Compare, Logic, Ternary
|
||||
from .Arith import Arith, Unary, IncDec
|
||||
from .SharedMethodKit import SharedMethodKit
|
||||
from .FieldKit import FieldKit
|
||||
from .FieldKeyBuilder import FieldKeyBuilder
|
||||
from .MethodKit import MethodKit
|
||||
|
||||
__all__ = [
|
||||
'Expr', 'ExprList',
|
||||
'Const', 'NullExpr', 'Id', 'Map', 'Array', 'RangeArray',
|
||||
'Compare', 'Logic', 'Ternary',
|
||||
'Arith', 'Unary', 'IncDec',
|
||||
'SharedMethodKit',
|
||||
'FieldKit',
|
||||
'FieldKeyBuilder',
|
||||
'MethodKit'
|
||||
]
|
||||
1
pyenjoy/template/ext/__init__.py
Normal file
1
pyenjoy/template/ext/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Python package initialization
|
||||
36
pyenjoy/template/ext/directive/CallDirective.py
Normal file
36
pyenjoy/template/ext/directive/CallDirective.py
Normal file
@@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal CallDirective - Call Directive
|
||||
"""
|
||||
|
||||
from ...Directive import Directive
|
||||
from ...Env import Env
|
||||
from ...stat.Scope import Scope
|
||||
|
||||
class CallDirective(Directive):
|
||||
"""Call directive for calling template functions"""
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute call directive
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
if self.expr_list:
|
||||
# Get function name
|
||||
function_name = self.expr_list.eval(scope)
|
||||
|
||||
if function_name:
|
||||
# Get function from env
|
||||
func = env.get_function(str(function_name))
|
||||
|
||||
if func:
|
||||
# Call function
|
||||
func.exec(env, scope, writer)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "CallDirective()"
|
||||
39
pyenjoy/template/ext/directive/DateDirective.py
Normal file
39
pyenjoy/template/ext/directive/DateDirective.py
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal DateDirective - Date Directive
|
||||
"""
|
||||
|
||||
from ...Directive import Directive
|
||||
from ...Env import Env
|
||||
from ...stat.Scope import Scope
|
||||
|
||||
class DateDirective(Directive):
|
||||
"""Date directive for date formatting"""
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute date directive
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
if self.expr_list:
|
||||
# Get date value and pattern
|
||||
values = self.expr_list.eval_expr_list(scope)
|
||||
|
||||
if values:
|
||||
date_value = values[0]
|
||||
pattern = values[1] if len(values) > 1 else "yyyy-MM-dd HH:mm"
|
||||
|
||||
# Format date
|
||||
from ...kit.TimeKit import TimeKit
|
||||
if date_value:
|
||||
formatted_date = TimeKit.format(date_value, pattern)
|
||||
if hasattr(writer, 'write'):
|
||||
writer.write(formatted_date)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "DateDirective()"
|
||||
34
pyenjoy/template/ext/directive/EscapeDirective.py
Normal file
34
pyenjoy/template/ext/directive/EscapeDirective.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal EscapeDirective - Escape Directive
|
||||
"""
|
||||
|
||||
from ...Directive import Directive
|
||||
from ...Env import Env
|
||||
from ...stat.Scope import Scope
|
||||
|
||||
class EscapeDirective(Directive):
|
||||
"""Escape directive for HTML escaping"""
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute escape directive
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
if self.expr_list:
|
||||
# Get value to escape
|
||||
value = self.expr_list.eval(scope)
|
||||
|
||||
if value:
|
||||
# Escape HTML special characters
|
||||
escaped = str(value).replace('&', '&').replace('<', '<').replace('>', '>').replace('"', '"').replace("'", ''')
|
||||
if hasattr(writer, 'write'):
|
||||
writer.write(escaped)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "EscapeDirective()"
|
||||
34
pyenjoy/template/ext/directive/NumberDirective.py
Normal file
34
pyenjoy/template/ext/directive/NumberDirective.py
Normal file
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal NumberDirective - Number Directive
|
||||
"""
|
||||
|
||||
from ...Directive import Directive
|
||||
from ...Env import Env
|
||||
from ...stat.Scope import Scope
|
||||
|
||||
class NumberDirective(Directive):
|
||||
"""Number directive for number formatting"""
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute number directive
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
if self.expr_list:
|
||||
# Get number value
|
||||
number_value = self.expr_list.eval(scope)
|
||||
|
||||
if number_value is not None:
|
||||
# Format number
|
||||
formatted = str(number_value)
|
||||
if hasattr(writer, 'write'):
|
||||
writer.write(formatted)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "NumberDirective()"
|
||||
48
pyenjoy/template/ext/directive/RandomDirective.py
Normal file
48
pyenjoy/template/ext/directive/RandomDirective.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal RandomDirective - Random Directive
|
||||
"""
|
||||
|
||||
import random
|
||||
from ...Directive import Directive
|
||||
from ...Env import Env
|
||||
from ...stat.Scope import Scope
|
||||
|
||||
class RandomDirective(Directive):
|
||||
"""Random directive for generating random numbers"""
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute random directive
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
if self.expr_list:
|
||||
# Get parameters
|
||||
values = self.expr_list.eval_expr_list(scope)
|
||||
|
||||
if len(values) == 0:
|
||||
# No parameters, generate random float between 0 and 1
|
||||
random_value = random.random()
|
||||
elif len(values) == 1:
|
||||
# One parameter, generate random int between 0 and max
|
||||
max_value = int(values[0])
|
||||
random_value = random.randint(0, max_value - 1)
|
||||
elif len(values) == 2:
|
||||
# Two parameters, generate random int between min and max
|
||||
min_value = int(values[0])
|
||||
max_value = int(values[1])
|
||||
random_value = random.randint(min_value, max_value)
|
||||
else:
|
||||
# Too many parameters
|
||||
random_value = random.random()
|
||||
|
||||
if hasattr(writer, 'write'):
|
||||
writer.write(str(random_value))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "RandomDirective()"
|
||||
38
pyenjoy/template/ext/directive/RenderDirective.py
Normal file
38
pyenjoy/template/ext/directive/RenderDirective.py
Normal file
@@ -0,0 +1,38 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal RenderDirective - Render Directive
|
||||
"""
|
||||
|
||||
from ...Directive import Directive
|
||||
from ...Env import Env
|
||||
from ...stat.Scope import Scope
|
||||
|
||||
class RenderDirective(Directive):
|
||||
"""Render directive for template rendering"""
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute render directive
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
# Simplified implementation
|
||||
if self.expr_list:
|
||||
# Get template name
|
||||
template_name = self.expr_list.eval(scope)
|
||||
|
||||
if template_name:
|
||||
# Get engine from env
|
||||
from ...Engine import Engine
|
||||
engine = Engine.use()
|
||||
|
||||
# Render template
|
||||
template = engine.get_template(str(template_name))
|
||||
template.render(scope.get_data(), writer)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "RenderDirective()"
|
||||
33
pyenjoy/template/ext/directive/StringDirective.py
Normal file
33
pyenjoy/template/ext/directive/StringDirective.py
Normal file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal StringDirective - String Directive
|
||||
"""
|
||||
|
||||
from ...Directive import Directive
|
||||
from ...Env import Env
|
||||
from ...stat.Scope import Scope
|
||||
|
||||
class StringDirective(Directive):
|
||||
"""String directive for string operations"""
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute string directive
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
if self.expr_list:
|
||||
# Get string value
|
||||
string_value = self.expr_list.eval(scope)
|
||||
|
||||
if string_value is not None:
|
||||
# Output string
|
||||
if hasattr(writer, 'write'):
|
||||
writer.write(str(string_value))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "StringDirective()"
|
||||
1
pyenjoy/template/ext/directive/__init__.py
Normal file
1
pyenjoy/template/ext/directive/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Python package initialization
|
||||
27
pyenjoy/template/ext/sharedmethod/SharedMethodLib.py
Normal file
27
pyenjoy/template/ext/sharedmethod/SharedMethodLib.py
Normal file
@@ -0,0 +1,27 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal SharedMethodLib - Shared Method Library
|
||||
"""
|
||||
|
||||
class SharedMethodLib:
|
||||
"""Shared method library for template functions"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize shared method library"""
|
||||
pass
|
||||
|
||||
def add_shared_method(self, method_name: str, method):
|
||||
"""Add shared method"""
|
||||
pass
|
||||
|
||||
def get_shared_method(self, method_name: str):
|
||||
"""Get shared method by name"""
|
||||
return None
|
||||
|
||||
def has_shared_method(self, method_name: str) -> bool:
|
||||
"""Check if shared method exists"""
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "SharedMethodLib()"
|
||||
1
pyenjoy/template/ext/sharedmethod/__init__.py
Normal file
1
pyenjoy/template/ext/sharedmethod/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Python package initialization
|
||||
28
pyenjoy/template/io/EncoderFactory.py
Normal file
28
pyenjoy/template/io/EncoderFactory.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal EncoderFactory - Encoder Factory
|
||||
"""
|
||||
|
||||
class EncoderFactory:
|
||||
"""Encoder factory for template output"""
|
||||
|
||||
encoding = "UTF-8"
|
||||
|
||||
def get_encoder(self):
|
||||
"""Get encoder"""
|
||||
from .JdkEncoder import JdkEncoder
|
||||
return JdkEncoder()
|
||||
|
||||
@classmethod
|
||||
def set_encoding(cls, encoding: str):
|
||||
"""Set encoding"""
|
||||
cls.encoding = encoding
|
||||
|
||||
@classmethod
|
||||
def get_encoding(cls) -> str:
|
||||
"""Get encoding"""
|
||||
return cls.encoding
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"EncoderFactory(encoding={self.encoding})"
|
||||
47
pyenjoy/template/io/JdkEncoder.py
Normal file
47
pyenjoy/template/io/JdkEncoder.py
Normal file
@@ -0,0 +1,47 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal JdkEncoder - JDK Encoder
|
||||
"""
|
||||
|
||||
class JdkEncoder:
|
||||
"""JDK encoder for template output"""
|
||||
|
||||
def encode(self, str_val: str) -> str:
|
||||
"""
|
||||
Encode string
|
||||
|
||||
Args:
|
||||
str_val: String to encode
|
||||
|
||||
Returns:
|
||||
Encoded string
|
||||
"""
|
||||
return str_val
|
||||
|
||||
def encode_half(self, str_val: str) -> str:
|
||||
"""
|
||||
Encode string (half encoding)
|
||||
|
||||
Args:
|
||||
str_val: String to encode
|
||||
|
||||
Returns:
|
||||
Encoded string
|
||||
"""
|
||||
return str_val
|
||||
|
||||
def encode_all(self, str_val: str) -> str:
|
||||
"""
|
||||
Encode string (full encoding)
|
||||
|
||||
Args:
|
||||
str_val: String to encode
|
||||
|
||||
Returns:
|
||||
Encoded string
|
||||
"""
|
||||
return str_val
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "JdkEncoder()"
|
||||
22
pyenjoy/template/io/JdkEncoderFactory.py
Normal file
22
pyenjoy/template/io/JdkEncoderFactory.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal JdkEncoderFactory - JDK Encoder Factory
|
||||
"""
|
||||
|
||||
from .EncoderFactory import EncoderFactory
|
||||
|
||||
class JdkEncoderFactory(EncoderFactory):
|
||||
"""JDK encoder factory"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize JDK encoder factory"""
|
||||
super().__init__()
|
||||
|
||||
def get_encoder(self):
|
||||
"""Get JDK encoder"""
|
||||
from .JdkEncoder import JdkEncoder
|
||||
return JdkEncoder()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "JdkEncoderFactory()"
|
||||
53
pyenjoy/template/io/WriterBuffer.py
Normal file
53
pyenjoy/template/io/WriterBuffer.py
Normal file
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal WriterBuffer - Writer Buffer
|
||||
"""
|
||||
|
||||
class WriterBuffer:
|
||||
"""Writer buffer for template output"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize writer buffer"""
|
||||
self._encoding = "UTF-8"
|
||||
self._buffer_size = 8192
|
||||
self._reentrant_buffer_size = 1024
|
||||
self._encoder_factory = None
|
||||
|
||||
def set_encoding(self, encoding: str):
|
||||
"""Set encoding"""
|
||||
self._encoding = encoding
|
||||
if self._encoder_factory:
|
||||
# Set encoding on encoder factory if available
|
||||
pass
|
||||
|
||||
def set_buffer_size(self, buffer_size: int):
|
||||
"""Set buffer size"""
|
||||
self._buffer_size = buffer_size
|
||||
|
||||
def set_reentrant_buffer_size(self, reentrant_buffer_size: int):
|
||||
"""Set reentrant buffer size"""
|
||||
self._reentrant_buffer_size = reentrant_buffer_size
|
||||
|
||||
def set_encoder_factory(self, encoder_factory):
|
||||
"""Set encoder factory"""
|
||||
self._encoder_factory = encoder_factory
|
||||
|
||||
def get_encoding(self) -> str:
|
||||
"""Get encoding"""
|
||||
return self._encoding
|
||||
|
||||
def get_buffer_size(self) -> int:
|
||||
"""Get buffer size"""
|
||||
return self._buffer_size
|
||||
|
||||
def get_reentrant_buffer_size(self) -> int:
|
||||
"""Get reentrant buffer size"""
|
||||
return self._reentrant_buffer_size
|
||||
|
||||
def get_encoder_factory(self):
|
||||
"""Get encoder factory"""
|
||||
return self._encoder_factory
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"WriterBuffer(encoding={self._encoding}, buffer_size={self._buffer_size})"
|
||||
1
pyenjoy/template/io/__init__.py
Normal file
1
pyenjoy/template/io/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Python package initialization
|
||||
109
pyenjoy/template/source/ClassPathSource.py
Normal file
109
pyenjoy/template/source/ClassPathSource.py
Normal file
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal ClassPathSource - Class Path Source
|
||||
"""
|
||||
|
||||
import importlib.resources
|
||||
import os
|
||||
|
||||
class ClassPathSource:
|
||||
"""Class path source for template loading"""
|
||||
|
||||
def __init__(self, base_template_path: str, fileName: str, encoding: str):
|
||||
"""
|
||||
Initialize class path source
|
||||
|
||||
Args:
|
||||
base_template_path: Base template path
|
||||
fileName: File name
|
||||
encoding: Encoding
|
||||
"""
|
||||
self._fileName = fileName
|
||||
self._encoding = encoding
|
||||
self._baseTemplatePath = base_template_path
|
||||
|
||||
# Build resource path
|
||||
if base_template_path:
|
||||
self._resourcePath = os.path.join(base_template_path, fileName).replace('\\', '/')
|
||||
else:
|
||||
self._resourcePath = fileName.replace('\\', '/')
|
||||
|
||||
def get_content(self) -> str:
|
||||
"""
|
||||
Get class path resource content
|
||||
|
||||
Returns:
|
||||
Resource content as string
|
||||
"""
|
||||
try:
|
||||
# Try to load from classpath
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
# Try different approaches to load resource
|
||||
try:
|
||||
# Try direct resource loading
|
||||
with open(self._resourcePath, 'r', encoding=self._encoding) as f:
|
||||
return f.read()
|
||||
except:
|
||||
# Try as package resource
|
||||
if '.' in self._resourcePath:
|
||||
parts = self._resourcePath.split('/')
|
||||
if len(parts) > 1:
|
||||
package_name = '.'.join(parts[:-1])
|
||||
resource_name = parts[-1]
|
||||
try:
|
||||
with importlib.resources.open_text(package_name, resource_name, encoding=self._encoding) as f:
|
||||
return f.read()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fall back to file system
|
||||
return self._try_file_system()
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error reading classpath resource {self._resourcePath}: {e}")
|
||||
|
||||
def _try_file_system(self) -> str:
|
||||
"""
|
||||
Try to load from file system as fallback
|
||||
|
||||
Returns:
|
||||
File content as string
|
||||
"""
|
||||
# Try current directory
|
||||
try:
|
||||
current_dir = os.getcwd()
|
||||
file_path = os.path.join(current_dir, self._resourcePath)
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r', encoding=self._encoding) as f:
|
||||
return f.read()
|
||||
except:
|
||||
pass
|
||||
|
||||
# Try absolute path
|
||||
if os.path.isabs(self._resourcePath):
|
||||
try:
|
||||
with open(self._resourcePath, 'r', encoding=self._encoding) as f:
|
||||
return f.read()
|
||||
except:
|
||||
pass
|
||||
|
||||
raise RuntimeError(f"Resource not found: {self._resourcePath}")
|
||||
|
||||
def is_modified(self) -> bool:
|
||||
"""
|
||||
Check if resource has been modified
|
||||
|
||||
Returns:
|
||||
False for classpath resources (not supported)
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_file_name(self) -> str:
|
||||
"""Get file name"""
|
||||
return self._fileName
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ClassPathSource({self._fileName})"
|
||||
28
pyenjoy/template/source/ClassPathSourceFactory.py
Normal file
28
pyenjoy/template/source/ClassPathSourceFactory.py
Normal file
@@ -0,0 +1,28 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal ClassPathSourceFactory - Class Path Source Factory
|
||||
"""
|
||||
|
||||
from .ISourceFactory import ISourceFactory
|
||||
|
||||
class ClassPathSourceFactory(ISourceFactory):
|
||||
"""Class path source factory for template loading"""
|
||||
|
||||
def get_source(self, base_template_path: str, fileName: str, encoding: str):
|
||||
"""
|
||||
Get class path source
|
||||
|
||||
Args:
|
||||
base_template_path: Base template path
|
||||
fileName: File name
|
||||
encoding: Encoding
|
||||
|
||||
Returns:
|
||||
Class path source object
|
||||
"""
|
||||
from .ClassPathSource import ClassPathSource
|
||||
return ClassPathSource(base_template_path, fileName, encoding)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "ClassPathSourceFactory()"
|
||||
76
pyenjoy/template/source/FileSource.py
Normal file
76
pyenjoy/template/source/FileSource.py
Normal file
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal FileSource - File Source
|
||||
"""
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
class FileSource:
|
||||
"""File source for template loading"""
|
||||
|
||||
def __init__(self, base_template_path: str, fileName: str, encoding: str):
|
||||
"""
|
||||
Initialize file source
|
||||
|
||||
Args:
|
||||
base_template_path: Base template path
|
||||
fileName: File name
|
||||
encoding: Encoding
|
||||
"""
|
||||
self._fileName = fileName
|
||||
self._encoding = encoding
|
||||
self._lastModified = 0
|
||||
|
||||
# Build file path
|
||||
if base_template_path:
|
||||
self._filePath = os.path.join(base_template_path, fileName)
|
||||
else:
|
||||
self._filePath = fileName
|
||||
|
||||
# Normalize path
|
||||
self._filePath = os.path.normpath(self._filePath)
|
||||
|
||||
# Get last modified time
|
||||
self._update_last_modified()
|
||||
|
||||
def get_content(self) -> str:
|
||||
"""
|
||||
Get file content
|
||||
|
||||
Returns:
|
||||
File content as string
|
||||
"""
|
||||
try:
|
||||
with open(self._filePath, 'r', encoding=self._encoding) as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Error reading file {self._filePath}: {e}")
|
||||
|
||||
def is_modified(self) -> bool:
|
||||
"""
|
||||
Check if file has been modified
|
||||
|
||||
Returns:
|
||||
True if modified, False otherwise
|
||||
"""
|
||||
current_modified = os.path.getmtime(self._filePath)
|
||||
if current_modified > self._lastModified:
|
||||
self._lastModified = current_modified
|
||||
return True
|
||||
return False
|
||||
|
||||
def _update_last_modified(self):
|
||||
"""Update last modified time"""
|
||||
try:
|
||||
self._lastModified = os.path.getmtime(self._filePath)
|
||||
except:
|
||||
self._lastModified = 0
|
||||
|
||||
def get_file_name(self) -> str:
|
||||
"""Get file name"""
|
||||
return self._fileName
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"FileSource({self._fileName})"
|
||||
29
pyenjoy/template/source/FileSourceFactory.py
Normal file
29
pyenjoy/template/source/FileSourceFactory.py
Normal file
@@ -0,0 +1,29 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal FileSourceFactory - File Source Factory
|
||||
"""
|
||||
|
||||
import os
|
||||
from .ISourceFactory import ISourceFactory
|
||||
|
||||
class FileSourceFactory(ISourceFactory):
|
||||
"""File source factory for template loading"""
|
||||
|
||||
def get_source(self, base_template_path: str, fileName: str, encoding: str):
|
||||
"""
|
||||
Get file source
|
||||
|
||||
Args:
|
||||
base_template_path: Base template path
|
||||
fileName: File name
|
||||
encoding: Encoding
|
||||
|
||||
Returns:
|
||||
File source object
|
||||
"""
|
||||
from .FileSource import FileSource
|
||||
return FileSource(base_template_path, fileName, encoding)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return "FileSourceFactory()"
|
||||
22
pyenjoy/template/source/ISourceFactory.py
Normal file
22
pyenjoy/template/source/ISourceFactory.py
Normal file
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal ISourceFactory - Source Factory Interface
|
||||
"""
|
||||
|
||||
class ISourceFactory:
|
||||
"""Source factory interface for template loading"""
|
||||
|
||||
def get_source(self, base_template_path: str, fileName: str, encoding: str):
|
||||
"""
|
||||
Get source by file name
|
||||
|
||||
Args:
|
||||
base_template_path: Base template path
|
||||
fileName: File name
|
||||
encoding: Encoding
|
||||
|
||||
Returns:
|
||||
Source object
|
||||
"""
|
||||
raise NotImplementedError("ISourceFactory.get_source() must be implemented by subclasses")
|
||||
50
pyenjoy/template/source/StringSource.py
Normal file
50
pyenjoy/template/source/StringSource.py
Normal file
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal StringSource - String Source
|
||||
"""
|
||||
|
||||
class StringSource:
|
||||
"""String source for template loading"""
|
||||
|
||||
def __init__(self, content: str, cache_key: str = None):
|
||||
"""
|
||||
Initialize string source
|
||||
|
||||
Args:
|
||||
content: Template content
|
||||
cache_key: Cache key (optional)
|
||||
"""
|
||||
self._content = content
|
||||
self._cache_key = cache_key
|
||||
|
||||
def get_content(self) -> str:
|
||||
"""
|
||||
Get string content
|
||||
|
||||
Returns:
|
||||
Content as string
|
||||
"""
|
||||
return self._content
|
||||
|
||||
def is_modified(self) -> bool:
|
||||
"""
|
||||
Check if content has been modified
|
||||
|
||||
Returns:
|
||||
Always False for string sources
|
||||
"""
|
||||
return False
|
||||
|
||||
def get_cache_key(self) -> str:
|
||||
"""
|
||||
Get cache key
|
||||
|
||||
Returns:
|
||||
Cache key
|
||||
"""
|
||||
return self._cache_key
|
||||
|
||||
def __repr__(self) -> str:
|
||||
content_preview = self._content[:50] + "..." if len(self._content) > 50 else self._content
|
||||
return f"StringSource('{content_preview}')"
|
||||
1
pyenjoy/template/source/__init__.py
Normal file
1
pyenjoy/template/source/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Python package initialization
|
||||
45
pyenjoy/template/stat/Compressor.py
Normal file
45
pyenjoy/template/stat/Compressor.py
Normal file
@@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Compressor - Template Compressor
|
||||
"""
|
||||
|
||||
class Compressor:
|
||||
"""Template compressor for minimizing output"""
|
||||
|
||||
def __init__(self, separator: str = '\n'):
|
||||
"""
|
||||
Initialize compressor
|
||||
|
||||
Args:
|
||||
separator: Separator string
|
||||
"""
|
||||
self._separator = separator
|
||||
|
||||
def compress(self, content: str) -> str:
|
||||
"""
|
||||
Compress template content
|
||||
|
||||
Args:
|
||||
content: Content to compress
|
||||
|
||||
Returns:
|
||||
Compressed content
|
||||
"""
|
||||
# Simplified compression
|
||||
if not content:
|
||||
return content
|
||||
|
||||
# Remove extra whitespace
|
||||
lines = content.split(self._separator)
|
||||
compressed_lines = []
|
||||
|
||||
for line in lines:
|
||||
compressed_line = line.strip()
|
||||
if compressed_line:
|
||||
compressed_lines.append(compressed_line)
|
||||
|
||||
return self._separator.join(compressed_lines)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Compressor(separator={repr(self._separator)})"
|
||||
35
pyenjoy/template/stat/OutputDirectiveFactory.py
Normal file
35
pyenjoy/template/stat/OutputDirectiveFactory.py
Normal file
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal OutputDirectiveFactory - Output Directive Factory
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .ast.Stat import Stat
|
||||
from .Scope import Scope
|
||||
from ..Env import Env
|
||||
|
||||
class OutputDirectiveFactory:
|
||||
"""Factory for creating output directives"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize factory"""
|
||||
pass
|
||||
|
||||
def get_output_directive(self, expr_list, location) -> 'Output':
|
||||
"""
|
||||
Get output directive
|
||||
|
||||
Args:
|
||||
expr_list: Expression list
|
||||
location: Location information
|
||||
|
||||
Returns:
|
||||
Output directive
|
||||
"""
|
||||
from .ast.Output import Output
|
||||
return Output(expr_list)
|
||||
|
||||
|
||||
# Create singleton instance
|
||||
me = OutputDirectiveFactory()
|
||||
383
pyenjoy/template/stat/Parser.py
Normal file
383
pyenjoy/template/stat/Parser.py
Normal file
@@ -0,0 +1,383 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Parser - Template Parser
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .ast.Stat import Stat, StatList
|
||||
from ..Env import Env
|
||||
|
||||
class Parser:
|
||||
"""Template parser for parsing template files"""
|
||||
|
||||
def __init__(self, env: Env, content: str, name: Optional[str] = None):
|
||||
"""
|
||||
Initialize parser
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
content: Template content
|
||||
name: Template name (optional)
|
||||
"""
|
||||
self._env = env
|
||||
self._content = content
|
||||
self._name = name
|
||||
|
||||
def parse(self) -> Stat:
|
||||
"""
|
||||
Parse template content
|
||||
|
||||
Returns:
|
||||
Root statement
|
||||
"""
|
||||
from .ast.Text import Text
|
||||
from .ast.Output import Output
|
||||
from .ast.For import For
|
||||
|
||||
stat_list = StatList()
|
||||
content = self._content
|
||||
length = len(content)
|
||||
pos = 0
|
||||
|
||||
while pos < length:
|
||||
# Find the next # character
|
||||
next_hash = content.find("#", pos)
|
||||
if next_hash == -1:
|
||||
# No more # characters, add the remaining text
|
||||
if pos < length:
|
||||
stat_list.add_stat(Text(content[pos:]))
|
||||
break
|
||||
|
||||
# Add text before the # character
|
||||
if next_hash > pos:
|
||||
stat_list.add_stat(Text(content[pos:next_hash]))
|
||||
|
||||
# Check what follows the #
|
||||
if next_hash + 1 < length:
|
||||
next_char = content[next_hash + 1]
|
||||
|
||||
# Handle #( expression
|
||||
if next_char == "(":
|
||||
# Find the end of the expression
|
||||
end = self._find_matching_parenthesis(content, next_hash + 2)
|
||||
if end != -1:
|
||||
# Extract the expression content
|
||||
expr_content = content[next_hash + 2:end]
|
||||
|
||||
# Create a simple expression list for evaluation
|
||||
expr_list = SimpleExprList(expr_content)
|
||||
|
||||
# Add output stat
|
||||
stat_list.add_stat(Output(expr_list))
|
||||
|
||||
# Move to next position
|
||||
pos = end + 1
|
||||
continue
|
||||
|
||||
# Check for #for directive
|
||||
elif next_char == "f" and next_hash + 4 <= length and content[next_hash:next_hash + 4] == "#for":
|
||||
# Handle #for directive with optional parentheses and different syntax
|
||||
for_start = next_hash + 4
|
||||
|
||||
# Skip whitespace
|
||||
while for_start < length and content[for_start].isspace():
|
||||
for_start += 1
|
||||
|
||||
# Check if #for is followed by (
|
||||
if for_start < length and content[for_start] == "(":
|
||||
# Find the end of the parentheses
|
||||
for_paren_end = self._find_matching_parenthesis(content, for_start + 1)
|
||||
if for_paren_end != -1:
|
||||
# Parse for content inside parentheses
|
||||
for_content = content[for_start + 1:for_paren_end].strip()
|
||||
for_end = for_paren_end + 1
|
||||
else:
|
||||
# No matching parenthesis, treat as text
|
||||
stat_list.add_stat(Text(content[next_hash:next_hash + 4]))
|
||||
pos = next_hash + 4
|
||||
continue
|
||||
else:
|
||||
# Find the end of the #for line
|
||||
for_end = content.find("\n", next_hash)
|
||||
if for_end == -1:
|
||||
for_end = length
|
||||
|
||||
# Parse for content
|
||||
for_content = content[for_start:for_end].strip()
|
||||
|
||||
# Handle both "item in items" and "x : listAaa" syntax
|
||||
if " in " in for_content:
|
||||
# Python style: for item in items
|
||||
parts = for_content.split(" in ")
|
||||
var_name = parts[0].strip()
|
||||
iter_expr = parts[1].strip()
|
||||
elif " : " in for_content:
|
||||
# Java style: for x : listAaa
|
||||
parts = for_content.split(" : ")
|
||||
var_name = parts[0].strip()
|
||||
iter_expr = parts[1].strip()
|
||||
else:
|
||||
# Invalid for syntax, treat as text
|
||||
stat_list.add_stat(Text(content[next_hash:for_end]))
|
||||
pos = for_end
|
||||
continue
|
||||
|
||||
# Find the matching #end directive
|
||||
end_pos = self._find_matching_end(content, for_end)
|
||||
if end_pos != -1:
|
||||
# Extract the loop body content
|
||||
body_content = content[for_end:end_pos]
|
||||
|
||||
# Parse the loop body
|
||||
body_parser = Parser(self._env, body_content, self._name)
|
||||
body_stat = body_parser.parse()
|
||||
|
||||
# Create For directive
|
||||
for_directive = For(var_name, iter_expr, body_stat)
|
||||
|
||||
# Add for directive to stat list
|
||||
stat_list.add_stat(for_directive)
|
||||
|
||||
# Move to position after #end directive
|
||||
pos = end_pos + 4 # Skip "#end"
|
||||
continue
|
||||
|
||||
# If we get here, it's just a # character, add as text
|
||||
stat_list.add_stat(Text("#"))
|
||||
pos = next_hash + 1
|
||||
|
||||
return stat_list
|
||||
|
||||
def _find_matching_end(self, content: str, start: int) -> int:
|
||||
"""
|
||||
Find matching #end directive
|
||||
|
||||
Args:
|
||||
content: Content string
|
||||
start: Start position
|
||||
|
||||
Returns:
|
||||
Position of matching #end directive or -1 if not found
|
||||
"""
|
||||
pos = start
|
||||
length = len(content)
|
||||
depth = 1 # Start with 1 for the current #for
|
||||
|
||||
while pos < length:
|
||||
# Find the next # character
|
||||
next_hash = content.find("#", pos)
|
||||
if next_hash == -1:
|
||||
break
|
||||
|
||||
# Check for #for (increase depth)
|
||||
if next_hash + 5 <= length and content[next_hash:next_hash + 5] == "#for ":
|
||||
depth += 1
|
||||
pos = next_hash + 5
|
||||
# Check for #end (decrease depth)
|
||||
elif next_hash + 4 <= length and content[next_hash:next_hash + 4] == "#end":
|
||||
depth -= 1
|
||||
if depth == 0:
|
||||
return next_hash
|
||||
pos = next_hash + 4
|
||||
# Otherwise, move past this #
|
||||
else:
|
||||
pos = next_hash + 1
|
||||
|
||||
return -1
|
||||
|
||||
def _find_matching_parenthesis(self, content: str, start: int) -> int:
|
||||
"""
|
||||
Find matching parenthesis
|
||||
|
||||
Args:
|
||||
content: Content string
|
||||
start: Start position
|
||||
|
||||
Returns:
|
||||
Position of matching parenthesis or -1 if not found
|
||||
"""
|
||||
count = 1
|
||||
pos = start
|
||||
length = len(content)
|
||||
|
||||
while pos < length and count > 0:
|
||||
char = content[pos]
|
||||
if char == '(':
|
||||
count += 1
|
||||
elif char == ')':
|
||||
count -= 1
|
||||
if count == 0:
|
||||
return pos
|
||||
pos += 1
|
||||
|
||||
return -1
|
||||
|
||||
|
||||
class SimpleExprList:
|
||||
"""
|
||||
Simple expression list implementation for evaluating template expressions
|
||||
"""
|
||||
|
||||
def __init__(self, expr_str: str):
|
||||
"""
|
||||
Initialize simple expression list
|
||||
|
||||
Args:
|
||||
expr_str: Expression string
|
||||
"""
|
||||
self._expr_str = expr_str
|
||||
|
||||
def eval(self, scope: 'Scope') -> any:
|
||||
"""
|
||||
Evaluate expression in the given scope
|
||||
|
||||
Args:
|
||||
scope: Execution scope
|
||||
|
||||
Returns:
|
||||
Evaluation result
|
||||
"""
|
||||
try:
|
||||
# Create a local context with scope variables
|
||||
local_vars = scope._data.copy()
|
||||
|
||||
# Create a special dict wrapper that allows attribute access
|
||||
def wrap_dict(d):
|
||||
"""Wrap a dict to allow attribute access"""
|
||||
if isinstance(d, dict):
|
||||
# Create a wrapper that allows both dot access and bracket access
|
||||
class DictWrapper:
|
||||
def __init__(self, data):
|
||||
self.__dict__ = data
|
||||
def __getitem__(self, key):
|
||||
return data[key]
|
||||
return DictWrapper(d)
|
||||
return d
|
||||
|
||||
# Wrap all dict values in the local vars
|
||||
wrapped_vars = {}
|
||||
for key, value in local_vars.items():
|
||||
if isinstance(value, dict):
|
||||
wrapped_vars[key] = wrap_dict(value)
|
||||
elif isinstance(value, list):
|
||||
# Wrap dictionaries in lists
|
||||
wrapped_vars[key] = [wrap_dict(item) for item in value]
|
||||
else:
|
||||
wrapped_vars[key] = value
|
||||
|
||||
# Handle special case for 'for' variable which is a keyword in Python
|
||||
expr_str = self._expr_str
|
||||
if 'for' in wrapped_vars:
|
||||
# Create a wrapper to avoid keyword conflict
|
||||
wrapped_vars['_for'] = wrapped_vars['for']
|
||||
# Replace 'for.' with '_for.' in the expression
|
||||
expr_str = expr_str.replace('for.', '_for.')
|
||||
|
||||
# Remove newline characters from expression
|
||||
expr_str = expr_str.replace('\n', '').replace('\r', '')
|
||||
|
||||
# Split expression by | to handle filters
|
||||
expr_parts = expr_str.split('|')
|
||||
main_expr = expr_parts[0].strip()
|
||||
|
||||
# Evaluate main expression first
|
||||
if '??' in main_expr:
|
||||
# Custom evaluation for expressions with ?? operator
|
||||
def evaluate_null_coalescing(expr):
|
||||
if '??' not in expr:
|
||||
try:
|
||||
return eval(expr, {}, wrapped_vars)
|
||||
except (NameError, AttributeError):
|
||||
return None
|
||||
|
||||
# Process from right to left for nested ??
|
||||
idx = expr.rfind('??')
|
||||
|
||||
# Find left expression
|
||||
left_expr = expr[:idx].strip()
|
||||
right_expr = expr[idx+2:].strip()
|
||||
|
||||
# Evaluate left part
|
||||
left_val = evaluate_null_coalescing(left_expr)
|
||||
|
||||
if left_val is not None:
|
||||
return left_val
|
||||
else:
|
||||
# Evaluate right part
|
||||
return evaluate_null_coalescing(right_expr)
|
||||
|
||||
result = evaluate_null_coalescing(main_expr)
|
||||
else:
|
||||
# Regular evaluation for expressions without ??
|
||||
try:
|
||||
result = eval(main_expr, {}, wrapped_vars)
|
||||
except (NameError, AttributeError):
|
||||
result = None
|
||||
|
||||
# Apply filters
|
||||
for filter_part in expr_parts[1:]:
|
||||
filter_part = filter_part.strip()
|
||||
if not filter_part:
|
||||
continue
|
||||
|
||||
# Parse filter name and arguments
|
||||
if '(' in filter_part:
|
||||
filter_name = filter_part[:filter_part.find('(')].strip()
|
||||
# Extract arguments
|
||||
args_str = filter_part[filter_part.find('(')+1:filter_part.rfind(')')].strip()
|
||||
# Simple argument parsing - split by commas, ignoring commas inside quotes
|
||||
args = []
|
||||
current_arg = ''
|
||||
in_quotes = False
|
||||
quote_char = ''
|
||||
for char in args_str:
|
||||
if char in "'\"" and (not in_quotes or char == quote_char):
|
||||
if not in_quotes:
|
||||
in_quotes = True
|
||||
quote_char = char
|
||||
else:
|
||||
in_quotes = False
|
||||
current_arg += char
|
||||
elif char == ',' and not in_quotes:
|
||||
args.append(current_arg.strip())
|
||||
current_arg = ''
|
||||
else:
|
||||
current_arg += char
|
||||
if current_arg:
|
||||
args.append(current_arg.strip())
|
||||
else:
|
||||
filter_name = filter_part.strip()
|
||||
args = []
|
||||
|
||||
# Apply filter
|
||||
if filter_name == 'join':
|
||||
# Join filter implementation
|
||||
if len(args) > 0:
|
||||
# Remove quotes from separator if present
|
||||
sep = args[0]
|
||||
if (sep.startswith('"') and sep.endswith('"')) or (sep.startswith("'") and sep.endswith("'")):
|
||||
sep = sep[1:-1]
|
||||
else:
|
||||
sep = ''
|
||||
|
||||
if isinstance(result, (list, tuple)):
|
||||
result = sep.join(str(item) for item in result)
|
||||
else:
|
||||
result = str(result)
|
||||
elif filter_name == 'upper':
|
||||
# Upper case filter
|
||||
result = str(result).upper()
|
||||
elif filter_name == 'lower':
|
||||
# Lower case filter
|
||||
result = str(result).lower()
|
||||
elif filter_name == 'strip':
|
||||
# Strip whitespace filter
|
||||
result = str(result).strip()
|
||||
|
||||
return result
|
||||
except Exception as e:
|
||||
# Handle evaluation errors gracefully
|
||||
return f"Error evaluating expression '{self._expr_str}': {e}"
|
||||
|
||||
|
||||
133
pyenjoy/template/stat/Scope.py
Normal file
133
pyenjoy/template/stat/Scope.py
Normal file
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Scope - Template Execution Scope
|
||||
"""
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
class Scope:
|
||||
"""Template execution scope"""
|
||||
|
||||
def __init__(self, data: Dict = None, shared_objects: Dict = None):
|
||||
"""
|
||||
Initialize scope
|
||||
|
||||
Args:
|
||||
data: Data dictionary
|
||||
shared_objects: Shared objects dictionary
|
||||
"""
|
||||
self._parent = None
|
||||
self._data = data or {}
|
||||
self._shared_objects = shared_objects or {}
|
||||
self._local_vars: Dict[str, Any] = {}
|
||||
|
||||
def set_parent(self, parent: 'Scope'):
|
||||
"""Set parent scope"""
|
||||
self._parent = parent
|
||||
|
||||
def get(self, key: str) -> Any:
|
||||
"""
|
||||
Get value by key, searching through scopes
|
||||
|
||||
Args:
|
||||
key: Variable key
|
||||
|
||||
Returns:
|
||||
Value or None
|
||||
"""
|
||||
# Check local vars first
|
||||
if key in self._local_vars:
|
||||
return self._local_vars[key]
|
||||
|
||||
# Check data
|
||||
if key in self._data:
|
||||
return self._data[key]
|
||||
|
||||
# Check shared objects
|
||||
if key in self._shared_objects:
|
||||
return self._shared_objects[key]
|
||||
|
||||
# Check parent scope
|
||||
if self._parent:
|
||||
return self._parent.get(key)
|
||||
|
||||
return None
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
"""
|
||||
Set value in local scope
|
||||
|
||||
Args:
|
||||
key: Variable key
|
||||
value: Value to set
|
||||
"""
|
||||
self._local_vars[key] = value
|
||||
|
||||
def set_local(self, key: str, value: Any):
|
||||
"""Set local variable"""
|
||||
self._local_vars[key] = value
|
||||
|
||||
def remove(self, key: str) -> Any:
|
||||
"""
|
||||
Remove and return value
|
||||
|
||||
Args:
|
||||
key: Variable key
|
||||
|
||||
Returns:
|
||||
Removed value or None
|
||||
"""
|
||||
# Try local vars
|
||||
if key in self._local_vars:
|
||||
return self._local_vars.pop(key)
|
||||
|
||||
# Try data
|
||||
if key in self._data:
|
||||
return self._data.pop(key, None)
|
||||
|
||||
return None
|
||||
|
||||
def contains(self, key: str) -> bool:
|
||||
"""Check if key exists in any scope"""
|
||||
return self.get(key) is not None
|
||||
|
||||
def keys(self) -> set:
|
||||
"""Get all keys in scope"""
|
||||
keys = set()
|
||||
keys.update(self._local_vars.keys())
|
||||
keys.update(self._data.keys())
|
||||
keys.update(self._shared_objects.keys())
|
||||
if self._parent:
|
||||
keys.update(self._parent.keys())
|
||||
return keys
|
||||
|
||||
def get_local_vars(self) -> Dict[str, Any]:
|
||||
"""Get local variables"""
|
||||
return self._local_vars.copy()
|
||||
|
||||
def get_data(self) -> Dict[str, Any]:
|
||||
"""Get data dictionary"""
|
||||
return self._data.copy()
|
||||
|
||||
def get_shared_objects(self) -> Dict[str, Any]:
|
||||
"""Get shared objects"""
|
||||
return self._shared_objects.copy()
|
||||
|
||||
def clear_local_vars(self):
|
||||
"""Clear local variables"""
|
||||
self._local_vars.clear()
|
||||
|
||||
def new_scope(self) -> 'Scope':
|
||||
"""
|
||||
Create a new scope with this scope as parent
|
||||
|
||||
Returns:
|
||||
New scope instance
|
||||
"""
|
||||
new_scope = Scope(self._data.copy(), self._shared_objects.copy())
|
||||
new_scope.set_parent(self)
|
||||
return new_scope
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Scope(local_vars={len(self._local_vars)}, data={len(self._data)}, shared={len(self._shared_objects)})"
|
||||
48
pyenjoy/template/stat/ast/Define.py
Normal file
48
pyenjoy/template/stat/ast/Define.py
Normal file
@@ -0,0 +1,48 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Define - Template Function Definition
|
||||
"""
|
||||
|
||||
from .Stat import Stat
|
||||
from ..Scope import Scope
|
||||
from ...Env import Env
|
||||
|
||||
class Define(Stat):
|
||||
"""Template function definition"""
|
||||
|
||||
def __init__(self, function_name: str):
|
||||
"""
|
||||
Initialize define statement
|
||||
|
||||
Args:
|
||||
function_name: Function name
|
||||
"""
|
||||
self._function_name = function_name
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute define statement
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
# Register the function with the environment
|
||||
env.add_function(self._function_name, self)
|
||||
|
||||
def get_function_name(self) -> str:
|
||||
"""Get function name"""
|
||||
return self._function_name
|
||||
|
||||
def set_env_for_dev_mode(self, env: Env):
|
||||
"""Set environment for dev mode"""
|
||||
pass
|
||||
|
||||
def is_source_modified_for_dev_mode(self) -> bool:
|
||||
"""Check if source is modified (for dev mode)"""
|
||||
return False
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Define({self._function_name})"
|
||||
130
pyenjoy/template/stat/ast/For.py
Normal file
130
pyenjoy/template/stat/ast/For.py
Normal file
@@ -0,0 +1,130 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal For - For Directive
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
from .Stat import Stat, StatList
|
||||
from ..Scope import Scope
|
||||
from ...Env import Env
|
||||
|
||||
class For(Stat):
|
||||
"""For loop directive"""
|
||||
|
||||
def __init__(self, var_name: str, iter_expr: str, body: Stat):
|
||||
"""
|
||||
Initialize for directive
|
||||
|
||||
Args:
|
||||
var_name: Loop variable name
|
||||
iter_expr: Expression to iterate over
|
||||
body: Loop body
|
||||
"""
|
||||
self._var_name = var_name
|
||||
self._iter_expr = iter_expr
|
||||
self._body = body
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute for loop
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
try:
|
||||
# Wrap dict values to allow attribute access
|
||||
def wrap_dict(d):
|
||||
"""Wrap a dict to allow attribute access"""
|
||||
if isinstance(d, dict):
|
||||
# Create a wrapper that allows both dot access and bracket access
|
||||
class DictWrapper:
|
||||
def __init__(self, data):
|
||||
self.__dict__ = data
|
||||
def __getitem__(self, key):
|
||||
return data[key]
|
||||
return DictWrapper(d)
|
||||
return d
|
||||
|
||||
# Create wrapped vars similar to SimpleExprList.eval()
|
||||
wrapped_vars = {}
|
||||
for key, value in scope._data.items():
|
||||
if isinstance(value, dict):
|
||||
wrapped_vars[key] = wrap_dict(value)
|
||||
elif isinstance(value, list):
|
||||
# Wrap dictionaries in lists
|
||||
wrapped_vars[key] = [wrap_dict(item) for item in value]
|
||||
else:
|
||||
wrapped_vars[key] = value
|
||||
|
||||
# Get the iterable from the expression
|
||||
iterable = eval(self._iter_expr, {}, wrapped_vars)
|
||||
|
||||
# Convert iterable to list for easier manipulation
|
||||
items = list(iterable)
|
||||
size = len(items)
|
||||
|
||||
# Create a ForLoopInfo class to hold loop information
|
||||
class ForLoopInfo:
|
||||
def __init__(self, size, index, outer=None):
|
||||
self._size = size
|
||||
self._index = index
|
||||
self._outer = outer
|
||||
|
||||
@property
|
||||
def size(self):
|
||||
return self._size
|
||||
|
||||
@property
|
||||
def index(self):
|
||||
return self._index
|
||||
|
||||
@property
|
||||
def count(self):
|
||||
return self._index + 1
|
||||
|
||||
@property
|
||||
def first(self):
|
||||
return self._index == 0
|
||||
|
||||
@property
|
||||
def last(self):
|
||||
return self._index == self._size - 1
|
||||
|
||||
@property
|
||||
def odd(self):
|
||||
return self._index % 2 == 0
|
||||
|
||||
@property
|
||||
def even(self):
|
||||
return self._index % 2 == 1
|
||||
|
||||
@property
|
||||
def outer(self):
|
||||
return self._outer
|
||||
|
||||
# Get outer for info if exists
|
||||
outer_for = scope._data.get("for")
|
||||
|
||||
# Iterate over the items
|
||||
for index, item in enumerate(items):
|
||||
# Create for loop info
|
||||
for_loop = ForLoopInfo(size, index, outer_for)
|
||||
|
||||
# Create a new scope for this iteration
|
||||
loop_scope = Scope(scope._data.copy(), scope._shared_objects.copy())
|
||||
loop_scope.set_parent(scope)
|
||||
loop_scope._data[self._var_name] = item
|
||||
loop_scope._data["for"] = for_loop
|
||||
|
||||
# Execute the loop body
|
||||
if self._body:
|
||||
self._body.exec(env, loop_scope, writer)
|
||||
except Exception as e:
|
||||
# Handle errors gracefully
|
||||
writer.write(f"Error in for loop: {e}")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"For(var={self._var_name}, expr={self._iter_expr})"
|
||||
40
pyenjoy/template/stat/ast/Output.py
Normal file
40
pyenjoy/template/stat/ast/Output.py
Normal file
@@ -0,0 +1,40 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Output - Output Directive
|
||||
"""
|
||||
|
||||
from .Stat import Stat
|
||||
from ..Scope import Scope
|
||||
from ...Env import Env
|
||||
from ...expr.ast import ExprList
|
||||
|
||||
class Output(Stat):
|
||||
"""Output directive for template expressions"""
|
||||
|
||||
def __init__(self, expr_list: ExprList):
|
||||
"""
|
||||
Initialize output directive
|
||||
|
||||
Args:
|
||||
expr_list: Expression list to evaluate
|
||||
"""
|
||||
self._expr_list = expr_list
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute output directive
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
if self._expr_list:
|
||||
result = self._expr_list.eval(scope)
|
||||
if result is not None:
|
||||
if hasattr(writer, 'write'):
|
||||
writer.write(str(result))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Output({self._expr_list})"
|
||||
79
pyenjoy/template/stat/ast/Stat.py
Normal file
79
pyenjoy/template/stat/ast/Stat.py
Normal file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Stat - Abstract Syntax Tree Base Class
|
||||
"""
|
||||
|
||||
from typing import Any
|
||||
from ..Scope import Scope
|
||||
from ...Env import Env
|
||||
|
||||
class Stat:
|
||||
"""Base class for statement AST nodes"""
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> Any:
|
||||
"""
|
||||
Execute the statement
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
|
||||
Returns:
|
||||
Execution result
|
||||
"""
|
||||
raise NotImplementedError("Stat.exec() must be implemented by subclasses")
|
||||
|
||||
def get_stat_list(self) -> 'StatList':
|
||||
"""Get as StatList if applicable"""
|
||||
return None
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Stat({self.__class__.__name__})"
|
||||
|
||||
|
||||
class StatList(Stat):
|
||||
"""Statement list containing multiple statements"""
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize statement list"""
|
||||
self._stats: list = []
|
||||
|
||||
def add_stat(self, stat: Stat):
|
||||
"""
|
||||
Add statement to list
|
||||
|
||||
Args:
|
||||
stat: Statement to add
|
||||
"""
|
||||
if stat:
|
||||
self._stats.append(stat)
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> Any:
|
||||
"""
|
||||
Execute all statements in list
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
|
||||
Returns:
|
||||
Last statement result or None
|
||||
"""
|
||||
result = None
|
||||
for stat in self._stats:
|
||||
result = stat.exec(env, scope, writer)
|
||||
return result
|
||||
|
||||
def get_stats(self) -> list:
|
||||
"""Get all statements"""
|
||||
return self._stats.copy()
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Get number of statements"""
|
||||
return len(self._stats)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"StatList({len(self._stats)} statements)"
|
||||
81
pyenjoy/template/stat/ast/Text.py
Normal file
81
pyenjoy/template/stat/ast/Text.py
Normal file
@@ -0,0 +1,81 @@
|
||||
#!/usr/bin/env python3.9
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
JFinal Text - Text Statement
|
||||
"""
|
||||
|
||||
from .Stat import Stat
|
||||
from ..Scope import Scope
|
||||
from ...Env import Env
|
||||
|
||||
class Text(Stat):
|
||||
"""Text statement for template content"""
|
||||
|
||||
def __init__(self, content: str):
|
||||
"""
|
||||
Initialize text statement
|
||||
|
||||
Args:
|
||||
content: Text content
|
||||
"""
|
||||
self._content = content
|
||||
|
||||
def exec(self, env: Env, scope: Scope, writer) -> None:
|
||||
"""
|
||||
Execute text statement
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
writer: Output writer
|
||||
"""
|
||||
if hasattr(writer, 'write'):
|
||||
# Parse and evaluate template expressions
|
||||
parsed_content = self._parse_expressions(env, scope)
|
||||
writer.write(parsed_content)
|
||||
|
||||
def _parse_expressions(self, env: Env, scope: Scope) -> str:
|
||||
"""
|
||||
Parse and evaluate template expressions in content
|
||||
|
||||
Args:
|
||||
env: Template environment
|
||||
scope: Execution scope
|
||||
|
||||
Returns:
|
||||
Parsed and evaluated content
|
||||
"""
|
||||
import re
|
||||
|
||||
def replace_expression(match):
|
||||
"""Replace expression with evaluated value"""
|
||||
expr_str = match.group(1).strip()
|
||||
|
||||
# Try to evaluate expression
|
||||
try:
|
||||
# First try variable access
|
||||
if expr_str.isidentifier():
|
||||
value = scope.get(expr_str)
|
||||
return str(value) if value is not None else ''
|
||||
|
||||
# Try arithmetic expression
|
||||
try:
|
||||
# Simple arithmetic evaluation
|
||||
# This is a simplified implementation
|
||||
result = eval(expr_str, {})
|
||||
return str(result)
|
||||
except:
|
||||
pass
|
||||
|
||||
# Fallback to original expression
|
||||
return match.group(0)
|
||||
|
||||
except Exception as e:
|
||||
return f"{match.group(0)} [Error: {e}]"
|
||||
|
||||
# Replace all #(...) expressions
|
||||
pattern = r'#\((.*?)\)'
|
||||
return re.sub(pattern, replace_expression, self._content)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"Text('{self._content[:50]}...')"
|
||||
12
pyenjoy/template/stat/ast/__init__.py
Normal file
12
pyenjoy/template/stat/ast/__init__.py
Normal file
@@ -0,0 +1,12 @@
|
||||
# Export statement AST classes
|
||||
from .Stat import Stat, StatList
|
||||
from .Text import Text
|
||||
from .Define import Define
|
||||
from .Output import Output
|
||||
|
||||
__all__ = [
|
||||
'Stat', 'StatList',
|
||||
'Text',
|
||||
'Define',
|
||||
'Output'
|
||||
]
|
||||
Reference in New Issue
Block a user