From 4ecef65b3158b5a09f90b556566c2422aa5d0743 Mon Sep 17 00:00:00 2001 From: Macros Meng Date: Sun, 26 Oct 2025 13:04:28 +0800 Subject: [PATCH 1/8] add converter for Lesson.start_time/end_time --- cses/structures.py | 16 ++++++++-------- cses/utils.py | 31 +++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/cses/structures.py b/cses/structures.py index 87f2cb9..a0f8cc1 100644 --- a/cses/structures.py +++ b/cses/structures.py @@ -8,9 +8,9 @@ from enum import Enum from collections import UserList from collections.abc import Sequence -from typing import override +from typing import override, Optional, Literal, Annotated -from pydantic import BaseModel, ValidationError +from pydantic import BaseModel, ValidationError, BeforeValidator import cses.utils as utils @@ -37,9 +37,9 @@ class Subject(BaseModel): 'A101' """ name: str - simplified_name: str = "" - teacher: str = "" - room: str = "" + simplified_name: Optional[str] = None + teacher: Optional[str] = None + room: Optional[str] = None class Lesson(BaseModel): @@ -62,8 +62,8 @@ class Lesson(BaseModel): datetime.time(8, 45) """ subject: Subject - start_time: datetime.time - end_time: datetime.time + start_time: Annotated[datetime.time, BeforeValidator(utils.ensure_time)] + end_time: Annotated[datetime.time, BeforeValidator(utils.ensure_time)] class WeekType(Enum): @@ -99,7 +99,7 @@ class SingleDaySchedule(BaseModel): >>> s.weeks """ - enable_day: int + enable_day: Literal[1, 2, 3, 4, 5, 6, 7] classes: list[Lesson] name: str weeks: WeekType diff --git a/cses/utils.py b/cses/utils.py index a45a33d..4df0213 100644 --- a/cses/utils.py +++ b/cses/utils.py @@ -2,6 +2,8 @@ 该模块包含了一些用于内部处理的辅助函数。您也可以在您的代码中独立调用这些函数。 """ import datetime +import re +from re import match def week_num(start_day: datetime.date, day: datetime.date) -> int: @@ -24,3 +26,32 @@ def week_num(start_day: datetime.date, day: datetime.date) -> int: 8 """ return (day - start_day).days // 7 + 1 + + +def ensure_time(any_time: str | int) -> datetime.time: + """ + 将时间字符串/整数值转换为 ``datetime.time`` 对象。 + + Args: + any_time (str | int): 时间字符串,格式为 ``HH:MM:SS`` 或一个表示一天中经过的秒数的整数。 + + Returns: + datetime.time: 对应的时间对象 + + Examples: + >>> convert_time("08:00") + datetime.time(8, 0) + >>> convert_time("13:30") + datetime.time(13, 30) + """ + pattern_for_str = re.compile(r"([01]\d|2[0-3]):([0-5]\d):([0-5]\d)") # CSES Schema 指定的时间格式 + + if isinstance(any_time, str): + if not (matched := pattern_for_str.match(any_time)): + raise ValueError(f"Invalid time format for CSES format: {any_time}") + else: + return datetime.time(*map(int, matched.groups())) + elif isinstance(any_time, int): + return datetime.time(any_time // 3600, (any_time // 60) % 60, any_time % 60) + else: + raise ValueError(f"Invalid time value for CSES format: {any_time}") From 043637e560a5d4faf77427c2dcebb1ab222c67ae Mon Sep 17 00:00:00 2001 From: Macros Meng Date: Sun, 26 Oct 2025 13:25:16 +0800 Subject: [PATCH 2/8] remove null line at last of errors.py --- cses/errors.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cses/errors.py b/cses/errors.py index 16e440b..5f08733 100644 --- a/cses/errors.py +++ b/cses/errors.py @@ -11,4 +11,3 @@ class ParseError(CSESError): class VersionError(CSESError): """解析 CSES 课程文件时,版本号错误抛出的异常。""" - From e2f9e42c9b50579b7c36b872972087b0ae12c9eb Mon Sep 17 00:00:00 2001 From: Macros Meng Date: Sun, 26 Oct 2025 13:28:38 +0800 Subject: [PATCH 3/8] add validator and convertor for Lesson.start_time/end_time --- cses/__init__.py | 41 ++++++++++++++++++++++++++--------------- cses/structures.py | 2 +- cses/utils.py | 32 +++++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 25 deletions(-) diff --git a/cses/__init__.py b/cses/__init__.py index 8434628..76dc9eb 100644 --- a/cses/__init__.py +++ b/cses/__init__.py @@ -15,7 +15,7 @@ class CSES: - ``subjects``: 科目列表,每个元素是一个 ``Subject`` 对象。 Examples: - >>> c = CSES(open('../cses_example.yaml', encoding='utf8').read()) + >>> c = CSES.from_file('../cses_example.yaml') >>> c.version # 只会为 1 1 >>> c.subjects # doctest: +NORMALIZE_WHITESPACE @@ -26,36 +26,35 @@ class CSES: """ - def __init__(self, content: str): + def __init__(self): """ - 初始化 CSES。 + 新建一个空CSES课表。 - Args: - content (str): CSES 课程文件的内容。 + .. warning:: 不应该直接调用 ``CSES()`` 构造函数, 而是应该使用 ``CSES.from_str()`` 工厂方法。 """ self.schedule = None self.version = None self.subjects = None - self._load(content) - - def _load(self, content: str): + @classmethod + def from_str(cls, content: str) -> 'CSES': """ - 从 ``content`` 加载 CSES 课程文件的内容。 + 从 ``content`` 新建一个 CSES 课表对象。 Args: content (str): CSES 课程文件的内容。 """ data = yaml.safe_load(content) + new_schedule = cls() # 版本处理&检查 - self.version = data['version'] - if self.version != 1: - raise err.VersionError(f'不支持的版本号: {self.version}') + new_schedule.version = data['version'] + if new_schedule.version != 1: + raise err.VersionError(f'不支持的版本号: {new_schedule.version}') # 科目处理&检查 try: - self.subjects = {s['name']: st.Subject(**s) for s in data['subjects']} + new_schedule.subjects = {s['name']: st.Subject(**s) for s in data['subjects']} except st.ValidationError as e: raise err.ParseError(f'科目数据有误: {data['subjects']}') from e @@ -68,11 +67,11 @@ def _load(self, content: str): for name, classes in schedule_classes.items(): for lesson in classes: built_lessons[name].append( - st.Lesson(**(lesson | {'subject': self.subjects[lesson['subject']]})) + st.Lesson(**(lesson | {'subject': new_schedule.subjects[lesson['subject']]})) ) # 从self.subjects中获取合法的Subject对象 # 从构造好的课程列表中构造课表 - self.schedule = [ + new_schedule.schedule = [ st.SingleDaySchedule( enable_day=day['enable_day'], classes=built_lessons[day['name']], @@ -84,3 +83,15 @@ def _load(self, content: str): except st.ValidationError as e: raise err.ParseError(f'课程数据有误: {data['schedules']}') from e + return new_schedule + + @classmethod + def from_file(cls, fp: str) -> 'CSES': + """ + 从路径 ``fp`` 中读取并新建一个 CSES 课表对象。 + + Args: + fp (str): CSES 课程文件的路径。 + """ + with open(fp, encoding='utf8') as f: + return CSES.from_str(f.read()) diff --git a/cses/structures.py b/cses/structures.py index a0f8cc1..6254a03 100644 --- a/cses/structures.py +++ b/cses/structures.py @@ -53,7 +53,7 @@ class Lesson(BaseModel): Examples: >>> l = Lesson(subject=Subject(name='语文', simplified_name='语', teacher='张三'), \ - start_time=datetime.time(8, 0, 0), end_time=datetime.time(8, 45, 0)) + start_time="08:00:00", end_time="08:45:00") >>> l.subject.name '语文' >>> l.start_time diff --git a/cses/utils.py b/cses/utils.py index 4df0213..27bfa4e 100644 --- a/cses/utils.py +++ b/cses/utils.py @@ -3,7 +3,13 @@ """ import datetime import re -from re import match +import logging + + +logging.basicConfig(level=logging.DEBUG, + format="[{asctime} - {module}:{lineno}] {levelname}: {message}", + style="{") +log = logging.getLogger(__name__) def week_num(start_day: datetime.date, day: datetime.date) -> int: @@ -25,33 +31,41 @@ def week_num(start_day: datetime.date, day: datetime.date) -> int: >>> week_num(datetime.date(2025, 9, 1), datetime.date(2025, 10, 24)) 8 """ - return (day - start_day).days // 7 + 1 + res = (day - start_day).days // 7 + 1 + log.debug(f"Calling week_num({start_day}, {day}) -> {res}") + return res -def ensure_time(any_time: str | int) -> datetime.time: +def ensure_time(any_time: str | int | datetime.time) -> datetime.time: """ 将时间字符串/整数值转换为 ``datetime.time`` 对象。 Args: - any_time (str | int): 时间字符串,格式为 ``HH:MM:SS`` 或一个表示一天中经过的秒数的整数。 + any_time (str | int | datetime.time): 时间字符串,格式为 ``HH:MM:SS`` 或一个表示一天中经过的秒数的整数。 Returns: datetime.time: 对应的时间对象 Examples: - >>> convert_time("08:00") + >>> ensure_time("08:00:00") datetime.time(8, 0) - >>> convert_time("13:30") - datetime.time(13, 30) + >>> ensure_time(10*3600 + 10*60 +10) # =36610 + datetime.time(10, 10, 10) """ + pattern_for_str = re.compile(r"([01]\d|2[0-3]):([0-5]\d):([0-5]\d)") # CSES Schema 指定的时间格式 - if isinstance(any_time, str): + if isinstance(any_time, str): # 使用regex处理字符串格式的时间 if not (matched := pattern_for_str.match(any_time)): raise ValueError(f"Invalid time format for CSES format: {any_time}") else: return datetime.time(*map(int, matched.groups())) - elif isinstance(any_time, int): + + elif isinstance(any_time, int): # 将秒数转换为时间对象 return datetime.time(any_time // 3600, (any_time // 60) % 60, any_time % 60) + + elif isinstance(any_time, datetime.time): # 已经是datetime.time对象,直接返回 + return any_time + else: raise ValueError(f"Invalid time value for CSES format: {any_time}") From c0b293d282ab699c1756bf3f0336bff268e30862 Mon Sep 17 00:00:00 2001 From: Macros Meng Date: Sun, 26 Oct 2025 13:38:51 +0800 Subject: [PATCH 4/8] add logger support --- cses/utils.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/cses/utils.py b/cses/utils.py index 27bfa4e..0c89499 100644 --- a/cses/utils.py +++ b/cses/utils.py @@ -4,12 +4,15 @@ import datetime import re import logging +from sys import stderr logging.basicConfig(level=logging.DEBUG, - format="[{asctime} - {module}:{lineno}] {levelname}: {message}", - style="{") + format="[{asctime} - {module}.{funcName}:{lineno}] {levelname}: {message}", + style="{", + stream=stderr) log = logging.getLogger(__name__) +log.info(f"Loaded logger: {log!r}") def week_num(start_day: datetime.date, day: datetime.date) -> int: @@ -53,19 +56,26 @@ def ensure_time(any_time: str | int | datetime.time) -> datetime.time: datetime.time(10, 10, 10) """ + log.debug(f"Validating & converting time: {any_time!r} (type: {type(any_time)})") + pattern_for_str = re.compile(r"([01]\d|2[0-3]):([0-5]\d):([0-5]\d)") # CSES Schema 指定的时间格式 if isinstance(any_time, str): # 使用regex处理字符串格式的时间 if not (matched := pattern_for_str.match(any_time)): - raise ValueError(f"Invalid time format for CSES format: {any_time}") + raise ValueError(f"Invalid time format for CSES format: {any_time!r}") else: - return datetime.time(*map(int, matched.groups())) + res = datetime.time(*map(int, matched.groups())) elif isinstance(any_time, int): # 将秒数转换为时间对象 - return datetime.time(any_time // 3600, (any_time // 60) % 60, any_time % 60) + res = datetime.time(any_time // 3600, (any_time // 60) % 60, any_time % 60) elif isinstance(any_time, datetime.time): # 已经是datetime.time对象,直接返回 - return any_time + log.debug("Got datetime.time any_time, no conversion needed.") + res = any_time else: + log.error(f"Unknown time type: {type(any_time)}, raising an error...") raise ValueError(f"Invalid time value for CSES format: {any_time}") + + log.debug(f"Returning time: {res!r}") + return res From d17b4b656d01fddca17711e08deacf7fcb386314 Mon Sep 17 00:00:00 2001 From: Macros Meng Date: Sun, 26 Oct 2025 18:17:51 +0800 Subject: [PATCH 5/8] add logs in parser & generator part 1 --- cses/__init__.py | 60 +++++++++++++++++++++++++++++++++++++++------- cses/structures.py | 35 +++++++++------------------ cses/utils.py | 30 ++++++++++++++++++----- cses_example.yaml | 9 ++++++- 4 files changed, 94 insertions(+), 40 deletions(-) diff --git a/cses/__init__.py b/cses/__init__.py index 76dc9eb..8de5110 100644 --- a/cses/__init__.py +++ b/cses/__init__.py @@ -1,8 +1,15 @@ """使用 ``CSES`` 类可以表示、解析一个 CSES 课程文件。""" +import datetime + import yaml import cses.structures as st import cses.errors as err +from cses.utils import log, repr_ +from cses import utils + + +yaml.add_representer(datetime.time, utils.serialize_time) class CSES: @@ -10,7 +17,7 @@ class CSES: 用来表示、解析一个 CSES 课程文件的类。 该类有如下属性: - - ``schedule``: 课程安排列表,每个元素是一个 ``SingleDaySchedule`` 对象。 + - ``schedules``: 课程安排列表,每个元素是一个 ``SingleDaySchedule`` 对象。 - ``version``: 课程文件的版本号。目前只能为 ``1`` ,参见 CSES 官方文档与 Schema 文件。 - ``subjects``: 科目列表,每个元素是一个 ``Subject`` 对象。 @@ -28,13 +35,13 @@ class CSES: def __init__(self): """ - 新建一个空CSES课表。 + 初始化一个空CSES课表。 .. warning:: 不应该直接调用 ``CSES()`` 构造函数, 而是应该使用 ``CSES.from_str()`` 工厂方法。 """ - self.schedule = None - self.version = None - self.subjects = None + self.version = 1 + self.subjects = {} + self.schedules = [] @classmethod def from_str(cls, content: str) -> 'CSES': @@ -44,22 +51,27 @@ def from_str(cls, content: str) -> 'CSES': Args: content (str): CSES 课程文件的内容。 """ + data = yaml.safe_load(content) new_schedule = cls() + log.info(f"Loading CSES schedules {repr_(content)}") # 版本处理&检查 + log.debug(f"Checking version: {data['version']}") new_schedule.version = data['version'] if new_schedule.version != 1: raise err.VersionError(f'不支持的版本号: {new_schedule.version}') # 科目处理&检查 try: + log.debug(f"Processing subjects: {repr_(data['subjects'])}") new_schedule.subjects = {s['name']: st.Subject(**s) for s in data['subjects']} except st.ValidationError as e: raise err.ParseError(f'科目数据有误: {data['subjects']}') from e # 课程处理&检查 schedules = data['schedules'] + log.debug(f"Processing schedules: {repr_(schedules)}") try: # 先构造课程列表,再构造课表 schedule_classes = {i['name']: i['classes'] for i in schedules} @@ -68,21 +80,24 @@ def from_str(cls, content: str) -> 'CSES': for lesson in classes: built_lessons[name].append( st.Lesson(**(lesson | {'subject': new_schedule.subjects[lesson['subject']]})) - ) # 从self.subjects中获取合法的Subject对象 + ) + log.debug(f"Built lessons: {repr_(built_lessons)}") # 从构造好的课程列表中构造课表 - new_schedule.schedule = [ + new_schedule.schedules = st.Schedule([ st.SingleDaySchedule( enable_day=day['enable_day'], classes=built_lessons[day['name']], name=day['name'], - weeks=st.WeekType(day['weeks']), + weeks=day['weeks'], ) for day in schedules - ] + ]) + log.debug(f"Built schedules: {repr_(new_schedule.schedules)}") except st.ValidationError as e: raise err.ParseError(f'课程数据有误: {data['schedules']}') from e + log.info(f"Created Schedule: {repr_(new_schedule)}") return new_schedule @classmethod @@ -95,3 +110,30 @@ def from_file(cls, fp: str) -> 'CSES': """ with open(fp, encoding='utf8') as f: return CSES.from_str(f.read()) + + def to_yaml(self) -> str: + """ + 将当前 CSES 课表对象转换为 YAML 字符串。 + + Returns: + str: 当前 CSES 课表对象的 YAML 字符串表示。 + """ + return yaml.dump(self._gen_dict(), + default_flow_style=False, + sort_keys=True, + allow_unicode=True, + indent=2) + + def _gen_dict(self) -> dict: + """ + 生成当前 CSES 课表对象的字典表示。 + + Returns: + dict: 当前 CSES 课表对象的字典表示。 + """ + # TODO: 输出时对Lesson.subject进行处理,只输出name + return { + 'version': self.version, + 'subjects': [subject.model_dump() for subject in self.subjects.values()], + 'schedules': [schedule.model_dump() for schedule in self.schedules], + } diff --git a/cses/structures.py b/cses/structures.py index 6254a03..bc59eed 100644 --- a/cses/structures.py +++ b/cses/structures.py @@ -5,7 +5,6 @@ .. caution:: 该模块中的数据结构仅用于表示课程结构(与其附属工具),不包含实际的读取/写入功能。 """ import datetime -from enum import Enum from collections import UserList from collections.abc import Sequence from typing import override, Optional, Literal, Annotated @@ -66,18 +65,6 @@ class Lesson(BaseModel): end_time: Annotated[datetime.time, BeforeValidator(utils.ensure_time)] -class WeekType(Enum): - """ - 周次类型。 - ALL: 适用于所有周 - ODD: 仅适用于单周 - EVEN: 仅适用于双周 - """ - ALL = "all" - ODD = "odd" - EVEN = "even" - - class SingleDaySchedule(BaseModel): """ 单日课程安排。 @@ -91,18 +78,18 @@ class SingleDaySchedule(BaseModel): Examples: >>> s = SingleDaySchedule(enable_day=1, classes=[Lesson(subject=Subject(name='语文', \ simplified_name='语', teacher='张三'), start_time=datetime.time(8, 0, 0), \ - end_time=datetime.time(8, 45, 0))], name='星期一', weeks=WeekType.ALL) + end_time=datetime.time(8, 45, 0))], name='星期一', weeks='all') >>> s.enable_day 1 >>> s.name '星期一' >>> s.weeks - + 'all' """ enable_day: Literal[1, 2, 3, 4, 5, 6, 7] classes: list[Lesson] name: str - weeks: WeekType + weeks: Literal['all', 'odd', 'even'] def is_enabled_on_week(self, week: int) -> bool: """ @@ -117,7 +104,7 @@ def is_enabled_on_week(self, week: int) -> bool: Examples: >>> s = SingleDaySchedule(enable_day=1, classes=[Lesson(subject=Subject(name='语文', \ simplified_name='语', teacher='张三'), start_time=datetime.time(8, 0, 0), \ - end_time=datetime.time(8, 45, 0))], name='星期一', weeks=WeekType.ODD) + end_time=datetime.time(8, 45, 0))], name='星期一', weeks='odd') >>> s.is_enabled_on_week(3) True >>> s.is_enabled_on_week(6) @@ -126,9 +113,9 @@ def is_enabled_on_week(self, week: int) -> bool: True """ return { - WeekType.ALL: True, # 适用于所有周 -> 永久启用 - WeekType.ODD: week % 2 == 1, # 单周 - WeekType.EVEN: week % 2 == 0 # 双周 + 'all': True, # 适用于所有周 -> 永久启用 + 'odd': week % 2 == 1, # 单周 + 'even': week % 2 == 0 # 双周 }[self.weeks] def is_enabled_on_day(self, start_day: datetime.date, day: datetime.date) -> bool: @@ -145,7 +132,7 @@ def is_enabled_on_day(self, start_day: datetime.date, day: datetime.date) -> boo Examples: >>> s = SingleDaySchedule(enable_day=1, classes=[Lesson(subject=Subject(name='语文', \ simplified_name='语', teacher='张三'), start_time=datetime.time(8, 0, 0), \ - end_time=datetime.time(8, 45, 0))], name='星期一', weeks=WeekType.ODD) + end_time=datetime.time(8, 45, 0))], name='星期一', weeks='odd') >>> s.is_enabled_on_day(datetime.date(2025, 9, 1), datetime.date(2025, 9, 4)) True >>> s.is_enabled_on_day(datetime.date(2025, 9, 1), datetime.date(2025, 9, 16)) @@ -161,7 +148,7 @@ class Schedule(UserList[SingleDaySchedule]): 存储每天课程安排的列表。列表会按照星期排序。 .. caution:: - 在访问一个Schedule中的项目时,注意索引从 1 开始,而不是从 0 开始。 + 在访问一个 ``Schedule`` 中的项目时,注意索引从 1 开始,而不是从 0 开始。 这是为了可以按照星期访问课表,而不是按照 Python 的逻辑,所以访问星期一的课表使用 ``schedule[1]`` 而不是 ``schedule[0]`` 。 若你想要以 Python 的逻辑访问课表,请使用 ``data`` 属性,如访问星期一的课表需要使用 ``schedule.data[0]`` 。 @@ -169,10 +156,10 @@ class Schedule(UserList[SingleDaySchedule]): >>> s = Schedule([ ... SingleDaySchedule(enable_day=1, classes=[Lesson(subject=Subject(name='语文', ... simplified_name='语', teacher='张三'), start_time=datetime.time(8, 0, 0), - ... end_time=datetime.time(8, 45, 0))], name='星期一', weeks=WeekType.ODD), + ... end_time=datetime.time(8, 45, 0))], name='星期一', weeks='odd'), ... SingleDaySchedule(enable_day=2, classes=[Lesson(subject=Subject(name='数学', ... simplified_name='数', teacher='李四'), start_time=datetime.time(9, 0, 0), - ... end_time=datetime.time(9, 45, 0))], name='星期二', weeks=WeekType.EVEN) + ... end_time=datetime.time(9, 45, 0))], name='星期二', weeks='even') ... ]) >>> s[1].enable_day 1 diff --git a/cses/utils.py b/cses/utils.py index 0c89499..825910f 100644 --- a/cses/utils.py +++ b/cses/utils.py @@ -4,16 +4,23 @@ import datetime import re import logging +import reprlib from sys import stderr +import yaml logging.basicConfig(level=logging.DEBUG, - format="[{asctime} - {module}.{funcName}:{lineno}] {levelname}: {message}", + format="[{asctime} - {module}.{funcName}:{lineno}] \t{levelname}:\t {message}", style="{", stream=stderr) log = logging.getLogger(__name__) log.info(f"Loaded logger: {log!r}") +repr_ = reprlib.Repr( + maxlevel=3, maxtuple=3, maxlist=3, maxarray=3, maxdict=3, maxset=3, maxfrozenset=3, maxdeque=3, + maxstring=30, maxlong=50, maxother=30, fillvalue=' ... ', indent=None +).repr + def week_num(start_day: datetime.date, day: datetime.date) -> int: """ @@ -55,9 +62,6 @@ def ensure_time(any_time: str | int | datetime.time) -> datetime.time: >>> ensure_time(10*3600 + 10*60 +10) # =36610 datetime.time(10, 10, 10) """ - - log.debug(f"Validating & converting time: {any_time!r} (type: {type(any_time)})") - pattern_for_str = re.compile(r"([01]\d|2[0-3]):([0-5]\d):([0-5]\d)") # CSES Schema 指定的时间格式 if isinstance(any_time, str): # 使用regex处理字符串格式的时间 @@ -70,12 +74,26 @@ def ensure_time(any_time: str | int | datetime.time) -> datetime.time: res = datetime.time(any_time // 3600, (any_time // 60) % 60, any_time % 60) elif isinstance(any_time, datetime.time): # 已经是datetime.time对象,直接返回 - log.debug("Got datetime.time any_time, no conversion needed.") res = any_time else: log.error(f"Unknown time type: {type(any_time)}, raising an error...") raise ValueError(f"Invalid time value for CSES format: {any_time}") - log.debug(f"Returning time: {res!r}") + log.debug(f"Time validation & conversion: {any_time!r} (type: {type(any_time)}) -> {res!r}") return res + + +def serialize_time(dumper: yaml.representer.BaseRepresenter, any_time: datetime.time) -> yaml.nodes.ScalarNode: + """ + 适用于 ``datetime.time`` 对象的PyYAML钩子。 + + Args: + dumper: PyYAML Dumper对象,用于序列化 + any_time (datetime.time): 要转换的时间对象 + + Returns: + str: 对应的时间字符串,格式为 ``HH:MM:SS`` + """ + res = any_time.strftime("%H:%M:%S") + return dumper.represent_scalar('tag:yaml.org,2002:str', res) diff --git a/cses_example.yaml b/cses_example.yaml index d2fab62..20f1ee6 100644 --- a/cses_example.yaml +++ b/cses_example.yaml @@ -48,4 +48,11 @@ schedules: end_time: "09:00:00" - subject: 英语 start_time: "09:00:00" - end_time: "10:00:00" \ No newline at end of file + end_time: "10:00:00" + - name: HackerTest + enable_day: 7 + weeks: all + classes: + - subject: 英语 + start_time: 00:01:10 + end_time: 11:01:01 \ No newline at end of file From 1597c207db469d619579d39dcc1644bf5861fdec Mon Sep 17 00:00:00 2001 From: Macros Meng Date: Fri, 31 Oct 2025 19:24:30 +0800 Subject: [PATCH 6/8] remove logs in functions of utils.py --- cses/utils.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/cses/utils.py b/cses/utils.py index 825910f..d319594 100644 --- a/cses/utils.py +++ b/cses/utils.py @@ -42,7 +42,6 @@ def week_num(start_day: datetime.date, day: datetime.date) -> int: 8 """ res = (day - start_day).days // 7 + 1 - log.debug(f"Calling week_num({start_day}, {day}) -> {res}") return res @@ -80,7 +79,6 @@ def ensure_time(any_time: str | int | datetime.time) -> datetime.time: log.error(f"Unknown time type: {type(any_time)}, raising an error...") raise ValueError(f"Invalid time value for CSES format: {any_time}") - log.debug(f"Time validation & conversion: {any_time!r} (type: {type(any_time)}) -> {res!r}") return res From 0adced3a600f76e54af7f00183e1205e01480405 Mon Sep 17 00:00:00 2001 From: Macros Meng Date: Fri, 31 Oct 2025 20:00:23 +0800 Subject: [PATCH 7/8] disable alias of YAML and add serializer for Lesson.start_time / end_time --- cses/__init__.py | 6 ++++-- cses/structures.py | 10 +++++++--- cses/utils.py | 8 ++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/cses/__init__.py b/cses/__init__.py index 8de5110..f33a5e7 100644 --- a/cses/__init__.py +++ b/cses/__init__.py @@ -10,6 +10,7 @@ yaml.add_representer(datetime.time, utils.serialize_time) +log.info("cseslib4py initialized!") class CSES: @@ -120,9 +121,10 @@ def to_yaml(self) -> str: """ return yaml.dump(self._gen_dict(), default_flow_style=False, - sort_keys=True, + sort_keys=False, allow_unicode=True, - indent=2) + indent=2, + Dumper=utils.NoAliasDumper) def _gen_dict(self) -> dict: """ diff --git a/cses/structures.py b/cses/structures.py index bc59eed..fd0a1e4 100644 --- a/cses/structures.py +++ b/cses/structures.py @@ -9,7 +9,7 @@ from collections.abc import Sequence from typing import override, Optional, Literal, Annotated -from pydantic import BaseModel, ValidationError, BeforeValidator +from pydantic import BaseModel, ValidationError, BeforeValidator, field_serializer import cses.utils as utils @@ -47,8 +47,8 @@ class Lesson(BaseModel): Args: subject (Subject): 课程的科目 - start_time (datetime.time): 开始的时间 - end_time (datetime.time): 结束的时间 + start_time (str | int | datetime.time): 开始的时间(若输入为 ``str`` 或 ``int`` ,则会转化为datetime.time对象) + end_time (str | int | datetime.time): 结束的时间(若输入为 ``str`` 或 ``int`` ,则会转化为datetime.time对象) Examples: >>> l = Lesson(subject=Subject(name='语文', simplified_name='语', teacher='张三'), \ @@ -64,6 +64,10 @@ class Lesson(BaseModel): start_time: Annotated[datetime.time, BeforeValidator(utils.ensure_time)] end_time: Annotated[datetime.time, BeforeValidator(utils.ensure_time)] + @field_serializer("start_time", "end_time") + def serialize_time(self, time: datetime.time) -> str: + return time.strftime("%H:%M:%S") + class SingleDaySchedule(BaseModel): """ diff --git a/cses/utils.py b/cses/utils.py index d319594..9e29f02 100644 --- a/cses/utils.py +++ b/cses/utils.py @@ -95,3 +95,11 @@ def serialize_time(dumper: yaml.representer.BaseRepresenter, any_time: datetime. """ res = any_time.strftime("%H:%M:%S") return dumper.represent_scalar('tag:yaml.org,2002:str', res) + + +class NoAliasDumper(yaml.Dumper): + """ + 禁用PyYAML的别名功能,确保每个对象都被序列化。这样就不会出现 ``&id001`` 等类似的引用。 + """ + def ignore_aliases(self, data): + return True From 860bb8128600bee8fecaeff24be46795cff37401 Mon Sep 17 00:00:00 2001 From: Macros Meng Date: Fri, 31 Oct 2025 20:47:50 +0800 Subject: [PATCH 8/8] fix Lesson constructing and add function to_file() & __eq__ --- cses/__init__.py | 38 +++++++++++++++++++++++++++++--------- cses/structures.py | 12 +++++++----- cses/utils.py | 4 ++++ 3 files changed, 40 insertions(+), 14 deletions(-) diff --git a/cses/__init__.py b/cses/__init__.py index f33a5e7..5dcaf06 100644 --- a/cses/__init__.py +++ b/cses/__init__.py @@ -1,5 +1,6 @@ """使用 ``CSES`` 类可以表示、解析一个 CSES 课程文件。""" import datetime +import os import yaml @@ -8,7 +9,6 @@ from cses.utils import log, repr_ from cses import utils - yaml.add_representer(datetime.time, utils.serialize_time) log.info("cseslib4py initialized!") @@ -80,7 +80,7 @@ def from_str(cls, content: str) -> 'CSES': for name, classes in schedule_classes.items(): for lesson in classes: built_lessons[name].append( - st.Lesson(**(lesson | {'subject': new_schedule.subjects[lesson['subject']]})) + st.Lesson(**lesson) ) log.debug(f"Built lessons: {repr_(built_lessons)}") @@ -119,12 +119,27 @@ def to_yaml(self) -> str: Returns: str: 当前 CSES 课表对象的 YAML 字符串表示。 """ - return yaml.dump(self._gen_dict(), - default_flow_style=False, - sort_keys=False, - allow_unicode=True, - indent=2, - Dumper=utils.NoAliasDumper) + res = yaml.dump(self._gen_dict(), + default_flow_style=False, + sort_keys=False, + allow_unicode=True, + indent=2, + Dumper=utils.NoAliasDumper) + log.debug(f"Generated YAML: {repr_(res)}") + return res + + def to_file(self, fp: str, mode: str = 'w'): + """ + 将当前 CSES 课表对象转换为 YAML CSES 课程CSES入路径 ``fp`` 中。若文件夹/文件不存在,则会自动创建。 + + Args: + fp (str): 要写入的文件路径。 + mode (str, optional): 写入模式,默认值为 ``'w'`` ,即覆盖写入。 + """ + os.makedirs(os.path.dirname(fp), exist_ok=True) + with open(fp, mode, encoding='utf8') as f: + f.write(self.to_yaml()) + log.info(f"Written CSES schedule file to {repr_(fp)}.") def _gen_dict(self) -> dict: """ @@ -133,9 +148,14 @@ def _gen_dict(self) -> dict: Returns: dict: 当前 CSES 课表对象的字典表示。 """ - # TODO: 输出时对Lesson.subject进行处理,只输出name return { 'version': self.version, 'subjects': [subject.model_dump() for subject in self.subjects.values()], 'schedules': [schedule.model_dump() for schedule in self.schedules], } + + def __eq__(self, other): + if isinstance(other, type(self)): + return self._gen_dict() == other._gen_dict() + else: + return NotImplemented diff --git a/cses/structures.py b/cses/structures.py index fd0a1e4..d317d8c 100644 --- a/cses/structures.py +++ b/cses/structures.py @@ -46,21 +46,23 @@ class Lesson(BaseModel): 单节课程。 Args: - subject (Subject): 课程的科目 + subject (str): 课程的科目,应与 ``Subject`` 中之一的 ``name`` 属性相同 start_time (str | int | datetime.time): 开始的时间(若输入为 ``str`` 或 ``int`` ,则会转化为datetime.time对象) end_time (str | int | datetime.time): 结束的时间(若输入为 ``str`` 或 ``int`` ,则会转化为datetime.time对象) + .. warning:: + ``start_time`` 与 ``end_time`` 均为 ``datetime.time`` 对象,即使输入为(合法的) ``str`` (针对时间文字) 或 ``int`` (针对一天中的秒数)。 + Examples: - >>> l = Lesson(subject=Subject(name='语文', simplified_name='语', teacher='张三'), \ - start_time="08:00:00", end_time="08:45:00") - >>> l.subject.name + >>> l = Lesson(subject='语文', start_time="08:00:00", end_time="08:45:00") + >>> l.subject '语文' >>> l.start_time datetime.time(8, 0) >>> l.end_time datetime.time(8, 45) """ - subject: Subject + subject: str start_time: Annotated[datetime.time, BeforeValidator(utils.ensure_time)] end_time: Annotated[datetime.time, BeforeValidator(utils.ensure_time)] diff --git a/cses/utils.py b/cses/utils.py index 9e29f02..e989693 100644 --- a/cses/utils.py +++ b/cses/utils.py @@ -103,3 +103,7 @@ class NoAliasDumper(yaml.Dumper): """ def ignore_aliases(self, data): return True + + def increase_indent(self, flow=False, indentless=False): + # 确保列表项使用两个空格缩进 + return super().increase_indent(flow, False)