diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..09631f0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,179 @@ +.DS_Store +# 字节编译/优化/编译后的文件 +**/__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# 通常这些文件是根据平台/打包方式生成的 +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +media/ + +# Flask stuff +instance/ +.webassets-cache + +# Scrapy stuff +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# 对于pyenv的版本文件,如果你想版本控制则注释掉 +.python-version + +# pipenv +# 如果你想控制Pipfile.lock则注释掉 +Pipfile.lock + +# poetry +# poetry.lock 如果你想版本控制则注释掉 +# poetry.lock + +# pdm +# pdm.lock 如果你想版本控制则注释掉 +# pdm.lock +.pdm.toml + +# PEP 582; 用于本地包安装的__pypackages__/目录 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# 编辑器相关文件 +# VSCode +.vscode/ +# IntelliJ/PyCharm +.idea/ +*.iml +*.iws +*.ipr +# Vim +*.swp +*.swo +# Emacs +*~ +\#*\# +/.emacs.desktop +/.emacs.desktop.lock +*.elc +auto-save-list +tramp +.\\#* + +# 操作系统相关文件 +# macOS +.DS_Store +.AppleDouble +.LSOverride +# Windows +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ +*.cab +*.msi +*.msix +*.msm +*.msp +*.lnk +# Linux +*.swp +.*.sw? + diff --git a/README.py b/README.py new file mode 100644 index 0000000..747ace8 --- /dev/null +++ b/README.py @@ -0,0 +1,197 @@ +#!/usr/bin/env python3.9 +# -*- coding: utf-8 -*- +""" +JFinal Template Engine Python Implementation +============================================= + +This is a Python 3.9+ implementation of the JFinal Template Engine (Enjoy). + +Project Structure +----------------- + +py_enjoy/ +├── __init__.py # Main package initialization +├── kit/ # Utility classes +│ ├── StrKit.py # String utilities +│ ├── HashKit.py # Hash and encryption utilities +│ ├── Kv.py # Key-Value dictionary +│ ├── Okv.py # Ordered Key-Value dictionary +│ ├── Prop.py # Properties file handler +│ ├── PropKit.py # Properties file management +│ ├── TypeKit.py # Type conversion utilities +│ ├── TimeKit.py # Date and time utilities +│ ├── Func.py # Lambda function utilities +│ ├── ReflectKit.py # Reflection utilities +│ ├── JavaKeyword.py # Java keyword detection +│ └── SyncWriteMap.py # Thread-safe dictionary +│ +├── proxy/ # Dynamic proxy classes +│ ├── ProxyClass.py # Proxy class information +│ ├── ProxyClassLoader.py # Proxy class loader +│ └── ProxyCompiler.py # Proxy class compiler +│ +└── template/ # Template engine core + ├── Engine.py # Main template engine + ├── Template.py # Template rendering + ├── Env.py # Template environment + ├── EngineConfig.py # Engine configuration + ├── Directive.py # Base directive class + ├── TemplateException.py # Exception handling + │ + ├── expr/ # Expression parsing + │ └── ast/ + │ ├── Expr.py # Expression base class + │ ├── Const.py # Constant expressions + │ ├── Compare.py # Comparison expressions + │ └── Arith.py # Arithmetic expressions + │ + └── stat/ # Statement parsing + ├── Scope.py # Execution scope + └── ast/ + └── Stat.py # Statement base classes + +Features +-------- + +1. **Template Engine** + - String template rendering + - File-based template loading + - Template caching and hot-reloading + - Shared functions and objects + +2. **Expression Language** + - Arithmetic operations (+, -, *, /, %) + - Comparison operations (==, !=, >, <, >=, <=) + - Logical operations (&&, ||, !) + - Ternary expressions (condition ? true : false) + - Variable access and assignment + +3. **Directives** + - #if/#else/#end - Conditional statements + - #for - Loop statements + - #define - Template functions + - #include - Template inclusion + - #set - Variable assignment + - Custom directive support + +4. **Utility Classes** + - String manipulation (StrKit) + - Hash functions (HashKit) + - Type conversions (TypeKit) + - Date/time handling (TimeKit) + - Properties file management (PropKit) + - Key-Value containers (Kv, Okv) + +Requirements +------------ + +- Python 3.9.0 or higher +- No external dependencies + +Installation +------------ + +```bash +# Clone or download the project +cd py_enjoy + +# No installation required, just add to PYTHONPATH +import sys +sys.path.append('/path/to/py_enjoy') +``` + +Quick Start +----------- + +```python +from py_enjoy.template.Engine import Engine +from py_enjoy.kit.Kv import Kv + +# Get the template engine +engine = Engine.use() + +# Create a simple template +template_content = "Hello, #(name)! Today is #(date)." +template = engine.get_template_by_string(template_content) + +# Render with data +data = Kv.of("name", "World").set("date", "2024-01-01") +result = template.render_to_string(data) + +print(result) # Output: Hello, World! Today is 2024-01-01. +``` + +Advanced Usage +-------------- + +### Template with Logic + +```python +template_content = """ +#for(item in items) + #(item_index + 1). #(item.name) - #(item.price) +#if(item.is_last) + Total: #(total_price) +#end +#end +""" + +data = Kv.of("items", [ + {"name": "Apple", "price": 1.5, "is_last": False}, + {"name": "Banana", "price": 0.75, "is_last": True} +]).set("total_price", 2.25) +``` + +### Custom Directives + +```python +from py_enjoy.template.Directive import Directive + +class UpperDirective(Directive): + def exec(self, env, scope, writer): + value = self.expr_list.eval(scope) + if value: + writer.write(str(value).upper()) +``` + +Performance Considerations +-------------------------- + +1. **Template Caching**: Enable template caching in production + ```python + engine.cache_string_template = True + ``` + +2. **Dev Mode**: Disable in production + ```python + engine.dev_mode = False + ``` + +3. **Buffer Size**: Adjust for large outputs + ```python + engine.set_buffer_size(8192) + ``` + +Testing +------- + +```bash +python test.py +``` + +License +------- + +This project is licensed under the Apache License 2.0. +See the LICENSE file for details. + +Credits +------- + +Original Java implementation: James Zhan 詹波 (jfinal@126.com) +Python port: Automated conversion +""" + +__version__ = "5.2.2" +__author__ = "James Zhan 詹波 (original), Python port by hello@miw.cn" +__license__ = "Apache License 2.0" diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..3706e16 --- /dev/null +++ b/__init__.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3.9 +# -*- coding: utf-8 -*- +""" +py_enjoy - Python implementation of JFinal Template Engine +""" + +__version__ = "5.2.2" +__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", +] diff --git a/kit/Func.py b/kit/Func.py new file mode 100644 index 0000000..f80a1e3 --- /dev/null +++ b/kit/Func.py @@ -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) diff --git a/kit/HashKit.py b/kit/HashKit.py new file mode 100644 index 0000000..fbba87e --- /dev/null +++ b/kit/HashKit.py @@ -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 diff --git a/kit/JavaKeyword.py b/kit/JavaKeyword.py new file mode 100644 index 0000000..afbf16c --- /dev/null +++ b/kit/JavaKeyword.py @@ -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() diff --git a/kit/Kv.py b/kit/Kv.py new file mode 100644 index 0000000..81c054a --- /dev/null +++ b/kit/Kv.py @@ -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 diff --git a/kit/Okv.py b/kit/Okv.py new file mode 100644 index 0000000..39494dd --- /dev/null +++ b/kit/Okv.py @@ -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 diff --git a/kit/Prop.py b/kit/Prop.py new file mode 100644 index 0000000..675b997 --- /dev/null +++ b/kit/Prop.py @@ -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() diff --git a/kit/PropKit.py b/kit/PropKit.py new file mode 100644 index 0000000..1e083c4 --- /dev/null +++ b/kit/PropKit.py @@ -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 diff --git a/kit/ReflectKit.py b/kit/ReflectKit.py new file mode 100644 index 0000000..ffd7d8a --- /dev/null +++ b/kit/ReflectKit.py @@ -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 diff --git a/kit/StrKit.py b/kit/StrKit.py new file mode 100644 index 0000000..2d28996 --- /dev/null +++ b/kit/StrKit.py @@ -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 diff --git a/kit/SyncWriteMap.py b/kit/SyncWriteMap.py new file mode 100644 index 0000000..0ca51fc --- /dev/null +++ b/kit/SyncWriteMap.py @@ -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 diff --git a/kit/TimeKit.py b/kit/TimeKit.py new file mode 100644 index 0000000..dc51f8e --- /dev/null +++ b/kit/TimeKit.py @@ -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}") diff --git a/kit/TypeKit.py b/kit/TypeKit.py new file mode 100644 index 0000000..4be0354 --- /dev/null +++ b/kit/TypeKit.py @@ -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}") diff --git a/proxy/ProxyClass.py b/proxy/ProxyClass.py new file mode 100644 index 0000000..39d719a --- /dev/null +++ b/proxy/ProxyClass.py @@ -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})" diff --git a/proxy/ProxyClassLoader.py b/proxy/ProxyClassLoader.py new file mode 100644 index 0000000..92d08b7 --- /dev/null +++ b/proxy/ProxyClassLoader.py @@ -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) diff --git a/proxy/ProxyCompiler.py b/proxy/ProxyCompiler.py new file mode 100644 index 0000000..b6d8a13 --- /dev/null +++ b/proxy/ProxyCompiler.py @@ -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 diff --git a/template/Directive.py b/template/Directive.py new file mode 100644 index 0000000..620dcb3 --- /dev/null +++ b/template/Directive.py @@ -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 diff --git a/template/Engine.py b/template/Engine.py new file mode 100644 index 0000000..2029561 --- /dev/null +++ b/template/Engine.py @@ -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 diff --git a/template/EngineConfig.py b/template/EngineConfig.py new file mode 100644 index 0000000..e8e4062 --- /dev/null +++ b/template/EngineConfig.py @@ -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 diff --git a/template/Env.py b/template/Env.py new file mode 100644 index 0000000..7cff7d1 --- /dev/null +++ b/template/Env.py @@ -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})" diff --git a/template/Template.py b/template/Template.py new file mode 100644 index 0000000..d5bf784 --- /dev/null +++ b/template/Template.py @@ -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 diff --git a/template/TemplateException.py b/template/TemplateException.py new file mode 100644 index 0000000..ea28a13 --- /dev/null +++ b/template/TemplateException.py @@ -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})" diff --git a/template/expr/__init__.py b/template/expr/__init__.py new file mode 100644 index 0000000..55e74fb --- /dev/null +++ b/template/expr/__init__.py @@ -0,0 +1 @@ +# Python package initialization \ No newline at end of file diff --git a/template/expr/ast/Arith.py b/template/expr/ast/Arith.py new file mode 100644 index 0000000..d98fafe --- /dev/null +++ b/template/expr/ast/Arith.py @@ -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})" diff --git a/template/expr/ast/Compare.py b/template/expr/ast/Compare.py new file mode 100644 index 0000000..7232c53 --- /dev/null +++ b/template/expr/ast/Compare.py @@ -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})" diff --git a/template/expr/ast/Const.py b/template/expr/ast/Const.py new file mode 100644 index 0000000..7e05259 --- /dev/null +++ b/template/expr/ast/Const.py @@ -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})" diff --git a/template/expr/ast/Expr.py b/template/expr/ast/Expr.py new file mode 100644 index 0000000..ebb8f47 --- /dev/null +++ b/template/expr/ast/Expr.py @@ -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)" diff --git a/template/expr/ast/FieldKeyBuilder.py b/template/expr/ast/FieldKeyBuilder.py new file mode 100644 index 0000000..59e0271 --- /dev/null +++ b/template/expr/ast/FieldKeyBuilder.py @@ -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()" diff --git a/template/expr/ast/FieldKit.py b/template/expr/ast/FieldKit.py new file mode 100644 index 0000000..503fddb --- /dev/null +++ b/template/expr/ast/FieldKit.py @@ -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()" diff --git a/template/expr/ast/MethodKit.py b/template/expr/ast/MethodKit.py new file mode 100644 index 0000000..9b39243 --- /dev/null +++ b/template/expr/ast/MethodKit.py @@ -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()" diff --git a/template/expr/ast/SharedMethodKit.py b/template/expr/ast/SharedMethodKit.py new file mode 100644 index 0000000..02c1dd5 --- /dev/null +++ b/template/expr/ast/SharedMethodKit.py @@ -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)" diff --git a/template/expr/ast/__init__.py b/template/expr/ast/__init__.py new file mode 100644 index 0000000..7ce4271 --- /dev/null +++ b/template/expr/ast/__init__.py @@ -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' +] diff --git a/template/ext/__init__.py b/template/ext/__init__.py new file mode 100644 index 0000000..55e74fb --- /dev/null +++ b/template/ext/__init__.py @@ -0,0 +1 @@ +# Python package initialization \ No newline at end of file diff --git a/template/ext/directive/CallDirective.py b/template/ext/directive/CallDirective.py new file mode 100644 index 0000000..12db779 --- /dev/null +++ b/template/ext/directive/CallDirective.py @@ -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()" diff --git a/template/ext/directive/DateDirective.py b/template/ext/directive/DateDirective.py new file mode 100644 index 0000000..b631aaf --- /dev/null +++ b/template/ext/directive/DateDirective.py @@ -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()" diff --git a/template/ext/directive/EscapeDirective.py b/template/ext/directive/EscapeDirective.py new file mode 100644 index 0000000..5cc0fa7 --- /dev/null +++ b/template/ext/directive/EscapeDirective.py @@ -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()" diff --git a/template/ext/directive/NumberDirective.py b/template/ext/directive/NumberDirective.py new file mode 100644 index 0000000..dae9d6d --- /dev/null +++ b/template/ext/directive/NumberDirective.py @@ -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()" diff --git a/template/ext/directive/RandomDirective.py b/template/ext/directive/RandomDirective.py new file mode 100644 index 0000000..0d534e7 --- /dev/null +++ b/template/ext/directive/RandomDirective.py @@ -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()" diff --git a/template/ext/directive/RenderDirective.py b/template/ext/directive/RenderDirective.py new file mode 100644 index 0000000..dbac013 --- /dev/null +++ b/template/ext/directive/RenderDirective.py @@ -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()" diff --git a/template/ext/directive/StringDirective.py b/template/ext/directive/StringDirective.py new file mode 100644 index 0000000..bee99a0 --- /dev/null +++ b/template/ext/directive/StringDirective.py @@ -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()" diff --git a/template/ext/directive/__init__.py b/template/ext/directive/__init__.py new file mode 100644 index 0000000..55e74fb --- /dev/null +++ b/template/ext/directive/__init__.py @@ -0,0 +1 @@ +# Python package initialization \ No newline at end of file diff --git a/template/ext/sharedmethod/SharedMethodLib.py b/template/ext/sharedmethod/SharedMethodLib.py new file mode 100644 index 0000000..a0692e8 --- /dev/null +++ b/template/ext/sharedmethod/SharedMethodLib.py @@ -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()" diff --git a/template/ext/sharedmethod/__init__.py b/template/ext/sharedmethod/__init__.py new file mode 100644 index 0000000..55e74fb --- /dev/null +++ b/template/ext/sharedmethod/__init__.py @@ -0,0 +1 @@ +# Python package initialization \ No newline at end of file diff --git a/template/io/EncoderFactory.py b/template/io/EncoderFactory.py new file mode 100644 index 0000000..3e054e5 --- /dev/null +++ b/template/io/EncoderFactory.py @@ -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})" diff --git a/template/io/JdkEncoder.py b/template/io/JdkEncoder.py new file mode 100644 index 0000000..101c4a5 --- /dev/null +++ b/template/io/JdkEncoder.py @@ -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()" diff --git a/template/io/JdkEncoderFactory.py b/template/io/JdkEncoderFactory.py new file mode 100644 index 0000000..744a2eb --- /dev/null +++ b/template/io/JdkEncoderFactory.py @@ -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()" diff --git a/template/io/WriterBuffer.py b/template/io/WriterBuffer.py new file mode 100644 index 0000000..db19b5d --- /dev/null +++ b/template/io/WriterBuffer.py @@ -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})" diff --git a/template/io/__init__.py b/template/io/__init__.py new file mode 100644 index 0000000..55e74fb --- /dev/null +++ b/template/io/__init__.py @@ -0,0 +1 @@ +# Python package initialization \ No newline at end of file diff --git a/template/source/ClassPathSource.py b/template/source/ClassPathSource.py new file mode 100644 index 0000000..7646027 --- /dev/null +++ b/template/source/ClassPathSource.py @@ -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})" diff --git a/template/source/ClassPathSourceFactory.py b/template/source/ClassPathSourceFactory.py new file mode 100644 index 0000000..2b2924a --- /dev/null +++ b/template/source/ClassPathSourceFactory.py @@ -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()" diff --git a/template/source/FileSource.py b/template/source/FileSource.py new file mode 100644 index 0000000..6b9c957 --- /dev/null +++ b/template/source/FileSource.py @@ -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})" diff --git a/template/source/FileSourceFactory.py b/template/source/FileSourceFactory.py new file mode 100644 index 0000000..50e496d --- /dev/null +++ b/template/source/FileSourceFactory.py @@ -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()" diff --git a/template/source/ISourceFactory.py b/template/source/ISourceFactory.py new file mode 100644 index 0000000..b872df3 --- /dev/null +++ b/template/source/ISourceFactory.py @@ -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") diff --git a/template/source/StringSource.py b/template/source/StringSource.py new file mode 100644 index 0000000..466ac16 --- /dev/null +++ b/template/source/StringSource.py @@ -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}')" diff --git a/template/source/__init__.py b/template/source/__init__.py new file mode 100644 index 0000000..55e74fb --- /dev/null +++ b/template/source/__init__.py @@ -0,0 +1 @@ +# Python package initialization \ No newline at end of file diff --git a/template/stat/Compressor.py b/template/stat/Compressor.py new file mode 100644 index 0000000..fe46c05 --- /dev/null +++ b/template/stat/Compressor.py @@ -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)})" diff --git a/template/stat/OutputDirectiveFactory.py b/template/stat/OutputDirectiveFactory.py new file mode 100644 index 0000000..e71c769 --- /dev/null +++ b/template/stat/OutputDirectiveFactory.py @@ -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() diff --git a/template/stat/Parser.py b/template/stat/Parser.py new file mode 100644 index 0000000..e7a4d89 --- /dev/null +++ b/template/stat/Parser.py @@ -0,0 +1,283 @@ +#!/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.') + + # Evaluate the expression + return eval(expr_str, {}, wrapped_vars) + except Exception as e: + # Handle evaluation errors gracefully + return f"Error evaluating expression '{self._expr_str}': {e}" + + diff --git a/template/stat/Scope.py b/template/stat/Scope.py new file mode 100644 index 0000000..797d171 --- /dev/null +++ b/template/stat/Scope.py @@ -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)})" diff --git a/template/stat/ast/Define.py b/template/stat/ast/Define.py new file mode 100644 index 0000000..bc17634 --- /dev/null +++ b/template/stat/ast/Define.py @@ -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})" diff --git a/template/stat/ast/For.py b/template/stat/ast/For.py new file mode 100644 index 0000000..139c63d --- /dev/null +++ b/template/stat/ast/For.py @@ -0,0 +1,106 @@ +#!/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: + # Get the iterable from the expression + iterable = eval(self._iter_expr, {}, scope._data.copy()) + + # 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})" diff --git a/template/stat/ast/Output.py b/template/stat/ast/Output.py new file mode 100644 index 0000000..7b67a6d --- /dev/null +++ b/template/stat/ast/Output.py @@ -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})" diff --git a/template/stat/ast/Stat.py b/template/stat/ast/Stat.py new file mode 100644 index 0000000..fd6406a --- /dev/null +++ b/template/stat/ast/Stat.py @@ -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)" diff --git a/template/stat/ast/Text.py b/template/stat/ast/Text.py new file mode 100644 index 0000000..e1ab378 --- /dev/null +++ b/template/stat/ast/Text.py @@ -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]}...')" diff --git a/template/stat/ast/__init__.py b/template/stat/ast/__init__.py new file mode 100644 index 0000000..d3125b3 --- /dev/null +++ b/template/stat/ast/__init__.py @@ -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' +] diff --git a/test.py b/test.py new file mode 100644 index 0000000..b4caea2 --- /dev/null +++ b/test.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python3.9 +# -*- coding: utf-8 -*- +""" +JFinal Test - Basic Test Script for JFinal Python Template Engine +""" + +import sys +import os + +# Add the parent directory to Python path to allow importing from py_enjoy +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +print(f"Python path: {sys.path}") +def test_kit_classes(): + """Test kit classes""" + print("Testing kit classes...") + + # Test StrKit + from py_enjoy.kit.StrKit import StrKit + assert StrKit.first_char_to_lower_case("Hello") == "hello" + assert StrKit.first_char_to_upper_case("hello") == "Hello" + assert StrKit.is_blank(" ") == True + assert StrKit.is_blank("test") == False + assert StrKit.not_blank("test") == True + print("✓ StrKit tests passed") + + # Test HashKit + from py_enjoy.kit.HashKit import HashKit + md5_result = HashKit.md5("test") + assert len(md5_result) == 32 + assert HashKit.slow_equals(b"test", b"test") == True + assert HashKit.slow_equals(b"test", b"test2") == False + print("✓ HashKit tests passed") + + # Test Kv + from py_enjoy.kit.Kv import Kv + kv = Kv.of("name", "John").set("age", 25) + assert kv.get("name") == "John" + assert kv.get_int("age") == 25 + assert kv.get_str("name") == "John" + print("✓ Kv tests passed") + + # Test Prop + from py_enjoy.kit.Prop import Prop + # Create a test properties file + test_content = """ +name=John +age=25 +enabled=true + """ + prop = Prop(test_content, is_file=False) + assert prop.get("name") == "John" + assert prop.get_int("age") == 25 + assert prop.get_boolean("enabled") == True + print("✓ Prop tests passed") + + print("All kit class tests passed!") + + +def test_template_engine(): + """Test template engine""" + print("\nTesting template engine...") + + from py_enjoy.template.Engine import Engine + from py_enjoy.kit.Kv import Kv + + engine = Engine.use() + + # Test basic template rendering + template = engine.get_template_by_string("Hello, #(name)!") + result = template.render_to_string(Kv.of("name", "World")) + assert result == "Hello, World!" + print("✓ Basic template test passed") + + # Test expression evaluation + template2 = engine.get_template_by_string("Result: #(1 + 2 * 3)") + result2 = template2.render_to_string() + assert "7" in result2 + print("✓ Expression template test passed") + + print("All template engine tests passed!") + + +def test_type_conversions(): + """Test type conversions""" + print("\nTesting type conversions...") + + from py_enjoy.kit.TypeKit import TypeKit + + assert TypeKit.to_int("123") == 123 + assert TypeKit.to_float("3.14") == 3.14 + assert TypeKit.to_boolean("true") == True + assert TypeKit.to_boolean("1") == True + assert TypeKit.to_boolean("0") == False + assert TypeKit.to_int(None) is None + print("✓ TypeKit tests passed") + + print("All type conversion tests passed!") + + +def main(): + """Run all tests""" + print("Running JFinal Python Template Engine Tests...") + print("=" * 50) + + try: + test_kit_classes() + test_type_conversions() + test_template_engine() + + print("\n" + "=" * 50) + print("✓ All tests passed successfully!") + + except Exception as e: + print(f"\n✗ Test failed: {e}") + import traceback + traceback.print_exc() + sys.exit(1) + + +if __name__ == "__main__": + main()