Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 23 additions & 19 deletions server/coder/coder.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@
#


import glob
import hashlib
import importlib.util
import os
import re
import sys
Expand All @@ -44,6 +42,7 @@
from utils.sys import SYS_UTILS

from .filters import FILTERS
from .generator_loader import GeneratorLoader


class Coder:
Expand All @@ -59,7 +58,15 @@ def __init__(self, project: Project, summary: Summary):
"generate": Signal("generate"),
}

sys.path = SYS_UTILS.sys_path() + [f"{project.hal_folder()}/tools/generator"]
hal = self._project.gen.hal
halVersion = self._project.gen.halVersion
hal_folder = Path(self._project.hal_folder())

self._package_name = f"{hal}_{halVersion}_generator"
self._filters_package_name = f"{self._package_name}.filters"
self._generator_folder = hal_folder / "tools" / "generator"
self._filters_folder = self._generator_folder / "filters"

self._generator = self._load_generator()
self.files_table = self._get_files_table()

Expand Down Expand Up @@ -167,16 +174,15 @@ def files_list(self) -> list[str]:
return list(self.files_table.keys())

def _load_generator(self) -> ModuleType | None:
if self._check_hal_folder():
spec = importlib.util.spec_from_file_location(
"coder", f"{self._project.hal_folder()}/tools/generator/generator.py"
)
if spec is None or spec.loader is None:
return None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
return None
if not self._check_hal_folder():
return None

try:
loader = GeneratorLoader(self._package_name, self._generator_folder)
return loader.load()
except Exception as e:
logger.error(f"Failed to load generator: {e}")
return None

def _get_files_table(self) -> dict[str, dict[str, str]]:
if self._generator is None:
Expand Down Expand Up @@ -313,13 +319,11 @@ def _get_environment(self) -> jinja2.Environment:
env.add_extension("jinja2.ext.do")
env.add_extension("jinja2.ext.loopcontrols")

files = glob.glob(f"{package_folder}/tools/generator/filters/*.py")
files = self._filters_folder.glob("*.py")
for file in files:
spec = importlib.util.spec_from_file_location(Path(file).stem, file)
if spec is None or spec.loader is None:
break
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
stem = file.stem
full_name = f"{self._filters_package_name}.{stem}"
module = sys.modules[full_name]
functions = [
name for name in dir(module) if callable(getattr(module, name))
]
Expand Down
126 changes: 126 additions & 0 deletions server/coder/generator_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#!/usr/bin/env python3
# -*- coding:utf-8 -*-

# Licensed under the Apache License v. 2 (the "License")
# You may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0.html
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Copyright (C) 2025-2025 xqyjlj<xqyjlj@126.com>
#
# @author xqyjlj
# @file generator_loader.py
#
# Change Logs:
# Date Author Notes
# ------------ ---------- -----------------------------------------------
# 2025-07-07 xqyjlj initial version
#

import importlib.util
import sys
from pathlib import Path
from types import ModuleType
from typing import Dict, Optional

from loguru import logger
from utils.io import IO_UTILS


class GeneratorLoader:

def __init__(self, name: str, folder: Path):
self._package_name = name
self._folder = folder
self._filters_folder = self._folder / "filters"
self._filters_package_name = f"{name}.filters"
self._file_sha1_cache: Dict[str, str] = {}

def load(self) -> Optional[ModuleType]:
self._create_packages()
self._load_filters()
return self._load_generator()

def _create_packages(self) -> None:
if self._package_name not in sys.modules:
pkg = ModuleType(self._package_name)
pkg.__path__ = [str(self._folder)]
sys.modules[self._package_name] = pkg

if self._filters_package_name not in sys.modules:
filters_pkg = ModuleType(self._filters_package_name)
filters_pkg.__path__ = [str(self._filters_folder)]
sys.modules[self._filters_package_name] = filters_pkg

def _load_filters(self) -> None:
if not self._filters_folder.exists():
return
for file in self._filters_folder.glob("*.py"):
stem = file.stem
full_name = f"{self._filters_package_name}.{stem}"
try:
if full_name in sys.modules and not self._is_file_changed(file):
continue

if full_name in sys.modules:
del sys.modules[full_name]

self._load_module(full_name, file, [str(self._filters_folder)])
except Exception as e:
logger.warning(f"Failed to load filter {file}: {e}")

def _load_generator(self) -> Optional[ModuleType]:
gen_file = self._folder / "generator.py"
if not gen_file.exists():
return None

try:
if self._package_name in sys.modules and not self._is_file_changed(
gen_file
):
return sys.modules[self._package_name]

if self._package_name in sys.modules:
del sys.modules[self._package_name]

return self._load_module(self._package_name, gen_file, [str(self._folder)])
except Exception as e:
logger.error(f"Failed to load generator: {e}")
return None

def _load_module(self, name: str, file: Path, search_paths: list) -> ModuleType:
spec = importlib.util.spec_from_file_location(
name, file, submodule_search_locations=search_paths
)
if spec is None or spec.loader is None:
raise ImportError(f"Could not create spec for {file}")

module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module

def _is_file_changed(self, file_path: Path) -> bool:
file_key = str(file_path.absolute())
current_sha1 = IO_UTILS.sha1(file_path)

if file_key not in self._file_sha1_cache:
self._file_sha1_cache[file_key] = current_sha1
return True

if self._file_sha1_cache[file_key] != current_sha1:
self._file_sha1_cache[file_key] = current_sha1
return True

return False

@property
def filters_package_name(self) -> str:
return self._filters_package_name
13 changes: 12 additions & 1 deletion server/utils/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@
#


import hashlib
import os
from pathlib import Path
from typing import Any

from ruamel.yaml import YAML

Expand All @@ -39,10 +42,18 @@ def readlines(path: str) -> list[str]:
return f.readlines()

@staticmethod
def read_yaml(file: str):
def read_yaml(file: str) -> Any:
with open(file, "r", encoding="utf-8") as f:
yaml = YAML()
return yaml.load(f.read())

@staticmethod
def sha1(file: Path) -> str:
hash_sha1 = hashlib.sha1()
with open(file, "rb") as f:
for chunk in iter(lambda: f.read(4096), b""):
hash_sha1.update(chunk)
return hash_sha1.hexdigest()


IO_UTILS = IoUtils()
9 changes: 3 additions & 6 deletions server/utils/sys.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,15 @@
# 2025-07-07 xqyjlj initial version
#

import copy

import sys
from pathlib import Path


class SysUtils:
def __init__(self):
self._sys_path = copy.deepcopy(sys.path)
self._sys_path.append(self.public_folder())

def sys_path(self) -> list[str]:
return self._sys_path
if sys.path[-1] != self.public_folder():
sys.path.append(self.public_folder())

@staticmethod
def exe_folder() -> str:
Expand Down