with more logger
Add | more formatter and some more Fix | type mis match sync pyglet Enhance | logger with Template add lib-not-dr as requirement sync pyglet sync pyglet Add | add lto=yes to nuitka_build just incase sync pyglet sync lib_not_dr Remove | external requirement lib-not-dr some logger sync lib-not-dr sync pyglet sync lib-not-dr sync lib-not-dr sync pyglet sync pyglet Fix | console thread been block Update DR rs and DR sdk sync lib not dr sync lib-not-dr sync lib-not-dr sync pyglet and lib-not-dr sync pyglet 0.1.8 sync lib not dr logger almost done? almost! sync pyglet (clicpboard support!) sync lib not dr sync lib not dr color code and sync pyglet do not show memory and progress building localy sync pyglet synclibs
This commit is contained in:
parent
0e0c7ef700
commit
d84b490b99
5
DR.py
5
DR.py
@ -52,8 +52,9 @@ def start(start_time_ns: int) -> None:
|
||||
print(crash.all_process)
|
||||
for a_thread in threading.enumerate():
|
||||
print(a_thread)
|
||||
if a_thread.is_alive() and a_thread != threading.current_thread() and a_thread != threading.main_thread():
|
||||
a_thread.join(2) # wait for 2 sec
|
||||
if a_thread.is_alive() and not a_thread.daemon:
|
||||
if a_thread != threading.current_thread() and a_thread != threading.main_thread():
|
||||
a_thread.join(2) # wait for 2 sec
|
||||
import pyglet
|
||||
pyglet.app.exit() # make sure that pyglet has stopped
|
||||
|
||||
|
@ -7,8 +7,8 @@ fonts_folder = "assets/fonts"
|
||||
|
||||
[window]
|
||||
style = "None"
|
||||
width = 1112
|
||||
height = 793
|
||||
width = 1312
|
||||
height = 915
|
||||
visible = true
|
||||
gui_scale = 1
|
||||
caption = "Difficult Rocket v{DR_version}"
|
||||
|
@ -1,9 +1,17 @@
|
||||
|
||||
# DR game/DR rs 更新日志
|
||||
|
||||
- 最新版本号
|
||||
- DR game: 0.3.3.0
|
||||
- DR rs: 0.2.22.0
|
||||
- DR rs: 0.2.23.0
|
||||
|
||||
## 20231101 DR rs 0.2.23.0
|
||||
|
||||
### Dependency
|
||||
|
||||
- Update `DR rs` dependency
|
||||
- `quick-xml`: `0.30.0` -> `0.31.0`
|
||||
- `serde`: `1.0.186` -> `1.0.190`
|
||||
- `xml-rs`: `0.8.16` -> `0.8.19`
|
||||
|
||||
## 20230825 DR rs 0.2.22.0
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
# DR SDK 更新日志
|
||||
|
||||
- 最新版本号
|
||||
@ -14,6 +13,20 @@
|
||||
- 不再同时维护两份代码
|
||||
- No longer maintain two sets of code at the same time
|
||||
|
||||
### Fix
|
||||
|
||||
- 如果没有 DR_game 的情况下, 退出时会 join 控制台线程
|
||||
- 通过检测线程是否是守护线程来判断是否 join
|
||||
- If there is no DR_game, join the console thread when exiting
|
||||
- Determine whether to join by detecting whether the thread is a daemon thread
|
||||
|
||||
### Dependency
|
||||
|
||||
- 更新了所有的依赖版本号
|
||||
- 去看 `requirements.txt` 吧
|
||||
- Updated all dependency version numbers
|
||||
- Go see `requirements.txt`
|
||||
|
||||
## DR sdk 0.8.7.2
|
||||
|
||||
### Add
|
||||
|
@ -4,4 +4,4 @@
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
|
||||
__version__ = '0.1.7'
|
||||
__version__ = '0.1.8'
|
||||
|
33
libs/lib_not_dr/logger/__init__.py
Normal file
33
libs/lib_not_dr/logger/__init__.py
Normal file
@ -0,0 +1,33 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
import sys
|
||||
|
||||
from lib_not_dr.types.options import Options
|
||||
|
||||
COLOR_SUPPORT = True
|
||||
|
||||
if sys.platform == "win32":
|
||||
try:
|
||||
# https://stackoverflow.com/questions/36760127/...
|
||||
# how-to-use-the-new-support-for-ansi-escape-sequences-in-the-windows-10-console
|
||||
from ctypes import windll
|
||||
|
||||
kernel32 = windll.kernel32
|
||||
kernel32.SetConsoleMode(kernel32.GetStdHandle(-11), 7)
|
||||
except OSError: # pragma: no cover
|
||||
COLOR_SUPPORT = False
|
||||
|
||||
|
||||
class LogLevel(Options):
|
||||
name = 'LogLevel'
|
||||
notset: int = 0
|
||||
trace: int = 5
|
||||
fine: int = 7
|
||||
debug: int = 10
|
||||
info: int = 20
|
||||
warn: int = 30
|
||||
error: int = 40
|
||||
fatal: int = 50
|
285
libs/lib_not_dr/logger/formatter/__init__.py
Normal file
285
libs/lib_not_dr/logger/formatter/__init__.py
Normal file
@ -0,0 +1,285 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
from string import Template
|
||||
from typing import List, Union, Optional, Dict, Tuple, TYPE_CHECKING
|
||||
|
||||
from lib_not_dr.logger import LogLevel
|
||||
from lib_not_dr.types.options import Options
|
||||
from lib_not_dr.logger.structure import LogMessage, FormattingMessage
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from lib_not_dr.logger.formatter.colors import BaseColorFormatter
|
||||
|
||||
|
||||
class BaseFormatter(Options):
|
||||
name = 'BaseFormatter'
|
||||
|
||||
sub_formatter: List['BaseFormatter'] = []
|
||||
color_formatters: List['BaseColorFormatter'] = []
|
||||
default_template: str = '[${log_time}][${level}]|${logger_name}:${logger_tag}|${messages}'
|
||||
|
||||
@classmethod
|
||||
def add_info(cls, match: str, to: str, description: str) -> str:
|
||||
return f'- {to} -> ${{{match}}} : {description}'
|
||||
|
||||
@classmethod
|
||||
def info(cls) -> str:
|
||||
infos = {BaseFormatter.name: BaseFormatter._info()}
|
||||
cache = ''
|
||||
for formatter in cls.sub_formatter:
|
||||
infos[formatter.name] = formatter._info()
|
||||
infos[cls.name] = cls._info()
|
||||
for name, info in infos.items():
|
||||
cache += f"## {name}\n"
|
||||
cache += info
|
||||
cache += '\n'
|
||||
return cache
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
info = cls.add_info('logger_name', 'logger name', 'The name of the logger')
|
||||
info += '\n'
|
||||
info += cls.add_info('logger_tag', 'logger tag', 'The tag of the logger')
|
||||
return info
|
||||
|
||||
def format_message(self,
|
||||
message: LogMessage,
|
||||
template: Optional[Union[Template, str]] = None) -> str:
|
||||
"""
|
||||
Format message
|
||||
:param message: 输入的消息
|
||||
:param template: 日志输出模板
|
||||
:return:
|
||||
"""
|
||||
basic_info = message.format_for_message()
|
||||
message, info = self._format((message, basic_info))
|
||||
|
||||
if template is None:
|
||||
template = Template(self.default_template)
|
||||
elif isinstance(template, str):
|
||||
template = Template(template)
|
||||
|
||||
try:
|
||||
return template.substitute(**info)
|
||||
except (KeyError, ValueError):
|
||||
return template.safe_substitute(**info)
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
"""
|
||||
Format message
|
||||
:param message:
|
||||
:return:
|
||||
"""
|
||||
for formatter in self.sub_formatter:
|
||||
message = formatter._format(message)
|
||||
return message
|
||||
|
||||
@property
|
||||
def template(self) -> str:
|
||||
return self.default_template
|
||||
|
||||
@template.setter
|
||||
def template(self, template: str) -> None:
|
||||
if not isinstance(template, str):
|
||||
raise TypeError(f'The template must be str, not {type(template)}')
|
||||
self.default_template = template
|
||||
|
||||
|
||||
class LevelFormatter(BaseFormatter):
|
||||
name = 'LevelFormatter'
|
||||
|
||||
default_level: int = 20
|
||||
|
||||
# If True, the undefined level will be set to the higher nearest level.
|
||||
level_get_higher: bool = True
|
||||
|
||||
level_name_map = {
|
||||
LogLevel.notset: 'NOTSET',
|
||||
LogLevel.trace: ' TRACE',
|
||||
LogLevel.fine: ' FINE ',
|
||||
LogLevel.debug: ' DEBUG',
|
||||
LogLevel.info: ' INFO ',
|
||||
LogLevel.warn: ' WARN ',
|
||||
LogLevel.error: 'ERROR ',
|
||||
LogLevel.fatal: 'FATAL ',
|
||||
}
|
||||
name_level_map = {
|
||||
'NOTSET': LogLevel.notset,
|
||||
' TRACE': LogLevel.trace,
|
||||
' FINE ': LogLevel.fine,
|
||||
' DEBUG': LogLevel.debug,
|
||||
' INFO ': LogLevel.info,
|
||||
' WARN ': LogLevel.warn,
|
||||
'ERROR ': LogLevel.error,
|
||||
'FATAL ': LogLevel.fatal,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
return cls.add_info('level', 'log level', 'The log level')
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
if message[0].level in self.name_level_map:
|
||||
level_tag = self.level_name_map[message[0].level]
|
||||
else:
|
||||
if self.level_get_higher:
|
||||
for level in self.name_level_map:
|
||||
if message[0].level <= self.name_level_map[level]:
|
||||
level_tag = level
|
||||
break
|
||||
else:
|
||||
level_tag = 'FATAL'
|
||||
else:
|
||||
for level in self.name_level_map:
|
||||
if message[0].level >= self.name_level_map[level]:
|
||||
level_tag = level
|
||||
break
|
||||
else:
|
||||
level_tag = 'NOTSET'
|
||||
message[1]['level'] = level_tag
|
||||
return message
|
||||
|
||||
|
||||
class TraceFormatter(BaseFormatter):
|
||||
name = 'TraceFormatter'
|
||||
|
||||
time_format: str = '%Y-%m-%d %H:%M:%S'
|
||||
msec_time_format: str = '{}-{:03d}'
|
||||
use_absolute_path: bool = False
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
info = cls.add_info('log_time', 'formatted time when logging', 'The time format string'
|
||||
'. See https://docs.python.org/3/library/time'
|
||||
'.html#time.strftime for more information.')
|
||||
info += '\n'
|
||||
info += cls.add_info('log_source', 'logging file', 'the logging file name')
|
||||
info += '\n'
|
||||
info += cls.add_info('log_line', 'logging line', 'the logging line number')
|
||||
info += '\n'
|
||||
info += cls.add_info('log_function', 'logging function', 'the logging function name')
|
||||
return info
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
message = self._time_format(message)
|
||||
message = self._trace_format(message)
|
||||
return message
|
||||
|
||||
def _time_format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
time_mark = time.localtime(message[0].log_time / 1000000000)
|
||||
if self.msec_time_format:
|
||||
time_mark = self.msec_time_format.format(time.strftime(self.time_format, time_mark),
|
||||
message[0].create_msec_3)
|
||||
message[1]['log_time'] = time_mark
|
||||
return message
|
||||
|
||||
def _trace_format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
if message[0].stack_trace is None:
|
||||
return message
|
||||
path = Path(message[0].stack_trace.f_code.co_filename)
|
||||
if self.use_absolute_path:
|
||||
message[1]['log_source'] = path.absolute()
|
||||
message[1]['log_source'] = path
|
||||
message[1]['log_line'] = message[0].stack_trace.f_lineno
|
||||
message[1]['log_function'] = message[0].stack_trace.f_code.co_name
|
||||
return message
|
||||
|
||||
|
||||
class StdFormatter(BaseFormatter):
|
||||
name = 'StdFormatter'
|
||||
|
||||
enable_color: bool = True
|
||||
|
||||
sub_formatter: List[BaseFormatter] = [LevelFormatter(),
|
||||
TraceFormatter()]
|
||||
from lib_not_dr.logger.formatter.colors import (LevelColorFormatter,
|
||||
LoggerColorFormatter,
|
||||
TimeColorFormatter,
|
||||
TraceColorFormatter,
|
||||
MessageColorFormatter)
|
||||
color_formatters: List[BaseFormatter] = [LevelColorFormatter(),
|
||||
LoggerColorFormatter(),
|
||||
TimeColorFormatter(),
|
||||
TraceColorFormatter(),
|
||||
MessageColorFormatter()]
|
||||
|
||||
def __init__(self,
|
||||
enable_color: bool = True,
|
||||
sub_formatter: Optional[List[BaseFormatter]] = None,
|
||||
color_formatters: Optional[List[BaseFormatter]] = None,
|
||||
**kwargs) -> None:
|
||||
"""
|
||||
Initialize the StdFormatter
|
||||
:param enable_color: enable color
|
||||
:param sub_formatter: list of sub formatter
|
||||
:param color_formatters: list of color formatter
|
||||
:param kwargs: other options
|
||||
"""
|
||||
# 同 structures.LogMessage.__init__ 的注释 (逃)
|
||||
self.enable_color = enable_color
|
||||
if sub_formatter is not None:
|
||||
self.sub_formatter = sub_formatter
|
||||
if color_formatters is not None:
|
||||
self.color_formatters = color_formatters
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
super()._format(message)
|
||||
|
||||
if not self.enable_color:
|
||||
return message
|
||||
|
||||
for formatter in self.color_formatters:
|
||||
message = formatter._format(message)
|
||||
|
||||
return message
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
return 'None'
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import inspect
|
||||
|
||||
log_message = LogMessage(messages=['Hello World!'],
|
||||
level=7,
|
||||
stack_trace=inspect.currentframe(),
|
||||
logger_tag='tester',
|
||||
logger_name='test')
|
||||
|
||||
print(LevelFormatter.info())
|
||||
print(LevelFormatter().format_message(log_message))
|
||||
|
||||
print(TraceFormatter.info())
|
||||
print(TraceFormatter().format_message(log_message))
|
||||
|
||||
print(StdFormatter.info())
|
||||
print(StdFormatter().format_message(log_message))
|
||||
|
||||
std_format = StdFormatter()
|
||||
std_format.default_template = "${log_time}|${logger_name}|${logger_tag}|${log_source}:${log_line}|${log_function}|${level}|${messages}"
|
||||
|
||||
test_levels = (0, 5, 7, 10, 20, 30, 40, 50)
|
||||
|
||||
print("with color")
|
||||
|
||||
for test_level in test_levels:
|
||||
log_message.level = test_level
|
||||
print(std_format.format_message(log_message), end='')
|
||||
|
||||
print("without color")
|
||||
|
||||
std_format.enable_color = False
|
||||
|
||||
for test_level in test_levels:
|
||||
log_message.level = test_level
|
||||
print(std_format.format_message(log_message), end='')
|
||||
|
||||
print(std_format.as_markdown())
|
256
libs/lib_not_dr/logger/formatter/colors.py
Normal file
256
libs/lib_not_dr/logger/formatter/colors.py
Normal file
@ -0,0 +1,256 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
|
||||
from lib_not_dr.logger import LogLevel, COLOR_SUPPORT
|
||||
from lib_not_dr.logger.formatter import BaseFormatter
|
||||
from lib_not_dr.logger.structure import FormattingMessage
|
||||
|
||||
__all__ = [
|
||||
'BaseColorFormatter',
|
||||
|
||||
'LevelColorFormatter',
|
||||
'LoggerColorFormatter',
|
||||
'TimeColorFormatter',
|
||||
'TraceColorFormatter',
|
||||
'MessageColorFormatter',
|
||||
|
||||
'RESET_COLOR'
|
||||
]
|
||||
|
||||
RESET_COLOR = '\033[0m'
|
||||
|
||||
|
||||
class BaseColorFormatter(BaseFormatter):
|
||||
name = 'BaseColorFormatter'
|
||||
# TODO 迁移老 logger 颜色
|
||||
color = {
|
||||
# Notset: just black
|
||||
LogLevel.notset: '',
|
||||
# Trace: blue
|
||||
LogLevel.trace: '\033[38;2;138;173;244m',
|
||||
# Fine: green
|
||||
LogLevel.fine: '\033[0;32m',
|
||||
# Debug: cyan
|
||||
LogLevel.debug: '\033[0;36m',
|
||||
# Info: white
|
||||
LogLevel.info: '\033[0;37m',
|
||||
# Warn: yellow
|
||||
LogLevel.warn: '\033[0;33m',
|
||||
# Error: red
|
||||
LogLevel.error: '\033[0;31m',
|
||||
# Fatal: red background
|
||||
LogLevel.fatal: '\033[0;41m'
|
||||
}
|
||||
|
||||
def get_color(self, message: FormattingMessage) -> str:
|
||||
for level in self.color:
|
||||
if message[0].level <= level:
|
||||
break
|
||||
else:
|
||||
level = 90
|
||||
return self.color[level]
|
||||
|
||||
|
||||
class LevelColorFormatter(BaseColorFormatter):
|
||||
name = 'LevelColorFormatter'
|
||||
# TODO 迁移老 logger 颜色
|
||||
color = {
|
||||
# Notset: just black
|
||||
LogLevel.notset: '',
|
||||
# Trace: blue
|
||||
LogLevel.trace: '\033[38;2;138;173;244m',
|
||||
# Fine: green
|
||||
LogLevel.fine: '\033[35;48;2;44;44;54m',
|
||||
# Debug: cyan
|
||||
LogLevel.debug: '\033[38;2;133;138;149m',
|
||||
# Info: white
|
||||
LogLevel.info: '\033[0;37m',
|
||||
# Warn: yellow
|
||||
LogLevel.warn: '\033[0;33m',
|
||||
# Error: red
|
||||
LogLevel.error: '\033[0;31m',
|
||||
# Fatal: red background
|
||||
LogLevel.fatal: '\033[0;41m'
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
return cls.add_info('colored level', 'level', 'A colored level')
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
if isinstance(message[1].get('level'), int) or not COLOR_SUPPORT:
|
||||
return message
|
||||
# 获取颜色
|
||||
color = self.get_color(message)
|
||||
# 添加颜色
|
||||
if color == '' or color == RESET_COLOR:
|
||||
return message
|
||||
message[1]['level'] = f'{color}{message[1]["level"]}{RESET_COLOR}'
|
||||
return message
|
||||
|
||||
|
||||
class LoggerColorFormatter(BaseColorFormatter):
|
||||
name = 'LoggerColorFormatter'
|
||||
# TODO 迁移老 logger 颜色
|
||||
color = {
|
||||
# Notset: just black
|
||||
LogLevel.notset: '',
|
||||
# Trace: blue
|
||||
LogLevel.trace: '\033[38;2;138;173;244m',
|
||||
# Fine: green
|
||||
LogLevel.fine: '\033[0;32m',
|
||||
# Debug: cyan
|
||||
LogLevel.debug: '\033[0;36m',
|
||||
# Info: white
|
||||
LogLevel.info: '\033[0;37m',
|
||||
# Warn: yellow
|
||||
LogLevel.warn: '\033[0;33m',
|
||||
# Error: red
|
||||
LogLevel.error: '\033[0;31m',
|
||||
# Fatal: red background
|
||||
LogLevel.fatal: '\033[38;2;245;189;230m',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
return cls.add_info('colored logger name', 'logger name', 'A colored logger name')
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
if message[1].get('logger_name') is None or not COLOR_SUPPORT:
|
||||
return message
|
||||
# 获取颜色
|
||||
color = self.get_color(message)
|
||||
# 添加颜色
|
||||
if color == '' or color == RESET_COLOR:
|
||||
return message
|
||||
message[1]['logger_name'] = f'{color}{message[1]["logger_name"]}{RESET_COLOR}'
|
||||
if message[1].get('logger_tag') is not None and message[1].get('logger_tag') != ' ':
|
||||
message[1]['logger_tag'] = f'{color}{message[1]["logger_tag"]}{RESET_COLOR}'
|
||||
return message
|
||||
|
||||
|
||||
class TimeColorFormatter(BaseColorFormatter):
|
||||
name = 'TimeColorFormatter'
|
||||
# TODO 迁移老 logger 颜色
|
||||
color = {
|
||||
# Notset: just black
|
||||
LogLevel.notset: '',
|
||||
# Trace: blue
|
||||
LogLevel.trace: '\033[38;2;138;173;244m',
|
||||
# Fine: green
|
||||
LogLevel.fine: '\033[0;32m',
|
||||
# Debug: cyan
|
||||
LogLevel.debug: '\033[0;36m',
|
||||
# Info: white
|
||||
LogLevel.info: '\033[0;37m',
|
||||
# Warn: yellow
|
||||
LogLevel.warn: '\033[0;33m',
|
||||
# Error: red
|
||||
LogLevel.error: '\033[0;31m',
|
||||
# Fatal: red background
|
||||
LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
return cls.add_info('colored time', 'time', 'A colored time')
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
if message[1].get('log_time') is None or not COLOR_SUPPORT:
|
||||
return message
|
||||
# 获取颜色
|
||||
color = self.get_color(message)
|
||||
# 添加颜色
|
||||
if color == '' or color == RESET_COLOR:
|
||||
return message
|
||||
message[1]['log_time'] = f'{color}{message[1]["log_time"]}{RESET_COLOR}'
|
||||
return message
|
||||
|
||||
|
||||
class TraceColorFormatter(BaseColorFormatter):
|
||||
name = 'TraceColorFormatter'
|
||||
# TODO 迁移老 logger 颜色
|
||||
color = {
|
||||
# Notset: just black
|
||||
LogLevel.notset: '\033[38;2;0;255;180m',
|
||||
# Trace: blue
|
||||
LogLevel.trace: '\033[38;2;0;255;180m',
|
||||
# Fine: green
|
||||
LogLevel.fine: '\033[38;2;0;255;180m',
|
||||
# Debug: cyan
|
||||
LogLevel.debug: '\033[38;2;0;255;180m',
|
||||
# Info: white
|
||||
LogLevel.info: '\033[38;2;0;255;180m',
|
||||
# Warn: yellow
|
||||
LogLevel.warn: '\033[38;2;0;255;180m',
|
||||
# Error: red
|
||||
LogLevel.error: '\033[38;2;0;255;180m',
|
||||
# Fatal: red background
|
||||
LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
info = cls.add_info('colored logging file', 'log_source', 'A colored logging file name')
|
||||
info += '\n'
|
||||
info += cls.add_info('colored logging line', 'log_line', 'A colored logging line number')
|
||||
info += '\n'
|
||||
info += cls.add_info('colored logging function', 'log_function', 'A colored logging function name')
|
||||
return info
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
if message[0].stack_trace is None or not COLOR_SUPPORT:
|
||||
return message
|
||||
# 获取颜色
|
||||
color = self.get_color(message)
|
||||
# 添加颜色
|
||||
if color == '' or color == RESET_COLOR:
|
||||
return message
|
||||
message[1]['log_source'] = f'{color}{message[1]["log_source"]}{RESET_COLOR}'
|
||||
message[1]['log_line'] = f'{color}{message[1]["log_line"]}{RESET_COLOR}'
|
||||
message[1]['log_function'] = f'{color}{message[1]["log_function"]}{RESET_COLOR}'
|
||||
return message
|
||||
|
||||
|
||||
class MessageColorFormatter(BaseColorFormatter):
|
||||
name = 'MessageColorFormatter'
|
||||
|
||||
color = {
|
||||
# Notset: just black
|
||||
LogLevel.notset: '',
|
||||
# Trace: blue
|
||||
LogLevel.trace: '\033[38;2;138;173;244m',
|
||||
# Fine: blue
|
||||
LogLevel.fine: '\033[38;2;138;173;244m',
|
||||
# Debug: blue
|
||||
LogLevel.debug: '\033[38;2;138;173;244m',
|
||||
# Info: no color
|
||||
LogLevel.info: '',
|
||||
# Warn: yellow
|
||||
LogLevel.warn: '\033[0;33m',
|
||||
# Error: red
|
||||
LogLevel.error: '\033[0;31m',
|
||||
# Fatal: red background
|
||||
LogLevel.fatal: '\033[38;2;255;255;0;48;2;120;10;10m',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _info(cls) -> str:
|
||||
return cls.add_info('colored message', 'message', 'A colored message')
|
||||
|
||||
def _format(self, message: FormattingMessage) -> FormattingMessage:
|
||||
if message[1].get('messages') is None or not COLOR_SUPPORT:
|
||||
return message
|
||||
# 获取颜色
|
||||
color = self.get_color(message)
|
||||
# 添加颜色
|
||||
if color == '' or color == RESET_COLOR:
|
||||
return message
|
||||
if message[1]['messages'][-1] == '\n':
|
||||
message[1]['messages'] = f'{color}{message[1]["messages"][:-1]}{RESET_COLOR}\n'
|
||||
else:
|
||||
message[1]['messages'] = f'{color}{message[1]["messages"]}{RESET_COLOR}'
|
||||
return message
|
267
libs/lib_not_dr/logger/logger.py
Normal file
267
libs/lib_not_dr/logger/logger.py
Normal file
@ -0,0 +1,267 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
|
||||
import time
|
||||
import inspect
|
||||
from types import FrameType
|
||||
from typing import List, Optional
|
||||
|
||||
from lib_not_dr.logger import LogLevel
|
||||
from lib_not_dr.types.options import Options
|
||||
from lib_not_dr.logger.structure import LogMessage
|
||||
from lib_not_dr.logger.outstream import BaseOutputStream, StdioOutputStream
|
||||
|
||||
|
||||
class Logger(Options):
|
||||
name = 'Logger-v2'
|
||||
|
||||
outputs: List[BaseOutputStream] = [StdioOutputStream()]
|
||||
|
||||
log_name: str = 'root'
|
||||
|
||||
enable: bool = True
|
||||
level: int = 20 # info
|
||||
|
||||
def log_for(self, level: int) -> bool:
|
||||
"""
|
||||
Check if logging is enabled for a specific level.
|
||||
|
||||
Args:
|
||||
level (int): The logging level to check.
|
||||
|
||||
Returns:
|
||||
bool: True if logging is enabled for the given level, False otherwise.
|
||||
"""
|
||||
return self.enable and level >= self.level
|
||||
|
||||
def add_output(self, output: BaseOutputStream) -> None:
|
||||
"""
|
||||
Add an output to the list of outputs.
|
||||
|
||||
Args:
|
||||
output (BaseOutputStream): The output to be added.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.outputs.append(output)
|
||||
self.level = min(self.level, output.level)
|
||||
|
||||
def remove_output(self, output: BaseOutputStream) -> None:
|
||||
"""
|
||||
Removes the specified output from the list of outputs.
|
||||
|
||||
Args:
|
||||
output (BaseOutputStream): The output to be removed.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.outputs.remove(output)
|
||||
self.level = max(self.level, *[output.level for output in self.outputs])
|
||||
|
||||
@property
|
||||
def global_level(self) -> int:
|
||||
"""
|
||||
Get the global logging level.
|
||||
|
||||
Returns:
|
||||
int: The global logging level.
|
||||
"""
|
||||
return self.level
|
||||
|
||||
@global_level.setter
|
||||
def global_level(self, level: int) -> None:
|
||||
"""
|
||||
Set the global logging level.
|
||||
|
||||
Args:
|
||||
level (int): The global logging level.
|
||||
|
||||
Returns:
|
||||
None
|
||||
"""
|
||||
self.level = level
|
||||
for output in self.outputs:
|
||||
output.level = level
|
||||
|
||||
def make_log(self,
|
||||
messages: List[str],
|
||||
tag: Optional[str] = None,
|
||||
end: str = '\n',
|
||||
split: str = ' ',
|
||||
flush: bool = True,
|
||||
level: int = 20, # info
|
||||
# log_time: Optional[float] = None,
|
||||
# logger_name: str = 'root',
|
||||
# logger_tag: Optional[str] = None,
|
||||
stack_trace: Optional[FrameType] = None) -> None:
|
||||
# 检查是否需要记录
|
||||
if not self.log_for(level):
|
||||
return
|
||||
log_time = time.time_ns()
|
||||
# 处理堆栈信息
|
||||
if stack_trace is None:
|
||||
# 尝试获取堆栈信息
|
||||
if (stack := inspect.currentframe()) is not None:
|
||||
# 如果可能 尝试获取上两层的堆栈信息
|
||||
if (up_stack := stack.f_back) is not None:
|
||||
if (upper_stack := up_stack.f_back) is not None:
|
||||
stack_trace = upper_stack
|
||||
else:
|
||||
stack_trace = up_stack
|
||||
else:
|
||||
stack_trace = stack
|
||||
|
||||
message = LogMessage(messages=messages,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=level,
|
||||
log_time=log_time,
|
||||
logger_name=self.log_name,
|
||||
logger_tag=tag,
|
||||
stack_trace=stack_trace)
|
||||
if level >= 30: # WARN
|
||||
for output in self.outputs:
|
||||
output.write_stderr(message)
|
||||
else:
|
||||
for output in self.outputs:
|
||||
output.write_stdout(message)
|
||||
# done?
|
||||
# 20231106 00:06
|
||||
|
||||
@staticmethod
|
||||
def get_logger_by_name(name: str) -> 'Logger':
|
||||
"""
|
||||
Get a logger by name.
|
||||
|
||||
Args:
|
||||
name (str): The name of the logger.
|
||||
|
||||
Returns:
|
||||
Logger: The logger with the specified name.
|
||||
"""
|
||||
return Logger(log_name=name)
|
||||
|
||||
def info(self,
|
||||
*message,
|
||||
tag: Optional[str] = None,
|
||||
end: str = '\n',
|
||||
split: str = ' ',
|
||||
flush: bool = True,
|
||||
stack_trace: Optional[FrameType] = None) -> None:
|
||||
if not self.log_for(LogLevel.info):
|
||||
return
|
||||
self.make_log(messages=list(message),
|
||||
tag=tag,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=LogLevel.info,
|
||||
stack_trace=stack_trace)
|
||||
|
||||
def trace(self,
|
||||
*message,
|
||||
tag: Optional[str] = None,
|
||||
end: str = '\n',
|
||||
split: str = ' ',
|
||||
flush: bool = True,
|
||||
stack_trace: Optional[FrameType] = None) -> None:
|
||||
if not self.log_for(LogLevel.trace):
|
||||
return
|
||||
self.make_log(messages=list(message),
|
||||
tag=tag,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=LogLevel.trace,
|
||||
stack_trace=stack_trace)
|
||||
|
||||
def fine(self,
|
||||
*message,
|
||||
tag: Optional[str] = None,
|
||||
end: str = '\n',
|
||||
split: str = ' ',
|
||||
flush: bool = True,
|
||||
stack_trace: Optional[FrameType] = None) -> None:
|
||||
if not self.log_for(LogLevel.fine):
|
||||
return
|
||||
self.make_log(messages=list(message),
|
||||
tag=tag,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=LogLevel.fine,
|
||||
stack_trace=stack_trace)
|
||||
|
||||
def debug(self,
|
||||
*message,
|
||||
tag: Optional[str] = None,
|
||||
end: str = '\n',
|
||||
split: str = ' ',
|
||||
flush: bool = True,
|
||||
stack_trace: Optional[FrameType] = None) -> None:
|
||||
if not self.log_for(LogLevel.debug):
|
||||
return
|
||||
self.make_log(messages=list(message),
|
||||
tag=tag,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=LogLevel.debug,
|
||||
stack_trace=stack_trace)
|
||||
|
||||
def warn(self,
|
||||
*message,
|
||||
tag: Optional[str] = None,
|
||||
end: str = '\n',
|
||||
split: str = ' ',
|
||||
flush: bool = True,
|
||||
stack_trace: Optional[FrameType] = None) -> None:
|
||||
if not self.log_for(LogLevel.warn):
|
||||
return
|
||||
self.make_log(messages=list(message),
|
||||
tag=tag,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=LogLevel.warn,
|
||||
stack_trace=stack_trace)
|
||||
|
||||
def error(self,
|
||||
*message,
|
||||
tag: Optional[str] = None,
|
||||
end: str = '\n',
|
||||
split: str = ' ',
|
||||
flush: bool = True,
|
||||
stack_trace: Optional[FrameType] = None) -> None:
|
||||
if not self.log_for(LogLevel.error):
|
||||
return
|
||||
self.make_log(messages=list(message),
|
||||
tag=tag,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=LogLevel.error,
|
||||
stack_trace=stack_trace)
|
||||
|
||||
def fatal(self,
|
||||
*message,
|
||||
tag: Optional[str] = None,
|
||||
end: str = '\n',
|
||||
split: str = ' ',
|
||||
flush: bool = True,
|
||||
stack_trace: Optional[FrameType] = None) -> None:
|
||||
if not self.log_for(LogLevel.fatal):
|
||||
return
|
||||
self.make_log(messages=list(message),
|
||||
tag=tag,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=LogLevel.fatal,
|
||||
stack_trace=stack_trace)
|
236
libs/lib_not_dr/logger/outstream.py
Normal file
236
libs/lib_not_dr/logger/outstream.py
Normal file
@ -0,0 +1,236 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import string
|
||||
import atexit
|
||||
import threading
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from lib_not_dr.logger import LogLevel
|
||||
from lib_not_dr.types.options import Options
|
||||
from lib_not_dr.logger.structure import LogMessage
|
||||
from lib_not_dr.logger.formatter import BaseFormatter, StdFormatter
|
||||
|
||||
__all__ = [
|
||||
'BaseOutputStream',
|
||||
'StdioOutputStream',
|
||||
'FileCacheOutputStream'
|
||||
]
|
||||
|
||||
|
||||
class BaseOutputStream(Options):
|
||||
name = 'BaseOutputStream'
|
||||
|
||||
level: int = LogLevel.info
|
||||
enable: bool = True
|
||||
|
||||
formatter: BaseFormatter
|
||||
|
||||
def write_stdout(self, message: LogMessage) -> None:
|
||||
raise NotImplementedError(f'{self.__class__.__name__}.write_stdout is not implemented')
|
||||
|
||||
def write_stderr(self, message: LogMessage) -> None:
|
||||
raise NotImplementedError(f'{self.__class__.__name__}.write_stderr is not implemented')
|
||||
|
||||
def flush(self) -> None:
|
||||
raise NotImplementedError(f'{self.__class__.__name__}.flush is not implemented')
|
||||
|
||||
def close(self) -> None:
|
||||
self.enable = False
|
||||
|
||||
|
||||
class StdioOutputStream(BaseOutputStream):
|
||||
name = 'StdioOutputStream'
|
||||
|
||||
level: int = LogLevel.info
|
||||
formatter: BaseFormatter = StdFormatter()
|
||||
use_stderr: bool = True
|
||||
|
||||
def write_stdout(self, message: LogMessage) -> None:
|
||||
if not self.enable:
|
||||
return None
|
||||
if message.level < self.level:
|
||||
return None
|
||||
print(self.formatter.format_message(message), end='', flush=message.flush)
|
||||
return None
|
||||
|
||||
def write_stderr(self, message: LogMessage) -> None:
|
||||
if not self.enable:
|
||||
return None
|
||||
if message.level < self.level:
|
||||
return None
|
||||
if self.use_stderr:
|
||||
print(self.formatter.format_message(message), end='', flush=message.flush, file=sys.stderr)
|
||||
else:
|
||||
print(self.formatter.format_message(message), end='', flush=message.flush)
|
||||
return None
|
||||
|
||||
def flush(self) -> None:
|
||||
"""
|
||||
flush stdout and stderr
|
||||
:return: None
|
||||
"""
|
||||
print('', end='', flush=True)
|
||||
print('', end='', flush=True, file=sys.stderr)
|
||||
return None
|
||||
|
||||
|
||||
class FileCacheOutputStream(BaseOutputStream):
|
||||
name = 'FileCacheOutputStream'
|
||||
|
||||
level: int = LogLevel.info
|
||||
formatter: BaseFormatter = StdFormatter(enable_color=False)
|
||||
text_cache: io.StringIO = None
|
||||
|
||||
flush_counter: int = 0
|
||||
# 默认 10 次 flush 一次
|
||||
flush_count_limit: int = 10
|
||||
flush_time_limit: int = 10 # time limit in sec, 0 means no limit
|
||||
flush_timer: threading.Timer = None
|
||||
|
||||
file_path: Optional[Path] = Path('./logs')
|
||||
file_name: str
|
||||
# file mode: always 'a'
|
||||
file_encoding: str = 'utf-8'
|
||||
# do file swap or not
|
||||
file_swap: bool = False
|
||||
at_exit_register: bool = False
|
||||
|
||||
file_swap_counter: int = 0
|
||||
file_swap_name_template: str = '${name}-${counter}.log'
|
||||
# ${name} -> file_name
|
||||
# ${counter} -> file_swap_counter
|
||||
# ${log_time} -> time when file swap ( round(time.time()) )
|
||||
# ${start_time} -> start time of output stream ( round(time.time()) )
|
||||
current_file_name: str = None
|
||||
file_start_time: int = None
|
||||
|
||||
# log file swap triggers
|
||||
# 0 -> no limit
|
||||
file_size_limit: int = 0 # size limit in kb
|
||||
file_time_limit: int = 0 # time limit in sec 0
|
||||
file_swap_on_both: bool = False # swap file when both size and time limit reached
|
||||
|
||||
def init(self, **kwargs) -> bool:
|
||||
self.file_start_time = round(time.time())
|
||||
if self.text_cache is None:
|
||||
self.text_cache = io.StringIO()
|
||||
self.get_file_path()
|
||||
return False
|
||||
|
||||
def _write(self, message: LogMessage) -> None:
|
||||
"""
|
||||
write message to text cache
|
||||
默认已经检查过了
|
||||
:param message: message to write
|
||||
:return: None
|
||||
"""
|
||||
self.text_cache.write(self.formatter.format_message(message))
|
||||
self.flush_counter += 1
|
||||
if message.flush or self.flush_counter >= self.flush_count_limit:
|
||||
self.flush()
|
||||
else:
|
||||
if self.flush_time_limit > 0:
|
||||
if self.flush_timer is None or not self.flush_timer.is_alive():
|
||||
self.flush_timer = threading.Timer(self.flush_time_limit, self.flush)
|
||||
self.flush_timer.daemon = True
|
||||
self.flush_timer.start()
|
||||
if not self.at_exit_register:
|
||||
atexit.register(self.flush)
|
||||
self.at_exit_register = True
|
||||
return None
|
||||
|
||||
def write_stdout(self, message: LogMessage) -> None:
|
||||
if not self.enable:
|
||||
return None
|
||||
if message.level < self.level:
|
||||
return None
|
||||
self._write(message)
|
||||
return None
|
||||
|
||||
def write_stderr(self, message: LogMessage) -> None:
|
||||
if not self.enable:
|
||||
return None
|
||||
if message.level < self.level:
|
||||
return None
|
||||
self._write(message)
|
||||
return None
|
||||
|
||||
def get_file_path(self) -> Path:
|
||||
"""
|
||||
get file path
|
||||
:return:
|
||||
"""
|
||||
if (current_file := self.current_file_name) is None:
|
||||
if not self.file_swap:
|
||||
# 直接根据 file name 生成文件
|
||||
current_file = Path(self.file_path) / self.file_name
|
||||
self.current_file_name = str(current_file)
|
||||
return current_file
|
||||
template = string.Template(self.file_swap_name_template)
|
||||
file_name = template.safe_substitute(name=self.file_name,
|
||||
counter=self.file_swap_counter,
|
||||
log_time=round(time.time()),
|
||||
start_time=self.file_start_time)
|
||||
current_file = Path(self.file_path) / file_name
|
||||
self.current_file_name = str(current_file)
|
||||
else:
|
||||
current_file = Path(current_file)
|
||||
return current_file
|
||||
|
||||
def check_flush(self) -> Path:
|
||||
current_file = self.get_file_path()
|
||||
# 获取当前文件的路径
|
||||
if not self.file_swap:
|
||||
# 不需要 swap 直接返回
|
||||
return current_file
|
||||
# 检查是否需要 swap
|
||||
size_pass = True
|
||||
if self.file_size_limit > 0:
|
||||
file_size = current_file.stat().st_size / 1024 # kb
|
||||
if file_size > self.file_size_limit: # kb
|
||||
size_pass = False
|
||||
time_pass = True
|
||||
if self.file_time_limit > 0:
|
||||
file_time = round(time.time()) - current_file.stat().st_mtime
|
||||
if file_time > self.file_time_limit:
|
||||
time_pass = False
|
||||
if (self.file_swap_on_both and size_pass and time_pass) or \
|
||||
(not self.file_swap_on_both and (size_pass or time_pass)):
|
||||
# 两个都满足
|
||||
# 或者只有一个满足
|
||||
if size_pass and time_pass:
|
||||
self.file_swap_counter += 1
|
||||
# 生成新的文件名
|
||||
return self.get_file_path()
|
||||
|
||||
def flush(self) -> None:
|
||||
new_cache = io.StringIO() # 创建新的缓存
|
||||
self.flush_counter = 0 # atomic, no lock
|
||||
old_cache, self.text_cache = self.text_cache, new_cache
|
||||
text = old_cache.getvalue()
|
||||
old_cache.close() # 关闭旧的缓存
|
||||
if text == '':
|
||||
return None
|
||||
current_file = self.check_flush()
|
||||
if not current_file.exists():
|
||||
current_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
current_file.touch(exist_ok=True)
|
||||
with current_file.open('a', encoding=self.file_encoding) as f:
|
||||
f.write(text)
|
||||
return None
|
||||
|
||||
def close(self) -> None:
|
||||
super().close()
|
||||
self.flush()
|
||||
self.text_cache.close()
|
||||
atexit.unregister(self.flush)
|
||||
return None
|
99
libs/lib_not_dr/logger/structure.py
Normal file
99
libs/lib_not_dr/logger/structure.py
Normal file
@ -0,0 +1,99 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
from types import FrameType
|
||||
from typing import List, Optional, Tuple, Dict, Union
|
||||
|
||||
from lib_not_dr.types.options import Options
|
||||
|
||||
__all__ = ['LogMessage',
|
||||
'FormattingMessage']
|
||||
|
||||
|
||||
class LogMessage(Options):
|
||||
name = 'LogMessage'
|
||||
|
||||
# 消息内容本身的属性
|
||||
messages: List[str] = []
|
||||
end: str = '\n'
|
||||
split: str = ' '
|
||||
|
||||
# 消息的属性
|
||||
flush: bool = None
|
||||
level: int = 20
|
||||
log_time: float = None # time.time_ns()
|
||||
logger_name: str = 'root'
|
||||
logger_tag: Optional[str] = None
|
||||
stack_trace: Optional[FrameType] = None
|
||||
|
||||
def __init__(self,
|
||||
messages: Optional[List[str]] = None,
|
||||
end: Optional[str] = '\n',
|
||||
split: Optional[str] = ' ',
|
||||
flush: Optional[bool] = None,
|
||||
level: Optional[int] = 20,
|
||||
log_time: Optional[float] = None,
|
||||
logger_name: Optional[str] = 'root',
|
||||
logger_tag: Optional[str] = None,
|
||||
stack_trace: Optional[FrameType] = None,
|
||||
**kwargs) -> None:
|
||||
"""
|
||||
Init for LogMessage
|
||||
:param messages: message list for log
|
||||
:param end: end of message
|
||||
:param split: split for messages
|
||||
:param flush: do flush or not
|
||||
:param level: level of message
|
||||
:param log_time: time of message (default: time.time_ns())
|
||||
:param logger_name: name of logger
|
||||
:param logger_tag: tag of logger
|
||||
:param stack_trace: stack trace of logger
|
||||
:param kwargs: other options
|
||||
"""
|
||||
# 为了方便使用 单独覆盖了 __init__ 方法来提供代码补全的选项
|
||||
super().__init__(messages=messages,
|
||||
end=end,
|
||||
split=split,
|
||||
flush=flush,
|
||||
level=level,
|
||||
log_time=log_time,
|
||||
logger_name=logger_name,
|
||||
logger_tag=logger_tag,
|
||||
stack_trace=stack_trace,
|
||||
**kwargs)
|
||||
|
||||
def init(self, **kwargs) -> bool:
|
||||
if self.log_time is None:
|
||||
self.log_time = time.time_ns()
|
||||
if not isinstance(self.flush, bool) and self.flush is not None:
|
||||
self.flush = True if self.flush else False
|
||||
return False
|
||||
|
||||
def format_message(self) -> str:
|
||||
return self.split.join(self.messages) + self.end
|
||||
|
||||
def format_for_message(self) -> Dict[str, str]:
|
||||
basic_info = self.option()
|
||||
|
||||
if self.logger_tag is None:
|
||||
basic_info['logger_tag'] = ' '
|
||||
|
||||
basic_info['messages'] = self.format_message()
|
||||
|
||||
return basic_info
|
||||
|
||||
@property
|
||||
def create_msec_3(self) -> int:
|
||||
return int(self.log_time / 1000000) % 1000
|
||||
|
||||
|
||||
FormattingMessage = Tuple[LogMessage, Dict[str, Union[str, Path]]]
|
||||
|
||||
if __name__ == '__main__':
|
||||
print(LogMessage().as_markdown())
|
@ -64,6 +64,7 @@ class Options:
|
||||
"""
|
||||
name = 'Option Base'
|
||||
cached_options: Dict[str, Union[str, Any]] = {}
|
||||
_check_options: bool = True
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
@ -75,7 +76,7 @@ class Options:
|
||||
self._options: Dict[str, Union[Callable, object]] = {}
|
||||
self.flush_option()
|
||||
for option, value in kwargs.items():
|
||||
if option not in self.cached_options:
|
||||
if option not in self.cached_options and self._check_options:
|
||||
raise OptionNameNotDefined(f"option: {option} with value: {value} is not defined")
|
||||
setattr(self, option, value)
|
||||
run_load_file = True
|
||||
|
@ -1,12 +1,16 @@
|
||||
"""Holds type aliases used throughout the codebase."""
|
||||
import ctypes
|
||||
import sys
|
||||
|
||||
from typing import Union
|
||||
|
||||
|
||||
__all__ = [
|
||||
"Buffer"
|
||||
"Buffer",
|
||||
]
|
||||
|
||||
# Backwards compatible placeholder for `collections.abc.Buffer` from Python 3.12
|
||||
Buffer = Union[bytes, bytearray, memoryview, ctypes.Array]
|
||||
|
||||
if sys.version_info >= (3, 12):
|
||||
from collections.abc import Buffer
|
||||
else:
|
||||
# Best-effort placeholder for older Python versions
|
||||
Buffer = Union[bytes, bytearray, memoryview, ctypes.Array]
|
||||
|
6
libs/pyglet/experimental/README.md
Normal file
6
libs/pyglet/experimental/README.md
Normal file
@ -0,0 +1,6 @@
|
||||
Experimental modules
|
||||
====================
|
||||
|
||||
This package contains experimental modules, which are included here for
|
||||
wider testing and feedback. Anything contined within may be broken, refactored,
|
||||
or removed without notice.
|
@ -1,6 +0,0 @@
|
||||
"""Experimental modules.
|
||||
|
||||
This package contains experimental modules, included here for wider testing
|
||||
and experimentation. Anything contined within may be broken, refactored, or
|
||||
removed without notice.
|
||||
"""
|
@ -148,27 +148,15 @@ fragment_array_source = """#version 150 core
|
||||
|
||||
|
||||
def get_default_shader():
|
||||
try:
|
||||
return pyglet.gl.current_context.pyglet_sprite_default_shader
|
||||
except AttributeError:
|
||||
vert_shader = graphics.shader.Shader(vertex_source, 'vertex')
|
||||
geom_shader = graphics.shader.Shader(geometry_source, 'geometry')
|
||||
frag_shader = graphics.shader.Shader(fragment_source, 'fragment')
|
||||
default_shader_program = graphics.shader.ShaderProgram(vert_shader, geom_shader, frag_shader)
|
||||
pyglet.gl.current_context.pyglet_sprite_default_shader = default_shader_program
|
||||
return pyglet.gl.current_context.pyglet_sprite_default_shader
|
||||
return pyglet.gl.current_context.create_program((vertex_source, 'vertex'),
|
||||
(geometry_source, 'geometry'),
|
||||
(fragment_source, 'fragment'))
|
||||
|
||||
|
||||
def get_default_array_shader():
|
||||
try:
|
||||
return pyglet.gl.current_context.pyglet_sprite_default_array_shader
|
||||
except AttributeError:
|
||||
vert_shader = graphics.shader.Shader(vertex_source, 'vertex')
|
||||
geom_shader = graphics.shader.Shader(geometry_source, 'geometry')
|
||||
frag_shader = graphics.shader.Shader(fragment_array_source, 'fragment')
|
||||
default_shader_program = graphics.shader.ShaderProgram(vert_shader, geom_shader, frag_shader)
|
||||
pyglet.gl.current_context.pyglet_sprite_default_array_shader = default_shader_program
|
||||
return pyglet.gl.current_context.pyglet_sprite_default_array_shader
|
||||
return pyglet.gl.current_context.create_program((vertex_source, 'vertex'),
|
||||
(geometry_source, 'geometry'),
|
||||
(fragment_array_source, 'fragment'))
|
||||
|
||||
|
||||
class SpriteGroup(graphics.Group):
|
||||
|
247
libs/pyglet/experimental/net.py
Normal file
247
libs/pyglet/experimental/net.py
Normal file
@ -0,0 +1,247 @@
|
||||
"""Experimental networking
|
||||
|
||||
This module contains experiments in making user-friendly Server and Client
|
||||
classes that integrate with pyglet's event system. These are very basic,
|
||||
socket server/client examples, and are not ready to be used in production.
|
||||
They are included here to solicit feedback, and possibly spark further
|
||||
development. Basic Server usage::
|
||||
|
||||
server = net.Server(address='0.0.0.0', port=1234)
|
||||
active_connections = weakref.WeakSet()
|
||||
|
||||
def pong(connection, message):
|
||||
print(f"Received '{message}' from '{connection}'")
|
||||
connection.send(b'pong')
|
||||
|
||||
@server.event
|
||||
def on_connection(connection):
|
||||
print(f"New client connected: {connection}")
|
||||
connection.set_handler('on_receive', pong)
|
||||
active_connections.add(connection)
|
||||
|
||||
@server.event
|
||||
def on_disconnect(connection):
|
||||
print(f"Client disconnected: {connection}")
|
||||
active_connections.discard(connection)
|
||||
|
||||
|
||||
Basic Client example::
|
||||
|
||||
client = net.Client(address='localhost', port=1234)
|
||||
|
||||
@client.event
|
||||
def on_receive(client, message):
|
||||
print(f"Received: {message}")
|
||||
|
||||
@client.event
|
||||
def on_disconnect(client):
|
||||
print(f"Disconnected: {client}")
|
||||
|
||||
client.send(b'ping')
|
||||
|
||||
"""
|
||||
|
||||
|
||||
import queue as _queue
|
||||
import struct as _struct
|
||||
import socket as _socket
|
||||
import asyncio as _asyncio
|
||||
import threading as _threading
|
||||
|
||||
from pyglet.event import EventDispatcher as _EventDispatcher
|
||||
|
||||
from pyglet.util import debug_print
|
||||
|
||||
_debug_net = debug_print('debug_net')
|
||||
|
||||
|
||||
class Client(_EventDispatcher):
|
||||
def __init__(self, address, port):
|
||||
"""Create a Client connection to a Server."""
|
||||
self._socket = _socket.create_connection((address, port))
|
||||
self._address = address
|
||||
self._port = port
|
||||
|
||||
self._terminate = _threading.Event()
|
||||
self._queue = _queue.Queue()
|
||||
|
||||
_threading.Thread(target=self._recv, daemon=True).start()
|
||||
_threading.Thread(target=self._send, daemon=True).start()
|
||||
|
||||
self._sentinal = object() # poison pill
|
||||
|
||||
def close(self):
|
||||
"""Close the connection."""
|
||||
self._queue.put(self._sentinal)
|
||||
self._socket.shutdown(1)
|
||||
if not self._terminate.is_set():
|
||||
self._terminate.set()
|
||||
self.dispatch_event('on_disconnect', self)
|
||||
|
||||
def send(self, message):
|
||||
"""Queue a message to send.
|
||||
|
||||
Put a string of bytes into the queue to send.
|
||||
raises a `ConnectionError` if the connection
|
||||
has been closed or dropped.
|
||||
|
||||
:Parameters:
|
||||
`message` : bytes
|
||||
A string of bytes to send.
|
||||
"""
|
||||
if self._terminate.is_set():
|
||||
raise ConnectionError("Connection is closed.")
|
||||
self._queue.put(message)
|
||||
|
||||
def _send(self): # Thread
|
||||
"""Background Thread to send messages from the queue."""
|
||||
while not self._terminate.is_set():
|
||||
message = self._queue.get()
|
||||
if message == self._sentinal: # bail out on poison pill
|
||||
break
|
||||
try:
|
||||
# Attach a 4byte header to the front of the message:
|
||||
packet = _struct.pack('I', len(message)) + message
|
||||
self._socket.sendall(packet)
|
||||
except (ConnectionError, OSError):
|
||||
self.close()
|
||||
break
|
||||
|
||||
assert _debug_net("Exiting _send thread")
|
||||
|
||||
|
||||
def _recv(self): # Thread
|
||||
socket = self._socket
|
||||
|
||||
while not self._terminate.is_set():
|
||||
try:
|
||||
header = socket.recv(4)
|
||||
while len(header) < 4:
|
||||
header += socket.recv(4 - len(header))
|
||||
size = _struct.unpack('I', header)[0]
|
||||
|
||||
message = socket.recv(size)
|
||||
while len(message) < size:
|
||||
message += socket.recv(size)
|
||||
self.dispatch_event('on_receive', self, message)
|
||||
except (ConnectionError, OSError):
|
||||
self.close()
|
||||
break
|
||||
|
||||
assert _debug_net("Exiting _recv thread")
|
||||
|
||||
def on_receive(self, connection, message):
|
||||
"""Event for received messages."""
|
||||
|
||||
def on_disconnect(self, connection):
|
||||
"""Event for disconnection. """
|
||||
|
||||
def __repr__(self):
|
||||
return f"Client(address={self._address}, port={self._port})"
|
||||
|
||||
|
||||
Client.register_event_type('on_receive')
|
||||
Client.register_event_type('on_disconnect')
|
||||
|
||||
|
||||
class ClientConnection(_EventDispatcher):
|
||||
|
||||
def __init__(self, reader, writer):
|
||||
self._reader = reader
|
||||
self._writer = writer
|
||||
self._closed = False
|
||||
self._loop = _asyncio.get_event_loop()
|
||||
_asyncio.run_coroutine_threadsafe(self._recv(), self._loop)
|
||||
|
||||
def close(self):
|
||||
if not self._closed:
|
||||
self._writer.transport.close()
|
||||
self._closed = True
|
||||
self.dispatch_event('on_disconnect', self)
|
||||
|
||||
async def _recv(self):
|
||||
while not self._closed:
|
||||
try:
|
||||
header = await self._reader.readexactly(4)
|
||||
size = _struct.unpack('I', header)[0]
|
||||
message = await self._reader.readexactly(size)
|
||||
self._loop.call_soon(self.dispatch_event, 'on_receive', self, message)
|
||||
|
||||
except _asyncio.IncompleteReadError:
|
||||
self.close()
|
||||
break
|
||||
|
||||
async def _send(self, message):
|
||||
try:
|
||||
packet = _struct.pack('I', len(message)) + message
|
||||
self._writer.write(packet)
|
||||
await self._writer.drain()
|
||||
except ConnectionResetError:
|
||||
self.close()
|
||||
|
||||
def send(self, message):
|
||||
# Synchrounously send a message in an async coroutine.
|
||||
if self._writer.transport is None or self._writer.transport.is_closing():
|
||||
self.close()
|
||||
return
|
||||
_future = _asyncio.run_coroutine_threadsafe(self._send(message), self._loop)
|
||||
|
||||
def on_receive(self, connection, message):
|
||||
"""Event for received messages."""
|
||||
|
||||
def on_disconnect(self, connection):
|
||||
"""Event for disconnection. """
|
||||
|
||||
def __del__(self):
|
||||
assert _debug_net(f"Connection garbage collected: {self}")
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({id(self)})"
|
||||
|
||||
|
||||
ClientConnection.register_event_type('on_receive')
|
||||
ClientConnection.register_event_type('on_disconnect')
|
||||
|
||||
|
||||
class Server(_EventDispatcher):
|
||||
|
||||
def __init__(self, address, port):
|
||||
self._address = address
|
||||
self._port = port
|
||||
|
||||
self._server = None
|
||||
|
||||
self._thread = _threading.Thread(target=self._run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
blurb = f"Server listening on {address}:{port}"
|
||||
assert _debug_net(f"{'-' * len(blurb)}\n{blurb}\n{'-' * len(blurb)}")
|
||||
|
||||
|
||||
async def handle_connection(self, reader, writer):
|
||||
connection = ClientConnection(reader, writer)
|
||||
self.dispatch_event('on_connection', connection)
|
||||
|
||||
async def _start_server(self):
|
||||
self._server = await _asyncio.start_server(self.handle_connection, self._address, self._port)
|
||||
async with self._server:
|
||||
await self._server.serve_forever()
|
||||
|
||||
def _run(self):
|
||||
try:
|
||||
_asyncio.run(self._start_server())
|
||||
except KeyboardInterrupt:
|
||||
self._server.close()
|
||||
|
||||
def on_connection(self, connection):
|
||||
"""Event for new Client connections."""
|
||||
assert _debug_net(f"Connected <--- {connection}")
|
||||
connection.set_handler('on_disconnect', self.on_disconnect)
|
||||
|
||||
def on_disconnect(self, connection):
|
||||
"""Event for disconnected Clients."""
|
||||
assert _debug_net(f"Disconnected ---> {connection}")
|
||||
|
||||
|
||||
Server.register_event_type('on_connection')
|
||||
Server.register_event_type('on_disconnect')
|
@ -266,30 +266,29 @@ class DWRITE_CLUSTER_METRICS(ctypes.Structure):
|
||||
class IDWriteFontFileStream(com.IUnknown):
|
||||
_methods_ = [
|
||||
('ReadFileFragment',
|
||||
com.STDMETHOD(c_void_p, POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))),
|
||||
com.STDMETHOD(POINTER(c_void_p), UINT64, UINT64, POINTER(c_void_p))),
|
||||
('ReleaseFileFragment',
|
||||
com.STDMETHOD(c_void_p, c_void_p)),
|
||||
com.STDMETHOD(c_void_p)),
|
||||
('GetFileSize',
|
||||
com.STDMETHOD(c_void_p, POINTER(UINT64))),
|
||||
com.STDMETHOD(POINTER(UINT64))),
|
||||
('GetLastWriteTime',
|
||||
com.STDMETHOD(c_void_p, POINTER(UINT64))),
|
||||
com.STDMETHOD(POINTER(UINT64))),
|
||||
]
|
||||
|
||||
|
||||
class IDWriteFontFileLoader_LI(com.IUnknown): # Local implementation use only.
|
||||
_methods_ = [
|
||||
('CreateStreamFromKey',
|
||||
com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
|
||||
com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
|
||||
]
|
||||
|
||||
|
||||
class IDWriteFontFileLoader(com.pIUnknown):
|
||||
_methods_ = [
|
||||
('CreateStreamFromKey',
|
||||
com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
|
||||
com.STDMETHOD(c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileStream))))
|
||||
]
|
||||
|
||||
|
||||
class IDWriteLocalFontFileLoader(IDWriteFontFileLoader, com.pIUnknown):
|
||||
_methods_ = [
|
||||
('GetFilePathLengthFromKey',
|
||||
@ -452,13 +451,13 @@ DWRITE_READING_DIRECTION_LEFT_TO_RIGHT = 0
|
||||
class IDWriteTextAnalysisSource(com.IUnknown):
|
||||
_methods_ = [
|
||||
('GetTextAtPosition',
|
||||
com.METHOD(HRESULT, c_void_p, UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
|
||||
com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
|
||||
('GetTextBeforePosition',
|
||||
com.STDMETHOD(UINT32, c_wchar_p, POINTER(UINT32))),
|
||||
com.STDMETHOD(UINT32, POINTER(c_wchar_p), POINTER(UINT32))),
|
||||
('GetParagraphReadingDirection',
|
||||
com.METHOD(DWRITE_READING_DIRECTION)),
|
||||
('GetLocaleName',
|
||||
com.STDMETHOD(c_void_p, UINT32, POINTER(UINT32), POINTER(c_wchar_p))),
|
||||
com.STDMETHOD(UINT32, POINTER(UINT32), POINTER(c_wchar_p))),
|
||||
('GetNumberSubstitution',
|
||||
com.STDMETHOD(UINT32, POINTER(UINT32), c_void_p)),
|
||||
]
|
||||
@ -467,7 +466,7 @@ class IDWriteTextAnalysisSource(com.IUnknown):
|
||||
class IDWriteTextAnalysisSink(com.IUnknown):
|
||||
_methods_ = [
|
||||
('SetScriptAnalysis',
|
||||
com.STDMETHOD(c_void_p, UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))),
|
||||
com.STDMETHOD(UINT32, UINT32, POINTER(DWRITE_SCRIPT_ANALYSIS))),
|
||||
('SetLineBreakpoints',
|
||||
com.STDMETHOD(UINT32, UINT32, c_void_p)),
|
||||
('SetBidiLevel',
|
||||
@ -524,7 +523,7 @@ class TextAnalysis(com.COMObject):
|
||||
|
||||
analyzer.AnalyzeScript(self, 0, text_length, self)
|
||||
|
||||
def SetScriptAnalysis(self, this, textPosition, textLength, scriptAnalysis):
|
||||
def SetScriptAnalysis(self, textPosition, textLength, scriptAnalysis):
|
||||
# textPosition - The index of the first character in the string that the result applies to
|
||||
# textLength - How many characters of the string from the index that the result applies to
|
||||
# scriptAnalysis - The analysis information for all glyphs starting at position for length.
|
||||
@ -542,10 +541,10 @@ class TextAnalysis(com.COMObject):
|
||||
return 0
|
||||
# return 0x80004001
|
||||
|
||||
def GetTextBeforePosition(self, this, textPosition, textString, textLength):
|
||||
def GetTextBeforePosition(self, textPosition, textString, textLength):
|
||||
raise Exception("Currently not implemented.")
|
||||
|
||||
def GetTextAtPosition(self, this, textPosition, textString, textLength):
|
||||
def GetTextAtPosition(self, textPosition, textString, textLength):
|
||||
# This method will retrieve a substring of the text in this layout
|
||||
# to be used in an analysis step.
|
||||
# Arguments:
|
||||
@ -568,7 +567,7 @@ class TextAnalysis(com.COMObject):
|
||||
def GetParagraphReadingDirection(self):
|
||||
return 0
|
||||
|
||||
def GetLocaleName(self, this, textPosition, textLength, localeName):
|
||||
def GetLocaleName(self, textPosition, textLength, localeName):
|
||||
self.__local_name = c_wchar_p("") # TODO: Add more locales.
|
||||
localeName[0] = self.__local_name
|
||||
textLength[0] = self._textlength - textPosition
|
||||
@ -954,16 +953,16 @@ class IDWriteTextLayout1(IDWriteTextLayout, IDWriteTextFormat, com.pIUnknown):
|
||||
class IDWriteFontFileEnumerator(com.IUnknown):
|
||||
_methods_ = [
|
||||
('MoveNext',
|
||||
com.STDMETHOD(c_void_p, POINTER(BOOL))),
|
||||
com.STDMETHOD(POINTER(BOOL))),
|
||||
('GetCurrentFontFile',
|
||||
com.STDMETHOD(c_void_p, c_void_p)),
|
||||
com.STDMETHOD(c_void_p)),
|
||||
]
|
||||
|
||||
|
||||
class IDWriteFontCollectionLoader(com.IUnknown):
|
||||
_methods_ = [
|
||||
('CreateEnumeratorFromKey',
|
||||
com.STDMETHOD(c_void_p, c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))),
|
||||
com.STDMETHOD(c_void_p, c_void_p, UINT32, POINTER(POINTER(IDWriteFontFileEnumerator)))),
|
||||
]
|
||||
|
||||
|
||||
@ -971,20 +970,12 @@ class MyFontFileStream(com.COMObject):
|
||||
_interfaces_ = [IDWriteFontFileStream]
|
||||
|
||||
def __init__(self, data):
|
||||
super().__init__()
|
||||
self._data = data
|
||||
self._size = len(data)
|
||||
self._ptrs = []
|
||||
|
||||
def AddRef(self, this):
|
||||
return 1
|
||||
|
||||
def Release(self, this):
|
||||
return 1
|
||||
|
||||
def QueryInterface(self, this, refiid, tester):
|
||||
return 0
|
||||
|
||||
def ReadFileFragment(self, this, fragmentStart, fileOffset, fragmentSize, fragmentContext):
|
||||
def ReadFileFragment(self, fragmentStart, fileOffset, fragmentSize, fragmentContext):
|
||||
if fileOffset + fragmentSize > self._size:
|
||||
return 0x80004005 # E_FAIL
|
||||
|
||||
@ -997,14 +988,14 @@ class MyFontFileStream(com.COMObject):
|
||||
fragmentContext[0] = None
|
||||
return 0
|
||||
|
||||
def ReleaseFileFragment(self, this, fragmentContext):
|
||||
def ReleaseFileFragment(self, fragmentContext):
|
||||
return 0
|
||||
|
||||
def GetFileSize(self, this, fileSize):
|
||||
def GetFileSize(self, fileSize):
|
||||
fileSize[0] = self._size
|
||||
return 0
|
||||
|
||||
def GetLastWriteTime(self, this, lastWriteTime):
|
||||
def GetLastWriteTime(self, lastWriteTime):
|
||||
return 0x80004001 # E_NOTIMPL
|
||||
|
||||
|
||||
@ -1012,21 +1003,13 @@ class LegacyFontFileLoader(com.COMObject):
|
||||
_interfaces_ = [IDWriteFontFileLoader_LI]
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._streams = {}
|
||||
|
||||
def QueryInterface(self, this, refiid, tester):
|
||||
return 0
|
||||
|
||||
def AddRef(self, this):
|
||||
return 1
|
||||
|
||||
def Release(self, this):
|
||||
return 1
|
||||
|
||||
def CreateStreamFromKey(self, this, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
|
||||
def CreateStreamFromKey(self, fontfileReferenceKey, fontFileReferenceKeySize, fontFileStream):
|
||||
convert_index = cast(fontfileReferenceKey, POINTER(c_uint32))
|
||||
|
||||
self._ptr = ctypes.cast(self._streams[convert_index.contents.value]._pointers[IDWriteFontFileStream],
|
||||
self._ptr = ctypes.cast(self._streams[convert_index.contents.value].as_interface(IDWriteFontFileStream),
|
||||
POINTER(IDWriteFontFileStream))
|
||||
fontFileStream[0] = self._ptr
|
||||
return 0
|
||||
@ -1039,6 +1022,7 @@ class MyEnumerator(com.COMObject):
|
||||
_interfaces_ = [IDWriteFontFileEnumerator]
|
||||
|
||||
def __init__(self, factory, loader):
|
||||
super().__init__()
|
||||
self.factory = cast(factory, IDWriteFactory)
|
||||
self.key = "pyglet_dwrite"
|
||||
self.size = len(self.key)
|
||||
@ -1057,7 +1041,7 @@ class MyEnumerator(com.COMObject):
|
||||
def AddFontData(self, fonts):
|
||||
self._font_data = fonts
|
||||
|
||||
def MoveNext(self, this, hasCurrentFile):
|
||||
def MoveNext(self, hasCurrentFile):
|
||||
|
||||
self.current_index += 1
|
||||
if self.current_index != len(self._font_data):
|
||||
@ -1087,7 +1071,7 @@ class MyEnumerator(com.COMObject):
|
||||
|
||||
pass
|
||||
|
||||
def GetCurrentFontFile(self, this, fontFile):
|
||||
def GetCurrentFontFile(self, fontFile):
|
||||
fontFile = cast(fontFile, POINTER(IDWriteFontFile))
|
||||
fontFile[0] = self._font_files[self.current_index]
|
||||
return 0
|
||||
@ -1097,24 +1081,14 @@ class LegacyCollectionLoader(com.COMObject):
|
||||
_interfaces_ = [IDWriteFontCollectionLoader]
|
||||
|
||||
def __init__(self, factory, loader):
|
||||
super().__init__()
|
||||
self._enumerator = MyEnumerator(factory, loader)
|
||||
|
||||
def AddFontData(self, fonts):
|
||||
self._enumerator.AddFontData(fonts)
|
||||
|
||||
def AddRef(self, this):
|
||||
self._i = 1
|
||||
return 1
|
||||
|
||||
def Release(self, this):
|
||||
self._i = 0
|
||||
return 1
|
||||
|
||||
def QueryInterface(self, this, refiid, tester):
|
||||
return 0
|
||||
|
||||
def CreateEnumeratorFromKey(self, this, factory, key, key_size, enumerator):
|
||||
self._ptr = ctypes.cast(self._enumerator._pointers[IDWriteFontFileEnumerator],
|
||||
def CreateEnumeratorFromKey(self, factory, key, key_size, enumerator):
|
||||
self._ptr = ctypes.cast(self._enumerator.as_interface(IDWriteFontFileEnumerator),
|
||||
POINTER(IDWriteFontFileEnumerator))
|
||||
|
||||
enumerator[0] = self._ptr
|
||||
@ -2418,7 +2392,7 @@ class Win32DirectWriteFont(base.Font):
|
||||
|
||||
# Note: RegisterFontLoader takes a pointer. However, for legacy we implement our own callback interface.
|
||||
# Therefore we need to pass to the actual pointer directly.
|
||||
cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI])
|
||||
cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI))
|
||||
|
||||
cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
|
||||
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
|
||||
@ -2472,7 +2446,7 @@ class Win32DirectWriteFont(base.Font):
|
||||
cls._font_collection_loader = LegacyCollectionLoader(cls._write_factory, cls._font_loader)
|
||||
|
||||
cls._write_factory.RegisterFontCollectionLoader(cls._font_collection_loader)
|
||||
cls._write_factory.RegisterFontFileLoader(cls._font_loader.pointers[IDWriteFontFileLoader_LI])
|
||||
cls._write_factory.RegisterFontFileLoader(cls._font_loader.as_interface(IDWriteFontFileLoader_LI))
|
||||
|
||||
cls._font_collection_loader.AddFontData(cls._font_cache)
|
||||
|
||||
|
@ -336,7 +336,7 @@ class GDIPlusGlyphRenderer(Win32GlyphRenderer):
|
||||
pass
|
||||
|
||||
def _create_bitmap(self, width, height):
|
||||
self._data = (ctypes.c_byte * (4 * width * height))()
|
||||
self._data = (BYTE * (4 * width * height))()
|
||||
self._bitmap = ctypes.c_void_p()
|
||||
self._format = PixelFormat32bppARGB
|
||||
gdiplus.GdipCreateBitmapFromScan0(width, height, width * 4,
|
||||
@ -532,6 +532,12 @@ class GDIPlusFont(Win32Font):
|
||||
self._name = name
|
||||
|
||||
family = ctypes.c_void_p()
|
||||
|
||||
# GDI will add @ in front of a localized font for some Asian languages. However, GDI will also not find it
|
||||
# based on that name (???). Here we remove it before checking font collections.
|
||||
if name[0] == "@":
|
||||
name = name[1:]
|
||||
|
||||
name = ctypes.c_wchar_p(name)
|
||||
|
||||
# Look in private collection first:
|
||||
@ -540,6 +546,9 @@ class GDIPlusFont(Win32Font):
|
||||
|
||||
# Then in system collection:
|
||||
if not family:
|
||||
if _debug_font:
|
||||
print(f"Warning: Font '{name}' was not found. Defaulting to: {self._default_name}")
|
||||
|
||||
gdiplus.GdipCreateFontFamilyFromName(name, None, ctypes.byref(family))
|
||||
|
||||
# Nothing found, use default font.
|
||||
|
@ -1,6 +1,7 @@
|
||||
import weakref
|
||||
|
||||
from enum import Enum
|
||||
import threading
|
||||
from typing import Tuple
|
||||
|
||||
import pyglet
|
||||
@ -209,11 +210,12 @@ class CanvasConfig(Config):
|
||||
|
||||
class ObjectSpace:
|
||||
def __init__(self):
|
||||
# Textures and buffers scheduled for deletion
|
||||
# the next time this object space is active.
|
||||
# Objects scheduled for deletion the next time this object space is active.
|
||||
self.doomed_textures = []
|
||||
self.doomed_buffers = []
|
||||
self.doomed_shader_programs = []
|
||||
self.doomed_shaders = []
|
||||
self.doomed_renderbuffers = []
|
||||
|
||||
|
||||
class Context:
|
||||
@ -236,6 +238,7 @@ class Context:
|
||||
self.canvas = None
|
||||
|
||||
self.doomed_vaos = []
|
||||
self.doomed_framebuffers = []
|
||||
|
||||
if context_share:
|
||||
self.object_space = context_share.object_space
|
||||
@ -277,28 +280,49 @@ class Context:
|
||||
self._info = gl_info.GLInfo()
|
||||
self._info.set_active_context()
|
||||
|
||||
# Release Textures, Buffers, and VAOs on this context scheduled for
|
||||
# deletion. Note that the garbage collector may introduce a race
|
||||
# condition, so operate on a copy, and clear the list afterward.
|
||||
if self.object_space.doomed_textures:
|
||||
textures = self.object_space.doomed_textures[:]
|
||||
textures = (gl.GLuint * len(textures))(*textures)
|
||||
gl.glDeleteTextures(len(textures), textures)
|
||||
self.object_space.doomed_textures.clear()
|
||||
self._delete_objects(self.object_space.doomed_textures, gl.glDeleteTextures)
|
||||
if self.object_space.doomed_buffers:
|
||||
buffers = self.object_space.doomed_buffers[:]
|
||||
buffers = (gl.GLuint * len(buffers))(*buffers)
|
||||
gl.glDeleteBuffers(len(buffers), buffers)
|
||||
self.object_space.doomed_buffers.clear()
|
||||
self._delete_objects(self.object_space.doomed_buffers, gl.glDeleteBuffers)
|
||||
if self.object_space.doomed_shader_programs:
|
||||
for program_id in self.object_space.doomed_shader_programs:
|
||||
gl.glDeleteProgram(program_id)
|
||||
self.object_space.doomed_shader_programs.clear()
|
||||
self._delete_objects_one_by_one(self.object_space.doomed_shader_programs,
|
||||
gl.glDeleteProgram)
|
||||
if self.object_space.doomed_shaders:
|
||||
self._delete_objects_one_by_one(self.object_space.doomed_shaders, gl.glDeleteShader)
|
||||
if self.object_space.doomed_renderbuffers:
|
||||
self._delete_objects(self.object_space.doomed_renderbuffers, gl.glDeleteRenderbuffers)
|
||||
|
||||
if self.doomed_vaos:
|
||||
vaos = self.doomed_vaos[:]
|
||||
vaos = (gl.GLuint * len(vaos))(*vaos)
|
||||
gl.glDeleteVertexArrays(len(vaos), vaos)
|
||||
self.doomed_vaos.clear()
|
||||
self._delete_objects(self.doomed_vaos, gl.glDeleteVertexArrays)
|
||||
if self.doomed_framebuffers:
|
||||
self._delete_objects(self.doomed_framebuffers, gl.glDeleteFramebuffers)
|
||||
|
||||
# For the functions below:
|
||||
# The garbage collector introduces a race condition.
|
||||
# The provided list might be appended to (and only appended to) while this
|
||||
# method runs, as it's a `doomed_*` list either on the context or its
|
||||
# object space. If `count` wasn't stored in a local, this method might
|
||||
# leak objects.
|
||||
def _delete_objects(self, list_, deletion_func):
|
||||
"""Release all OpenGL objects in the given list using the supplied
|
||||
deletion function with the signature ``(GLuint count, GLuint *names)``.
|
||||
"""
|
||||
count = len(list_)
|
||||
to_delete = list_[:count]
|
||||
del list_[:count]
|
||||
|
||||
deletion_func(count, (gl.GLuint * count)(*to_delete))
|
||||
|
||||
def _delete_objects_one_by_one(self, list_, deletion_func):
|
||||
"""Similar to ``_delete_objects``, but assumes the deletion functions's
|
||||
signature to be ``(GLuint name)``, calling it once for each object.
|
||||
"""
|
||||
count = len(list_)
|
||||
to_delete = list_[:count]
|
||||
del list_[:count]
|
||||
|
||||
for name in to_delete:
|
||||
deletion_func(gl.GLuint(name))
|
||||
|
||||
def destroy(self):
|
||||
"""Release the context.
|
||||
@ -318,6 +342,27 @@ class Context:
|
||||
if gl._shadow_window is not None:
|
||||
gl._shadow_window.switch_to()
|
||||
|
||||
def _safe_to_operate_on_object_space(self):
|
||||
"""Return whether it is safe to interact with this context's object
|
||||
space.
|
||||
|
||||
This is considered to be the case if the currently active context's
|
||||
object space is the same as this context's object space and this
|
||||
method is called from the main thread.
|
||||
"""
|
||||
return (
|
||||
self.object_space is gl.current_context.object_space and
|
||||
threading.current_thread() is threading.main_thread()
|
||||
)
|
||||
|
||||
def _safe_to_operate_on(self):
|
||||
"""Return whether it is safe to interact with this context.
|
||||
|
||||
This is considered to be the case if it's the current context and this
|
||||
method is called from the main thread.
|
||||
"""
|
||||
return gl.current_context is self and threading.current_thread() is threading.main_thread()
|
||||
|
||||
def create_program(self, *sources: Tuple[str, str], program_class=None):
|
||||
"""Create a ShaderProgram from OpenGL GLSL source.
|
||||
|
||||
@ -347,25 +392,30 @@ class Context:
|
||||
return program
|
||||
|
||||
def delete_texture(self, texture_id):
|
||||
"""Safely delete a Texture belonging to this context.
|
||||
"""Safely delete a Texture belonging to this context's object space.
|
||||
|
||||
Usually, the Texture is released immediately using
|
||||
``glDeleteTextures``, however if another context that does not share
|
||||
this context's object space is currently active, the deletion will
|
||||
be deferred until an appropriate context is activated.
|
||||
This method will delete the texture immediately via
|
||||
``glDeleteTextures`` if the current context's object space is the same
|
||||
as this context's object space and it is called from the main thread.
|
||||
|
||||
Otherwise, the texture will only be marked for deletion, postponing
|
||||
it until any context with the same object space becomes active again.
|
||||
|
||||
This makes it safe to call from anywhere, including other threads.
|
||||
|
||||
:Parameters:
|
||||
`texture_id` : int
|
||||
The OpenGL name of the Texture to delete.
|
||||
|
||||
"""
|
||||
if self.object_space is gl.current_context.object_space:
|
||||
if self._safe_to_operate_on_object_space():
|
||||
gl.glDeleteTextures(1, gl.GLuint(texture_id))
|
||||
else:
|
||||
self.object_space.doomed_textures.append(texture_id)
|
||||
|
||||
def delete_buffer(self, buffer_id):
|
||||
"""Safely delete a Buffer object belonging to this context.
|
||||
"""Safely delete a Buffer object belonging to this context's object
|
||||
space.
|
||||
|
||||
This method behaves similarly to `delete_texture`, though for
|
||||
``glDeleteBuffers`` instead of ``glDeleteTextures``.
|
||||
@ -376,30 +426,14 @@ class Context:
|
||||
|
||||
.. versionadded:: 1.1
|
||||
"""
|
||||
if self.object_space is gl.current_context.object_space and False:
|
||||
if self._safe_to_operate_on_object_space():
|
||||
gl.glDeleteBuffers(1, gl.GLuint(buffer_id))
|
||||
else:
|
||||
self.object_space.doomed_buffers.append(buffer_id)
|
||||
|
||||
def delete_vao(self, vao_id):
|
||||
"""Safely delete a Vertex Array Object belonging to this context.
|
||||
|
||||
This method behaves similarly to `delete_texture`, though for
|
||||
``glDeleteVertexArrays`` instead of ``glDeleteTextures``.
|
||||
|
||||
:Parameters:
|
||||
`vao_id` : int
|
||||
The OpenGL name of the Vertex Array to delete.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if gl.current_context is self:
|
||||
gl.glDeleteVertexArrays(1, gl.GLuint(vao_id))
|
||||
else:
|
||||
self.doomed_vaos.append(vao_id)
|
||||
|
||||
def delete_shader_program(self, program_id):
|
||||
"""Safely delete a Shader Program belonging to this context.
|
||||
"""Safely delete a Shader Program belonging to this context's
|
||||
object space.
|
||||
|
||||
This method behaves similarly to `delete_texture`, though for
|
||||
``glDeleteProgram`` instead of ``glDeleteTextures``.
|
||||
@ -410,11 +444,84 @@ class Context:
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if gl.current_context is self:
|
||||
gl.glDeleteProgram(program_id)
|
||||
if self._safe_to_operate_on_object_space():
|
||||
gl.glDeleteProgram(gl.GLuint(program_id))
|
||||
else:
|
||||
self.object_space.doomed_shader_programs.append(program_id)
|
||||
|
||||
def delete_shader(self, shader_id):
|
||||
"""Safely delete a Shader belonging to this context's object space.
|
||||
|
||||
This method behaves similarly to `delete_texture`, though for
|
||||
``glDeleteShader`` instead of ``glDeleteTextures``.
|
||||
|
||||
:Parameters:
|
||||
`shader_id` : int
|
||||
The OpenGL name of the Shader to delete.
|
||||
|
||||
.. versionadded:: 2.0.10
|
||||
"""
|
||||
if self._safe_to_operate_on_object_space():
|
||||
gl.glDeleteShader(gl.GLuint(shader_id))
|
||||
else:
|
||||
self.object_space.doomed_shaders.append(shader_id)
|
||||
|
||||
def delete_renderbuffer(self, rbo_id):
|
||||
"""Safely delete a Renderbuffer Object belonging to this context's
|
||||
object space.
|
||||
|
||||
This method behaves similarly to `delete_texture`, though for
|
||||
``glDeleteRenderbuffers`` instead of ``glDeleteTextures``.
|
||||
|
||||
:Parameters:
|
||||
`rbo_id` : int
|
||||
The OpenGL name of the Shader Program to delete.
|
||||
|
||||
.. versionadded:: 2.0.10
|
||||
"""
|
||||
if self._safe_to_operate_on_object_space():
|
||||
gl.glDeleteRenderbuffers(1, gl.GLuint(rbo_id))
|
||||
else:
|
||||
self.object_space.doomed_renderbuffers.append(rbo_id)
|
||||
|
||||
def delete_vao(self, vao_id):
|
||||
"""Safely delete a Vertex Array Object belonging to this context.
|
||||
|
||||
If this context is not the current context or this method is not
|
||||
called from the main thread, its deletion will be postponed until
|
||||
this context is next made active again.
|
||||
|
||||
Otherwise, this method will immediately delete the VAO via
|
||||
``glDeleteVertexArrays``.
|
||||
|
||||
:Parameters:
|
||||
`vao_id` : int
|
||||
The OpenGL name of the Vertex Array to delete.
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
if self._safe_to_operate_on():
|
||||
gl.glDeleteVertexArrays(1, gl.GLuint(vao_id))
|
||||
else:
|
||||
self.doomed_vaos.append(vao_id)
|
||||
|
||||
def delete_framebuffer(self, fbo_id):
|
||||
"""Safely delete a Framebuffer Object belonging to this context.
|
||||
|
||||
This method behaves similarly to `delete_vao`, though for
|
||||
``glDeleteFramebuffers`` instead of ``glDeleteVertexArrays``.
|
||||
|
||||
:Parameters:
|
||||
`fbo_id` : int
|
||||
The OpenGL name of the Framebuffer Object to delete.
|
||||
|
||||
.. versionadded:: 2.0.10
|
||||
"""
|
||||
if self._safe_to_operate_on():
|
||||
gl.glDeleteFramebuffers(1, gl.GLuint(fbo_id))
|
||||
else:
|
||||
self.doomed_framebuffers.append(fbo_id)
|
||||
|
||||
def get_info(self):
|
||||
"""Get the OpenGL information for this context.
|
||||
|
||||
|
@ -62,8 +62,7 @@ def errcheck(result, func, arguments):
|
||||
print(name)
|
||||
|
||||
from pyglet import gl
|
||||
context = gl.current_context
|
||||
if not context:
|
||||
if not gl.current_context:
|
||||
raise GLException('No GL context; create a Window first')
|
||||
error = gl.glGetError()
|
||||
if error:
|
||||
|
@ -244,7 +244,7 @@ class Win32ARBContext(_BaseWin32Context):
|
||||
if self.config.forward_compatible:
|
||||
flags |= wglext_arb.WGL_CONTEXT_FORWARD_COMPATIBLE_BIT_ARB
|
||||
if self.config.debug:
|
||||
flags |= wglext_arb.WGL_DEBUG_BIT_ARB
|
||||
flags |= wglext_arb.WGL_CONTEXT_DEBUG_BIT_ARB
|
||||
if flags:
|
||||
attribs.extend([wglext_arb.WGL_CONTEXT_FLAGS_ARB, flags])
|
||||
attribs.append(0)
|
||||
|
@ -55,10 +55,11 @@ def draw(size, mode, **data):
|
||||
assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt
|
||||
|
||||
buffer = BufferObject(size * attribute.stride)
|
||||
attribute.set_region(buffer, 0, size, array)
|
||||
data = (attribute.c_type * len(array))(*array)
|
||||
buffer.set_data(data)
|
||||
|
||||
attribute.enable()
|
||||
attribute.set_pointer(buffer.ptr)
|
||||
|
||||
buffers.append(buffer) # Don't garbage collect it.
|
||||
|
||||
glDrawArrays(mode, 0, size)
|
||||
@ -108,10 +109,12 @@ def draw_indexed(size, mode, indices, **data):
|
||||
assert size == len(array) // attribute.count, 'Data for %s is incorrect length' % fmt
|
||||
|
||||
buffer = BufferObject(size * attribute.stride)
|
||||
attribute.set_region(buffer, 0, size, array)
|
||||
data = (attribute.c_type * len(array))(*array)
|
||||
buffer.set_data(data)
|
||||
|
||||
attribute.enable()
|
||||
attribute.set_pointer(buffer.ptr)
|
||||
buffers.append(buffer)
|
||||
buffers.append(buffer) # Don't garbage collect it.
|
||||
|
||||
if size <= 0xff:
|
||||
index_type = GL_UNSIGNED_BYTE
|
||||
|
@ -170,14 +170,14 @@ class Attribute:
|
||||
self.name = name
|
||||
self.location = location
|
||||
self.count = count
|
||||
|
||||
self.gl_type = gl_type
|
||||
self.c_type = _c_types[gl_type]
|
||||
self.normalize = normalize
|
||||
|
||||
self.align = sizeof(self.c_type)
|
||||
self.size = count * self.align
|
||||
self.stride = self.size
|
||||
self.c_type = _c_types[gl_type]
|
||||
|
||||
self.element_size = sizeof(self.c_type)
|
||||
self.byte_size = count * self.element_size
|
||||
self.stride = self.byte_size
|
||||
|
||||
def enable(self):
|
||||
"""Enable the attribute."""
|
||||
@ -207,20 +207,16 @@ class Attribute:
|
||||
will be ``3 * 4 = 12``.
|
||||
|
||||
:Parameters:
|
||||
`buffer` : `AbstractMappable`
|
||||
`buffer` : `AttributeBufferObject`
|
||||
The buffer to map.
|
||||
`start` : int
|
||||
Offset of the first vertex to map.
|
||||
`count` : int
|
||||
Number of vertices to map
|
||||
|
||||
:rtype: `AbstractBufferRegion`
|
||||
:rtype: `BufferObjectRegion`
|
||||
"""
|
||||
byte_start = self.stride * start
|
||||
byte_size = self.stride * count
|
||||
array_count = self.count * count
|
||||
ptr_type = POINTER(self.c_type * array_count)
|
||||
return buffer.get_region(byte_start, byte_size, ptr_type)
|
||||
return buffer.get_region(start, count)
|
||||
|
||||
def set_region(self, buffer, start, count, data):
|
||||
"""Set the data over a region of the buffer.
|
||||
@ -234,11 +230,7 @@ class Attribute:
|
||||
Number of vertices to set.
|
||||
`data` : A sequence of data components.
|
||||
"""
|
||||
byte_start = self.stride * start
|
||||
byte_size = self.stride * count
|
||||
array_count = self.count * count
|
||||
data = (self.c_type * array_count)(*data)
|
||||
buffer.set_data_region(data, byte_start, byte_size)
|
||||
buffer.set_region(start, count, data)
|
||||
|
||||
def __repr__(self):
|
||||
return f"Attribute(name='{self.name}', location={self.location}, count={self.count})"
|
||||
@ -682,6 +674,7 @@ class Shader:
|
||||
"""
|
||||
|
||||
def __init__(self, source_string: str, shader_type: str):
|
||||
self._context = pyglet.gl.current_context
|
||||
self._id = None
|
||||
self.type = shader_type
|
||||
|
||||
@ -697,6 +690,7 @@ class Shader:
|
||||
source_length = c_int(len(shader_source_utf8))
|
||||
|
||||
shader_id = glCreateShader(shader_type)
|
||||
self._id = shader_id
|
||||
glShaderSource(shader_id, 1, byref(source_buffer_pointer), source_length)
|
||||
glCompileShader(shader_id)
|
||||
|
||||
@ -717,8 +711,6 @@ class Shader:
|
||||
elif _debug_gl_shaders:
|
||||
print(self._get_shader_log(shader_id))
|
||||
|
||||
self._id = shader_id
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
@ -743,16 +735,19 @@ class Shader:
|
||||
glGetShaderSource(shader_id, source_length, None, source_str)
|
||||
return source_str.value.decode('utf8')
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
glDeleteShader(self._id)
|
||||
if _debug_gl_shaders:
|
||||
print(f"Destroyed {self.type} Shader '{self._id}'")
|
||||
def delete(self):
|
||||
glDeleteShader(self._id)
|
||||
self._id = None
|
||||
|
||||
except Exception:
|
||||
# Interpreter is shutting down,
|
||||
# or Shader failed to compile.
|
||||
pass
|
||||
def __del__(self):
|
||||
if self._id is not None:
|
||||
try:
|
||||
self._context.delete_shader(self._id)
|
||||
if _debug_gl_shaders:
|
||||
print(f"Destroyed {self.type} Shader '{self._id}'")
|
||||
self._id = None
|
||||
except (AttributeError, ImportError):
|
||||
pass # Interpreter is shutting down
|
||||
|
||||
def __repr__(self):
|
||||
return "{0}(id={1}, type={2})".format(self.__class__.__name__, self.id, self.type)
|
||||
@ -764,6 +759,8 @@ class ShaderProgram:
|
||||
__slots__ = '_id', '_context', '_attributes', '_uniforms', '_uniform_blocks', '__weakref__'
|
||||
|
||||
def __init__(self, *shaders: Shader):
|
||||
self._id = None
|
||||
|
||||
assert shaders, "At least one Shader object is required."
|
||||
self._id = _link_program(*shaders)
|
||||
self._context = pyglet.gl.current_context
|
||||
@ -807,13 +804,17 @@ class ShaderProgram:
|
||||
def __exit__(self, *_):
|
||||
glUseProgram(0)
|
||||
|
||||
def delete(self):
|
||||
glDeleteProgram(self._id)
|
||||
self._id = None
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self._context.delete_shader_program(self.id)
|
||||
except Exception:
|
||||
# Interpreter is shutting down,
|
||||
# or ShaderProgram failed to link.
|
||||
pass
|
||||
if self._id is not None:
|
||||
try:
|
||||
self._context.delete_shader_program(self._id)
|
||||
self._id = None
|
||||
except (AttributeError, ImportError):
|
||||
pass # Interpreter is shutting down
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
try:
|
||||
@ -938,6 +939,8 @@ class ComputeShaderProgram:
|
||||
|
||||
def __init__(self, source: str):
|
||||
"""Create an OpenGL ComputeShaderProgram from source."""
|
||||
self._id = None
|
||||
|
||||
if not (gl_info.have_version(4, 3) or gl_info.have_extension("GL_ARB_compute_shader")):
|
||||
raise ShaderException("Compute Shader not supported. OpenGL Context version must be at least "
|
||||
"4.3 or higher, or 4.2 with the 'GL_ARB_compute_shader' extension.")
|
||||
@ -1010,13 +1013,17 @@ class ComputeShaderProgram:
|
||||
def __exit__(self, *_):
|
||||
glUseProgram(0)
|
||||
|
||||
def delete(self):
|
||||
glDeleteProgram(self._id)
|
||||
self._id = None
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self._context.delete_shader_program(self.id)
|
||||
except Exception:
|
||||
# Interpreter is shutting down,
|
||||
# or ShaderProgram failed to link.
|
||||
pass
|
||||
if self._id is not None:
|
||||
try:
|
||||
self._context.delete_shader_program(self._id)
|
||||
self._id = None
|
||||
except (AttributeError, ImportError):
|
||||
pass # Interpreter is shutting down
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
try:
|
||||
|
@ -27,10 +27,8 @@ class VertexArray:
|
||||
glBindVertexArray(0)
|
||||
|
||||
def delete(self):
|
||||
try:
|
||||
glDeleteVertexArrays(1, self._id)
|
||||
except Exception:
|
||||
pass
|
||||
glDeleteVertexArrays(1, self._id)
|
||||
self._id = None
|
||||
|
||||
__enter__ = bind
|
||||
|
||||
@ -38,11 +36,12 @@ class VertexArray:
|
||||
glBindVertexArray(0)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self._context.delete_vao(self.id)
|
||||
# Python interpreter is shutting down:
|
||||
except ImportError:
|
||||
pass
|
||||
if self._id is not None:
|
||||
try:
|
||||
self._context.delete_vao(self.id)
|
||||
self._id = None
|
||||
except (ImportError, AttributeError):
|
||||
pass # Interpreter is shutting down
|
||||
|
||||
def __repr__(self):
|
||||
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
||||
|
@ -11,6 +11,8 @@ the buffer.
|
||||
import sys
|
||||
import ctypes
|
||||
|
||||
from functools import lru_cache
|
||||
|
||||
import pyglet
|
||||
from pyglet.gl import *
|
||||
|
||||
@ -98,36 +100,6 @@ class AbstractBuffer:
|
||||
raise NotImplementedError('abstract')
|
||||
|
||||
|
||||
class AbstractMappable:
|
||||
|
||||
def get_region(self, start, size, ptr_type):
|
||||
"""Map a region of the buffer into a ctypes array of the desired
|
||||
type. This region does not need to be unmapped, but will become
|
||||
invalid if the buffer is resized.
|
||||
|
||||
Note that although a pointer type is required, an array is mapped.
|
||||
For example::
|
||||
|
||||
get_region(0, ctypes.sizeof(c_int) * 20, ctypes.POINTER(c_int * 20))
|
||||
|
||||
will map bytes 0 to 80 of the buffer to an array of 20 ints.
|
||||
|
||||
Changes to the array may not be recognised until the region's
|
||||
:py:meth:`AbstractBufferRegion.invalidate` method is called.
|
||||
|
||||
:Parameters:
|
||||
`start` : int
|
||||
Offset into the buffer to map from, in bytes
|
||||
`size` : int
|
||||
Size of the buffer region to map, in bytes
|
||||
`ptr_type` : ctypes pointer type
|
||||
Pointer type describing the array format to create
|
||||
|
||||
:rtype: :py:class:`AbstractBufferRegion`
|
||||
"""
|
||||
raise NotImplementedError('abstract')
|
||||
|
||||
|
||||
class BufferObject(AbstractBuffer):
|
||||
"""Lightweight representation of an OpenGL Buffer Object.
|
||||
|
||||
@ -180,7 +152,8 @@ class BufferObject(AbstractBuffer):
|
||||
|
||||
def map(self):
|
||||
glBindBuffer(GL_ARRAY_BUFFER, self.id)
|
||||
ptr = ctypes.cast(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY), ctypes.POINTER(ctypes.c_byte * self.size)).contents
|
||||
ptr = ctypes.cast(glMapBuffer(GL_ARRAY_BUFFER, GL_WRITE_ONLY),
|
||||
ctypes.POINTER(ctypes.c_byte * self.size)).contents
|
||||
return ptr
|
||||
|
||||
def map_range(self, start, size, ptr_type):
|
||||
@ -191,21 +164,18 @@ class BufferObject(AbstractBuffer):
|
||||
def unmap(self):
|
||||
glUnmapBuffer(GL_ARRAY_BUFFER)
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
if self.id is not None:
|
||||
self._context.delete_buffer(self.id)
|
||||
except:
|
||||
pass
|
||||
|
||||
def delete(self):
|
||||
buffer_id = GLuint(self.id)
|
||||
try:
|
||||
glDeleteBuffers(1, buffer_id)
|
||||
except Exception:
|
||||
pass
|
||||
glDeleteBuffers(1, self.id)
|
||||
self.id = None
|
||||
|
||||
def __del__(self):
|
||||
if self.id is not None:
|
||||
try:
|
||||
self._context.delete_buffer(self.id)
|
||||
self.id = None
|
||||
except (AttributeError, ImportError):
|
||||
pass # Interpreter is shutting down
|
||||
|
||||
def resize(self, size):
|
||||
# Map, create a copy, then reinitialize.
|
||||
temp = (ctypes.c_byte * size)()
|
||||
@ -222,27 +192,31 @@ class BufferObject(AbstractBuffer):
|
||||
return f"{self.__class__.__name__}(id={self.id}, size={self.size})"
|
||||
|
||||
|
||||
class MappableBufferObject(BufferObject, AbstractMappable):
|
||||
class AttributeBufferObject(BufferObject):
|
||||
"""A buffer with system-memory backed store.
|
||||
|
||||
Updates to the data via `set_data`, `set_data_region` and `map` will be
|
||||
held in local memory until `bind` is called. The advantage is that fewer
|
||||
OpenGL calls are needed, increasing performance.
|
||||
|
||||
There may also be less performance penalty for resizing this buffer.
|
||||
|
||||
Updates to data via :py:meth:`map` are committed immediately.
|
||||
Updates to the data via `set_data` and `set_data_region` will be held
|
||||
in local memory until `buffer_data` is called. The advantage is that
|
||||
fewer OpenGL calls are needed, which can increasing performance at the
|
||||
expense of system memory.
|
||||
"""
|
||||
def __init__(self, size, usage=GL_DYNAMIC_DRAW):
|
||||
super(MappableBufferObject, self).__init__(size, usage)
|
||||
|
||||
def __init__(self, size, attribute, usage=GL_DYNAMIC_DRAW):
|
||||
super().__init__(size, usage)
|
||||
self._size = size
|
||||
self.data = (ctypes.c_byte * size)()
|
||||
self.data_ptr = ctypes.addressof(self.data)
|
||||
self._dirty_min = sys.maxsize
|
||||
self._dirty_max = 0
|
||||
|
||||
def bind(self):
|
||||
# Commit pending data
|
||||
super(MappableBufferObject, self).bind()
|
||||
self.attribute_stride = attribute.stride
|
||||
self.attribute_count = attribute.count
|
||||
self.attribute_ctype = attribute.c_type
|
||||
|
||||
self._array = self.get_region(0, size).array
|
||||
|
||||
def bind(self, target=GL_ARRAY_BUFFER):
|
||||
super().bind(target)
|
||||
size = self._dirty_max - self._dirty_min
|
||||
if size > 0:
|
||||
if size == self.size:
|
||||
@ -252,28 +226,27 @@ class MappableBufferObject(BufferObject, AbstractMappable):
|
||||
self._dirty_min = sys.maxsize
|
||||
self._dirty_max = 0
|
||||
|
||||
def set_data(self, data):
|
||||
super(MappableBufferObject, self).set_data(data)
|
||||
ctypes.memmove(self.data, data, self.size)
|
||||
self._dirty_min = 0
|
||||
self._dirty_max = self.size
|
||||
@lru_cache(maxsize=None)
|
||||
def get_region(self, start, count):
|
||||
byte_start = self.attribute_stride * start # byte offset
|
||||
byte_size = self.attribute_stride * count # number of bytes
|
||||
array_count = self.attribute_count * count # number of values
|
||||
|
||||
def set_data_region(self, data, start, length):
|
||||
ctypes.memmove(self.data_ptr + start, data, length)
|
||||
self._dirty_min = min(start, self._dirty_min)
|
||||
self._dirty_max = max(start + length, self._dirty_max)
|
||||
ptr_type = ctypes.POINTER(self.attribute_ctype * array_count)
|
||||
array = ctypes.cast(self.data_ptr + byte_start, ptr_type).contents
|
||||
return BufferObjectRegion(self, byte_start, byte_start + byte_size, array)
|
||||
|
||||
def map(self, invalidate=False):
|
||||
self._dirty_min = 0
|
||||
self._dirty_max = self.size
|
||||
return self.data
|
||||
def set_region(self, start, count, data):
|
||||
byte_start = self.attribute_stride * start # byte offset
|
||||
byte_size = self.attribute_stride * count # number of bytes
|
||||
|
||||
def unmap(self):
|
||||
pass
|
||||
array_start = start * self.attribute_count
|
||||
array_end = count * self.attribute_count + array_start
|
||||
|
||||
def get_region(self, start, size, ptr_type):
|
||||
array = ctypes.cast(self.data_ptr + start, ptr_type).contents
|
||||
return BufferObjectRegion(self, start, start + size, array)
|
||||
self._array[array_start:array_end] = data
|
||||
|
||||
self._dirty_min = min(self._dirty_min, byte_start)
|
||||
self._dirty_max = max(self._dirty_max, byte_start + byte_size)
|
||||
|
||||
def resize(self, size):
|
||||
data = (ctypes.c_byte * size)()
|
||||
@ -289,6 +262,9 @@ class MappableBufferObject(BufferObject, AbstractMappable):
|
||||
self._dirty_min = sys.maxsize
|
||||
self._dirty_max = 0
|
||||
|
||||
self._array = self.get_region(0, size).array
|
||||
self.get_region.cache_clear()
|
||||
|
||||
|
||||
class BufferObjectRegion:
|
||||
"""A mapped region of a MappableBufferObject."""
|
||||
|
@ -23,11 +23,9 @@ primitives of the same OpenGL primitive mode.
|
||||
|
||||
import ctypes
|
||||
|
||||
import pyglet
|
||||
|
||||
from pyglet.gl import *
|
||||
from pyglet.graphics import allocation, shader, vertexarray
|
||||
from pyglet.graphics.vertexbuffer import BufferObject, MappableBufferObject
|
||||
from pyglet.graphics.vertexbuffer import BufferObject, AttributeBufferObject
|
||||
|
||||
|
||||
def _nearest_pow2(v):
|
||||
@ -66,22 +64,39 @@ _gl_types = {
|
||||
}
|
||||
|
||||
|
||||
def _make_attribute_property(name):
|
||||
|
||||
def _attribute_getter(self):
|
||||
attribute = self.domain.attribute_names[name]
|
||||
region = attribute.buffer.get_region(self.start, self.count)
|
||||
region.invalidate()
|
||||
return region.array
|
||||
|
||||
def _attribute_setter(self, data):
|
||||
attribute = self.domain.attribute_names[name]
|
||||
attribute.buffer.set_region(self.start, self.count, data)
|
||||
|
||||
return property(_attribute_getter, _attribute_setter)
|
||||
|
||||
|
||||
|
||||
class VertexDomain:
|
||||
"""Management of a set of vertex lists.
|
||||
|
||||
Construction of a vertex domain is usually done with the
|
||||
:py:func:`create_domain` function.
|
||||
"""
|
||||
version = 0
|
||||
_initial_count = 16
|
||||
|
||||
def __init__(self, program, attribute_meta):
|
||||
self.program = program
|
||||
self.program = program # Needed a reference for migration
|
||||
self.attribute_meta = attribute_meta
|
||||
self.allocator = allocation.Allocator(self._initial_count)
|
||||
|
||||
self.attributes = []
|
||||
self.buffer_attributes = [] # list of (buffer, attributes)
|
||||
self.attribute_names = {} # name: attribute
|
||||
self.buffer_attributes = [] # list of (buffer, attributes)
|
||||
|
||||
self._property_dict = {} # name: property(_getter, _setter)
|
||||
|
||||
for name, meta in attribute_meta.items():
|
||||
assert meta['format'][0] in _gl_types, f"'{meta['format']}' is not a valid atrribute format for '{name}'."
|
||||
@ -90,14 +105,19 @@ class VertexDomain:
|
||||
gl_type = _gl_types[meta['format'][0]]
|
||||
normalize = 'n' in meta['format']
|
||||
attribute = shader.Attribute(name, location, count, gl_type, normalize)
|
||||
self.attributes.append(attribute)
|
||||
self.attribute_names[attribute.name] = attribute
|
||||
|
||||
# Create buffer:
|
||||
attribute.buffer = MappableBufferObject(attribute.stride * self.allocator.capacity)
|
||||
attribute.buffer.element_size = attribute.stride
|
||||
attribute.buffer.attributes = (attribute,)
|
||||
attribute.buffer = AttributeBufferObject(attribute.stride * self.allocator.capacity, attribute)
|
||||
|
||||
self.buffer_attributes.append((attribute.buffer, (attribute,)))
|
||||
|
||||
# Create custom property to be used in the VertexList:
|
||||
self._property_dict[attribute.name] = _make_attribute_property(name)
|
||||
|
||||
# Make a custom VertexList class w/ properties for each attribute in the ShaderProgram:
|
||||
self._vertexlist_class = type("VertexList", (VertexList,), self._property_dict)
|
||||
|
||||
self.vao = vertexarray.VertexArray()
|
||||
self.vao.bind()
|
||||
for buffer, attributes in self.buffer_attributes:
|
||||
@ -107,29 +127,14 @@ class VertexDomain:
|
||||
attribute.set_pointer(buffer.ptr)
|
||||
self.vao.unbind()
|
||||
|
||||
# Create named attributes for each attribute
|
||||
self.attribute_names = {}
|
||||
for attribute in self.attributes:
|
||||
self.attribute_names[attribute.name] = attribute
|
||||
|
||||
def __del__(self):
|
||||
# Break circular refs that Python GC seems to miss even when forced
|
||||
# collection.
|
||||
for attribute in self.attributes:
|
||||
try:
|
||||
del attribute.buffer
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
def safe_alloc(self, count):
|
||||
"""Allocate vertices, resizing the buffers if necessary."""
|
||||
try:
|
||||
return self.allocator.alloc(count)
|
||||
except allocation.AllocatorMemoryException as e:
|
||||
capacity = _nearest_pow2(e.requested_capacity)
|
||||
self.version += 1
|
||||
for buffer, _ in self.buffer_attributes:
|
||||
buffer.resize(capacity * buffer.element_size)
|
||||
buffer.resize(capacity * buffer.attribute_stride)
|
||||
self.allocator.set_capacity(capacity)
|
||||
return self.allocator.alloc(count)
|
||||
|
||||
@ -139,9 +144,8 @@ class VertexDomain:
|
||||
return self.allocator.realloc(start, count, new_count)
|
||||
except allocation.AllocatorMemoryException as e:
|
||||
capacity = _nearest_pow2(e.requested_capacity)
|
||||
self.version += 1
|
||||
for buffer, _ in self.buffer_attributes:
|
||||
buffer.resize(capacity * buffer.element_size)
|
||||
buffer.resize(capacity * buffer.attribute_stride)
|
||||
self.allocator.set_capacity(capacity)
|
||||
return self.allocator.realloc(start, count, new_count)
|
||||
|
||||
@ -157,7 +161,7 @@ class VertexDomain:
|
||||
:rtype: :py:class:`VertexList`
|
||||
"""
|
||||
start = self.safe_alloc(count)
|
||||
return VertexList(self, start, count)
|
||||
return self._vertexlist_class(self, start, count)
|
||||
|
||||
def draw(self, mode):
|
||||
"""Draw all vertices in the domain.
|
||||
@ -221,8 +225,6 @@ class VertexList:
|
||||
self.domain = domain
|
||||
self.start = start
|
||||
self.count = count
|
||||
self._caches = {}
|
||||
self._cache_versions = {}
|
||||
|
||||
def draw(self, mode):
|
||||
"""Draw this vertex list in the given OpenGL mode.
|
||||
@ -247,7 +249,7 @@ class VertexList:
|
||||
new_start = self.domain.safe_realloc(self.start, self.count, count)
|
||||
if new_start != self.start:
|
||||
# Copy contents to new location
|
||||
for attribute in self.domain.attributes:
|
||||
for attribute in self.domain.attribute_names.values():
|
||||
old = attribute.get_region(attribute.buffer, self.start, self.count)
|
||||
new = attribute.get_region(attribute.buffer, new_start, self.count)
|
||||
new.array[:] = old.array[:]
|
||||
@ -255,9 +257,6 @@ class VertexList:
|
||||
self.start = new_start
|
||||
self.count = count
|
||||
|
||||
for version in self._cache_versions:
|
||||
self._cache_versions[version] = None
|
||||
|
||||
def delete(self):
|
||||
"""Delete this group."""
|
||||
self.domain.allocator.dealloc(self.start, self.count)
|
||||
@ -287,33 +286,10 @@ class VertexList:
|
||||
self.domain = domain
|
||||
self.start = new_start
|
||||
|
||||
for version in self._cache_versions:
|
||||
self._cache_versions[version] = None
|
||||
|
||||
def set_attribute_data(self, name, data):
|
||||
attribute = self.domain.attribute_names[name]
|
||||
attribute.set_region(attribute.buffer, self.start, self.count, data)
|
||||
|
||||
def __getattr__(self, name):
|
||||
"""dynamic access to vertex attributes, for backwards compatibility.
|
||||
"""
|
||||
domain = self.domain
|
||||
if self._cache_versions.get(name, None) != domain.version:
|
||||
attribute = domain.attribute_names[name]
|
||||
self._caches[name] = attribute.get_region(attribute.buffer, self.start, self.count)
|
||||
self._cache_versions[name] = domain.version
|
||||
|
||||
region = self._caches[name]
|
||||
region.invalidate()
|
||||
return region.array
|
||||
|
||||
def __setattr__(self, name, value):
|
||||
# Allow setting vertex attributes directly without overwriting them:
|
||||
if 'domain' in self.__dict__ and name in self.__dict__['domain'].attribute_names:
|
||||
getattr(self, name)[:] = value
|
||||
return
|
||||
super().__setattr__(name, value)
|
||||
|
||||
|
||||
class IndexedVertexDomain(VertexDomain):
|
||||
"""Management of a set of indexed vertex lists.
|
||||
@ -337,13 +313,15 @@ class IndexedVertexDomain(VertexDomain):
|
||||
self.index_buffer.bind_to_index_buffer()
|
||||
self.vao.unbind()
|
||||
|
||||
# Make a custom VertexList class w/ properties for each attribute in the ShaderProgram:
|
||||
self._vertexlist_class = type("IndexedVertexList", (IndexedVertexList,), self._property_dict)
|
||||
|
||||
def safe_index_alloc(self, count):
|
||||
"""Allocate indices, resizing the buffers if necessary."""
|
||||
try:
|
||||
return self.index_allocator.alloc(count)
|
||||
except allocation.AllocatorMemoryException as e:
|
||||
capacity = _nearest_pow2(e.requested_capacity)
|
||||
self.version += 1
|
||||
self.index_buffer.resize(capacity * self.index_element_size)
|
||||
self.index_allocator.set_capacity(capacity)
|
||||
return self.index_allocator.alloc(count)
|
||||
@ -354,7 +332,6 @@ class IndexedVertexDomain(VertexDomain):
|
||||
return self.index_allocator.realloc(start, count, new_count)
|
||||
except allocation.AllocatorMemoryException as e:
|
||||
capacity = _nearest_pow2(e.requested_capacity)
|
||||
self.version += 1
|
||||
self.index_buffer.resize(capacity * self.index_element_size)
|
||||
self.index_allocator.set_capacity(capacity)
|
||||
return self.index_allocator.realloc(start, count, new_count)
|
||||
@ -371,7 +348,7 @@ class IndexedVertexDomain(VertexDomain):
|
||||
"""
|
||||
start = self.safe_alloc(count)
|
||||
index_start = self.safe_index_alloc(index_count)
|
||||
return IndexedVertexList(self, start, count, index_start, index_count)
|
||||
return self._vertexlist_class(self, start, count, index_start, index_count)
|
||||
|
||||
def get_index_region(self, start, count):
|
||||
"""Get a data from a region of the index buffer.
|
||||
|
@ -433,6 +433,34 @@ class TextEntry(WidgetBase):
|
||||
assert type(value) is str, "This Widget's value must be a string."
|
||||
self._doc.text = value
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
return self._width
|
||||
|
||||
@width.setter
|
||||
def width(self, value):
|
||||
self._width = value
|
||||
self._layout.width = value
|
||||
self._outline.width = value
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
return self._height
|
||||
|
||||
@height.setter
|
||||
def height(self, value):
|
||||
self._height = value
|
||||
self._layout.height = value
|
||||
self._outline.height = value
|
||||
|
||||
@property
|
||||
def focus(self) -> bool:
|
||||
return self._focus
|
||||
|
||||
@focus.setter
|
||||
def focus(self, value: bool) -> None:
|
||||
self._set_focus(value)
|
||||
|
||||
def _check_hit(self, x, y):
|
||||
return self._x < x < self._x + self._width and self._y < y < self._y + self._height
|
||||
|
||||
|
@ -1220,11 +1220,20 @@ class Texture(AbstractImage):
|
||||
self.id = tex_id
|
||||
self._context = pyglet.gl.current_context
|
||||
|
||||
def delete(self):
|
||||
"""Delete this texture and the memory it occupies.
|
||||
After this, it may not be used anymore.
|
||||
"""
|
||||
glDeleteTextures(1, self.id)
|
||||
self.id = None
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
self._context.delete_texture(self.id)
|
||||
except Exception:
|
||||
pass
|
||||
if self.id is not None:
|
||||
try:
|
||||
self._context.delete_texture(self.id)
|
||||
self.id = None
|
||||
except (AttributeError, ImportError):
|
||||
pass # Interpreter is shutting down
|
||||
|
||||
def bind(self, texture_unit: int = 0):
|
||||
"""Bind to a specific Texture Unit by number."""
|
||||
@ -1479,8 +1488,13 @@ class TextureRegion(Texture):
|
||||
return "{}(id={}, size={}x{}, owner={}x{})".format(self.__class__.__name__, self.id, self.width, self.height,
|
||||
self.owner.width, self.owner.height)
|
||||
|
||||
def delete(self):
|
||||
"""Deleting a TextureRegion has no effect. Operate on the owning
|
||||
texture instead.
|
||||
"""
|
||||
pass
|
||||
|
||||
def __del__(self):
|
||||
# only the owner Texture should handle deletion
|
||||
pass
|
||||
|
||||
|
||||
|
@ -13,6 +13,7 @@ class Renderbuffer:
|
||||
|
||||
def __init__(self, width, height, internal_format, samples=1):
|
||||
"""Create an instance of a Renderbuffer object."""
|
||||
self._context = pyglet.gl.current_context
|
||||
self._id = GLuint()
|
||||
self._width = width
|
||||
self._height = height
|
||||
@ -49,13 +50,15 @@ class Renderbuffer:
|
||||
|
||||
def delete(self):
|
||||
glDeleteRenderbuffers(1, self._id)
|
||||
self._id = None
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
glDeleteRenderbuffers(1, self._id)
|
||||
# Python interpreter is shutting down:
|
||||
except Exception:
|
||||
pass
|
||||
if self._id is not None:
|
||||
try:
|
||||
self._context.delete_renderbuffer(self._id.value)
|
||||
self._id = None
|
||||
except (AttributeError, ImportError):
|
||||
pass # Interpreter is shutting down
|
||||
|
||||
def __repr__(self):
|
||||
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
||||
@ -71,6 +74,7 @@ class Framebuffer:
|
||||
|
||||
.. versionadded:: 2.0
|
||||
"""
|
||||
self._context = pyglet.gl.current_context
|
||||
self._id = GLuint()
|
||||
glGenFramebuffers(1, self._id)
|
||||
self._attachment_types = 0
|
||||
@ -105,10 +109,16 @@ class Framebuffer:
|
||||
self.unbind()
|
||||
|
||||
def delete(self):
|
||||
try:
|
||||
glDeleteFramebuffers(1, self._id)
|
||||
except Exception:
|
||||
pass
|
||||
glDeleteFramebuffers(1, self._id)
|
||||
self._id = None
|
||||
|
||||
def __del__(self):
|
||||
if self._id is not None:
|
||||
try:
|
||||
self._context.delete_framebuffer(self._id.value)
|
||||
self._id = None
|
||||
except (AttributeError, ImportError):
|
||||
pass # Interpreter is shutting down
|
||||
|
||||
@property
|
||||
def is_complete(self):
|
||||
@ -203,12 +213,5 @@ class Framebuffer:
|
||||
self._height = max(renderbuffer.height, self._height)
|
||||
self.unbind()
|
||||
|
||||
def __del__(self):
|
||||
try:
|
||||
glDeleteFramebuffers(1, self._id)
|
||||
# Python interpreter is shutting down:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def __repr__(self):
|
||||
return "{}(id={})".format(self.__class__.__name__, self._id.value)
|
||||
|
@ -601,7 +601,7 @@ class WICEncoder(ImageEncoder):
|
||||
|
||||
frame.SetPixelFormat(byref(default_format))
|
||||
|
||||
data = (c_byte * size).from_buffer(bytearray(image_data))
|
||||
data = (BYTE * size).from_buffer(bytearray(image_data))
|
||||
|
||||
frame.WritePixels(image.height, pitch, size, data)
|
||||
|
||||
|
@ -4,7 +4,6 @@ import fcntl
|
||||
import ctypes
|
||||
import warnings
|
||||
|
||||
from os import readv
|
||||
from ctypes import c_uint16 as _u16
|
||||
from ctypes import c_int16 as _s16
|
||||
from ctypes import c_uint32 as _u32
|
||||
@ -22,6 +21,8 @@ from pyglet.input.base import Device, RelativeAxis, AbsoluteAxis, Button, Joysti
|
||||
from pyglet.input.base import DeviceOpenException, ControllerManager
|
||||
from pyglet.input.controller import get_mapping, Relation, create_guid
|
||||
|
||||
c = pyglet.lib.load_library('c')
|
||||
|
||||
_IOC_NRBITS = 8
|
||||
_IOC_TYPEBITS = 8
|
||||
_IOC_SIZEBITS = 14
|
||||
@ -408,7 +409,7 @@ class EvdevDevice(XlibSelectDevice, Device):
|
||||
return
|
||||
|
||||
try:
|
||||
bytes_read = readv(self._fileno, self._event_buffer)
|
||||
bytes_read = c.read(self._fileno, self._event_buffer, self._event_size)
|
||||
except OSError:
|
||||
self.close()
|
||||
return
|
||||
|
@ -113,8 +113,8 @@ ERROR_SUCCESS = 0
|
||||
class XINPUT_GAMEPAD(Structure):
|
||||
_fields_ = [
|
||||
('wButtons', WORD),
|
||||
('bLeftTrigger', UBYTE),
|
||||
('bRightTrigger', UBYTE),
|
||||
('bLeftTrigger', BYTE),
|
||||
('bRightTrigger', BYTE),
|
||||
('sThumbLX', SHORT),
|
||||
('sThumbLY', SHORT),
|
||||
('sThumbRX', SHORT),
|
||||
|
@ -218,6 +218,7 @@ NSApplicationDidUnhideNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUn
|
||||
NSApplicationDidUpdateNotification = c_void_p.in_dll(appkit, 'NSApplicationDidUpdateNotification')
|
||||
NSPasteboardURLReadingFileURLsOnlyKey = c_void_p.in_dll(appkit, 'NSPasteboardURLReadingFileURLsOnlyKey')
|
||||
NSPasteboardTypeURL = c_void_p.in_dll(appkit, 'NSPasteboardTypeURL')
|
||||
NSPasteboardTypeString = c_void_p.in_dll(appkit, 'NSPasteboardTypeString')
|
||||
NSDragOperationGeneric = 4
|
||||
|
||||
# /System/Library/Frameworks/AppKit.framework/Headers/NSEvent.h
|
||||
|
@ -211,7 +211,18 @@ _user32.RegisterDeviceNotificationW.restype = HANDLE
|
||||
_user32.RegisterDeviceNotificationW.argtypes = [HANDLE, LPVOID, DWORD]
|
||||
_user32.UnregisterDeviceNotification.restype = BOOL
|
||||
_user32.UnregisterDeviceNotification.argtypes = [HANDLE]
|
||||
|
||||
_user32.SetClipboardData.restype = HANDLE
|
||||
_user32.SetClipboardData.argtypes = [UINT, HANDLE]
|
||||
_user32.EmptyClipboard.restype = BOOL
|
||||
_user32.EmptyClipboard.argtypes = []
|
||||
_user32.OpenClipboard.restype = BOOL
|
||||
_user32.OpenClipboard.argtypes = [HWND]
|
||||
_user32.CloseClipboard.restype = BOOL
|
||||
_user32.CloseClipboard.argtypes = []
|
||||
_user32.GetClipboardData.restype = HANDLE
|
||||
_user32.GetClipboardData.argtypes = [UINT]
|
||||
_user32.SetClipboardData.restype = HANDLE
|
||||
_user32.SetClipboardData.argtypes = [UINT, HANDLE]
|
||||
|
||||
# dwmapi
|
||||
_dwmapi.DwmIsCompositionEnabled.restype = c_int
|
||||
|
@ -18,7 +18,7 @@ Interfaces can define methods::
|
||||
...
|
||||
]
|
||||
|
||||
Only use STDMETHOD or METHOD for the method types (not ordinary ctypes
|
||||
Only use METHOD, STDMETHOD or VOIDMETHOD for the method types (not ordinary ctypes
|
||||
function types). The 'this' pointer is bound automatically... e.g., call::
|
||||
|
||||
device = IDirectSound8()
|
||||
@ -50,7 +50,7 @@ class GUID(ctypes.Structure):
|
||||
('Data1', ctypes.c_ulong),
|
||||
('Data2', ctypes.c_ushort),
|
||||
('Data3', ctypes.c_ushort),
|
||||
('Data4', ctypes.c_ubyte * 8)
|
||||
('Data4', ctypes.c_ubyte * 8),
|
||||
]
|
||||
|
||||
def __init__(self, l, w1, w2, b1, b2, b3, b4, b5, b6, b7, b8):
|
||||
@ -64,11 +64,6 @@ class GUID(ctypes.Structure):
|
||||
return 'GUID(%x, %x, %x, %x, %x, %x, %x, %x, %x, %x, %x)' % (
|
||||
self.Data1, self.Data2, self.Data3, b1, b2, b3, b4, b5, b6, b7, b8)
|
||||
|
||||
def __cmp__(self, other):
|
||||
if isinstance(other, GUID):
|
||||
return ctypes.cmp(bytes(self), bytes(other))
|
||||
return -1
|
||||
|
||||
def __eq__(self, other):
|
||||
return isinstance(other, GUID) and bytes(self) == bytes(other)
|
||||
|
||||
@ -80,6 +75,10 @@ LPGUID = ctypes.POINTER(GUID)
|
||||
IID = GUID
|
||||
REFIID = ctypes.POINTER(IID)
|
||||
|
||||
S_OK = 0x00000000
|
||||
E_NOTIMPL = 0x80004001
|
||||
E_NOINTERFACE = 0x80004002
|
||||
|
||||
|
||||
class METHOD:
|
||||
"""COM method."""
|
||||
@ -88,244 +87,147 @@ class METHOD:
|
||||
self.restype = restype
|
||||
self.argtypes = args
|
||||
|
||||
def get_field(self):
|
||||
# ctypes caches WINFUNCTYPE's so this should be ok.
|
||||
return ctypes.WINFUNCTYPE(self.restype, *self.argtypes)
|
||||
self.prototype = ctypes.WINFUNCTYPE(self.restype, *self.argtypes)
|
||||
self.direct_prototype = ctypes.WINFUNCTYPE(self.restype, ctypes.c_void_p, *self.argtypes)
|
||||
|
||||
def get_com_proxy(self, i, name):
|
||||
return self.prototype(i, name)
|
||||
|
||||
|
||||
class STDMETHOD(METHOD):
|
||||
"""COM method with HRESULT return value."""
|
||||
|
||||
def __init__(self, *args):
|
||||
super(STDMETHOD, self).__init__(ctypes.HRESULT, *args)
|
||||
super().__init__(ctypes.HRESULT, *args)
|
||||
|
||||
|
||||
class COMMethodInstance:
|
||||
"""Binds a COM interface method."""
|
||||
class VOIDMETHOD(METHOD):
|
||||
"""COM method with no return value."""
|
||||
|
||||
def __init__(self, name, i, method):
|
||||
self.name = name
|
||||
self.i = i
|
||||
self.method = method
|
||||
|
||||
def __get__(self, obj, tp):
|
||||
if obj is not None:
|
||||
def _call(*args):
|
||||
assert _debug_com('COM: #{} IN {}({}, {})'.format(self.i, self.name, obj.__class__.__name__, args))
|
||||
ret = self.method.get_field()(self.i, self.name)(obj, *args)
|
||||
assert _debug_com('COM: #{} OUT {}({}, {})'.format(self.i, self.name, obj.__class__.__name__, args))
|
||||
assert _debug_com('COM: RETURN {}'.format(ret))
|
||||
return ret
|
||||
|
||||
return _call
|
||||
|
||||
raise AttributeError()
|
||||
def __init__(self, *args):
|
||||
super().__init__(None, *args)
|
||||
|
||||
|
||||
class COMInterface(ctypes.Structure):
|
||||
"""Dummy struct to serve as the type of all COM pointers."""
|
||||
_fields_ = [
|
||||
('lpVtbl', ctypes.c_void_p),
|
||||
]
|
||||
_DummyPointerType = ctypes.POINTER(ctypes.c_int)
|
||||
_PointerMeta = type(_DummyPointerType)
|
||||
_StructMeta = type(ctypes.Structure)
|
||||
|
||||
|
||||
class InterfacePtrMeta(type(ctypes.POINTER(COMInterface))):
|
||||
"""Allows interfaces to be subclassed as ctypes POINTER and expects to be populated with data from a COM object.
|
||||
TODO: Phase this out and properly use POINTER(Interface) where applicable.
|
||||
"""
|
||||
class _InterfaceMeta(_StructMeta):
|
||||
def __new__(cls, name, bases, dct, /, create_pointer_type=True):
|
||||
if len(bases) > 1:
|
||||
assert _debug_com(f"Ignoring {len(bases) - 1} bases on {name}")
|
||||
bases = (bases[0],)
|
||||
|
||||
if not '_methods_' in dct:
|
||||
dct['_methods_'] = ()
|
||||
|
||||
inh_methods = []
|
||||
if bases[0] is not ctypes.Structure: # Method does not exist for first definition below
|
||||
for interface_type in (bases[0].get_interface_inheritance()):
|
||||
inh_methods.extend(interface_type.__dict__['_methods_'])
|
||||
|
||||
inh_methods = tuple(inh_methods)
|
||||
new_methods = tuple(dct['_methods_'])
|
||||
|
||||
vtbl_own_offset = len(inh_methods)
|
||||
|
||||
all_methods = tuple(inh_methods) + new_methods
|
||||
for i, (method_name, mt) in enumerate(all_methods):
|
||||
assert _debug_com(f"{name}[{i}]: {method_name}: "
|
||||
f"{(', '.join(t.__name__ for t in mt.argtypes) or 'void')} -> "
|
||||
f"{'void' if mt.restype is None else mt.restype.__name__}")
|
||||
|
||||
vtbl_struct_type = _StructMeta(f"Vtable_{name}",
|
||||
(ctypes.Structure,),
|
||||
{'_fields_': [(n, x.direct_prototype) for n, x in all_methods]})
|
||||
dct['_vtbl_struct_type'] = vtbl_struct_type
|
||||
dct['vtbl_own_offset'] = vtbl_own_offset
|
||||
|
||||
dct['_fields_'] = (('vtbl_ptr', ctypes.POINTER(vtbl_struct_type)),)
|
||||
|
||||
res_type = super().__new__(cls, name, bases, dct)
|
||||
if create_pointer_type:
|
||||
# If we're not being created from a pInterface subclass as helper Interface (so likely
|
||||
# being explicitly defined from user code for later use), create the special
|
||||
# pInterface pointer subclass so it registers itself into the pointer cache
|
||||
_pInterfaceMeta(f"p{name}", (ctypes.POINTER(bases[0]),), {'_type_': res_type})
|
||||
|
||||
return res_type
|
||||
|
||||
|
||||
class _pInterfaceMeta(_PointerMeta):
|
||||
def __new__(cls, name, bases, dct):
|
||||
methods = []
|
||||
for base in bases[::-1]:
|
||||
methods.extend(base.__dict__.get('_methods_', ()))
|
||||
methods.extend(dct.get('_methods_', ()))
|
||||
# Interfaces can also be declared by inheritance of pInterface subclasses.
|
||||
# If this happens, create the interface and then become pointer to its struct.
|
||||
|
||||
for i, (n, method) in enumerate(methods):
|
||||
dct[n] = COMMethodInstance(n, i, method)
|
||||
target = dct.get('_type_', None)
|
||||
# If we weren't created due to an Interface subclass definition (don't have a _type_),
|
||||
# just define that Interface subclass from our base's _type_
|
||||
if target is None:
|
||||
interface_base = bases[0]._type_
|
||||
|
||||
dct['_type_'] = COMInterface
|
||||
# Create corresponding interface type and then set it as target
|
||||
target = _InterfaceMeta(f"_{name}_HelperInterface",
|
||||
(interface_base,),
|
||||
{'_methods_': dct.get('_methods_', ())},
|
||||
create_pointer_type=False)
|
||||
dct['_type_'] = target
|
||||
|
||||
return super(InterfacePtrMeta, cls).__new__(cls, name, bases, dct)
|
||||
# Create method proxies that will forward ourselves into the interface's methods
|
||||
for i, (method_name, method) in enumerate(target._methods_):
|
||||
m = method.get_com_proxy(i + target.vtbl_own_offset, method_name)
|
||||
def pinterface_method_forward(self, *args, _m=m, _i=i):
|
||||
assert _debug_com(f'Calling COM {_i} of {target.__name__} ({_m}) through '
|
||||
f'pointer: ({", ".join(map(repr, (self, *args)))})')
|
||||
return _m(self, *args)
|
||||
dct[method_name] = pinterface_method_forward
|
||||
|
||||
pointer_type = super().__new__(cls, name, bases, dct)
|
||||
|
||||
class pInterface(ctypes.POINTER(COMInterface), metaclass=InterfacePtrMeta):
|
||||
"""Base COM interface pointer."""
|
||||
|
||||
|
||||
class COMInterfaceMeta(type):
|
||||
"""This differs in the original as an implemented interface object, not a POINTER object.
|
||||
Used when the user must implement their own functions within an interface rather than
|
||||
being created and generated by the COM object itself. The types are automatically inserted in the ctypes type
|
||||
cache so it can recognize the type arguments.
|
||||
"""
|
||||
|
||||
def __new__(mcs, name, bases, dct):
|
||||
methods = dct.pop("_methods_", None)
|
||||
cls = type.__new__(mcs, name, bases, dct)
|
||||
|
||||
if methods is not None:
|
||||
cls._methods_ = methods
|
||||
|
||||
if not bases:
|
||||
_ptr_bases = (cls, COMPointer)
|
||||
else:
|
||||
_ptr_bases = (cls, ctypes.POINTER(bases[0]))
|
||||
|
||||
# Class type is dynamically created inside __new__ based on metaclass inheritence; update ctypes cache manually.
|
||||
# Hack selves into the ctypes pointer cache so all uses of `ctypes.POINTER` on the
|
||||
# interface type will yield it instead of the inflexible standard pointer type.
|
||||
# NOTE: This is done pretty much exclusively to help convert COMObjects.
|
||||
# Some additional work from callers like
|
||||
# RegisterCallback(callback_obj.as_interface(ICallback))
|
||||
# instead of
|
||||
# RegisterCallback(callback_obj)
|
||||
# could make it obsolete.
|
||||
from ctypes import _pointer_type_cache
|
||||
_pointer_type_cache[cls] = type(COMPointer)("POINTER({})".format(cls.__name__),
|
||||
_ptr_bases,
|
||||
{"__interface__": cls})
|
||||
_pointer_type_cache[target] = pointer_type
|
||||
|
||||
return cls
|
||||
return pointer_type
|
||||
|
||||
def __get_subclassed_methodcount(self):
|
||||
"""Returns the amount of COM methods in all subclasses to determine offset of methods.
|
||||
Order must be exact from the source when calling COM methods.
|
||||
|
||||
class Interface(ctypes.Structure, metaclass=_InterfaceMeta, create_pointer_type=False):
|
||||
@classmethod
|
||||
def get_interface_inheritance(cls):
|
||||
"""Returns the types of all interfaces implemented by this interface, up to but not
|
||||
including the base `Interface`.
|
||||
`Interface` does not represent an actual interface, but merely the base concept of
|
||||
them, so viewing it as part of an interface's inheritance chain is meaningless.
|
||||
"""
|
||||
try:
|
||||
result = 0
|
||||
for itf in self.mro()[1:-1]:
|
||||
result += len(itf.__dict__["_methods_"])
|
||||
return result
|
||||
except KeyError as err:
|
||||
(name,) = err.args
|
||||
if name == "_methods_":
|
||||
raise TypeError("Interface '{}' requires a _methods_ attribute.".format(itf.__name__))
|
||||
raise
|
||||
return cls.__mro__[:cls.__mro__.index(Interface)]
|
||||
|
||||
|
||||
class COMPointerMeta(type(ctypes.c_void_p), COMInterfaceMeta):
|
||||
"""Required to prevent metaclass conflicts with inheritance."""
|
||||
|
||||
|
||||
class COMPointer(ctypes.c_void_p, metaclass=COMPointerMeta):
|
||||
"""COM Pointer base, could use c_void_p but need to override from_param ."""
|
||||
class pInterface(_DummyPointerType, metaclass=_pInterfaceMeta):
|
||||
_type_ = Interface
|
||||
|
||||
@classmethod
|
||||
def from_param(cls, obj):
|
||||
"""Allows obj to return ctypes pointers, even if its base is not a ctype.
|
||||
In this case, all we simply want is a ctypes pointer matching the cls interface from the obj.
|
||||
"""
|
||||
if obj is None:
|
||||
return
|
||||
"""When dealing with a COMObject, pry a fitting interface out of it"""
|
||||
|
||||
try:
|
||||
ptr_dct = obj._pointers
|
||||
except AttributeError:
|
||||
raise Exception("Interface method argument specified incorrectly, or passed wrong argument.", cls)
|
||||
else:
|
||||
try:
|
||||
return ptr_dct[cls.__interface__]
|
||||
except KeyError:
|
||||
raise TypeError("Interface {} doesn't have a pointer in this class.".format(cls.__name__))
|
||||
if not isinstance(obj, COMObject):
|
||||
return obj
|
||||
|
||||
return obj.as_interface(cls._type_)
|
||||
|
||||
|
||||
def _missing_impl(interface_name, method_name):
|
||||
"""Functions that are not implemented use this to prevent errors when called."""
|
||||
|
||||
def missing_cb_func(*args):
|
||||
"""Return E_NOTIMPL because the method is not implemented."""
|
||||
assert _debug_com("Undefined method: {0} was called in interface: {1}".format(method_name, interface_name))
|
||||
return 0
|
||||
|
||||
return missing_cb_func
|
||||
|
||||
|
||||
def _found_impl(interface_name, method_name, method_func):
|
||||
"""If a method was found in class, we can set it as a callback."""
|
||||
|
||||
def cb_func(*args, **kw):
|
||||
try:
|
||||
result = method_func(*args, **kw)
|
||||
except Exception as err:
|
||||
raise err
|
||||
|
||||
if not result: # QOL so callbacks don't need to specify a return for assumed OK's.
|
||||
return 0
|
||||
|
||||
return result
|
||||
|
||||
return cb_func
|
||||
|
||||
|
||||
def _make_callback_func(interface, name, method_func):
|
||||
"""Create a callback function for ctypes if possible."""
|
||||
if method_func is None:
|
||||
return _missing_impl(interface, name)
|
||||
|
||||
return _found_impl(interface, name, method_func)
|
||||
|
||||
|
||||
# Store structures with same fields to prevent duplicate table creations.
|
||||
_cached_structures = {}
|
||||
|
||||
|
||||
def create_vtbl_structure(fields, interface):
|
||||
"""Create virtual table structure with fields for use in COM's."""
|
||||
try:
|
||||
return _cached_structures[fields]
|
||||
except KeyError:
|
||||
Vtbl = type("Vtbl_{}".format(interface.__name__), (ctypes.Structure,), {"_fields_": fields})
|
||||
_cached_structures[fields] = Vtbl
|
||||
return Vtbl
|
||||
|
||||
|
||||
class COMObject:
|
||||
"""A base class for defining a COM object for use with callbacks and custom implementations."""
|
||||
_interfaces_ = []
|
||||
|
||||
def __new__(cls, *args, **kw):
|
||||
new_cls = super(COMObject, cls).__new__(cls)
|
||||
assert len(cls._interfaces_) > 0, "Atleast one interface must be defined to use a COMObject."
|
||||
new_cls._pointers = {}
|
||||
new_cls.__create_interface_pointers()
|
||||
return new_cls
|
||||
|
||||
def __create_interface_pointers(cls):
|
||||
"""Create a custom ctypes structure to handle COM functions in a COM Object."""
|
||||
interfaces = tuple(cls._interfaces_)
|
||||
for itf in interfaces[::-1]:
|
||||
methods = []
|
||||
fields = []
|
||||
for interface in itf.__mro__[-2::-1]:
|
||||
for method in interface._methods_:
|
||||
name, com_method = method
|
||||
|
||||
found_method = getattr(cls, name, None)
|
||||
mth = _make_callback_func(itf.__name__, name, found_method)
|
||||
|
||||
proto = ctypes.WINFUNCTYPE(com_method.restype, *com_method.argtypes)
|
||||
|
||||
fields.append((name, proto))
|
||||
methods.append(proto(mth))
|
||||
|
||||
# Make a structure dynamically with the fields given.
|
||||
itf_structure = create_vtbl_structure(tuple(fields), interface)
|
||||
|
||||
# Assign the methods to the fields
|
||||
vtbl = itf_structure(*methods)
|
||||
|
||||
cls._pointers[itf] = ctypes.pointer(ctypes.pointer(vtbl))
|
||||
|
||||
@property
|
||||
def pointers(self):
|
||||
"""Returns pointers to the implemented interfaces in this COMObject. Read-only.
|
||||
|
||||
:type: dict
|
||||
"""
|
||||
return self._pointers
|
||||
|
||||
class Interface(metaclass=COMInterfaceMeta):
|
||||
_methods_ = []
|
||||
|
||||
|
||||
class IUnknown(metaclass=COMInterfaceMeta):
|
||||
"""These methods are not implemented by default yet. Strictly for COM method ordering."""
|
||||
class IUnknown(Interface):
|
||||
_methods_ = [
|
||||
('QueryInterface', STDMETHOD(ctypes.c_void_p, REFIID, ctypes.c_void_p)),
|
||||
('AddRef', METHOD(ctypes.c_int, ctypes.c_void_p)),
|
||||
('Release', METHOD(ctypes.c_int, ctypes.c_void_p))
|
||||
('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
|
||||
('AddRef', METHOD(ctypes.c_int)),
|
||||
('Release', METHOD(ctypes.c_int)),
|
||||
]
|
||||
|
||||
|
||||
@ -333,5 +235,163 @@ class pIUnknown(pInterface):
|
||||
_methods_ = [
|
||||
('QueryInterface', STDMETHOD(REFIID, ctypes.c_void_p)),
|
||||
('AddRef', METHOD(ctypes.c_int)),
|
||||
('Release', METHOD(ctypes.c_int))
|
||||
('Release', METHOD(ctypes.c_int)),
|
||||
]
|
||||
|
||||
|
||||
def _missing_impl(interface_name, method_name):
|
||||
"""Create a callback returning E_NOTIMPL for methods not present on a COMObject."""
|
||||
|
||||
def missing_cb_func(*_):
|
||||
assert _debug_com(f"Non-implemented method {method_name} called in {interface_name}")
|
||||
return E_NOTIMPL
|
||||
|
||||
return missing_cb_func
|
||||
|
||||
|
||||
def _found_impl(interface_name, method_name, method_func, self_distance):
|
||||
"""If a method was found in class, create a callback extracting self from the struct
|
||||
pointer.
|
||||
"""
|
||||
|
||||
def self_extracting_cb_func(p, *args):
|
||||
assert _debug_com(f"COMObject method {method_name} called through interface {interface_name}")
|
||||
self = ctypes.cast(p + self_distance, ctypes.POINTER(ctypes.py_object)).contents.value
|
||||
result = method_func(self, *args)
|
||||
# Assume no return statement translates to success
|
||||
return S_OK if result is None else result
|
||||
|
||||
return self_extracting_cb_func
|
||||
|
||||
|
||||
def _adjust_impl(interface_name, method_name, original_method, offset):
|
||||
"""A method implemented in a previous interface modifies the COMOboject pointer so it
|
||||
corresponds to an earlier interface and passes it on to the actual implementation.
|
||||
"""
|
||||
|
||||
def adjustor_cb_func(p, *args):
|
||||
assert _debug_com(f"COMObject method {method_name} called through interface "
|
||||
f"{interface_name}, adjusting pointer by {offset}")
|
||||
return original_method(p + offset, *args)
|
||||
|
||||
return adjustor_cb_func
|
||||
|
||||
|
||||
class COMObject:
|
||||
"""A COMObject for implementing C callbacks in Python.
|
||||
Specify the interface types it supports in `_interfaces_`, and any methods to be implemented
|
||||
by those interfaces as standard python methods. If the names match, they will be run as
|
||||
callbacks with all arguments supplied as the types specified in the corresponding interface,
|
||||
and `self` available as usual.
|
||||
Remember to call `super().__init__()`.
|
||||
|
||||
COMObjects can be passed to ctypes functions directly as long as the corresponding argtype is
|
||||
an `Interface` pointer, or a `pInterface` subclass.
|
||||
|
||||
IUnknown's methods will be autogenerated in case IUnknown is implemented.
|
||||
"""
|
||||
|
||||
def __init_subclass__(cls, /, **kwargs):
|
||||
super().__init_subclass__(**kwargs)
|
||||
|
||||
implemented_leaf_interfaces = cls.__dict__.get('_interfaces_', ())
|
||||
if not implemented_leaf_interfaces:
|
||||
raise TypeError("At least one interface must be defined to use a COMObject")
|
||||
|
||||
for interface_type in implemented_leaf_interfaces:
|
||||
for other in implemented_leaf_interfaces:
|
||||
if interface_type is other:
|
||||
continue
|
||||
if issubclass(interface_type, other):
|
||||
raise TypeError("Only specify the leaf interfaces")
|
||||
|
||||
# Sanity check done
|
||||
|
||||
_ptr_size = ctypes.sizeof(ctypes.c_void_p)
|
||||
|
||||
_vtbl_pointers = []
|
||||
implemented_methods = {}
|
||||
|
||||
# Map all leaf and inherited interfaces to the offset of the vtable containing
|
||||
# their implementations
|
||||
_interface_to_vtbl_offset = {}
|
||||
for i, interface_type in enumerate(implemented_leaf_interfaces):
|
||||
bases = interface_type.get_interface_inheritance()
|
||||
for base in bases:
|
||||
if base not in _interface_to_vtbl_offset:
|
||||
_interface_to_vtbl_offset[base] = i * _ptr_size
|
||||
|
||||
if IUnknown in _interface_to_vtbl_offset:
|
||||
def QueryInterface(self, iid_ptr, res_ptr):
|
||||
ctypes.cast(res_ptr, ctypes.POINTER(ctypes.c_void_p))[0] = 0
|
||||
return E_NOINTERFACE
|
||||
|
||||
def AddRef(self):
|
||||
self._vrefcount += 1
|
||||
return self._vrefcount
|
||||
|
||||
def Release(self):
|
||||
if self._vrefcount <= 0:
|
||||
assert _debug_com(
|
||||
f"COMObject {self}: Release while refcount was {self._vrefcount}"
|
||||
)
|
||||
self._vrefcount -= 1
|
||||
return self._vrefcount
|
||||
|
||||
cls.QueryInterface = QueryInterface
|
||||
cls.AddRef = AddRef
|
||||
cls.Release = Release
|
||||
|
||||
for i, interface_type in enumerate(implemented_leaf_interfaces):
|
||||
wrappers = []
|
||||
|
||||
for method_name, method_type in interface_type._vtbl_struct_type._fields_:
|
||||
if method_name in implemented_methods:
|
||||
# Method is already implemented on a previous interface; redirect to it
|
||||
# See https://devblogs.microsoft.com/oldnewthing/20040206-00/?p=40723
|
||||
# NOTE: Never tested, might be totally wrong
|
||||
func, implementing_vtbl_idx = implemented_methods[method_name]
|
||||
mth = _adjust_impl(interface_type.__name__,
|
||||
method_name,
|
||||
func,
|
||||
(implementing_vtbl_idx - i) * _ptr_size)
|
||||
|
||||
else:
|
||||
if (found_method := getattr(cls, method_name, None)) is None:
|
||||
mth = _missing_impl(interface_type.__name__, method_name)
|
||||
else:
|
||||
mth = _found_impl(interface_type.__name__,
|
||||
method_name,
|
||||
found_method,
|
||||
(len(implemented_leaf_interfaces) - i) * _ptr_size)
|
||||
|
||||
implemented_methods[method_name] = (mth, i)
|
||||
|
||||
wrappers.append(method_type(mth))
|
||||
|
||||
vtbl = interface_type._vtbl_struct_type(*wrappers)
|
||||
_vtbl_pointers.append(ctypes.pointer(vtbl))
|
||||
|
||||
fields = []
|
||||
for i, itf in enumerate(implemented_leaf_interfaces):
|
||||
fields.append((f'vtbl_ptr_{i}', ctypes.POINTER(itf._vtbl_struct_type)))
|
||||
fields.append(('self_', ctypes.py_object))
|
||||
|
||||
cls._interface_to_vtbl_offset = _interface_to_vtbl_offset
|
||||
cls._vtbl_pointers = _vtbl_pointers
|
||||
cls._struct_type = _StructMeta(f"{cls.__name__}_Struct", (ctypes.Structure,), {'_fields_': fields})
|
||||
|
||||
def __init__(self):
|
||||
self._vrefcount = 1
|
||||
self._struct = self._struct_type(*self._vtbl_pointers, ctypes.py_object(self))
|
||||
|
||||
def as_interface(self, interface_type):
|
||||
# This method ignores the QueryInterface mechanism completely; no GUIDs are
|
||||
# associated with Interfaces on the python side, it can't be supported.
|
||||
# Still works, as so far none of the python-made COMObjects are expected to
|
||||
# support it by any C code.
|
||||
# (Also no need to always implement it, some COMObjects do not inherit from IUnknown.)
|
||||
if (offset := self._interface_to_vtbl_offset.get(interface_type, None)) is None:
|
||||
raise TypeError(f"Does not implement {interface_type}")
|
||||
|
||||
return ctypes.byref(self._struct, offset)
|
||||
|
@ -1,4 +1,5 @@
|
||||
import ctypes
|
||||
import sys
|
||||
|
||||
from ctypes import *
|
||||
from ctypes.wintypes import *
|
||||
@ -45,7 +46,6 @@ def POINTER_(obj):
|
||||
|
||||
c_void_p = POINTER_(c_void)
|
||||
INT = c_int
|
||||
UBYTE = c_ubyte
|
||||
LPVOID = c_void_p
|
||||
HCURSOR = HANDLE
|
||||
LRESULT = LPARAM
|
||||
@ -62,6 +62,11 @@ HDROP = HANDLE
|
||||
LPTSTR = LPWSTR
|
||||
LPSTREAM = c_void_p
|
||||
|
||||
# Fixed in python 3.12. Is c_byte on other versions.
|
||||
# Ensure it's the same across all versions.
|
||||
if sys.version_info < (3, 12):
|
||||
BYTE = c_ubyte
|
||||
|
||||
LF_FACESIZE = 32
|
||||
CCHDEVICENAME = 32
|
||||
CCHFORMNAME = 32
|
||||
@ -572,6 +577,7 @@ class IStream(com.pIUnknown):
|
||||
com.STDMETHOD()),
|
||||
]
|
||||
|
||||
|
||||
class DEV_BROADCAST_HDR(Structure):
|
||||
_fields_ = (
|
||||
('dbch_size', DWORD),
|
||||
@ -579,6 +585,7 @@ class DEV_BROADCAST_HDR(Structure):
|
||||
('dbch_reserved', DWORD),
|
||||
)
|
||||
|
||||
|
||||
class DEV_BROADCAST_DEVICEINTERFACE(Structure):
|
||||
_fields_ = (
|
||||
('dbcc_size', DWORD),
|
||||
|
@ -26,6 +26,7 @@ from collections.abc import Iterator as _Iterator
|
||||
|
||||
|
||||
number = _typing.Union[float, int]
|
||||
Mat3T = _typing.TypeVar("Mat3T", bound="Mat3")
|
||||
Mat4T = _typing.TypeVar("Mat4T", bound="Mat4")
|
||||
|
||||
|
||||
@ -627,7 +628,7 @@ class Mat3(tuple):
|
||||
the "@" operator.
|
||||
"""
|
||||
|
||||
def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) -> Mat3:
|
||||
def __new__(cls: type[Mat3T], values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0)) -> Mat3T:
|
||||
"""Create a 3x3 Matrix
|
||||
|
||||
A Mat3 can be created with a list or tuple of 9 values.
|
||||
@ -639,7 +640,7 @@ class Mat3(tuple):
|
||||
`values` : tuple of float or int
|
||||
A tuple or list containing 9 floats or ints.
|
||||
"""
|
||||
new = super().__new__(Mat3, values)
|
||||
new = super().__new__(cls, values)
|
||||
assert len(new) == 9, "A 3x3 Matrix requires 9 values"
|
||||
return new
|
||||
|
||||
@ -720,10 +721,10 @@ class Mat3(tuple):
|
||||
|
||||
class Mat4(tuple):
|
||||
|
||||
def __new__(cls, values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,)) -> Mat4:
|
||||
def __new__(cls: type[Mat4T], values: _Iterable[float] = (1.0, 0.0, 0.0, 0.0,
|
||||
0.0, 1.0, 0.0, 0.0,
|
||||
0.0, 0.0, 1.0, 0.0,
|
||||
0.0, 0.0, 0.0, 1.0,)) -> Mat4T:
|
||||
"""Create a 4x4 Matrix.
|
||||
|
||||
`Mat4` is an immutable 4x4 Matrix, which includs most common
|
||||
@ -738,7 +739,7 @@ class Mat4(tuple):
|
||||
.. note:: Matrix multiplication is performed using the "@" operator.
|
||||
"""
|
||||
|
||||
new = super().__new__(Mat4, values)
|
||||
new = super().__new__(cls, values)
|
||||
assert len(new) == 16, "A 4x4 Matrix requires 16 values"
|
||||
return new
|
||||
|
||||
@ -1011,3 +1012,106 @@ class Mat4(tuple):
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}{self[0:4]}\n {self[4:8]}\n {self[8:12]}\n {self[12:16]}"
|
||||
|
||||
|
||||
class Quaternion(tuple):
|
||||
"""Quaternion"""
|
||||
|
||||
def __new__(cls, w: float = 1.0, x: float = 0.0, y: float = 0.0, z: float = 0.0) -> Quaternion:
|
||||
return super().__new__(Quaternion, (w, x, y, z))
|
||||
|
||||
@classmethod
|
||||
def from_mat3(cls) -> Quaternion:
|
||||
raise NotImplementedError("Not yet implemented")
|
||||
|
||||
@classmethod
|
||||
def from_mat4(cls) -> Quaternion:
|
||||
raise NotImplementedError("Not yet implemented")
|
||||
|
||||
def to_mat4(self) -> Mat4:
|
||||
w = self.w
|
||||
x = self.x
|
||||
y = self.y
|
||||
z = self.z
|
||||
|
||||
a = 1 - (y ** 2 + z ** 2) * 2
|
||||
b = 2 * (x * y - z * w)
|
||||
c = 2 * (x * z + y * w)
|
||||
|
||||
e = 2 * (x * y + z * w)
|
||||
f = 1 - (x ** 2 + z ** 2) * 2
|
||||
g = 2 * (y * z - x * w)
|
||||
|
||||
i = 2 * (x * z - y * w)
|
||||
j = 2 * (y * z + x * w)
|
||||
k = 1 - (x ** 2 + y ** 2) * 2
|
||||
|
||||
# a, b, c, -
|
||||
# e, f, g, -
|
||||
# i, j, k, -
|
||||
# -, -, -, -
|
||||
|
||||
return Mat4((a, b, c, 0.0, e, f, g, 0.0, i, j, k, 0.0, 0.0, 0.0, 0.0, 1.0))
|
||||
|
||||
def to_mat3(self) -> Mat3:
|
||||
w = self.w
|
||||
x = self.x
|
||||
y = self.y
|
||||
z = self.z
|
||||
|
||||
a = 1 - (y ** 2 + z ** 2) * 2
|
||||
b = 2 * (x * y - z * w)
|
||||
c = 2 * (x * z + y * w)
|
||||
|
||||
e = 2 * (x * y + z * w)
|
||||
f = 1 - (x ** 2 + z ** 2) * 2
|
||||
g = 2 * (y * z - x * w)
|
||||
|
||||
i = 2 * (x * z - y * w)
|
||||
j = 2 * (y * z + x * w)
|
||||
k = 1 - (x ** 2 + y ** 2) * 2
|
||||
|
||||
# a, b, c, -
|
||||
# e, f, g, -
|
||||
# i, j, k, -
|
||||
# -, -, -, -
|
||||
|
||||
return Mat3((a, b, c, e, f, g, i, j, k))
|
||||
|
||||
@property
|
||||
def w(self) -> float:
|
||||
return self[0]
|
||||
|
||||
@property
|
||||
def x(self) -> float:
|
||||
return self[1]
|
||||
|
||||
@property
|
||||
def y(self) -> float:
|
||||
return self[2]
|
||||
|
||||
@property
|
||||
def z(self) -> float:
|
||||
return self[3]
|
||||
|
||||
def conjugate(self) -> Quaternion:
|
||||
return Quaternion(self.w, -self.x, -self.y, -self.z)
|
||||
|
||||
@property
|
||||
def mag(self) -> float:
|
||||
return self.__abs__()
|
||||
|
||||
def normalize(self) -> Quaternion:
|
||||
m = self.__abs__()
|
||||
if m == 0:
|
||||
return self
|
||||
return Quaternion(self[0] / m, self[1] / m, self[2] / m, self[3] / m)
|
||||
|
||||
def __abs__(self) -> float:
|
||||
return _math.sqrt(self.w ** 2 + self.x ** 2 + self.y ** 2 + self.z ** 2)
|
||||
|
||||
def __invert__(self) -> Quaternion:
|
||||
raise NotImplementedError("Not yet implemented")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self.__class__.__name__}(w={self[0]}, x={self[1]}, y={self[2]}, z={self[3]})"
|
||||
|
@ -1,6 +1,6 @@
|
||||
"""Use ffmpeg to decode audio and video media.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from collections import deque
|
||||
from ctypes import (c_int, c_int32, c_uint8, c_char_p,
|
||||
addressof, byref, cast, POINTER, Structure, create_string_buffer, memmove)
|
||||
@ -8,7 +8,7 @@ from ctypes import (c_int, c_int32, c_uint8, c_char_p,
|
||||
import pyglet
|
||||
import pyglet.lib
|
||||
from pyglet import image
|
||||
from pyglet.util import asbytes, asbytes_filename, asstr
|
||||
from pyglet.util import asbytes, asstr
|
||||
from . import MediaDecoder
|
||||
from .base import AudioData, SourceInfo, StaticSource
|
||||
from .base import StreamingSource, VideoFormat, AudioFormat
|
||||
@ -510,10 +510,12 @@ class FFmpegSource(StreamingSource):
|
||||
self._file = None
|
||||
self._memory_file = None
|
||||
|
||||
encoded_filename = filename.encode(sys.getfilesystemencoding())
|
||||
|
||||
if file:
|
||||
self._file, self._memory_file = ffmpeg_open_memory_file(asbytes_filename(filename), file)
|
||||
self._file, self._memory_file = ffmpeg_open_memory_file(encoded_filename, file)
|
||||
else:
|
||||
self._file = ffmpeg_open_filename(asbytes_filename(filename))
|
||||
self._file = ffmpeg_open_filename(encoded_filename)
|
||||
|
||||
if not self._file:
|
||||
raise FFmpegException('Could not open "{0}"'.format(filename))
|
||||
|
@ -520,9 +520,9 @@ class WMFSource(Source):
|
||||
imfmedia.GetGUID(MF_MT_SUBTYPE, ctypes.byref(guid_compressed))
|
||||
|
||||
if guid_compressed == MFAudioFormat_PCM or guid_compressed == MFAudioFormat_Float:
|
||||
assert _debug('WMFAudioDecoder: Found Uncompressed Audio:', guid_compressed)
|
||||
assert _debug(f'WMFAudioDecoder: Found Uncompressed Audio: {guid_compressed}')
|
||||
else:
|
||||
assert _debug('WMFAudioDecoder: Found Compressed Audio:', guid_compressed)
|
||||
assert _debug(f'WMFAudioDecoder: Found Compressed Audio: {guid_compressed}')
|
||||
# If audio is compressed, attempt to decompress it by forcing source reader to use PCM
|
||||
mf_mediatype = IMFMediaType()
|
||||
|
||||
|
@ -83,15 +83,15 @@ IID_IMMDeviceEnumerator = com.GUID(0xa95664d2, 0x9614, 0x4f35, 0xa7, 0x46, 0xde,
|
||||
class IMMNotificationClient(com.IUnknown):
|
||||
_methods_ = [
|
||||
('OnDeviceStateChanged',
|
||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, DWORD)),
|
||||
com.STDMETHOD(LPCWSTR, DWORD)),
|
||||
('OnDeviceAdded',
|
||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)),
|
||||
com.STDMETHOD(LPCWSTR)),
|
||||
('OnDeviceRemoved',
|
||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR)),
|
||||
com.STDMETHOD(LPCWSTR)),
|
||||
('OnDefaultDeviceChanged',
|
||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, EDataFlow, ERole, LPCWSTR)),
|
||||
com.STDMETHOD(EDataFlow, ERole, LPCWSTR)),
|
||||
('OnPropertyValueChanged',
|
||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, LPCWSTR, PROPERTYKEY)),
|
||||
com.STDMETHOD(LPCWSTR, PROPERTYKEY)),
|
||||
]
|
||||
|
||||
|
||||
@ -113,7 +113,7 @@ class AudioNotificationCB(com.COMObject):
|
||||
self.audio_devices = audio_devices
|
||||
self._lost = False
|
||||
|
||||
def OnDeviceStateChanged(self, this, pwstrDeviceId, dwNewState):
|
||||
def OnDeviceStateChanged(self, pwstrDeviceId, dwNewState):
|
||||
device = self.audio_devices.get_cached_device(pwstrDeviceId)
|
||||
|
||||
old_state = device.state
|
||||
@ -126,17 +126,17 @@ class AudioNotificationCB(com.COMObject):
|
||||
device.state = dwNewState
|
||||
self.audio_devices.dispatch_event('on_device_state_changed', device, pyglet_old_state, pyglet_new_state)
|
||||
|
||||
def OnDeviceAdded(self, this, pwstrDeviceId):
|
||||
def OnDeviceAdded(self, pwstrDeviceId):
|
||||
dev = self.audio_devices.add_device(pwstrDeviceId)
|
||||
assert _debug(f"Audio device was added {pwstrDeviceId}: {dev}")
|
||||
self.audio_devices.dispatch_event('on_device_added', dev)
|
||||
|
||||
def OnDeviceRemoved(self, this, pwstrDeviceId):
|
||||
def OnDeviceRemoved(self, pwstrDeviceId):
|
||||
dev = self.audio_devices.remove_device(pwstrDeviceId)
|
||||
assert _debug(f"Audio device was removed {pwstrDeviceId} : {dev}")
|
||||
self.audio_devices.dispatch_event('on_device_removed', dev)
|
||||
|
||||
def OnDefaultDeviceChanged(self, this, flow, role, pwstrDeviceId):
|
||||
def OnDefaultDeviceChanged(self, flow, role, pwstrDeviceId):
|
||||
# Only support eConsole role right now
|
||||
if role == 0:
|
||||
if pwstrDeviceId is None:
|
||||
@ -149,7 +149,7 @@ class AudioNotificationCB(com.COMObject):
|
||||
|
||||
self.audio_devices.dispatch_event('on_default_changed', device, pyglet_flow)
|
||||
|
||||
def OnPropertyValueChanged(self, this, pwstrDeviceId, key):
|
||||
def OnPropertyValueChanged(self, pwstrDeviceId, key):
|
||||
pass
|
||||
|
||||
|
||||
@ -259,7 +259,7 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
|
||||
cached_dev.state = dev_state
|
||||
return cached_dev
|
||||
except OSError as err:
|
||||
assert _debug("No default audio output was found.", err)
|
||||
assert _debug(f"No default audio output was found. {err}")
|
||||
return None
|
||||
|
||||
def get_default_input(self) -> Optional[Win32AudioDevice]:
|
||||
@ -274,7 +274,7 @@ class Win32AudioDeviceManager(base.AbstractAudioDeviceManager):
|
||||
cached_dev.state = dev_state
|
||||
return cached_dev
|
||||
except OSError as err:
|
||||
assert _debug("No default input output was found.", err)
|
||||
assert _debug(f"No default input output was found. {err}")
|
||||
return None
|
||||
|
||||
def get_cached_device(self, dev_id) -> Win32AudioDevice:
|
||||
|
@ -145,11 +145,11 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
||||
|
||||
def _refill(self, write_size):
|
||||
while write_size > 0:
|
||||
assert _debug('_refill, write_size =', write_size)
|
||||
assert _debug(f'_refill, write_size = {write_size}')
|
||||
audio_data = self._get_audiodata()
|
||||
|
||||
if audio_data is not None:
|
||||
assert _debug('write', audio_data.length)
|
||||
assert _debug(f'write {audio_data.length}')
|
||||
length = min(write_size, audio_data.length)
|
||||
self.write(audio_data, length)
|
||||
write_size -= length
|
||||
@ -191,14 +191,14 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
||||
# Set the write cursor back to eos_cursor or play_cursor to prevent gaps
|
||||
if self._play_cursor < self._eos_cursor:
|
||||
cursor_diff = self._write_cursor - self._eos_cursor
|
||||
assert _debug('Moving cursor back', cursor_diff)
|
||||
assert _debug(f'Moving cursor back {cursor_diff}')
|
||||
self._write_cursor = self._eos_cursor
|
||||
self._write_cursor_ring -= cursor_diff
|
||||
self._write_cursor_ring %= self._buffer_size
|
||||
|
||||
else:
|
||||
cursor_diff = self._play_cursor - self._eos_cursor
|
||||
assert _debug('Moving cursor back', cursor_diff)
|
||||
assert _debug(f'Moving cursor back {cursor_diff}')
|
||||
self._write_cursor = self._play_cursor
|
||||
self._write_cursor_ring -= cursor_diff
|
||||
self._write_cursor_ring %= self._buffer_size
|
||||
@ -207,7 +207,7 @@ class DirectSoundAudioPlayer(AbstractAudioPlayer):
|
||||
for event in audio_data.events:
|
||||
event_cursor = self._write_cursor + event.timestamp * \
|
||||
self.source.audio_format.bytes_per_second
|
||||
assert _debug('Adding event', event, 'at', event_cursor)
|
||||
assert _debug(f'Adding event {event} at {event_cursor}')
|
||||
self._events.append((event_cursor, event))
|
||||
|
||||
def _add_audiodata_timestamp(self, audio_data):
|
||||
|
@ -270,7 +270,6 @@ class IDirectSound(com.pIUnknown):
|
||||
('Initialize',
|
||||
com.STDMETHOD(com.LPGUID)),
|
||||
]
|
||||
_type_ = com.COMInterface
|
||||
|
||||
DirectSoundCreate = lib.DirectSoundCreate
|
||||
DirectSoundCreate.argtypes = \
|
||||
|
@ -245,7 +245,7 @@ class OpenALAudioPlayer(AbstractAudioPlayer):
|
||||
return False
|
||||
|
||||
def _refill(self, write_size):
|
||||
assert _debug('_refill', write_size)
|
||||
assert _debug(f'_refill {write_size}')
|
||||
|
||||
while write_size > self.min_buffer_size:
|
||||
audio_data = self._get_audiodata()
|
||||
|
@ -236,7 +236,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
|
||||
|
||||
while self._events and self._events[0][0] <= read_index:
|
||||
_, event = self._events.pop(0)
|
||||
assert _debug('PulseAudioPlayer: Dispatch event', event)
|
||||
assert _debug(f'PulseAudioPlayer: Dispatch event {event}')
|
||||
event.sync_dispatch_to_player(self.player)
|
||||
|
||||
def _add_event_at_write_index(self, event_name):
|
||||
@ -313,7 +313,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
|
||||
else:
|
||||
read_index = 0
|
||||
|
||||
assert _debug('_get_read_index ->', read_index)
|
||||
assert _debug(f'_get_read_index -> {read_index}')
|
||||
return read_index
|
||||
|
||||
def _get_write_index(self):
|
||||
@ -323,7 +323,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
|
||||
else:
|
||||
write_index = 0
|
||||
|
||||
assert _debug('_get_write_index ->', write_index)
|
||||
assert _debug(f'_get_write_index -> {write_index}')
|
||||
return write_index
|
||||
|
||||
def _get_timing_info(self):
|
||||
@ -365,7 +365,7 @@ class PulseAudioPlayer(AbstractAudioPlayer):
|
||||
dt /= 1000000
|
||||
time = timestamp + dt
|
||||
|
||||
assert _debug('get_time ->', time)
|
||||
assert _debug('get_time -> {time}')
|
||||
return time
|
||||
|
||||
def set_volume(self, volume):
|
||||
|
@ -213,7 +213,7 @@ class XAudio2AudioPlayer(AbstractAudioPlayer):
|
||||
def _add_audiodata_events(self, audio_data):
|
||||
for event in audio_data.events:
|
||||
event_cursor = self._write_cursor + event.timestamp * self.source.audio_format.bytes_per_second
|
||||
assert _debug('Adding event', event, 'at', event_cursor)
|
||||
assert _debug(f'Adding event {event} at {event_cursor}')
|
||||
self._events.append((event_cursor, event))
|
||||
|
||||
def _add_audiodata_timestamp(self, audio_data):
|
||||
|
@ -192,17 +192,19 @@ XAUDIO2_NO_VIRTUAL_AUDIO_CLIENT = 0x10000 # Used in CreateMasteringVoice to cr
|
||||
class IXAudio2VoiceCallback(com.Interface):
|
||||
_methods_ = [
|
||||
('OnVoiceProcessingPassStart',
|
||||
com.STDMETHOD(UINT32)),
|
||||
com.VOIDMETHOD(UINT32)),
|
||||
('OnVoiceProcessingPassEnd',
|
||||
com.STDMETHOD()),
|
||||
('onStreamEnd',
|
||||
com.STDMETHOD()),
|
||||
('onBufferStart',
|
||||
com.STDMETHOD(ctypes.c_void_p)),
|
||||
com.VOIDMETHOD()),
|
||||
('OnStreamEnd',
|
||||
com.VOIDMETHOD()),
|
||||
('OnBufferStart',
|
||||
com.VOIDMETHOD(ctypes.c_void_p)),
|
||||
('OnBufferEnd',
|
||||
com.STDMETHOD(ctypes.c_void_p)),
|
||||
com.VOIDMETHOD(ctypes.c_void_p)),
|
||||
('OnLoopEnd',
|
||||
com.STDMETHOD(ctypes.c_void_p)),
|
||||
com.VOIDMETHOD(ctypes.c_void_p)),
|
||||
('OnVoiceError',
|
||||
com.VOIDMETHOD(ctypes.c_void_p, HRESULT))
|
||||
]
|
||||
|
||||
|
||||
@ -220,20 +222,9 @@ class XA2SourceCallback(com.COMObject):
|
||||
_interfaces_ = [IXAudio2VoiceCallback]
|
||||
|
||||
def __init__(self, xa2_player):
|
||||
super().__init__()
|
||||
self.xa2_player = xa2_player
|
||||
|
||||
def OnVoiceProcessingPassStart(self, bytesRequired):
|
||||
pass
|
||||
|
||||
def OnVoiceProcessingPassEnd(self):
|
||||
pass
|
||||
|
||||
def onStreamEnd(self):
|
||||
pass
|
||||
|
||||
def onBufferStart(self, pBufferContext):
|
||||
pass
|
||||
|
||||
def OnBufferEnd(self, pBufferContext):
|
||||
"""At the end of playing one buffer, attempt to refill again.
|
||||
Even if the player is out of sources, it needs to be called to purge all buffers.
|
||||
@ -241,10 +232,7 @@ class XA2SourceCallback(com.COMObject):
|
||||
if self.xa2_player:
|
||||
self.xa2_player.refill_source_player()
|
||||
|
||||
def OnLoopEnd(self, this, pBufferContext):
|
||||
pass
|
||||
|
||||
def onVoiceError(self, this, pBufferContext, hresult):
|
||||
def OnVoiceError(self, pBufferContext, hresult):
|
||||
raise Exception("Error occurred during audio playback.", hresult)
|
||||
|
||||
|
||||
@ -362,24 +350,18 @@ class IXAudio2MasteringVoice(IXAudio2Voice):
|
||||
class IXAudio2EngineCallback(com.Interface):
|
||||
_methods_ = [
|
||||
('OnProcessingPassStart',
|
||||
com.METHOD(ctypes.c_void_p)),
|
||||
com.VOIDMETHOD()),
|
||||
('OnProcessingPassEnd',
|
||||
com.METHOD(ctypes.c_void_p)),
|
||||
com.VOIDMETHOD()),
|
||||
('OnCriticalError',
|
||||
com.METHOD(ctypes.c_void_p, ctypes.c_void_p, ctypes.c_ulong)),
|
||||
com.VOIDMETHOD(HRESULT)),
|
||||
]
|
||||
|
||||
|
||||
class XA2EngineCallback(com.COMObject):
|
||||
_interfaces_ = [IXAudio2EngineCallback]
|
||||
|
||||
def OnProcessingPassStart(self):
|
||||
pass
|
||||
|
||||
def OnProcessingPassEnd(self):
|
||||
pass
|
||||
|
||||
def OnCriticalError(self, this, hresult):
|
||||
def OnCriticalError(self, hresult):
|
||||
raise Exception("Critical Error:", hresult)
|
||||
|
||||
|
||||
|
@ -5,12 +5,11 @@ such as Rectangles, Circles, and Lines. These shapes are made
|
||||
internally from OpenGL primitives, and provide excellent performance
|
||||
when drawn as part of a :py:class:`~pyglet.graphics.Batch`.
|
||||
Convenience methods are provided for positioning, changing color
|
||||
and opacity, and rotation (where applicable). To create more
|
||||
complex shapes than what is provided here, the lower level
|
||||
graphics API is more appropriate.
|
||||
You can also use the ``in`` operator to check whether a point is
|
||||
inside a shape.
|
||||
See the :ref:`guide_graphics` for more details.
|
||||
and opacity, and rotation (where applicable).
|
||||
The Python ``in`` operator to check whether a point is inside a shape.
|
||||
|
||||
To create more complex shapes than what is provided here, the lower level
|
||||
graphics API is more appropriate. See the :ref:`guide_graphics` for more details.
|
||||
|
||||
A simple example of drawing shapes::
|
||||
|
||||
@ -1466,6 +1465,122 @@ class BorderedRectangle(ShapeBase):
|
||||
self._update_color()
|
||||
|
||||
|
||||
class Box(ShapeBase):
|
||||
def __init__(self, x, y, width, height, thickness=1, color=(255, 255, 255, 255), batch=None, group=None):
|
||||
"""Create an unfilled rectangular shape, with optional thickness.
|
||||
|
||||
The box's anchor point defaults to the (x, y) coordinates,
|
||||
which are at the bottom left.
|
||||
Changing the thickness of the box will extend the walls inward;
|
||||
the outward dimesions will not be affected.
|
||||
|
||||
:Parameters:
|
||||
`x` : float
|
||||
The X coordinate of the box.
|
||||
`y` : float
|
||||
The Y coordinate of the box.
|
||||
`width` : float
|
||||
The width of the box.
|
||||
`height` : float
|
||||
The height of the box.
|
||||
`thickness` : float
|
||||
The thickness of the lines that make up the box.
|
||||
`color` : (int, int, int, int)
|
||||
The RGB or RGBA color of the box, specified as a tuple
|
||||
of 3 or 4 ints in the range of 0-255. RGB colors will
|
||||
be treated as having an opacity of 255.
|
||||
`batch` : `~pyglet.graphics.Batch`
|
||||
Optional batch to add the box to.
|
||||
`group` : `~pyglet.graphics.Group`
|
||||
Optional parent group of the box.
|
||||
"""
|
||||
self._x = x
|
||||
self._y = y
|
||||
self._width = width
|
||||
self._height = height
|
||||
self._thickness = thickness
|
||||
|
||||
self._num_verts = 8
|
||||
|
||||
r, g, b, *a = color
|
||||
self._rgba = r, g, b, a[0] if a else 255
|
||||
|
||||
program = get_default_shader()
|
||||
self._batch = batch or Batch()
|
||||
self._group = self.group_class(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA, program, group)
|
||||
|
||||
self._create_vertex_list()
|
||||
self._update_vertices()
|
||||
|
||||
def __contains__(self, point):
|
||||
assert len(point) == 2
|
||||
point = _rotate_point((self._x, self._y), point, math.radians(self._rotation))
|
||||
x, y = self._x - self._anchor_x, self._y - self._anchor_y
|
||||
return x < point[0] < x + self._width and y < point[1] < y + self._height
|
||||
|
||||
def _create_vertex_list(self):
|
||||
# 3 6
|
||||
# 2 7
|
||||
# 1 4
|
||||
# 0 5
|
||||
indices = [0, 1, 2, 0, 2, 3, 0, 5, 4, 0, 4, 1, 4, 5, 6, 4, 6, 7, 2, 7, 6, 2, 6, 3]
|
||||
self._vertex_list = self._group.program.vertex_list_indexed(
|
||||
self._num_verts, self._draw_mode, indices, self._batch, self._group,
|
||||
colors=('Bn', self._rgba * self._num_verts),
|
||||
translation=('f', (self._x, self._y) * self._num_verts))
|
||||
|
||||
def _update_color(self):
|
||||
self._vertex_list.colors[:] = self._rgba * self._num_verts
|
||||
|
||||
def _update_vertices(self):
|
||||
if not self._visible:
|
||||
self._vertex_list.position[:] = (0, 0) * self._num_verts
|
||||
else:
|
||||
|
||||
t = self._thickness
|
||||
left = -self._anchor_x
|
||||
bottom = -self._anchor_y
|
||||
right = left + self._width
|
||||
top = bottom + self._height
|
||||
|
||||
x1 = left
|
||||
x2 = left + t
|
||||
x3 = right - t
|
||||
x4 = right
|
||||
y1 = bottom
|
||||
y2 = bottom + t
|
||||
y3 = top - t
|
||||
y4 = top
|
||||
# 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7
|
||||
self._vertex_list.position[:] = x1, y1, x2, y2, x2, y3, x1, y4, x3, y2, x4, y1, x4, y4, x3, y3
|
||||
|
||||
@property
|
||||
def width(self):
|
||||
"""The width of the Box.
|
||||
|
||||
:type: float
|
||||
"""
|
||||
return self._width
|
||||
|
||||
@width.setter
|
||||
def width(self, value):
|
||||
self._width = value
|
||||
self._update_vertices()
|
||||
|
||||
@property
|
||||
def height(self):
|
||||
"""The height of the Box.
|
||||
|
||||
:type: float
|
||||
"""
|
||||
return self._height
|
||||
|
||||
@height.setter
|
||||
def height(self, value):
|
||||
self._height = value
|
||||
self._update_vertices()
|
||||
|
||||
|
||||
class Triangle(ShapeBase):
|
||||
def __init__(self, x, y, x2, y2, x3, y3, color=(255, 255, 255, 255),
|
||||
batch=None, group=None):
|
||||
@ -1776,4 +1891,5 @@ class Polygon(ShapeBase):
|
||||
self._vertex_list.position[:] = tuple(value for coordinate in triangles for value in coordinate)
|
||||
|
||||
|
||||
__all__ = 'Arc', 'BezierCurve', 'Circle', 'Ellipse', 'Line', 'Rectangle', 'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector'
|
||||
__all__ = ('Arc', 'Box', 'BezierCurve', 'Circle', 'Ellipse', 'Line', 'Rectangle',
|
||||
'BorderedRectangle', 'Triangle', 'Star', 'Polygon', 'Sector', 'ShapeBase')
|
||||
|
@ -271,6 +271,8 @@ class DocumentLabel(layout.TextLayout):
|
||||
|
||||
@color.setter
|
||||
def color(self, color):
|
||||
r, g, b, *a = color
|
||||
color = r, g, b, a[0] if a else 255
|
||||
self.document.set_style(0, len(self.document.text), {'color': color})
|
||||
|
||||
@property
|
||||
|
@ -98,7 +98,7 @@ class Caret:
|
||||
|
||||
colors = r, g, b, self._visible_alpha, r, g, b, self._visible_alpha
|
||||
|
||||
self._list = self._group.program.vertex_list(2, gl.GL_LINES, batch, self._group, colors=('Bn', colors))
|
||||
self._list = self._group.program.vertex_list(2, gl.GL_LINES, self._batch, self._group, colors=('Bn', colors))
|
||||
self._ideal_x = None
|
||||
self._ideal_line = None
|
||||
self._next_attributes = {}
|
||||
@ -127,6 +127,7 @@ class Caret:
|
||||
|
||||
Also disconnects the caret from further layout events.
|
||||
"""
|
||||
clock.unschedule(self._blink)
|
||||
self._list.delete()
|
||||
self._layout.remove_handlers(self)
|
||||
|
||||
|
@ -540,18 +540,15 @@ class _GlyphBox(_AbstractBox):
|
||||
x1 = x2
|
||||
|
||||
if background_vertices:
|
||||
background_indices = []
|
||||
bg_count = len(background_vertices) // 2
|
||||
bg_count = len(background_vertices) // 3
|
||||
background_indices = [(0, 1, 2, 0, 2, 3)[i % 6] for i in range(bg_count * 3)]
|
||||
decoration_program = get_default_decoration_shader()
|
||||
for bg_idx in range(bg_count):
|
||||
background_indices.extend([element + (bg_idx * 4) for element in [0, 1, 2, 0, 2, 3]])
|
||||
|
||||
background_list = decoration_program.vertex_list_indexed(bg_count * 4, GL_TRIANGLES, background_indices,
|
||||
background_list = decoration_program.vertex_list_indexed(bg_count, GL_TRIANGLES, background_indices,
|
||||
layout.batch, layout.background_decoration_group,
|
||||
position=('f', background_vertices),
|
||||
colors=('Bn', background_colors),
|
||||
rotation=('f', (rotation,) * 4),
|
||||
anchor=('f', (anchor_x, anchor_y) * 4))
|
||||
rotation=('f', (rotation,) * bg_count),
|
||||
anchor=('f', (anchor_x, anchor_y) * bg_count))
|
||||
context.add_list(background_list)
|
||||
|
||||
if underline_vertices:
|
||||
|
@ -3,11 +3,13 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
from typing import Optional, Union, Callable
|
||||
|
||||
import pyglet
|
||||
from pyglet.customtypes import Buffer
|
||||
|
||||
|
||||
def asbytes(s):
|
||||
def asbytes(s: Union[str, Buffer]) -> bytes:
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
elif isinstance(s, str):
|
||||
@ -16,56 +18,68 @@ def asbytes(s):
|
||||
return bytes(s)
|
||||
|
||||
|
||||
def asbytes_filename(s):
|
||||
if isinstance(s, bytes):
|
||||
return s
|
||||
elif isinstance(s, str):
|
||||
return s.encode(encoding=sys.getfilesystemencoding())
|
||||
|
||||
|
||||
def asstr(s):
|
||||
def asstr(s: Optional[Union[str, Buffer]]) -> str:
|
||||
if s is None:
|
||||
return ''
|
||||
if isinstance(s, str):
|
||||
return s
|
||||
return s.decode("utf-8")
|
||||
return s.decode("utf-8") # type: ignore
|
||||
|
||||
|
||||
def debug_print(enabled_or_option='debug'):
|
||||
"""Get a debug printer that is enabled based on a boolean input or a pyglet option.
|
||||
The debug print function returned should be used in an assert. This way it can be
|
||||
optimized out when running python with the -O flag.
|
||||
# Keep these outside of the function since we don't need to re-define
|
||||
# the function each time we make a call since no state is persisted.
|
||||
def _debug_print_real(arg: str) -> bool:
|
||||
print(arg)
|
||||
return True
|
||||
|
||||
|
||||
def _debug_print_dummy(arg: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
def debug_print(pyglet_option_name: str = 'debug') -> Callable[[str], bool]:
|
||||
"""Get a debug printer controlled by the given ``pyglet.options`` name.
|
||||
|
||||
This allows repurposing ``assert`` to write cleaner, more efficient
|
||||
debug output:
|
||||
|
||||
#. Debug printers fit into a one-line ``assert`` statements instead
|
||||
of longer, slower key-lookup ``if`` statements
|
||||
#. Running Python with the ``-O`` flag makes pyglet run faster by
|
||||
skipping all ``assert`` statements
|
||||
|
||||
Usage example::
|
||||
|
||||
from pyglet.debug import debug_print
|
||||
from pyglet.util import debug_print
|
||||
|
||||
|
||||
_debug_media = debug_print('debug_media')
|
||||
|
||||
|
||||
def some_func():
|
||||
# Python will skip the line below when run with -O
|
||||
assert _debug_media('My debug statement')
|
||||
|
||||
:parameters:
|
||||
`enabled_or_options` : bool or str
|
||||
If a bool is passed, debug printing is enabled if it is True. If str is passed
|
||||
debug printing is enabled if the pyglet option with that name is True.
|
||||
# The rest of the function will run as normal
|
||||
...
|
||||
|
||||
For more information, please see `the Python command line
|
||||
documentation <https://docs.python.org/3/using/cmdline.html#cmdoption-O>`_.
|
||||
|
||||
Args:
|
||||
`pyglet_option_name` :
|
||||
The name of a key in :attr:`pyglet.options` to read the
|
||||
debug flag's value from.
|
||||
|
||||
Returns:
|
||||
A callable which prints a passed string and returns ``True``
|
||||
to allow auto-removal when running with ``-O``.
|
||||
|
||||
:returns: Function for debug printing.
|
||||
"""
|
||||
if isinstance(enabled_or_option, bool):
|
||||
enabled = enabled_or_option
|
||||
else:
|
||||
enabled = pyglet.options.get(enabled_or_option, False)
|
||||
|
||||
enabled = pyglet.options.get(pyglet_option_name, False)
|
||||
if enabled:
|
||||
def _debug_print(*args, **kwargs):
|
||||
print(*args, **kwargs)
|
||||
return True
|
||||
|
||||
else:
|
||||
def _debug_print(*args, **kwargs):
|
||||
return True
|
||||
|
||||
return _debug_print
|
||||
return _debug_print_real
|
||||
return _debug_print_dummy
|
||||
|
||||
|
||||
class CodecRegistry:
|
||||
|
@ -273,9 +273,10 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
||||
conventions. This will ensure it is not obscured by other windows,
|
||||
and appears on an appropriate screen for the user.
|
||||
|
||||
To render into a window, you must first call `switch_to`, to make
|
||||
it the current OpenGL context. If you use only one window in the
|
||||
application, there is no need to do this.
|
||||
To render into a window, you must first call its :py:meth:`.switch_to`
|
||||
method to make it the active OpenGL context. If you use only one
|
||||
window in your application, you can skip this step as it will always
|
||||
be the active context.
|
||||
"""
|
||||
|
||||
# Filled in by metaclass with the names of all methods on this (sub)class
|
||||
@ -638,7 +639,8 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
||||
"""Clear the window.
|
||||
|
||||
This is a convenience method for clearing the color and depth
|
||||
buffer. The window must be the active context (see `switch_to`).
|
||||
buffer. The window must be the active context (see
|
||||
:py:meth:`.switch_to`).
|
||||
"""
|
||||
gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_DEPTH_BUFFER_BIT)
|
||||
|
||||
@ -646,10 +648,12 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
||||
"""Close the window.
|
||||
|
||||
After closing the window, the GL context will be invalid. The
|
||||
window instance cannot be reused once closed (see also `set_visible`).
|
||||
window instance cannot be reused once closed. To re-use windows,
|
||||
see :py:meth:`.set_visible` instead.
|
||||
|
||||
The `pyglet.app.EventLoop.on_window_close` event is dispatched on
|
||||
`pyglet.app.event_loop` when this method is called.
|
||||
The :py:meth:`pyglet.app.EventLoop.on_window_close` event is
|
||||
dispatched by the :py:attr:`pyglet.app.event_loop` when this method
|
||||
is called.
|
||||
"""
|
||||
from pyglet import app
|
||||
if not self._context:
|
||||
@ -676,7 +680,7 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
||||
and advanced applications that must integrate their event loop
|
||||
into another framework.
|
||||
|
||||
Typical applications should use `pyglet.app.run`.
|
||||
Typical applications should use :py:func:`pyglet.app.run`.
|
||||
"""
|
||||
raise NotImplementedError('abstract')
|
||||
|
||||
@ -715,11 +719,14 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
||||
"""Swap the OpenGL front and back buffers.
|
||||
|
||||
Call this method on a double-buffered window to update the
|
||||
visible display with the back buffer. The contents of the back buffer
|
||||
is undefined after this operation.
|
||||
visible display with the back buffer. Windows are
|
||||
double-buffered by default unless you turn this feature off.
|
||||
|
||||
Windows are double-buffered by default. This method is called
|
||||
automatically by `EventLoop` after the :py:meth:`~pyglet.window.Window.on_draw` event.
|
||||
The contents of the back buffer are undefined after this operation.
|
||||
|
||||
The default :py:attr:`~pyglet.app.event_loop` automatically
|
||||
calls this method after the window's
|
||||
:py:meth:`~pyglet.window.Window.on_draw` event.
|
||||
"""
|
||||
raise NotImplementedError('abstract')
|
||||
|
||||
@ -791,6 +798,25 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def get_clipboard_text(self) -> str:
|
||||
"""Access the system clipboard and attempt to retrieve text.
|
||||
|
||||
:rtype: `str`
|
||||
:return: A string from the clipboard. String will be empty if no text found.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def set_clipboard_text(self, text: str):
|
||||
"""Access the system clipboard and set a text string as the clipboard data.
|
||||
|
||||
This will clear the existing clipboard.
|
||||
|
||||
:Parameters:
|
||||
`text` : str
|
||||
Text you want to place in the clipboard.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def minimize(self):
|
||||
"""Minimize the window.
|
||||
"""
|
||||
@ -1158,10 +1184,13 @@ class BaseWindow(EventDispatcher, metaclass=_WindowMetaclass):
|
||||
def switch_to(self):
|
||||
"""Make this window the current OpenGL rendering context.
|
||||
|
||||
Only one OpenGL context can be active at a time. This method sets
|
||||
the current window's context to be current. You should use this
|
||||
method in preference to `pyglet.gl.Context.set_current`, as it may
|
||||
perform additional initialisation functions.
|
||||
Only one OpenGL context can be active at a time. This method
|
||||
sets the current window context as the active one.
|
||||
|
||||
In most cases, you should use this method instead of directly
|
||||
calling :py:meth:`pyglet.gl.Context.set_current`. The latter
|
||||
will not perform platform-specific state management tasks for
|
||||
you.
|
||||
"""
|
||||
raise NotImplementedError('abstract')
|
||||
|
||||
|
@ -21,6 +21,7 @@ NSColor = cocoapy.ObjCClass('NSColor')
|
||||
NSEvent = cocoapy.ObjCClass('NSEvent')
|
||||
NSArray = cocoapy.ObjCClass('NSArray')
|
||||
NSImage = cocoapy.ObjCClass('NSImage')
|
||||
NSPasteboard = cocoapy.ObjCClass('NSPasteboard')
|
||||
|
||||
quartz = cocoapy.quartz
|
||||
cf = cocoapy.cf
|
||||
@ -567,5 +568,32 @@ class CocoaWindow(BaseWindow):
|
||||
NSApp = NSApplication.sharedApplication()
|
||||
NSApp.setPresentationOptions_(options)
|
||||
|
||||
def set_clipboard_text(self, text: str):
|
||||
with AutoReleasePool():
|
||||
pasteboard = NSPasteboard.generalPasteboard()
|
||||
|
||||
pasteboard.clearContents()
|
||||
|
||||
array = NSArray.arrayWithObject_(cocoapy.NSPasteboardTypeString)
|
||||
pasteboard.declareTypes_owner_(array, None)
|
||||
|
||||
text_nsstring = cocoapy.get_NSString(text)
|
||||
|
||||
pasteboard.setString_forType_(text_nsstring, cocoapy.NSPasteboardTypeString)
|
||||
|
||||
def get_clipboard_text(self) -> str:
|
||||
text = ''
|
||||
with AutoReleasePool():
|
||||
pasteboard = NSPasteboard.generalPasteboard()
|
||||
|
||||
if pasteboard.types().containsObject_(cocoapy.NSPasteboardTypeString):
|
||||
text_obj = pasteboard.stringForType_(cocoapy.NSPasteboardTypeString)
|
||||
if text_obj:
|
||||
text = text_obj.UTF8String().decode('utf-8')
|
||||
|
||||
return text
|
||||
|
||||
|
||||
|
||||
|
||||
__all__ = ["CocoaWindow"]
|
||||
|
@ -653,6 +653,43 @@ class Win32Window(BaseWindow):
|
||||
|
||||
return icon
|
||||
|
||||
def set_clipboard_text(self, text: str):
|
||||
valid = _user32.OpenClipboard(self._view_hwnd)
|
||||
if not valid:
|
||||
return
|
||||
|
||||
_user32.EmptyClipboard()
|
||||
|
||||
size = (len(text) + 1) * sizeof(WCHAR) # UTF-16
|
||||
|
||||
cb_data = _kernel32.GlobalAlloc(GMEM_MOVEABLE, size)
|
||||
locked_data = _kernel32.GlobalLock(cb_data)
|
||||
memmove(locked_data, text, size) # Trying to encode in utf-16 causes garbled text. Accepts str fine?
|
||||
_kernel32.GlobalUnlock(cb_data)
|
||||
|
||||
_user32.SetClipboardData(CF_UNICODETEXT, cb_data)
|
||||
|
||||
_user32.CloseClipboard()
|
||||
|
||||
def get_clipboard_text(self) -> str:
|
||||
text = ''
|
||||
|
||||
valid = _user32.OpenClipboard(self._view_hwnd)
|
||||
if not valid:
|
||||
print("Could not open clipboard")
|
||||
return ''
|
||||
|
||||
cb_obj = _user32.GetClipboardData(CF_UNICODETEXT)
|
||||
if cb_obj:
|
||||
locked_data = _kernel32.GlobalLock(cb_obj)
|
||||
if locked_data:
|
||||
text = ctypes.wstring_at(locked_data)
|
||||
|
||||
_kernel32.GlobalUnlock(cb_obj)
|
||||
|
||||
_user32.CloseClipboard()
|
||||
return text
|
||||
|
||||
# Private util
|
||||
|
||||
def _client_to_window_size(self, width, height):
|
||||
|
@ -4,6 +4,7 @@ import urllib.parse
|
||||
|
||||
from ctypes import *
|
||||
from functools import lru_cache
|
||||
from typing import Optional
|
||||
|
||||
import pyglet
|
||||
|
||||
@ -28,6 +29,7 @@ try:
|
||||
except ImportError:
|
||||
_have_xsync = False
|
||||
|
||||
_debug = pyglet.options['debug_x11']
|
||||
|
||||
class mwmhints_t(Structure):
|
||||
_fields_ = [
|
||||
@ -47,6 +49,7 @@ _can_detect_autorepeat = None
|
||||
|
||||
XA_CARDINAL = 6 # Xatom.h:14
|
||||
XA_ATOM = 4
|
||||
XA_STRING = 31
|
||||
|
||||
XDND_VERSION = 5
|
||||
|
||||
@ -142,6 +145,9 @@ class XlibWindow(BaseWindow):
|
||||
if _can_detect_autorepeat:
|
||||
self.pressed_keys = set()
|
||||
|
||||
# Store clipboard string to not query as much for pasting a lot.
|
||||
self._clipboard_str: Optional[str] = None
|
||||
|
||||
def _recreate(self, changes):
|
||||
# If flipping to/from fullscreen, need to recreate the window. (This
|
||||
# is the case with both override_redirect method and
|
||||
@ -287,6 +293,12 @@ class XlibWindow(BaseWindow):
|
||||
# Atoms required for Xdnd
|
||||
self._create_xdnd_atoms(self._x_display)
|
||||
|
||||
# Clipboard related atoms
|
||||
self._clipboard_atom = xlib.XInternAtom(self._x_display, asbytes('CLIPBOARD'), False)
|
||||
self._utf8_atom = xlib.XInternAtom(self._x_display, asbytes('UTF8_STRING'), False)
|
||||
self._target_atom = xlib.XInternAtom(self._x_display, asbytes('TARGETS'), False)
|
||||
self._incr_atom = xlib.XInternAtom(self._x_display, asbytes('INCR'), False)
|
||||
|
||||
# Support for drag and dropping files needs to be enabled.
|
||||
if self._file_drops:
|
||||
# Some variables set because there are 4 different drop events that need shared data.
|
||||
@ -798,6 +810,73 @@ class XlibWindow(BaseWindow):
|
||||
xlib.XChangeProperty(self._x_display, self._window, atom, XA_CARDINAL,
|
||||
32, xlib.PropModeReplace, buffer, len(data)//sizeof(c_ulong))
|
||||
|
||||
def set_clipboard_text(self, text: str):
|
||||
xlib.XSetSelectionOwner(self._x_display,
|
||||
self._clipboard_atom,
|
||||
self._window,
|
||||
xlib.CurrentTime)
|
||||
|
||||
if xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) == self._window:
|
||||
self._clipboard_str = text
|
||||
str_bytes = text.encode('utf-8')
|
||||
size = len(str_bytes)
|
||||
|
||||
xlib.XChangeProperty(self._x_display, self._window,
|
||||
self._clipboard_atom, self._utf8_atom, 8, xlib.PropModeReplace,
|
||||
(c_ubyte * size).from_buffer_copy(str_bytes), size)
|
||||
else:
|
||||
if _debug:
|
||||
print("X11: Couldn't become owner of clipboard.")
|
||||
|
||||
def get_clipboard_text(self) -> str:
|
||||
if self._clipboard_str is not None:
|
||||
return self._clipboard_str
|
||||
|
||||
owner = xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom)
|
||||
|
||||
if not owner:
|
||||
return ''
|
||||
|
||||
text = ''
|
||||
if owner == self._window:
|
||||
data, size, actual_atom = self.get_single_property(self._window, self._clipboard_atom,
|
||||
self._utf8_atom)
|
||||
else:
|
||||
notification = xlib.XEvent()
|
||||
|
||||
# Convert to selection notification.
|
||||
xlib.XConvertSelection(self._x_display,
|
||||
self._clipboard_atom,
|
||||
self._utf8_atom,
|
||||
self._clipboard_atom,
|
||||
self._window,
|
||||
xlib.CurrentTime)
|
||||
|
||||
while not xlib.XCheckTypedWindowEvent(self._x_display, self._window, xlib.SelectionNotify, byref(notification)):
|
||||
self.dispatch_platform_event(notification)
|
||||
|
||||
if not notification.xselection.property:
|
||||
return ''
|
||||
|
||||
data, size, actual_atom = self.get_single_property(notification.xselection.requestor, notification.xselection.property,
|
||||
self._utf8_atom)
|
||||
|
||||
if actual_atom == self._incr_atom:
|
||||
# Not implemented.
|
||||
if _debug:
|
||||
print("X11: Clipboard data is too large, not implemented.")
|
||||
|
||||
elif actual_atom == self._utf8_atom:
|
||||
if data:
|
||||
text_bytes = string_at(data, size)
|
||||
|
||||
text = text_bytes.decode('utf-8')
|
||||
|
||||
self._clipboard_str = text
|
||||
|
||||
xlib.XFree(data)
|
||||
return text
|
||||
|
||||
# Private utility
|
||||
|
||||
def _set_wm_normal_hints(self):
|
||||
@ -1328,7 +1407,7 @@ class XlibWindow(BaseWindow):
|
||||
xlib.XFree(data)
|
||||
|
||||
def get_single_property(self, window, atom_property, atom_type):
|
||||
""" Returns the length and data of a window property. """
|
||||
""" Returns the length, data, and actual atom of a window property. """
|
||||
actualAtom = xlib.Atom()
|
||||
actualFormat = c_int()
|
||||
itemCount = c_ulong()
|
||||
@ -1343,14 +1422,14 @@ class XlibWindow(BaseWindow):
|
||||
byref(bytesAfter),
|
||||
data)
|
||||
|
||||
return data, itemCount.value
|
||||
return data, itemCount.value, actualAtom.value
|
||||
|
||||
@XlibEventHandler(xlib.SelectionNotify)
|
||||
def _event_selection_notification(self, ev):
|
||||
if ev.xselection.property != 0 and ev.xselection.selection == self._xdnd_atoms['XdndSelection']:
|
||||
if self._xdnd_format:
|
||||
# This will get the data
|
||||
data, count = self.get_single_property(ev.xselection.requestor,
|
||||
data, count, _ = self.get_single_property(ev.xselection.requestor,
|
||||
ev.xselection.property,
|
||||
ev.xselection.target)
|
||||
|
||||
@ -1514,5 +1593,61 @@ class XlibWindow(BaseWindow):
|
||||
self._mapped = False
|
||||
self.dispatch_event('on_hide')
|
||||
|
||||
@XlibEventHandler(xlib.SelectionClear)
|
||||
def _event_selection_clear(self, ev):
|
||||
if ev.xselectionclear.selection == self._clipboard_atom:
|
||||
# Another application cleared the clipboard.
|
||||
self._clipboard_str = None
|
||||
|
||||
@XlibEventHandler(xlib.SelectionRequest)
|
||||
def _event_selection_request(self, ev):
|
||||
request = ev.xselectionrequest
|
||||
|
||||
if _debug:
|
||||
rt = xlib.XGetAtomName(self._x_display, request.target)
|
||||
rp = xlib.XGetAtomName(self._x_display, request.property)
|
||||
print(f"X11 debug: request target {rt}")
|
||||
print(f"X11 debug: request property {rp}")
|
||||
|
||||
out_event = xlib.XEvent()
|
||||
out_event.xany.type = xlib.SelectionNotify
|
||||
out_event.xselection.selection = request.selection
|
||||
out_event.xselection.display = request.display
|
||||
out_event.xselection.target = 0
|
||||
out_event.xselection.property = 0
|
||||
out_event.xselection.requestor = request.requestor
|
||||
out_event.xselection.time = request.time
|
||||
|
||||
if (xlib.XGetSelectionOwner(self._x_display, self._clipboard_atom) == self._window and
|
||||
ev.xselection.target == self._clipboard_atom):
|
||||
if request.target == self._target_atom:
|
||||
atoms_ar = (xlib.Atom * 1)(self._utf8_atom)
|
||||
ptr = cast(pointer(atoms_ar), POINTER(c_ubyte))
|
||||
|
||||
xlib.XChangeProperty(self._x_display, request.requestor,
|
||||
request.property, XA_ATOM, 32,
|
||||
xlib.PropModeReplace,
|
||||
ptr, sizeof(atoms_ar)//sizeof(c_ulong))
|
||||
out_event.xselection.property = request.property
|
||||
out_event.xselection.target = request.target
|
||||
|
||||
elif request.target == self._utf8_atom:
|
||||
# We are being requested for a UTF-8 string.
|
||||
text = self._clipboard_str.encode('utf-8')
|
||||
size = len(self._clipboard_str)
|
||||
xlib.XChangeProperty(self._x_display, request.requestor,
|
||||
request.property, request.target, 8,
|
||||
xlib.PropModeReplace,
|
||||
(c_ubyte * size).from_buffer_copy(text), size)
|
||||
|
||||
out_event.xselection.property = request.property
|
||||
out_event.xselection.target = request.target
|
||||
|
||||
# Send request event back to requestor with updated changes.
|
||||
xlib.XSendEvent(self._x_display, request.requestor, 0, 0, byref(out_event))
|
||||
|
||||
# Seems to work find without it. May add later.
|
||||
#xlib.XSync(self._x_display, False)
|
||||
|
||||
|
||||
__all__ = ["XlibEventHandler", "XlibWindow"]
|
||||
|
@ -1086,9 +1086,6 @@ def test_logger(the_logger: Logger):
|
||||
the_logger.debug('debugging')
|
||||
the_logger.info("Hello World!!")
|
||||
the_logger.info("Hello World!!")
|
||||
the_logger.info("Hello World!!")
|
||||
the_logger.info("Hello World!!")
|
||||
the_logger.info("Hello World!!")
|
||||
the_logger.warn('warning')
|
||||
the_logger.warn('warning')
|
||||
the_logger.error('error haaaa')
|
||||
@ -1111,9 +1108,9 @@ if __name__ == "__main__":
|
||||
a_logger.error('error haaaa')
|
||||
a_logger.fatal('oh no')
|
||||
logger.info('my name is:', logger.name)
|
||||
for _ in range(5):
|
||||
test_logger(logger)
|
||||
test_logger(a_logger)
|
||||
# for _ in range(5):
|
||||
test_logger(logger)
|
||||
test_logger(a_logger)
|
||||
print(Message_content(log_time=time.time(), text='aaa', level=4, marker='abc', end='abc', flush=False,
|
||||
frame=inspect.currentframe()))
|
||||
print(ColorCodeEnum.code_line.name)
|
||||
|
@ -1,7 +0,0 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
|
||||
from lib_not_dr.types.options import Options
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
# -------------------------------
|
||||
# Difficult Rocket
|
||||
# Copyright © 2020-2023 by shenjackyuanjie 3695888@qq.com
|
||||
# All rights reserved
|
||||
# -------------------------------
|
||||
|
||||
import time
|
||||
|
||||
from types import FrameType
|
||||
from typing import List, Optional
|
||||
|
||||
from lib_not_dr.types.options import Options
|
||||
|
||||
|
||||
class LogMessage(Options):
|
||||
name = 'LogMessage'
|
||||
|
||||
# 消息内容本身的属性
|
||||
messages: List[str] = []
|
||||
end: str = '\n'
|
||||
split: str = ' '
|
||||
|
||||
# 消息的属性
|
||||
flush: bool = True
|
||||
level: int = 20
|
||||
log_time: float = time.time_ns()
|
||||
logger_name: str = 'root'
|
||||
logger_tag: Optional[str] = None
|
||||
stack_trace: Optional[FrameType] = None
|
||||
|
||||
|
18
mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock
generated
18
mods/dr_game/Difficult_Rocket_rs/src/Cargo.lock
generated
@ -116,7 +116,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "difficult_rocket_rs"
|
||||
version = "0.2.22"
|
||||
version = "0.2.23"
|
||||
dependencies = [
|
||||
"pyo3",
|
||||
"quick-xml",
|
||||
@ -415,9 +415,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eff6510e86862b57b210fd8cbe8ed3f0d7d600b9c2863cd4549a2e033c66e956"
|
||||
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"serde",
|
||||
@ -497,9 +497,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.186"
|
||||
version = "1.0.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9f5db24220c009de9bd45e69fb2938f4b6d2df856aa9304ce377b3180f83b7c1"
|
||||
checksum = "91d3c334ca1ee894a2c6f6ad698fe8c435b76d504b13d436f0685d648d6d96f7"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
@ -518,9 +518,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.186"
|
||||
version = "1.0.190"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ad697f7e0b65af4983a4ce8f56ed5b357e8d3c36651bf6a7e13639c17b8e670"
|
||||
checksum = "67c5609f394e5c2bd7fc51efda478004ea80ef42fee983d5c67a65e34f32c0e3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -708,6 +708,6 @@ checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||
|
||||
[[package]]
|
||||
name = "xml-rs"
|
||||
version = "0.8.16"
|
||||
version = "0.8.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47430998a7b5d499ccee752b41567bc3afc57e1327dc855b1a2aa44ce29b5fa1"
|
||||
checksum = "0fcb9cbac069e033553e8bb871be2fbdffcab578eb25bd0f7c508cedc6dcd75a"
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "difficult_rocket_rs"
|
||||
version = "0.2.22"
|
||||
version = "0.2.23"
|
||||
edition = "2021"
|
||||
license-file = '../../LICENSE'
|
||||
authors = [
|
||||
@ -22,15 +22,15 @@ opt-level = "s"
|
||||
# codegen-units = 1
|
||||
|
||||
[dependencies.quick-xml]
|
||||
version = "0.30.0"
|
||||
version = "0.31.0"
|
||||
features = ["serialize"]
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1.0.186"
|
||||
version = "1.0.190"
|
||||
features = ["derive"]
|
||||
|
||||
[dependencies.xml-rs]
|
||||
version = "0.8.16"
|
||||
version = "0.8.19"
|
||||
|
||||
[dependencies.serde-xml-rs]
|
||||
version = "0.6.0"
|
||||
|
@ -16,7 +16,7 @@ from Difficult_Rocket.api.mod import ModInfo
|
||||
from Difficult_Rocket.client import ClientWindow
|
||||
from Difficult_Rocket.api.types import Options, Version
|
||||
|
||||
DR_rust_version = Version("0.2.22.0") # DR_mod 的 Rust 编写部分的兼容版本
|
||||
DR_rust_version = Version("0.2.23.0") # DR_mod 的 Rust 编写部分的兼容版本
|
||||
|
||||
logger = logging.getLogger('client.dr_game')
|
||||
|
||||
|
@ -47,14 +47,10 @@ if __name__ == '__main__':
|
||||
if '--report' in sys.argv:
|
||||
compiler.save_report = True
|
||||
sys.argv.remove('--report')
|
||||
|
||||
# 检测 --output xx 参数
|
||||
if '--output' in sys.argv:
|
||||
# 输入的是输出目录
|
||||
out_path = sys.argv[sys.argv.index('--output') + 1]
|
||||
compiler.output_path = Path(out_path)
|
||||
sys.argv.remove('--output')
|
||||
sys.argv.remove(out_path)
|
||||
|
||||
if '--lto=yes' in sys.argv:
|
||||
compiler.use_lto = True
|
||||
sys.argv.remove('--lto=yes')
|
||||
|
||||
# 检测 --no-pyglet-opt 参数
|
||||
pyglet_optimizations = True
|
||||
@ -88,6 +84,16 @@ if __name__ == '__main__':
|
||||
pprint(compiler.option())
|
||||
else:
|
||||
compiler.output_path = Path(f'./build/nuitka-{platform.system().lower()}')
|
||||
compiler.show_memory = False
|
||||
compiler.show_progress = False
|
||||
|
||||
# 检测 --output xx 参数
|
||||
if '--output' in sys.argv:
|
||||
# 输入的是输出目录
|
||||
out_path = sys.argv[sys.argv.index('--output') + 1]
|
||||
compiler.output_path = Path(out_path)
|
||||
sys.argv.remove('--output')
|
||||
sys.argv.remove(out_path)
|
||||
|
||||
print(compiler.as_markdown())
|
||||
|
||||
|
@ -4,23 +4,22 @@
|
||||
|
||||
# for images
|
||||
# not for pypy >= 3.10
|
||||
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
|
||||
# for sys info
|
||||
psutil >= 5.9.5
|
||||
psutil >= 5.9.6
|
||||
|
||||
# for files
|
||||
rtoml >= 0.9.0
|
||||
tomlkit >= 0.11.8
|
||||
tomlkit >= 0.12.1
|
||||
defusedxml >= 0.7.1
|
||||
|
||||
# for report error
|
||||
objprint >= 0.2.2
|
||||
objprint >= 0.2.3
|
||||
|
||||
# for compile
|
||||
nuitka >= 1.8.2
|
||||
ordered-set >= 4.1.0
|
||||
imageio >= 2.31.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
wheel >= 0.40.0
|
||||
setuptools >= 67.8.0
|
||||
setuptools-rust >= 1.6.0
|
||||
nuitka >= 1.8.5
|
||||
imageio >= 2.31.6; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
wheel >= 0.41.3
|
||||
setuptools >= 68.2.2
|
||||
setuptools-rust >= 1.8.1
|
@ -5,25 +5,24 @@
|
||||
|
||||
# for images
|
||||
# not for pypy >= 3.10
|
||||
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
|
||||
# for sys info
|
||||
psutil >= 5.9.5
|
||||
psutil >= 5.9.6
|
||||
|
||||
# for files
|
||||
rtoml >= 0.9.0
|
||||
tomlkit >= 0.11.8
|
||||
tomlkit >= 0.12.1
|
||||
defusedxml >= 0.7.1
|
||||
|
||||
# for debug
|
||||
objprint >= 0.2.2
|
||||
viztracer >= 0.15.6; platform_python_implementation != "PyPy"
|
||||
objprint >= 0.2.3
|
||||
viztracer >= 0.16.0; platform_python_implementation != "PyPy"
|
||||
vizplugins >= 0.1.3; platform_python_implementation != "PyPy"
|
||||
|
||||
# for compile
|
||||
nuitka >= 1.8.2
|
||||
ordered-set >= 4.1.0
|
||||
imageio >= 2.31.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
wheel >= 0.40.0
|
||||
setuptools >= 67.8.0
|
||||
setuptools-rust >= 1.6.0
|
||||
nuitka >= 1.8.5
|
||||
imageio >= 2.31.6; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
wheel >= 0.41.3
|
||||
setuptools >= 68.2.2
|
||||
setuptools-rust >= 1.8.1
|
||||
|
@ -3,15 +3,15 @@
|
||||
|
||||
# for images
|
||||
# not for pypy >= 3.10
|
||||
pillow >= 10.0.0; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
pillow >= 10.0.1; (platform_python_implementation == "PyPy" and python_version < "3.10") or platform_python_implementation == "CPython"
|
||||
|
||||
# for sys info
|
||||
psutil >= 5.9.5
|
||||
psutil >= 5.9.6
|
||||
|
||||
# for files
|
||||
rtoml >= 0.9.0
|
||||
tomlkit >= 0.11.8
|
||||
tomlkit >= 0.12.1
|
||||
defusedxml >= 0.7.1
|
||||
|
||||
# for report error
|
||||
objprint >= 0.2.2
|
||||
objprint >= 0.2.3
|
||||
|
Loading…
Reference in New Issue
Block a user