diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index d2ef088..0000000 --- a/.coveragerc +++ /dev/null @@ -1,17 +0,0 @@ -[run] -omit = - borax\ui\*.py - borax\apps\festival_creator.py - borax\calendars\datepicker.py - borax\calendars\ui.py -[report] -exclude_lines = - pragma: no cover - def __repr__ - if self.debug: - if settings.DEBUG - raise AssertionError - raise NotImplementedError - if 0: - if __name__ == .__main__.: - def print_(self): \ No newline at end of file diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index b766dcd..fabc813 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.7', '3.8', '3.9', '3.10', '3.11'] + python-version: ['3.9', '3.10', '3.11', '3.12'] steps: - uses: actions/checkout@v2 @@ -42,6 +42,7 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@v1 with: + token: ${{secrets.CODECOV_TOKEN}} file: ./coverage.xml env_vars: OS,PYTHON name: codecov-umbrella diff --git a/LICENSE b/LICENSE index 4c517bc..c1008e1 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2022 kinegratii +Copyright (c) 2015-2024 kinegratii Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in diff --git a/MANIFEST.in b/MANIFEST.in index d736d8e..eb3509e 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1,2 @@ -include borax/calendars/FestivalData.csv +include borax/calendars/dataset/FestivalData.csv include borax/calendars/dataset/festivals_ext1.csv \ No newline at end of file diff --git a/README.md b/README.md index 68a150c..d70e03d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Borax - python3工具库 - 中国农历/中文数字/设计模式/树形结构 +# Borax - python农历&节日工具库 - 中文数字/设计模式/树形结构 [![PyPI](https://img.shields.io/pypi/v/borax.svg)](https://pypi.org/project/borax) @@ -33,10 +33,11 @@ Borax 是一个Python3工具集合库。包括了以下几个话题: Borax 的 python 版本要求如下 -| borax 版本 | python版本 | -| ------ | ------ | -| 4.x | 3.7+ | -| 3.x | 3.5+ | +| borax 版本 | python版本 | 维护状态 | +| ------ | ------ | ------ | +| 4.1.x | 3.9+ | 维护开发 | +| 4.0.0 | 3.7+ | 维护至2024年12月31日 | +| 3.x | 3.5+ | 不再维护 | 可以通过 *pip* 安装 : @@ -49,7 +50,7 @@ $ pip install borax Borax的版本符合 [语义化版本](https://semver.org/lang/zh-CN/) ,格式为 `<主版本号>.<副版本号>.<修正版本号>`, 推荐使用下面方式定义Borax的依赖版本号。 ```text -borax~=3.5 +borax~=4.1 ``` ## 使用示例 (Usage) @@ -58,7 +59,7 @@ borax~=3.5 一个支持1900-2100年的农历日期工具库。 -> 本模块的数据和算法参考自项目 [jjonline/calendar.js](https://github.com/jjonline/calendar.js) 。 +> 本模块的数据和算法参考自项目 [jjonline/calendar.js](https://github.com/jjonline/calendar.js) ,部分算法和数据有所修改。 创建日期,日期推算 @@ -66,7 +67,7 @@ borax~=3.5 from datetime import timedelta from borax.calendars import LunarDate -# 获取今天的农历日期(农历2018年七月初一) +# 获取今天的农历日期(农历二〇一八年七月初一) print(LunarDate.today()) # LunarDate(2018, 7, 1, 0) # 将公历日期转化为农历日期 @@ -99,7 +100,7 @@ festival = LunarFestival(month=1, day=1) print(festival.description) # '农历每年正月初一' # 下一次春节的具体日期以及距离天数 -print(festival.countdown()) # (273, ) +print(festival.countdown()) # (273, ) # 接下来5个春节的日期 ['2022-02-01(二〇二二年正月初一)', '2023-01-22(二〇二三年正月初一)', '2024-02-10(二〇二四年正月初一)', '2025-01-29(二〇二五年正月初一)', '2026-02-17(二〇二六年正月初一)'] print([str(wd) for wd in festival.list_days(start_date=date.today(), count=5)]) @@ -202,13 +203,15 @@ print(FinanceNumbers.to_capital_str(decimal.Decimal(4.50))) # '肆元伍角零 ## 文档 (Document) -文档由 [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) 构建,另外备用文档使用 [docsify](https://docsify.js.org/) 构建。 +文档由 [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/) 构建 ~~,另外备用文档使用 [docsify](https://docsify.js.org/) 构建~~ 。 + +> 2024年1月起,仅保留 Read the Docs 文档源。 | 源 | 网址 | | ---- | ---- | | read-the-docs | [https://borax.readthedocs.io/zh_CN/latest/](https://borax.readthedocs.io/zh_CN/latest/) | -| github | [https://kinegratii.github.io/borax](https://kinegratii.github.io/borax) | -| gitee | [https://kinegratii.gitee.io/borax](https://kinegratii.gitee.io/borax) | +| github(已废弃) | [https://kinegratii.github.io/borax](https://kinegratii.github.io/borax) | +| gitee(已废弃) | [https://kinegratii.gitee.io/borax](https://kinegratii.gitee.io/borax) | ## 开发特性和规范 (Development Features) @@ -217,6 +220,15 @@ print(FinanceNumbers.to_capital_str(decimal.Decimal(4.50))) # '肆元伍角零 - [x] [nose2](https://pypi.org/project/nose2/) | [pytest](https://docs.pytest.org/en/latest/) - [x] [Github Action](https://github.com/kinegratii/borax/actions) - [x] [Code Coverage](https://codecov.io/) +- [x] [pyproject.toml build tools](https://packaging.python.org/) + +## 项目构建 (Project Build) + +从4.1.0 开始,borax 使用 *pyproject.toml* 作为项目构建的配置文件,使用以下命令构建 wheel 发行包。 + +```shell +python -m build -w +``` ## 开源协议 (License) diff --git a/borax/__init__.py b/borax/__init__.py index 17f8df3..c3df011 100644 --- a/borax/__init__.py +++ b/borax/__init__.py @@ -1,2 +1,2 @@ -__version__ = '4.0.0' +__version__ = '4.1.0' __author__ = 'kinegratii' diff --git a/borax/calendars/FestivalData.csv b/borax/calendars/dataset/FestivalData.csv similarity index 100% rename from borax/calendars/FestivalData.csv rename to borax/calendars/dataset/FestivalData.csv diff --git a/borax/calendars/dataset/__init__.py b/borax/calendars/dataset/__init__.py new file mode 100644 index 0000000..5226672 --- /dev/null +++ b/borax/calendars/dataset/__init__.py @@ -0,0 +1,16 @@ +from pathlib import Path + +__all__ = ['get_festival_dataset_path'] + +_FILE_DICT = { + 'basic': 'FestivalData.csv', + 'ext1': 'festivals_ext1.csv', + 'zh-Hans': 'FestivalData.csv' +} + + +def get_festival_dataset_path(identifier: str) -> Path: + """Return the full path for festival dataset csv file.""" + if identifier not in _FILE_DICT: + raise ValueError(f'Festival Dataset {identifier} not found!') + return Path(__file__).parent / _FILE_DICT.get(identifier) diff --git a/borax/calendars/festivals2.py b/borax/calendars/festivals2.py index e4cf839..e402ab5 100644 --- a/borax/calendars/festivals2.py +++ b/borax/calendars/festivals2.py @@ -4,11 +4,13 @@ import collections import csv import enum +import warnings from datetime import date, timedelta, datetime +from functools import cached_property from pathlib import Path -import warnings -from typing import List, Tuple, Optional, Union, Iterator, Set, Generator, Sequence +from typing import List, Tuple, Optional, Union, Iterator, Set, Generator, Sequence, Literal +from borax.calendars.dataset import get_festival_dataset_path from borax.calendars.lunardate import LunarDate, LCalendars, TermUtils, TextUtils, TERMS_CN __all__ = [ @@ -16,17 +18,31 @@ 'FreqConst', 'Festival', 'FestivalSchema', 'SolarFestival', 'LunarFestival', 'WeekFestival', 'TermFestival', 'encode', 'decode', 'decode_festival', - 'FestivalLibrary', + 'FestivalLibrary', 'ConditionUtils', 'FestivalDatasetNotExist' ] MixedDate = Union[date, LunarDate] +# Public Constants + class FreqConst: YEARLY = 0 MONTHLY = 1 +class FestivalCatalog: + basic = 'basic' + event = 'event' + life = 'life' + public = 'public' + tradition = 'tradition' + term = 'term' + other = 'other' + + CATALOGS = ['basic', 'term', 'public', 'tradition', 'event', 'life', 'other'] + + # Private Global Variables _IGNORE_LEAP_MONTH = 3 @@ -47,31 +63,27 @@ class FestivalSchema(enum.IntEnum): TERM = 4 -class FestivalCatalog: - basic = 'basic' - event = 'event' - life = 'life' - public = 'public' - tradition = 'tradition' - term = 'term' - other = 'other' - - CATALOGS = ['basic', 'term', 'public', 'tradition', 'event', 'life', 'other'] - - class WrappedDate: """A date object with solar and lunar calendars.""" - __slots__ = ['solar', 'lunar', 'name', '_fl'] + __slots__ = ['_solar', '_lunar', 'name', '_fl'] def __init__(self, date_obj: MixedDate, name: str = ''): - self.solar = LCalendars.cast_date(date_obj, date) - self.lunar = LCalendars.cast_date(date_obj, LunarDate) + self._solar = LCalendars.cast_date(date_obj, date) + self._lunar = LCalendars.cast_date(date_obj, LunarDate) self.name = name if isinstance(date_obj, date): self._fl = 's' else: self._fl = 'l' + @property + def solar(self): + return self._solar + + @property + def lunar(self): + return self._lunar + def __iter__(self): yield self.solar yield self.lunar @@ -118,13 +130,6 @@ def __hash__(self): def __eq__(self, other): return isinstance(self, type(other)) and self.__key() == other.__key() - def __getstate__(self): - return self.__key() - - def __setstate__(self, state): - _year, _month, _day = state - self.solar = date(_year, _month, _day) - def full_str(self): return f'{self.solar}({self.lunar.cn_str()})' @@ -146,11 +151,11 @@ def from_simple_str(cls, date_str: str) -> 'WrappedDate': def encode(self) -> str: if self._fl == 'l': festival = LunarFestival(month=self.lunar.month, day=self.lunar.day, leap=self.lunar.leap) - encoded_str = festival.encode() + encoded_str = festival.code return '{}{:04d}{}'.format(encoded_str[0], self.lunar.year, encoded_str[1:]) else: festival = SolarFestival(month=self.solar.month, day=self.solar.day) - encoded_str = festival.encode() + encoded_str = festival.code return '{}{:04d}{}'.format(encoded_str[0], self.solar.year, encoded_str[1:]) @classmethod @@ -228,6 +233,10 @@ def set_name(self, name): self._name = name return self + @cached_property + def code(self): + return self.encode() + @property def schema(self): return self._schema @@ -961,6 +970,10 @@ def description_contains(festival: Festival, description_contains: str): return description_contains in festival.description +class FestivalDatasetNotExist(Exception): + pass + + class FestivalLibrary(collections.UserList): """A festival collection. @@ -977,19 +990,22 @@ def print_(self): def get_code_set(self) -> Set[str]: """Get codes for all festivals. """ - return set([f.encode() for f in self.data]) + return set([f.code for f in self.data]) - def extend_unique(self, other): + def extend_unique( + self, + other: Union[collections.UserList, List[Union[str, Festival]], 'FestivalLibrary'] + ) -> 'FestivalLibrary': """Add a new festival if code does not exist. """ - f_codes = {f.encode() for f in self.data} + f_codes = {f.code for f in self.data} if isinstance(other, collections.UserList): new_data = other.data else: new_data = other for item in new_data: if isinstance(item, Festival): - if item.encode() not in f_codes: + if item.code not in f_codes: self.data.append(item) elif isinstance(item, str): try: @@ -1000,6 +1016,9 @@ def extend_unique(self, other): pass return self + def extend_term_festivals(self): + return self.extend_unique([f'400{i:02d}0' for i in range(24)]) + def delete_by_indexes(self, indexes: list): """Delete items by indexes.""" index_list = sorted(indexes, reverse=True) @@ -1111,7 +1130,8 @@ def list_days(self, start_date=None, end_date=None): return data_items def iter_month_daytuples(self, year: int, month: int, firstweekday: int = 0, return_pos: bool = False): - """迭代返回公历月份(含前后完整日期)中每个日期信息 + """return all day info for a whole solar month as (day_integer, day_text, wrapped_date) + The day_text show in the order:festival_name,term_name, lunar_day_text """ row = 0 cal = calendar.Calendar(firstweekday=firstweekday) @@ -1151,9 +1171,9 @@ def to_csv(self, path_or_buf): fileobj = path_or_buf.open('w', encoding='utf8', newline='') else: fileobj = path_or_buf - writer = csv.writer(fileobj, ) + writer = csv.writer(fileobj) for festival in self: - row = (festival.encode(), festival.name, festival.catalog) + row = (festival.code, festival.name, festival.catalog) writer.writerow(row) @classmethod @@ -1178,7 +1198,7 @@ def load_file(cls, file_path: Union[str, Path], unique: bool = False) -> 'Festiv fl.append(festival) except ValueError: continue - fl.sort(key=lambda x: x.encode()) + fl.sort(key=lambda x: x.code) return fl def filter(self, catalogs: Sequence = None) -> 'FestivalLibrary': @@ -1195,21 +1215,35 @@ def filter(self, catalogs: Sequence = None) -> 'FestivalLibrary': def load_term_festivals(self): """Add 24-term festivals.""" - return self.extend_unique([f'400{i:02d}0' for i in range(24)]) + warnings.warn('This function is deprecated. Use extend_term_festivals instead.', DeprecationWarning) + return self.extend_term_festivals() @classmethod - def load_builtin(cls, identifier: str = 'basic') -> 'FestivalLibrary': + def load_builtin(cls, identifier: Literal['basic', 'empty', 'ext1', 'zh-Hans'] = 'basic') -> 'FestivalLibrary': """Load builtin library in borax project. Available Identifiers: basic, zh-Hans, ext1, empty """ if identifier == 'empty': return FestivalLibrary() - if identifier == 'zh-Hans': # Old identifier - identifier = 'basic' - file_dict = { - 'basic': 'FestivalData.csv', - 'ext1': 'dataset/festivals_ext1.csv' - } - file_path = Path(__file__).parent / file_dict.get(identifier) - return cls.load_file(file_path) + if identifier == 'zh-Hans': + warnings.warn('identifier "zh-Hans" is deprecated.Use "basic" instead. ', DeprecationWarning) + return cls.load_file(get_festival_dataset_path(identifier)) + + @classmethod + def load(cls, identifier_or_path: Union[str, Path]) -> 'FestivalLibrary': + """Create a FestivalLibrary object from borax builtin dataset or custom file. + If dataset does not exist,a FestivalDatasetNotExist is raised. + """ + if identifier_or_path == 'empty': + return FestivalLibrary() + if isinstance(identifier_or_path, str): + try: + path_o = get_festival_dataset_path(identifier_or_path) + except ValueError: + path_o = Path(identifier_or_path) + else: + path_o = identifier_or_path + if not path_o.exists(): + raise FestivalDatasetNotExist(f'FestivalDataset does not exist:{identifier_or_path}') + return cls.load_file(path_o) diff --git a/borax/calendars/lunardate.py b/borax/calendars/lunardate.py index b2f08b8..c00591d 100644 --- a/borax/calendars/lunardate.py +++ b/borax/calendars/lunardate.py @@ -452,8 +452,8 @@ def gz2offset(gz: str) -> int: if x % 2 != y % 2: raise ValueError return (6 * x - 5 * y) % 60 - except (TypeError, ValueError): - raise ValueError(f'Invalid gz string: {gz}') + except (TypeError, ValueError) as e: + raise ValueError(f'Invalid gz string: {gz}') from e @staticmethod def offset2gz(offset: int) -> str: diff --git a/borax/calendars/ui.py b/borax/calendars/ui.py index 668d2a6..471aae5 100644 --- a/borax/calendars/ui.py +++ b/borax/calendars/ui.py @@ -35,8 +35,8 @@ def __init__(self, master=None, firstweekday: int = 0, year: int = 0, month: int else: self._library = festival_source self._v_day_matrix = [[tk.StringVar() for _ in range(7)] for _ in range(6)] - self._d_selected_date = None # type: Optional[WrappedDate] - self._callbacks = {} # type: Dict[str,Callable] + self._d_selected_date: Optional[WrappedDate] = None + self._callbacks: Dict[str, Callable] = {} self._day_cell_indexes = -1, -1 # The cell indexes of first and last day in this month. self._cal_obj = calendar.Calendar(firstweekday=self._firstweekday) @@ -51,7 +51,7 @@ def _init_widgets(self): bw, bh = 3, 1 tool_row_no, head_row_no, week_row_no, day_row_no = range(4) - today_btn = tk.Button(self, text='今日', relief=tk.GROOVE, command=lambda: self._nav_current_month()) + today_btn = tk.Button(self, text='今日', relief=tk.GROOVE, command=self._nav_current_month) today_btn.grid(row=0, column=5, sticky='wens', columnspan=2, pady=4) pre_btn = tk.Button(self, text='\u25C4', width=bw, height=bh, command=lambda: self.page_to(-1), relief=tk.GROOVE) @@ -85,12 +85,12 @@ def _update_calendar_cell_values(self, event=None): month = self._v_month.get() cell_index = 0 _mi, _ma, _left_zero = -1, -1, 0 - for day, text, wd in self._library.iter_month_daytuples(year, month): + for day, text, _ in self._library.iter_month_daytuples(year, month): if day == 0: day_text = '' _left_zero += int(_mi == -1) else: - day_text = '{}\n{}'.format(day, text) + day_text = f'{day}\n{text}' if day == 1: _mi = cell_index _ma += 1 @@ -183,7 +183,7 @@ class FestivalTableFrame(ttk.Frame): """A table frame displaying festivals with CURD feature.""" def __init__(self, master=None, columns: Sequence = None, festival_source: Union[str, FestivalLibrary] = 'empty', - **kwargs): + countdown_ordered: bool = False, **kwargs): super().__init__(master=master, **kwargs) self._adapter = FestivalItemAdapter(columns) if isinstance(festival_source, FestivalLibrary): @@ -198,6 +198,7 @@ def __init__(self, master=None, columns: Sequence = None, festival_source: Union for i, name in enumerate(self._adapter.displays, start=1): self._tree.column(f"# {i}", anchor=tk.CENTER, width=self._adapter.widths[i - 1]) self._tree.heading(f"# {i}", text=name) + self._countdown_ordered = countdown_ordered self.notify_data_changed() @@ -213,11 +214,15 @@ def festival_library(self) -> FestivalLibrary: def row_count(self): return len(self._tree.get_children()) + def change_festival_source(self, source: str): + self._library = FestivalLibrary.load_builtin(source) + self.notify_data_changed() + def notify_data_changed(self): item_iids = self._tree.get_children() if len(item_iids): self._tree.delete(*item_iids) - for ndays, wd, festival in self._library.list_days_in_countdown(countdown_ordered=False): + for ndays, wd, festival in self._library.list_days_in_countdown(countdown_ordered=self._countdown_ordered): values = self._adapter.object2values(festival, wd, ndays) self._tree.insert('', 'end', text="1", values=values) @@ -239,3 +244,8 @@ def delete_selected_festivals(self): indexes.append(self._tree.index(selected_item)) self._tree.delete(selected_item) self._library.delete_by_indexes(indexes) + + def clear_data(self): + iid_values = self._tree.get_children() + self._tree.delete(*iid_values) + self._library = FestivalLibrary() diff --git a/borax/apps/__init__.py b/borax/capp/__init__.py similarity index 100% rename from borax/apps/__init__.py rename to borax/capp/__init__.py diff --git a/borax/capp/__main__.py b/borax/capp/__main__.py new file mode 100644 index 0000000..5e81ac5 --- /dev/null +++ b/borax/capp/__main__.py @@ -0,0 +1,11 @@ +from borax.capp.borax_calendar_app import start_calendar_app +from borax.capp.festival_creator import start_festival_creator + +if __name__ == '__main__': + import sys + + pro_args = sys.argv[1:] + if 'creator' in pro_args: + start_festival_creator() + else: + start_calendar_app() diff --git a/borax/capp/borax_calendar_app.py b/borax/capp/borax_calendar_app.py new file mode 100644 index 0000000..bc5e2fb --- /dev/null +++ b/borax/capp/borax_calendar_app.py @@ -0,0 +1,367 @@ +""" +显示月历 +工具 +""" +import tkinter as tk +import webbrowser +from datetime import date, datetime, timedelta +from tkinter import ttk +from tkinter.messagebox import showinfo +from typing import Optional + +from borax import __version__ as borax_version +from borax.calendars.festivals2 import FestivalLibrary, WrappedDate +from borax.calendars.lunardate import TextUtils, TERMS_CN, TERM_PINYIN +from borax.calendars.ui import CalendarFrame, FestivalTableFrame +from borax.calendars.utils import ThreeNineUtils +from borax.capp.festival_creator import FestivalCreatePanel + +library = FestivalLibrary.load_builtin().sort_by_countdown() + +PROJECT_URLS = { + 'home': 'https://github.com/kinegratii/borax' +} + +style: ttk.Style = None + + +class WDateVar(tk.StringVar): + """A tkinter variable for WrappedDate object. + Use set_date/get_date instead of set/get function. + """ + + def __init__(self, master=None, value=None, name=None, date_fmt='%Y-%m-%d'): + super().__init__(master, value, name) + self._date_object = None + self._date_fmt = date_fmt + + def set_date(self, d: WrappedDate): + self._date_object = d + self.set(d.solar.strftime(self._date_fmt)) + + def get_date(self) -> Optional[WrappedDate]: + raw = self.get() + if raw: + solar = datetime.strptime(raw, self._date_fmt) + return WrappedDate(solar.date()) + else: + return None + + +class WCalendarToolDlg(ttk.Frame): + def __init__(self, master=None): + super().__init__(master) + + notebook = ttk.Notebook(self) + notebook.pack(side='left', expand=True, fill=tk.BOTH, padx=5, pady=5) + + self._current_selected_index = 0 + self._data_stores = { + 'd1': WDateVar(), + 'd2': WDateVar(), + 'd3': WDateVar(), + } + + self._entry_hints = { + 'd1': '第一个日期', + 'd2': '第二个日期', + 'd3': '起始日期' + } + + self._tool_form_frame = ttk.Frame(notebook) + self._tool_form_frame.pack(side='left', expand=True, fill=tk.BOTH) + + ttk.Label(self._tool_form_frame, text='第一个日期').grid(row=0, column=0, columnspan=2) + self.day1_entry = ttk.Entry(self._tool_form_frame, textvariable=self._data_stores['d1']) + self.day1_entry.bind('', lambda event: self.entry_picker_linked(event, 'd1')) + self.day1_entry.grid(row=0, column=2, columnspan=2) + ttk.Label(self._tool_form_frame, text='第二个日期').grid(row=1, column=0, columnspan=2) + self.day2_entry = ttk.Entry(self._tool_form_frame, textvariable=self._data_stores['d2']) + self.day2_entry.bind('', lambda event: self.entry_picker_linked(event, 'd2')) + self.day2_entry.grid(row=1, column=2, columnspan=2) + + ttk.Button(self._tool_form_frame, text='计算', command=self.run_date_delta).grid( + row=2, column=0, columnspan=4, pady=8) + self.result1_label = ttk.Label(self._tool_form_frame, text='') + self.result1_label.grid(row=3, column=0, columnspan=4) + notebook.add(self._tool_form_frame, text='日期间隔', padding=4) + + deduction_frame = ttk.Frame(notebook) + notebook.add(deduction_frame, text='日期推导', padding=4) + ttk.Label(deduction_frame, text='起始日期').grid(row=0, column=0, columnspan=2) + self.day3_entry = ttk.Entry(deduction_frame, textvariable=self._data_stores['d3']) + self.day3_entry.bind('', lambda event: self.entry_picker_linked(event, 'd3')) + self.day3_entry.grid(row=0, column=2, columnspan=2) + self.day_delta_s = tk.IntVar() + for i, item in enumerate([('向前', -1), ('向后', 1)]): + t, val = item + tk.Radiobutton(deduction_frame, text=t, value=val, variable=self.day_delta_s).grid(row=1, column=i * 2 + 1, + columnspan=2) + ttk.Label(deduction_frame, text='间隔天数').grid(row=2, column=0, columnspan=2) + self.delta_days = tk.IntVar() + delta_days_com = ttk.Combobox(deduction_frame, width=6, values=[30, 60, 90, 100, 200, 300, 1000], + textvariable=self.delta_days) + delta_days_com.grid(row=2, column=2, columnspan=2, sticky=tk.E + tk.W + tk.N + tk.S) + ttk.Button(deduction_frame, text='计算', command=self.run_date_deduction).grid( + row=3, column=0, columnspan=4, pady=8) + self.result2_label = ttk.Label(deduction_frame, text='') + self.result2_label.grid(row=4, column=0, columnspan=4) + # init + self.day_delta_s.set(1) + + # Date Pick Panel + right_frame = ttk.Frame(self) + right_frame.pack(side='left') + + self.picker_hint_label = ttk.Label(right_frame, text='请选择第一个日期') + self.picker_hint_label.pack(side='top', fill=tk.X) + date_picker = CalendarFrame(right_frame, festival_source=library) + date_picker.bind_date_selected(self.on_date_picked) + date_picker.pack(side='top', expand=True, fill=tk.X) + + def entry_picker_linked(self, event, entry_label: str): + self._current_selected_index = entry_label + self.picker_hint_label.config(text=f'请选择{self._entry_hints[entry_label]}') + + def on_date_picked(self, wd: WrappedDate): + if self._current_selected_index in self._data_stores: + self._data_stores[self._current_selected_index].set_date(wd) + + def run_date_delta(self): + d1, d2 = self._data_stores['d1'].get_date(), self._data_stores['d2'].get_date() + if d1 and d2: + ndays = (d2.solar - d1.solar).days + self.result1_label.config(text=f'相差 {ndays} 天') + else: + self.result1_label.config(text='未选择日期,无法计算') + + def run_date_deduction(self): + d3 = self._data_stores['d3'].get_date() + if d3: + result2 = d3 + timedelta(self.day_delta_s.get() * self.delta_days.get()) + self.result2_label.config(text=str(result2)) + else: + self.result2_label.config(text='未选择日期,无法计算') + + +class DateDetailFrame(ttk.LabelFrame): + def __init__(self, master=None, **kwargs): + super().__init__(master, text='*****', labelanchor='n', **kwargs) + + self.label_widgets = {} + + # 4 = 121 22 + # -XX- + self.label_widgets['solar_day'] = ttk.Label(self, text='5', font=('Helvatical bold', 40)) + self.label_widgets['solar_day'].grid(row=0, column=0, rowspan=2, columnspan=4) + self.label_widgets['solar_ym'] = ttk.Label(self, text='2022年5月') + self.label_widgets['solar_ym'].grid(row=2, column=0, columnspan=4) + + self.label_widgets['solar_lunar'] = ttk.Label(self, text='四月初三') + self.label_widgets['solar_lunar'].grid(row=0, column=5, columnspan=2) + self.label_widgets['solar_week'] = ttk.Label(self, text='星期三') + self.label_widgets['solar_week'].grid(row=0, column=7, columnspan=2) + self.label_widgets['solar_gz'] = ttk.Label(self, text='星期三') + self.label_widgets['solar_gz'].grid(row=1, column=5, columnspan=4) + self.label_widgets['festival'] = ttk.Label(self, text='') + self.label_widgets['festival'].grid(row=2, column=5, columnspan=4) + + def set_selected_date(self, wd: WrappedDate = None): + """Show a date detail in panel.Today is shown if wd is None.""" + if wd is None: + wd = WrappedDate(date.today()) + sd, ld = wd.solar, wd.lunar + + self.label_widgets['solar_day'].config(text=str(sd.day)) + self.label_widgets['solar_ym'].config(text=sd.strftime('%Y年%m月')) + self.label_widgets['solar_lunar'].config(text=ld.strftime('%L%M月%D')) + week_cn = ld.cn_week + self.label_widgets['solar_week'].config(text=f'星期{week_cn}') + self.label_widgets['solar_gz'].config(text=ld.gz_str()) + day_labels = library.get_festival_names(sd) + three_night_label = ThreeNineUtils.get_39label(sd) + if three_night_label: + day_labels.append(three_night_label) + self.label_widgets['festival'].config(text=' '.join(day_labels)) + + +class GanzhiPanel(ttk.Frame): + def __init__(self, master=None, **kwargs): + super().__init__(master, **kwargs) + + gz_grid_frame = ttk.Frame(self) + for offset in range(60): + row, col = offset // 10, offset % 10 + btn_text = '{} {}'.format(TextUtils.offset2gz(offset), offset + 1) + btn = tk.Button(gz_grid_frame, text=btn_text, width=5, height=1, + command=lambda go=offset: self._show_years(go), relief=tk.GROOVE) + btn.grid(row=row, column=col, ipadx=5, ipady=5) + gz_grid_frame.pack(side='left') + + self.year_list = ttk.Treeview(self, column=("年份",), show='headings', height=5) + self.year_list.column("# 1", anchor=tk.CENTER) + self.year_list.heading("# 1", text="农历年份") + self.year_list.pack(side='left', expand=True, fill=tk.BOTH) + + def _show_years(self, gz_offset: int): + for item in self.year_list.get_children(): + self.year_list.delete(item) + if 0 < gz_offset < 36: + start_year = 1924 + gz_offset + else: + start_year = 1864 + gz_offset + for year in range(start_year, 2101, 60): + self.year_list.insert('', 'end', text="1", values=(f"{year}",)) + + +class TermPanel(ttk.Frame): + def __init__(self, master=None, **kwargs): + super().__init__(master, **kwargs) + + columns = ("序号", "节气", "识别码(拼音首字母)", "太阳地心视黄经(度)") + self.term_table = ttk.Treeview(self, column=columns, show='headings', height=5) + self.term_table.pack(side='top', expand=True, fill=tk.BOTH) + for i, name in enumerate(columns, start=1): + self.term_table.column(f"# {i}", anchor=tk.CENTER) + self.term_table.heading(f"# {i}", text=name) + + for tindex, tname in enumerate(TERMS_CN): # 1-285 + dg = (285 + 15 * tindex) % 360 + self.term_table.insert('', 'end', text="1", values=(tindex, tname, TERM_PINYIN[tindex], dg)) + + +class CApp(ttk.Frame): + def __init__(self, master=None): + super().__init__(master) + left_frame = ttk.Frame(self) + left_frame.pack(side='left', expand=True, fill=tk.Y, padx=10, pady=10) + self.detail_frame = DateDetailFrame(left_frame) + self.detail_frame.set_selected_date() + self.detail_frame.pack(side='top', expand=True, fill=tk.BOTH, pady=5) + + self.cal_panel = CalendarFrame(left_frame, festival_source=library) + self.cal_panel.pack(side='top', expand=True, fill=tk.BOTH) + self.cal_panel.bind_date_selected(self.on_show_date_detail) + + ttk.Separator(self, orient=tk.VERTICAL).pack(side='left', fill=tk.Y, expand=True) + self._table_festival_library = library + columns = (("name", 120), ("description", 160), ("code", 80), ("next_day", 150), ("countdown", 60)) + self._cs = FestivalTableFrame(self, columns=columns, festival_source=library, countdown_ordered=True) + self._cs.pack(side='right', expand=True, fill=tk.BOTH, padx=10, pady=10) + + # cs.update_data() + self._style_var = tk.StringVar() + self._table_festival_source_var = tk.StringVar(value='basic') + self.create_top_menu() + self._tool_dlg = None + self._gz_dlg = None + self._festival_create_dlg = None + + def create_top_menu(self): + top = self.winfo_toplevel() + menu_bar = tk.Menu(top) + top['menu'] = menu_bar + + global style + + viewmenu = tk.Menu(menu_bar, tearoff=0) + for name in style.theme_names(): + viewmenu.add_radiobutton(label=name, variable=self._style_var, command=self._change_theme) + menu_bar.add_cascade(label='界面主题', menu=viewmenu) + menu_bar.add_command(label='日期计算', command=self.start_tool_dlg) + menu_bar.add_command(label='节气干支', command=self.start_gz_dlg) + menu_bar.add_command(label='创建节日', command=self.start_festival_dlg) + source_menu = tk.Menu(menu_bar) + for source in ('basic', 'ext1'): + source_menu.add_radiobutton(label=source, variable=self._table_festival_source_var, + command=self._change_source) + menu_bar.add_cascade(label='节日源', menu=source_menu) + about_menu = tk.Menu(menu_bar) + about_menu.add_command(label='项目主页', command=lambda: webbrowser.open(PROJECT_URLS['home'])) + about_menu.add_command(label='关于软件', command=self.show_about_info) + menu_bar.add_cascade(label='关于', menu=about_menu) + + def _change_theme(self): + global style + style.theme_use(self._style_var.get()) + + def _change_source(self): + self._cs.change_festival_source(self._table_festival_source_var.get()) + + def on_show_date_detail(self, wd: WrappedDate): + self.detail_frame.set_selected_date(wd) + + def _create_tool_dialog(self): + self._tool_dlg = tk.Toplevel(self) + self._tool_dlg.resizable(False, False) + d = WCalendarToolDlg(self._tool_dlg) + d.pack(side='top') + self._tool_dlg.lift() + + def start_tool_dlg(self): + if self._tool_dlg is None: + self._create_tool_dialog() + return + try: + self._tool_dlg.lift() + except tk.TclError: + self._create_tool_dialog() + + def _create_gz_dialog(self): + self._gz_dlg = tk.Toplevel(self) + self._gz_dlg.resizable(False, False) + notebook = ttk.Notebook(self._gz_dlg) + tp = TermPanel(notebook) + notebook.add(tp, text='节气') + d = GanzhiPanel(notebook) + notebook.add(d, text='干支') + notebook.pack(side='top') + self._gz_dlg.lift() + + def start_gz_dlg(self): + if self._gz_dlg is None: + self._create_gz_dialog() + return + try: + self._gz_dlg.lift() + except tk.TclError: + self._create_gz_dialog() + + def _create_festival_dialog(self): + self._festival_create_dlg = tk.Toplevel(self) + self._festival_create_dlg.title('创建节日') + self._festival_create_dlg.resizable(False, False) + festival_create_frame = FestivalCreatePanel(self._festival_create_dlg) + festival_create_frame.pack(side='top') + self._festival_create_dlg.lift() + + def start_festival_dlg(self): + if self._festival_create_dlg is None: + self._create_festival_dialog() + return + try: + self._festival_create_dlg.lift() + except tk.TclError: + self._create_festival_dialog() + + def show_about_info(self): + showinfo('关于', f' 日历v{borax_version}\n\n Powered by Borax{borax_version}') + + +def start_calendar_app(): + root = tk.Tk() + rw, rh = 920, 460 + x, y = int(root.winfo_screenwidth() / 2 - rw / 2), int(root.winfo_screenheight() / 2 - rh / 2) + root.geometry(f"{rw}x{rh}+{x}+{y}") + root.resizable(False, False) + root.title(f'日历 - v{borax_version}') + global style + style = ttk.Style(root) + # style.theme_use('alt') + app = CApp(root) + app.pack(expand=True, fill=tk.BOTH) + root.mainloop() + + +if __name__ == '__main__': + start_calendar_app() diff --git a/borax/apps/festival_creator.py b/borax/capp/festival_creator.py similarity index 86% rename from borax/apps/festival_creator.py rename to borax/capp/festival_creator.py index bb48b0a..e728f30 100644 --- a/borax/apps/festival_creator.py +++ b/borax/capp/festival_creator.py @@ -9,7 +9,7 @@ from borax.calendars.lunardate import TextUtils, TERMS_CN from borax.calendars.ui import FestivalTableFrame -__all__ = ['FestivalCreatePanel'] +__all__ = ['FestivalCreatePanel', 'start_festival_creator'] class ChoicesCombobox(ttk.Combobox): @@ -121,7 +121,7 @@ def splash(self, text: str, timeout: int = 1000, foreground='black', **kwargs): self.after(timeout, self._clear) def _clear(self): - self.config({'text': ''}) + self.config({'text': '', 'foreground': 'black'}) class FestivalCreatePanel(ttk.Frame): @@ -134,6 +134,8 @@ def __init__(self, master=None, **kwargs): lf.pack(side='top', fill='x', padx=10, pady=10, expand=True) ttk.Label(lf, text=' 本工具支持创建公历型、农历型、星期型、节气型节日,并导出为csv文件。').pack( side='top', padx=5, pady=5, fill='both', expand=True) + main_frame = tk.Frame(self) + main_frame.pack(side='top', fill='both') self._vm = VarModel() @@ -152,8 +154,8 @@ def __init__(self, master=None, **kwargs): delta_choices = [(0, '当日'), (-1, '之前'), (1, '之后')] gz_day_choices = list(TextUtils.BRANCHES + TextUtils.STEMS) - frame = ttk.Frame(self) - frame.pack(side='top', expand=True, fill=tk.BOTH, padx=10, pady=10) + frame = ttk.Frame(main_frame) + frame.pack(side='left', expand=True, fill=tk.BOTH, padx=10, pady=10) ttk.Label(frame, text='名称').grid(row=n_row, column=0) ttk.Entry(frame, textvariable=self._vm.vars['name']).grid(row=n_row, column=1, columnspan=3, sticky='we') ttk.Label(frame, text='分类').grid(row=n_row, column=4) @@ -219,17 +221,8 @@ def __init__(self, master=None, **kwargs): ttk.Label(frame, text='日').grid(row=t_row, column=7) # Toolbar 8 cols - ttk.Button(frame, text='创建节日', command=self._create).grid(row=btn_row, column=0) - ttk.Button(frame, text='删除所选', command=self._delete).grid(row=btn_row, column=1) - ttk.Label(frame, text='节日源:').grid(row=btn_row, column=3) - source_choices = (('empty', '空白'), ('basic', '基础(basic)'), ('ext1', '扩展1(ext1)'), ('custom', '自定义')) - self._source_var = tk.StringVar() - source_cc = ChoicesCombobox(frame, choices=source_choices, val_variable=self._source_var, - value_selected=self._on_source_selected, width=ccb_w) - source_cc.grid(row=btn_row, column=4) - source_cc.current(0) - ttk.Button(frame, text='打开文件', command=self._open_and_add).grid(row=btn_row, column=5) - ttk.Button(frame, text='导出文件', command=self._export).grid(row=btn_row, column=6) + ttk.Button(frame, text='创建节日', command=self._create).grid(row=btn_row, column=0, columnspan=8, + sticky='we') self._msg_label = MessageLabel(frame, text='') self._msg_label.grid(row=msg_row, column=0, columnspan=10) @@ -244,23 +237,46 @@ def __init__(self, master=None, **kwargs): frame2 = ttk.Frame(self) frame2.pack(side='top', expand=True, fill=tk.BOTH) ttk.Label(frame2, textvariable=self._festival_detail).pack(side='top') + ttk.Separator(main_frame, orient=tk.VERTICAL).pack(side='left', fill=tk.Y, expand=True) + right_frame = tk.Frame(main_frame) + right_frame.pack(side='right', expand=True, fill='both', padx=10, pady=10) + toolbar_frame = tk.Frame(right_frame) + toolbar_frame.pack(side='top', fill='x') + ttk.Label(toolbar_frame, text='节日源:').grid(row=0, column=0) + source_choices = (('empty', '空白'), ('basic', '基础(basic)'), ('ext1', '扩展1(ext1)'), ('custom', '自定义')) + self._source_var = tk.StringVar() + source_cc = ChoicesCombobox(toolbar_frame, choices=source_choices, val_variable=self._source_var, + value_selected=self._on_source_selected, width=ccb_w) + source_cc.grid(row=0, column=1) + source_cc.current(0) + ttk.Button(toolbar_frame, text='打开/加载', command=self._open_and_add).grid(row=0, column=2) + ttk.Button(toolbar_frame, text='删除所选', command=self._delete).grid(row=0, column=3) + ttk.Button(toolbar_frame, text='清空数据', command=self._clear_data).grid(row=0, column=4) + ttk.Button(toolbar_frame, text='导出文件', command=self._export).grid(row=0, column=5) - columns = (("name", 100), ("description", 200), ("code", 120), ("next_day", 200), ("countdown", 100)) - self._festival_table = FestivalTableFrame(self, festival_source='empty', columns=columns) - self._festival_table.pack(side='top', expand=True, fill=tk.BOTH, padx=10, pady=10) + columns = (("name", 100), ("description", 180), ("code", 80), ("next_day", 150), ("countdown", 60)) + self._festival_table = FestivalTableFrame(right_frame, festival_source='empty', columns=columns) + self._festival_table.pack(side='top', expand=True, fill=tk.BOTH) def _create(self, event=None): try: festival = self._vm.validate() self._festival_table.add_festival(festival) - self._festival_detail.set(f'{festival.description} {festival.encode()}') + self._msg_label.splash(f'{festival.description} {festival.encode()}', foreground='green') except ValidateError as e: self._msg_label.splash(str(e), foreground='red') def _delete(self, event=None): self._festival_table.delete_selected_festivals() + def _clear_data(self, event=None): + self._festival_table.clear_data() + self._msg_label.splash('清空成功!', foreground='green') + def _export(self, event=None): + if len(self._festival_table.tree_view.get_children()) == 0: + self._msg_label.splash('表格无数据!', foreground='red') + return filename = filedialog.asksaveasfilename(parent=self, title='保存到', defaultextension='.csv', filetypes=(('csv', 'csv'),)) if filename: @@ -286,7 +302,7 @@ def _load_new_festival_library(self, f_library: FestivalLibrary): self._msg_label.splash(f'加载成功,共{self._festival_table.row_count}条', foreground='green') -def main(): +def start_festival_creator(): root = tk.Tk() root.title('节日创建工具') root.resizable(False, False) @@ -296,4 +312,4 @@ def main(): if __name__ == '__main__': - main() + start_festival_creator() diff --git a/borax/ui/widgets.py b/borax/ui/widgets.py index 58c6296..7fddc82 100644 --- a/borax/ui/widgets.py +++ b/borax/ui/widgets.py @@ -55,3 +55,16 @@ def reset(self): @property def state(self): return self._state + + +class MessageLabel(tk.Label): + """A label that can show text in a short time.Variable binding is not supported.""" + _key2colors = {'error': 'red', 'warning': 'orange', 'success': 'green'} + + def show_text(self, text: str, text_color: str = 'black', splash_ms: int = 0): + self.config({'text': text, 'fg': MessageLabel._key2colors.get(text_color, text_color)}) + if splash_ms: + self.after(splash_ms, self._clear) + + def _clear(self): + self.config({'text': ''}) diff --git a/codecov.yml b/codecov.yml index ec0912d..17d3e1b 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,6 +4,7 @@ coverage: - borax/apps/festival_creator.py - borax/calendars/datepicker.py - borax/calendars/ui.py + - borax/capp/*.py status: project: default: diff --git a/docs/README.md b/docs/README.md index b09dd75..a5897c1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,4 +1,4 @@ -# Borax - python3工具库 - 中国农历/中文数字/设计模式/树形结构 +# Borax - python农历&节日工具库 - 中文数字/设计模式/树形结构 [![PyPI](https://img.shields.io/pypi/v/borax.svg)](https://pypi.org/project/borax) [![PyPI - Python Version](https://img.shields.io/pypi/pyversions/borax.svg)](https://pypi.org/project/borax) @@ -29,9 +29,11 @@ Borax 是一个Python3工具集合库。 ## 开发(Development) +- **开发环境**: python3.11.7 +- **集成测试环境**: python3.9 - 3.12 - **代码仓库**:[Github](https://github.com/kinegratii/borax/) | [Gitee (镜像)](https://gitee.com/kinegratii/borax) - **项目开发**: [版本日志](changelog) | [技术文档(外链)](http://fd8cc08f.wiz06.com/wapp/pages/view/share/s/3Zzc2f0LJQ3w2TWIQb0ZMSna1zg4gs1vPQmb2vlh9M2zhqK8) -- **发布日志**: [v3.5](release-note/v350) | [v3.5.6](release-note/v356) | [v4.0.0](release-note/v400) +- **发布日志**: [v3.5](release-note/v350) | [v3.5.6](release-note/v356) | [v4.0.0](release-note/v400) | [v4.1.0](release-note/v410) ## 快速开始(Quickstart) @@ -39,10 +41,11 @@ Borax 是一个Python3工具集合库。 Borax 的 python 版本要求如下 -| borax 版本 | python版本 | -| ------ | ------ | -| 4.x | 3.7+ | -| 3.x | 3.5+ | +| borax 版本 | python版本 | 维护状态 | +| ------ | ------ | ------ | +| 4.1.x | 3.9+ | 维护开发 | +| 4.0.0 | 3.7+ | 维护至2024年12月31日 | +| 3.x | 3.5+ | 不再维护 | 可以通过 *pip* 安装 : diff --git a/docs/_sidebar.md b/docs/_sidebar.md deleted file mode 100644 index 7dace10..0000000 --- a/docs/_sidebar.md +++ /dev/null @@ -1,33 +0,0 @@ -- **● 入门与教程** - - [首页](README) - - [农历与节日](guides/festivals2-usage) -- **● Borax.Calendar** - - [农历](guides/lunardate) - - [节日(festivals2)](guides/festivals2) - - [日期节日序列化](guides/festivals2-serialize) - - [节日(festivals)](guides/festival) - - [生日](guides/birthday) - - [工具类](guides/calendars-utils) - - [节日界面库 New!](guides/festivals2-ui) -- **● Borax.Datasets** - - [数据连接(Join)](guides/join) - - [列选择器(fetch)](guides/fetch) -- **● Borax.DataStructures** - - [树形结构](guides/tree) - - [cjson](guides/cjson) -- **● Borax.Numbers** - - [中文数字](guides/numbers) - - [百分数](guides/percentage) -- **● Borax.Pattern** - - [单例模式](guides/singleton) - - [选项Choices](guides/choices) -- **● Borax.Utils** - - [字符串工具模块](guides/strings) -- **● 其他** - - [序列号生成器(Pool)](guides/serial_pool) - - [Tkinter界面](guides/ui) -- **● 发布日志** - - [v3.5.6](release-note/v356) - - [v3.5](release-note/v350) -- **● 项目开发** - - [版本日志](changelog) \ No newline at end of file diff --git a/docs/changelog.md b/docs/changelog.md index 727d21a..9c4aa6d 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,8 +1,33 @@ # 更新日志 +## v4.1.0 (20240131) + +> Borax最低python版本要求为python3.9 + +[发布日志](/release-note/v410) + +- 功能更新 + - 新增 [Borax日历应用](/guides/borax_calendar_app) + - 包 `borax.apps` 变更为 `borax.capp` + - 新增方法 `FestivalLibrary.extend_term_festivals` + - 新增方法 `FestivalLibrary.load` + - 新增 `borax.ui.widgets.MessageLabel` 类 + - `Festival` 类新增 `code` 属性 + - `WrappedDate.solar` 和 `WrappedDate.lunar` 属性变更为只读属性,不可写入 +- 项目构建 + - 不再支持python3.7和python3.8 + - 本地开发环境更新至 python3.11.7 + - 使用 *pyproject.toml* 项目构建配置文件,构建命令 `python -m build -w` + - 支持python3.12 +- 项目文档 + - mkdocs-material 更新至 9.5.3 + - 不再支持 docsify 构建(index.html 冲突) + ## v4.0.0 (20221115) -- 新增基于 tkinter 的 [节日界面库](/guides/festivals-ui) +[发布日志](/release-note/v400) + +- 新增基于 tkinter 的 [节日界面库](/guides/festivals2-ui) - 移除源代码文件编码声明行 - 移除 `borax.calendars.festival` 模块 - 修正 `LunarDate` 显示星期错误的问题 ([#49](https://github.com/kinegratii/borax/issues/49)) diff --git a/docs/develop_note.md b/docs/develop_note.md new file mode 100644 index 0000000..4d5e984 --- /dev/null +++ b/docs/develop_note.md @@ -0,0 +1,63 @@ +# 开发笔记 + +## 代码编写 + +### python版本约束 + +Borax 4.1.0开始,要求python最低版本为3.9,主要是引入了新的特性,包括: + +- `functools.cached_property` 装饰器 (python3.8+) +- `typing.Literal` 类型注释(python3.8+) + +### 代码风格 + +项目代码风格以 [PEP8](https://peps.python.org/pep-0008/) + pycharm 的配置为基准,并增加下列的一些自定义规则。 + +- 代码每行长度限制为120 +- 函数复杂度限制为25 +- 禁止使用 `\` 作为代码行分割的标志,需使用括号 +- 不再接受注释方式的类型声明,如 `a = 2 # type:int` 应该为 `a:int = 2` (pyflake触发 `F401` 警告) + +### API稳定性 + +Borax 保证API的稳定性,使用 `warnings` 模块标识已经被废弃的类和函数,并在首次标识之后的2-3个系列版本移除这些类和函数。 + +## 项目构建 + +### 配置文件 + +Borax 默认使用 *pyproject.toml* 文件作为项目配置文件,具体包括单元测试、静态检查等内容。 + +*pyproject.toml* 配置文件目前包括以下内容: + +| 功能 | 开发库 | 独立配置文件 | pyproject.toml配置段 | 备注 | +| ------------ | -------- | ------------ | -------------------- | -------------------------- | +| 项目基本信息 | - | | [project] | | +| 单元测试 | nose2 | | | | +| 覆盖率 | coverage | | [tool.coverage] | | +| 静态检查 | flake8 | | [tool.flake8] | 通过 Flake8-pyproject 实现 | +| 静态检查 | pylint | .pylintrc | | 配置项过多,不进行迁移 | +| 项目构建 | build | | [tool.setuptool] | | + + + +### 项目构建 + +项目使用 `build` 作为包构建工具,使用下列命令生成 wheel 文件。 + +```shell +python -m build -w +``` + +## 文档 + +### 文档编写 + +除了常规的模块文档外,项目包括以下两种日志文档: + +- 更新日志:每个版本的changelog。 +- 发布日志:某些重要版本的 release note,每个版本单独一篇文章。 + +### 文档生成 + +Borax项目使用 [Material for MkDocs ](https://squidfunk.github.io/mkdocs-material/) 作为文档生成工具,不再支持 docsify 文档生成工具。 diff --git a/docs/guides/borax_calendar_app.md b/docs/guides/borax_calendar_app.md new file mode 100644 index 0000000..8ade6d5 --- /dev/null +++ b/docs/guides/borax_calendar_app.md @@ -0,0 +1,28 @@ +# Borax日历应用程序 + +> New in 4.1.0 + +从 Borax 4.1.0 开始,Borax 提供两个基于 Borax.Calendar 的日历应用。 + +| 应用程序 | 功能 | 启动命令 | +| ---- | ---- | ---- | +| 日历应用 | 公农历日期显示,及其他日期工具 | `python -m borax.capp` | +| 节日创建器 | 创建节日库 | `python -m borax.capp creator` | + +## 日历应用 + +![borax_calendar](../images/app_borax_calendar.png) + +主要功能: + +- 显示带有基本节日的日历 +- 日期计算工具 + +## 节日创建器 + +![festival_creator](../images/app_festival_creator.png) + +主要功能: + +- 创建节日 +- 导出 csv文件 \ No newline at end of file diff --git a/docs/guides/festivals2-serialize.md b/docs/guides/festivals2-serialize.md index 9327a39..f1739f0 100644 --- a/docs/guides/festivals2-serialize.md +++ b/docs/guides/festivals2-serialize.md @@ -2,6 +2,8 @@ > 模块: `borax.calendars.festivals2` +> Updated in 3.6.0:LunarDate类不再支持直接序列,必须先转化对应的 WrappedDate 对象。 +> > Updated in 3.5.6: 星期型节日(WeekFestival)类支持倒数序号。如:“国际麻风节(1月最后一个星期天)” > > Add in 3.5.0 diff --git a/docs/guides/festivals2-ui.md b/docs/guides/festivals2-ui.md index 00763d9..5a06088 100644 --- a/docs/guides/festivals2-ui.md +++ b/docs/guides/festivals2-ui.md @@ -120,15 +120,20 @@ cf.page_to(2022, 9, 1) # 2022年9月的下一个月 ### 创建组件 ```python -FestivalTableFrame(master=None, colunms:Sequeue=None, festival_source:Union[str,FestivalLibrary]='empty', **kwargs) +FestivalTableFrame(master=None, colunms:Sequeue=None, festival_source:Union[str,FestivalLibrary]='empty', countdown_ordered:bool=False, **kwargs) ``` 构建参数及其意义如下: -| 参数 | 描述 | -| -------------------------------------------------- | ---------------- | -| colunms:Sequeue | 列定义 | -| festival_source:Union[str,FestivalLibrary]='empty' | 节日源,默认为空 | +| 参数 | 描述 | +| -------------------------------------------------- | -------------------------------- | +| colunms:Sequeue | 列定义 | +| festival_source:Union[str,FestivalLibrary]='empty' | 节日源,默认为空 | +| countdown_ordered:bool=False | 是否按倒计天数排序。1 | + +备注: + +1. v4.1.0新增。 表格列定义方式如下: @@ -191,6 +196,10 @@ ftf.festival_library.sorted(key=lambda x:x.code) ftf.notifiy_data_changed() ``` +- `change_festival_source(source:str)` + +v4.1.0新增。更新指定数据源。 + ## 日期选择框:ask_date diff --git a/docs/guides/festivals2.md b/docs/guides/festivals2.md index fab0e25..0d5aa2c 100644 --- a/docs/guides/festivals2.md +++ b/docs/guides/festivals2.md @@ -2,6 +2,8 @@ > 模块: `borax.calendars.festivals2` +> Updated in 4.1.0:新增 Festival.code属性。 + > Updated in 3.5.6: 星期型节日(WeekFestival)类支持倒数序号。如:“国际麻风节(1月最后一个星期天)” > Updated in 3.5.6: 星期型节日(WeekFestival)类支持每月频率。 @@ -15,7 +17,7 @@ ### 常量定义 -`festival2` 定义了一些常量,这些常量通常归属于一个名称以“Const”结尾的类,并使用大写字母的变量命名形式。 +`festival2` 定义了一些常量,这些常量通常归属于一个类,并使用大写字母的变量命名形式。本文档仅列出那些属于 public 权限的常量类。 #### FreqConst @@ -26,16 +28,32 @@ FreqConst 表示节日的频率,用于设置 `Festival` 的 `freq` 参数。 | FreqConst.YEARLY = 0 | 表示每年 | | FreqConst.MONTHLY = 1 | 表示每月 | -#### LeapConst +#### FestivalCatalog + +FestivalCatalog 定义了一些节日的分类标签,可以通过 `Festival.catalog` 属性进行读写。 + +默认支持以下标签。 + +```python +class FestivalCatalog: + basic = 'basic' + event = 'event' + life = 'life' + public = 'public' + tradition = 'tradition' + term = 'term' + other = 'other' + + CATALOGS = ['basic', 'term', 'public', 'tradition', 'event', 'life', 'other'] +``` -LeapConst表示农历闰月的标志,用于 `Period` 、`Festival` 对象初始化操作。 +节日标签用于同一日期有多个节日时,这些节日之间的先后排序问题。 -| 定义 | 表示 | -| -------------------- | ---- | -| LeapConst.NORMAL = 0 | 平月 | -| LeapConst.LEAP = 1 | 闰月 | -| LeapConst.MIXED = 2 | 混合 | +```python +amy_birthday = SolarFestival(month=10,day=1, catalog='event') +``` +如上例子,`amy_birthday` 总是在国庆节(其标签为 basic)之后。 ## 基础数据结构 - WrappedDate @@ -88,12 +106,12 @@ print(ld) # LunarDate(2020, 11, 18, 0) Period 是一个工具类,提供了一系列方法,这些方法均返回一个包含起始日期和终止日期的二元素元组。 -| 方法 | 描述 | -| -------------------------------------------------------- | ----------------- | -| Period.solar_year(year) | 公历year年 | -| Period.solar_month(year, month) | 公历year年month月 | -| Period.lunar_year(year) | 农历year年 | -| Period.lunar_month(year, month, leap=_IGNORE_LEAP_MONTH) | 农历year年month月 | +| 方法 | 描述 | +| ------------------------------------------------------------ | ----------------- | +| Period.solar_year(year) -> Tuple[date, date] | 公历year年 | +| Period.solar_month(year, month) -> Tuple[date, date] | 公历year年month月 | +| Period.lunar_year(year) -> Tuple[LunarDate, LunarDate] | 农历year年 | +| Period.lunar_month(year, month, leap=_IGNORE_LEAP_MONTH) -> Tuple[LunarDate, LunarDate] | 农历year年month月 | 需要注意的是,当leap为默认值且农历year年month月有闰月时,将返回的是两个月时间段的起始日期。下面是 `lunar_month` 方法不同取值的返回的结果。 @@ -231,6 +249,14 @@ TermFestival(index=0) ## Festival属性 +### code + +> Add in 4.1.0 + +类型:str,编码字符串。 `FestivalLibrary` 以此属性作为唯一性的标志。 + +需要注意的是该属性使用 `cached_property` 进行修饰。 + ### name 类型:str,节日名称,如“元旦”、“中秋节”等。 @@ -376,7 +402,7 @@ Festival.get_one_day(start_date=None, end_date=None) -> Optional[WrappedDate] ### 倒计时 ```python -Festival.countdown(date_obj: MixedDate = None) -> Tuple[int, Optional[WrappedDate]]) +Festival.countdown(date_obj: MixedDate = None) -> Tuple[int, Optional[WrappedDate]] ``` 计算本 festival 匹配的日期距离 date_obj 的天数及其日期。 @@ -392,11 +418,36 @@ print(spring_festival.countdown()) # (273, Add in v3.5.1 @@ -417,6 +468,16 @@ FestivalLibrary.extend_unique(other) 添加多个节日对象,类似于 extend 方法,但是如果code已经存在则不再加入。 +### extend_term_festivals + +> Add in v4.0.1 + +``` +FestivalLibrary.extend_term_festivals() +``` + +添加24个节气节日。 + ### delete_by_indexes > Add in v4.0.0 @@ -427,6 +488,24 @@ FestivalLibrary.delete_by_indexes(indexes:List[int]) 按照位置删除多个元素。 +### load + +> Add in 4.1.0 + +```python +FestivalLibrary.load(cls, identifier_or_path: Union[str, Path]) -> 'FestivalLibrary' +``` + +加载Borax内部数据或自定义文件。 + +```python +fl = FestivalLibrary.load('basic') + +fl2 = FestivalLibrary.load('/usr/my/my_festivals.csv') +``` + + + ### load_file ```python @@ -438,7 +517,7 @@ FestivalLibrary.load_file(cls, file_path: Union[str, Path]) -> 'FestivalLibrary' ### load_builtin ```python -FestivalLibrary.load_builtin(cls, identifier: str = 'zh-Hans') -> 'FestivalLibrary' +FestivalLibrary.load_builtin(cls, identifier: str = 'basic') -> 'FestivalLibrary' ``` 加载Borax提供的节日库数据。 diff --git a/docs/guides/lunardate.md b/docs/guides/lunardate.md index 3bc8b69..baea60f 100644 --- a/docs/guides/lunardate.md +++ b/docs/guides/lunardate.md @@ -80,7 +80,7 @@ LunarDate(2020, 4, 1, 1) **▶ 公历日期** -将公历日期转化为农历日期。 +将公历日期转化为农历日期,支持公历年月日整型数字和 `datetime.date` 两种形式。 ``` >>>ld = LunarDate.from_solar_date(2018, 8, 11) diff --git a/docs/guides/numbers.md b/docs/guides/numbers.md index 21bf46f..22b1707 100644 --- a/docs/guides/numbers.md +++ b/docs/guides/numbers.md @@ -6,7 +6,7 @@ `numbers` 提供了下列的模块级常量。 -- **numbers.MAX_VALUE_LIMIT = 1_0000_0000_0000 ** +- **numbers.MAX_VALUE_LIMIT = 1_0000_0000_0000** 本模块可以处理的数字上限,值为一万亿(10^12) , 超过该值将抛出 `ValueError` 异常,适用本模块的所有函数。 diff --git a/docs/images/app_borax_calendar.png b/docs/images/app_borax_calendar.png new file mode 100644 index 0000000..b2b7808 Binary files /dev/null and b/docs/images/app_borax_calendar.png differ diff --git a/docs/images/app_festival_creator.png b/docs/images/app_festival_creator.png new file mode 100644 index 0000000..03f7be7 Binary files /dev/null and b/docs/images/app_festival_creator.png differ diff --git a/docs/index.html b/docs/index.html deleted file mode 100644 index 6ae88b9..0000000 --- a/docs/index.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - Borax - Python3工具集合库 - - - - - - - -
- - - - - - diff --git a/docs/release-note/v410.md b/docs/release-note/v410.md new file mode 100644 index 0000000..74b037b --- /dev/null +++ b/docs/release-note/v410.md @@ -0,0 +1,50 @@ +# v4.1.0发布日志 + +> 发布时间:2024年1月31日 + + + +## 1 项目开发 + +从 4.1.0 开始,Borax 在项目开发构建上有重大变更,具体包括: + +- **Borax不再支持 python3.7和python3.8,最低版本为3.9** +- 本地开发环境更新至3.11 +- 使用 *pyproject.toml* 取代原有的 *setup.py* 和 *setup.cfg* 文件。 +- 更新大量开发依赖库( *requirements_dev.txt* )的版本。 + + + +## 2 日历应用 + +Borax 提供了一个基于 tkinter 的日历应用程序,该日历应用包含了一些常见的功能: + +- 万年历显示 +- 日期计算相关工具 +- 查看节日、节气、干支信息 +- 创建和导出节日源 + +在安装 Borax 之后,使用 `python -m borax.capp` 启动该界面程序 。 + +## 3 其他功能 + +Borax 4.1.0 主要更新了 `borax.calendars.festivals2` 模块的功能。 + +### 3.1 WrappedDate + +`WrappedDate.solar` 和 `WrappedDate.lunar` 属性修改为 **只读属性,不可写入** 。 + +### 3.2 Festival + +`Festival` 新增 `code` 属性,表示节日的编码,该属性为惰性属性,使用 `cached_property` 装饰。 + +### 3.3 FestivalLibrary + +新增 `FestivalLibrary.load` 函数,这是 `load_file` 和 `load_builtin` 的混合接口 。 + +```python +fl1 = FestivalLibrary.load('basic') + +fl2 = FestivalLibrary.load('c:\\users\\samuel\\festival_data\\my_festivals.csv') +``` + diff --git a/mkdocs.yaml b/mkdocs.yaml index 1262c66..b80922b 100644 --- a/mkdocs.yaml +++ b/mkdocs.yaml @@ -8,18 +8,25 @@ theme: features: - navigation.tabs - navigation.sections + - navigation.footer +validation: + links: + absolute_links: ignore + unrecognized_links: ignore extra: social: - icon: fontawesome/brands/github link: https://github.com/kinegratii/borax - icon: fontawesome/brands/python link: https://pypi.org/project/borax -copyright: Copyright © 2018 - 2022 Samuel.Yan +copyright: Copyright © 2018 - 2024 Samuel.Yan markdown_extensions: - pymdownx.highlight - pymdownx.superfences + - mdx_truly_sane_lists nav: - 首页: README.md + - 日历应用: guides/borax_calendar_app.md - 农历与节日: - 农历: guides/lunardate.md - 节日: guides/festivals2.md @@ -41,9 +48,11 @@ nav: - 文章: - 农历与节日: guides/festivals2-usage.md - 农历开发笔记: posts/lunardate-development.md - - 更新日志: + - 开发&更新日志: - 更新日志: changelog.md + - 开发日志: develop_note.md - 发布日志: + - v4.1.0: release-note/v410.md - v4.0.0: release-note/v400.md - v3.5.6: release-note/v356.md - v3.5.0: release-note/v350.md \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..0996dc9 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,63 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "borax" +authors = [ + { name = "kinegratii", email = "zhenwei.yan@hotmail.com" }, +] +description = "A tool collections.(Chinese-Lunar-Calendars/Python-Patterns)" +readme = "README.md" +requires-python = ">=3.9" +keywords = ["chinese lunar calendar", "python tool"] +license = { text = "MIT License" } +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3 :: Only", + "Intended Audience :: Developers", + "License :: OSI Approved :: MIT License", + "Topic :: Software Development :: Libraries", + "Topic :: Utilities", + 'Operating System :: OS Independent' +] +dynamic = ["version"] + +[project.urls] +Homepage = "https://github.com/kinegratii/borax" +Documentation = "https://borax.readthedocs.io/zh_CN/latest/" +Repository = "https://github.com/kinegratii/borax" + +[tool.setuptools] +include-package-data = true + +[tool.setuptools.dynamic] +version = { attr = "borax.__version__" } + +[tool.coverage.run] +omit = [ + "borax\\ui\\*.py", + "borax\\capp\\*.py", + "borax\\apps\\*.py", + "borax\\calendars\\datepicker.py", + "borax\\calendars\\ui.py" +] +[tool.coverage.report] +exclude_lines = [ + "def __repr__", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "def print_(self):" +] +[tool.flake8] +ignore = ["E743", "E501"] +max-line-length = 120 +max-complexity = 25 +exclude = [".git", "__pycache__", "build", "dist"] \ No newline at end of file diff --git a/requirements_dev.txt b/requirements_dev.txt index d19263a..c165742 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,6 +1,8 @@ -nose2~=0.12 -coverage~=5.5 -flake8~=5.0 +nose2~=0.14 +coverage~=7.4 +flake8~=6.1 mccabe~=0.6 -wheel~=0.37 -setuptools~=65.0 \ No newline at end of file +wheel~=0.42 +setuptools~=65.0 +build~=1.0 +Flake8-pyproject~=1.2 \ No newline at end of file diff --git a/requirements_doc.txt b/requirements_doc.txt index 3218213..00c715b 100644 --- a/requirements_doc.txt +++ b/requirements_doc.txt @@ -1 +1,2 @@ -mkdocs-material==8.5.3 \ No newline at end of file +mkdocs-material==9.5.3 +mdx-truly-sane-lists==1.3 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 01b1fe8..28574e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -8,8 +8,3 @@ project_urls = [nosetests] traverse-namespace = 1 - -[flake8] -ignore = E743,E501,F401 -max-complexity = 25 -exclude = .git,__pycache__,docs/source/conf.py,old,build,dist \ No newline at end of file diff --git a/setup.py b/setup.py index ffaf816..b99d7a5 100644 --- a/setup.py +++ b/setup.py @@ -12,11 +12,10 @@ lib_classifiers = [ "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3 :: Only", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", @@ -31,7 +30,7 @@ setup( name='borax', version=version, - python_requires='>=3.7', + python_requires='>=3.9', packages=find_packages(exclude=['tests']), include_package_data=True, license='MIT', @@ -41,9 +40,4 @@ description='A tool collections.(Chinese-Lunar-Calendars/Python-Patterns)', long_description=long_description, long_description_content_type='text/markdown', - entry_points={ - "gui_scripts": [ - "festival_creator = borax.apps.festival_creator:main" - ] - }, ) diff --git a/tests/test_date_pickle.py b/tests/test_date_pickle.py index 1e0c29c..bb60b80 100644 --- a/tests/test_date_pickle.py +++ b/tests/test_date_pickle.py @@ -2,13 +2,24 @@ import pickle from io import BytesIO +from datetime import date + from borax.calendars.lunardate import LunarDate from borax.calendars.festivals2 import WrappedDate +class WrappedDateBasicTestCase(unittest.TestCase): + def test_wrapped_date(self): + ld = LunarDate.today() + wd = WrappedDate(ld) + self.assertEqual(ld, wd.lunar) + with self.assertRaises(AttributeError): + wd.lunar = LunarDate(2024, 1, 1) + + class DatePickleTestCase(unittest.TestCase): - def test_wd(self): + def test_lunardate_pickle(self): ld1 = LunarDate.today() fp = BytesIO() @@ -18,11 +29,13 @@ def test_wd(self): e_ld = pickle.load(fp) self.assertEqual(ld1, e_ld) - wd1 = WrappedDate(ld1) - fp2 = BytesIO() - - pickle.dump(wd1, fp2) - - fp2.seek(0) - wd2 = pickle.load(fp2) - self.assertEqual(wd2, wd1) + def test_wrapped_date_pickle(self): + wd_list = [WrappedDate(date.today()), WrappedDate(LunarDate.today())] + for wd in wd_list: + with self.subTest(wd=wd): + fp = BytesIO() + pickle.dump(wd, fp) + fp.seek(0) + new_wd = pickle.load(fp) + self.assertEqual(wd.solar, new_wd.solar) + self.assertEqual(wd.lunar, new_wd.lunar) diff --git a/tests/test_festival_library.py b/tests/test_festival_library.py index 4253640..095ee70 100644 --- a/tests/test_festival_library.py +++ b/tests/test_festival_library.py @@ -3,7 +3,8 @@ from io import StringIO from unittest.mock import MagicMock, patch -from borax.calendars.festivals2 import LunarFestival, TermFestival, FestivalLibrary, FestivalSchema +from borax.calendars.festivals2 import (LunarFestival, TermFestival, FestivalLibrary, FestivalSchema, + FestivalDatasetNotExist) class FestivalLibraryTestCase(unittest.TestCase): @@ -23,6 +24,12 @@ def test_library(self): self.assertIn('元旦', [g.name for g in gd_days]) + def test_new_load(self): + fl = FestivalLibrary.load('basic') + self.assertEqual(33, len(fl)) + with self.assertRaises(FestivalDatasetNotExist): + FestivalLibrary.load('not-found') + def test_list_days(self): fl = FestivalLibrary.load_builtin() fes_list = [] @@ -53,6 +60,13 @@ def test_unique(self): fl.extend_unique(['205026', '89005']) self.assertEqual(3, len(fl)) + def test_unique_for_basic_library(self): + fl = FestivalLibrary.load_builtin('basic') + total_1 = len(fl) + fl.extend_term_festivals() + total_2 = len(fl) + self.assertEqual(22, total_2 - total_1) + def test_edit(self): fl = FestivalLibrary() fl.load_term_festivals() @@ -81,6 +95,9 @@ def test_calendar(self): fl = FestivalLibrary.load_builtin() days = fl.monthdaycalendar(2022, 1) self.assertEqual(6, len(days)) + fl1 = FestivalLibrary(fl) + self.assertTrue(isinstance(fl1, FestivalLibrary)) + self.assertTrue(len(fl) == len(fl1)) class FestivalLibraryCURDTestCase(unittest.TestCase): @@ -128,3 +145,8 @@ def test_filter_exclude_copy(self): fl3 = fl.exclude_(schema__in=[FestivalSchema.WEEK, FestivalSchema.TERM]) self.assertEqual(len(fl), len(fl1) + len(fl2)) self.assertEqual(len(fl3), len(fl1)) + + def test_festivals_functional_program(self): + fl = FestivalLibrary.load_builtin() + fl2 = FestivalLibrary(filter(lambda f: f.schema == FestivalSchema.SOLAR, fl)) + self.assertTrue(isinstance(fl2, FestivalLibrary)) diff --git a/tests/test_lunardate.py b/tests/test_lunardate.py index 8e4d6c0..6bfc51e 100644 --- a/tests/test_lunardate.py +++ b/tests/test_lunardate.py @@ -97,6 +97,14 @@ def test_immutable_feature(self): ld1 = LunarDate(2018, 6, 1) ld2 = LunarDate(2018, 6, 1) self.assertEqual(1, len({ld1, ld2})) + # Act as dict keys + dic = {ld1: 'day1'} + self.assertEqual('day1', dic.get(ld1)) + ld3 = LunarDate(2018, 6, 1) + self.assertEqual('day1', dic.get(ld3)) + dic[ld3] = 'day2' + self.assertEqual(1, len(dic)) + self.assertEqual('day2', dic.get(ld1)) def test_term_ganzhi_feature(self): ld = LunarDate(2018, 6, 26) diff --git a/tests/test_runtime_measurer.py b/tests/test_runtime_measurer.py index 6742b80..e547d16 100644 --- a/tests/test_runtime_measurer.py +++ b/tests/test_runtime_measurer.py @@ -1,4 +1,3 @@ -import time import unittest from borax.devtools import RuntimeMeasurer