diff --git a/.gitignore b/.gitignore index f2506b7..e051176 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,7 @@ ENV/ .idea -node_modules \ No newline at end of file +node_modules +*.code-workspace + +demo.py \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 977c0ac..6aa1236 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,16 @@ language: python python: - - "3.5" - "3.6" - - "3.7-dev" + - "3.7" + - "3.8" branches: only: - master - develop - /^release-.*$/ + - /^beta-.*$/ install: - pip install -r requirements_dev.txt script: - - nosetests --with-coverage --cover-package borax + - nose2 --with-coverage --coverage borax - flake8 borax tests \ No newline at end of file diff --git a/README-en.md b/README-en.md index 942539e..5cb4110 100644 --- a/README-en.md +++ b/README-en.md @@ -131,7 +131,7 @@ See [online document](https://kinegratii.github.io/borax) for more detail, which - [x] [Typing Hints](https://www.python.org/dev/peps/pep-0484/) - [x] [Flake8 Code Style](http://flake8.pycqa.org/en/latest/) -- [x] [nose](https://pypi.org/project/nose/) +- [x] [nose2](https://pypi.org/project/nose2/) - [x] [Travis CI](https://travis-ci.org) - [x] [Docsify](https://docsify.js.org) diff --git a/README.md b/README.md index c2887f5..2409081 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ ## 概述 & 安装 -Borax 是一个的 Python3 开发工具集合库,涉及到: +Borax 是一个 Python3 开发工具集合库,涉及到: - 设计模式 - 数据结构及其实现 @@ -111,7 +111,7 @@ print(names) # ['Alice', 'Bob', 'Charlie'] - [x] [Typing Hints](https://www.python.org/dev/peps/pep-0484/) - [x] [Flake8 Code Style](http://flake8.pycqa.org/en/latest/) -- [x] [nose](https://pypi.org/project/nose/) +- [x] [nose2](https://pypi.org/project/nose2/) - [x] [Travis CI](https://travis-ci.org) - [x] [Docsify](https://docsify.js.org) diff --git a/borax/__init__.py b/borax/__init__.py index 34765a0..24b3c9b 100644 --- a/borax/__init__.py +++ b/borax/__init__.py @@ -1,4 +1,4 @@ # coding=utf8 -__version__ = '1.4.2' +__version__ = '3.1.0' __author__ = 'kinegratii' diff --git a/borax/calendars/festivals.py b/borax/calendars/festivals.py index b7ca643..90f509c 100644 --- a/borax/calendars/festivals.py +++ b/borax/calendars/festivals.py @@ -149,7 +149,12 @@ def __init__(self, month, day, year=YEAR_ANY, leap=0, ignore_leap=1, **kwargs): def match(self, date_obj): date_obj = self._normalize(date_obj) if self._ignore_leap and date_obj.leap == 1: - date_obj = date_obj.replace(leap=0) + try: + date_obj = date_obj.replace(leap=0) + except ValueError: + if date_obj.day == 30: # leap month has 30 days and normal one has 29 days + return False + raise return super().match(date_obj) def _resolve(self, year): diff --git a/borax/choices3.py b/borax/choices3.py new file mode 100644 index 0000000..ac80365 --- /dev/null +++ b/borax/choices3.py @@ -0,0 +1,100 @@ +# coding=utf8 + +from collections import OrderedDict + + +class BItem: + _order = 0 + + def __init__(self, value, label=None, *, order=-1): + self._value = value + self._label = label + if order is None: + BItem._order += 1 + self.order = BItem._order + else: + self.order = order + + @property + def value(self): + return self._value + + @property + def label(self): + return self._label + + def __eq__(self, other): + return self._value == other + + +class BChoicesMeta(type): + def __new__(cls, name, bases, attrs): + + fields = {} # {:} + + parents = [b for b in bases if isinstance(b, BChoicesMeta)] + for kls in parents: + for field_name in kls._fields: + fields[field_name] = kls._fields[field_name] + + for k, v in attrs.items(): + if k.startswith('_'): + continue + if isinstance(v, BItem): + fields[k] = v + elif isinstance(v, (tuple, list)) and len(v) == 2: + fields[k] = BItem(v[0], v[1]) + elif isinstance(v, (int, float, str, bytes)): + fields[k] = BItem(v, k.lower()) + + # FIXME unordered dict for python3.5 + + fields = OrderedDict(sorted(fields.items(), key=lambda x: x[1].order)) + for field_name, item in fields.items(): + attrs[field_name] = item + + new_cls = super().__new__(cls, name, bases, attrs) + new_cls._fields = fields + return new_cls + + @property + def fields(cls): + return cls._fields + + @property + def choices(cls): + return [(item.value, item.label) for _, item in cls.fields.items()] + + @property + def values(cls): + return [value for value, _ in cls.choices] + + @property + def labels(cls): + return [label for _, label in cls.choices] + + @property + def display_lookup(cls): + return {value: label for value, label in cls.choices} + + def get_value_display(cls, item): + return cls.display_lookup.get(item) + + def __getattr__(self, item): + if item in self.fields: + return self.fields[item] + return super().__getattr__(item) + + def __contains__(self, item): + return item in self.values + + def __iter__(self): + for item in self.choices: + yield item + + def __len__(self): + return len(self.choices) + + +class BChoices(metaclass=BChoicesMeta): + pass diff --git a/borax/counters/serials.py b/borax/counters/serials.py index 5dcd85d..82075d3 100644 --- a/borax/counters/serials.py +++ b/borax/counters/serials.py @@ -39,6 +39,9 @@ def generate(self, num: int) -> List[int]: self.__add_serials(result) return result + def generate_next_one(self) -> int: + return self.generate(1)[0] + def __add_serials(self, serials: Iterable[int]) -> None: for serial in serials: self._data_set.add(serial) @@ -67,6 +70,9 @@ def generate(self, num: int) -> List[str]: res = super().generate(num) return list(map(self._convert, res)) + def generate_next_one(self) -> str: + return self.generate(1)[0] + def add(self, elements: List[str]) -> None: elements = map(self._parse_serial, elements) super().add(elements) diff --git a/borax/datasets/__init__.py b/borax/datasets/__init__.py new file mode 100644 index 0000000..43c46f6 --- /dev/null +++ b/borax/datasets/__init__.py @@ -0,0 +1,5 @@ +# coding=utf8 + +from borax.datasets.dict_datasets import DictDataset + +__all__ = ['DictDataset'] diff --git a/borax/datasets/dict_datasets.py b/borax/datasets/dict_datasets.py new file mode 100644 index 0000000..669f724 --- /dev/null +++ b/borax/datasets/dict_datasets.py @@ -0,0 +1,35 @@ +# coding=utf8 + + +from borax.datasets.join_ import join_one, join + + +class DictDataset: + def __init__(self, data, primary_field=None): + self._data = [] + if data: + self._data = list(data) + self._primary_field = primary_field + + @property + def data(self): + return self._data + + def __iter__(self): + for item in self.data: + yield item + + def join(self, values, from_, to_, as_args=None, as_kwargs=None): + join( + self._data, + values=values, + from_=from_, + to_=to_, + as_args=as_args, + as_kwargs=as_kwargs, + ) + return self + + def join_one(self, values, from_, as_): + join_one(self._data, values=values, from_=from_, as_=as_) + return self diff --git a/borax/datasets/fetch.py b/borax/datasets/fetch.py new file mode 100644 index 0000000..f0b0b58 --- /dev/null +++ b/borax/datasets/fetch.py @@ -0,0 +1,76 @@ +# coding=utf8 +""" +fetch is a enhance module with fetch. And adjust the parameter order of calling to fit the habit. +""" +from functools import partial +from itertools import tee + +__all__ = ['Empty', 'fetch', 'ifetch', 'fetch_single', 'ifetch_multiple', 'ifetch_single', 'fetch_as_dict'] + + +class Empty(object): + pass + + +EMPTY = Empty() + + +def bget(obj, key, default=Empty): + try: + return getattr(obj, key) + except AttributeError: + pass + + try: + return obj[key] + except KeyError: + pass + if default is not EMPTY: + return default + + raise ValueError('Item %r has no attr or key for %r' % (obj, key)) + + +def ifetch_single(iterable, key, default=EMPTY, getter=None): + """ + getter() g(item, key):pass + """ + + def _getter(item): + if getter: + custom_getter = partial(getter, key=key) + return custom_getter(item) + else: + return partial(bget, key=key, default=default)(item) + + return map(_getter, iterable) + + +def fetch_single(iterable, key, default=EMPTY, getter=None): + return list(ifetch_single(iterable, key, default=default, getter=getter)) + + +def ifetch_multiple(iterable, *keys, defaults=None, getter=None): + defaults = defaults or {} + if len(keys) > 1: + iters = tee(iterable, len(keys)) + else: + iters = (iterable,) + iters = [ifetch_single(it, key, default=defaults.get(key, EMPTY), getter=getter) for it, key in zip(iters, keys)] + return iters + + +def ifetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None): + if len(keys) > 0: + keys = (key,) + keys + return map(list, ifetch_multiple(iterable, *keys, defaults=defaults, getter=getter)) + else: + return ifetch_single(iterable, key, default=default, getter=getter) + + +def fetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None): + return list(ifetch(iterable, key, *keys, default=default, defaults=defaults, getter=getter)) + + +def fetch_as_dict(data, key_field, value_value): + return dict([(bget(item, key_field), bget(item, value_value)) for item in data]) diff --git a/borax/datasets/join_.py b/borax/datasets/join_.py new file mode 100644 index 0000000..484cc8b --- /dev/null +++ b/borax/datasets/join_.py @@ -0,0 +1,30 @@ +# coding=utf8 + + +def join_one(data_list, values, from_, as_, default=None): + if isinstance(values, (list, tuple)): + values = dict(values) + if not isinstance(values, dict): + raise TypeError("Unsupported Type for values param.") + for item in data_list: + if from_ in item: + val = item[from_] + if val in values: + ref_val = values[val] + else: + ref_val = default + item[as_] = ref_val + return data_list + + +def join(data_list, values, from_, to_, as_args=None, as_kwargs=None): + as_args = as_args or [] + as_kwargs = as_kwargs or {} + as_fields = {**{a: a for a in as_args}, **as_kwargs} + dict_values = {v[to_]: v for v in values} + for item in data_list: + kv = item[from_] + val_dic = dict_values[kv] + for f1, f2 in as_fields.items(): + item[f2] = val_dic[f1] + return data_list diff --git a/borax/decorators/admin.py b/borax/decorators/admin.py deleted file mode 100644 index 2712e8b..0000000 --- a/borax/decorators/admin.py +++ /dev/null @@ -1,31 +0,0 @@ -# coding=utf8 -import warnings - -__all__ = ['attr', 'admin_action', 'action', 'display_field'] - -warnings.warn('This module is deprecated, use nickel.admin_utils.decorators instead.', DeprecationWarning) - - -def attr(**kwargs): - def _inner(fun): - for name, value in kwargs.items(): - setattr(fun, name, value) - return fun - - return _inner - - -def admin_action(short_description=None, allowed_permissions=None, **kwargs): - if allowed_permissions is not None: - kwargs.update({'allowed_permissions': allowed_permissions}) - return attr(short_description=short_description, **kwargs) - - -# Old name alias -action = admin_action - - -def display_field(short_description, admin_order_field=None, **kwargs): - if admin_order_field is not None: - kwargs.update({'admin_order_field': admin_order_field}) - return attr(short_description=short_description, **kwargs) diff --git a/borax/fetch.py b/borax/fetch.py index ad62805..3316167 100644 --- a/borax/fetch.py +++ b/borax/fetch.py @@ -1,76 +1,10 @@ # coding=utf8 -""" -fetch is a enhance module with fetch. And adjust the parameter order of calling to fit the habit. -""" -from functools import partial -from itertools import tee -__all__ = ['fetch', 'ifetch', 'fetch_single', 'ifetch_multiple', 'ifetch_single'] +import warnings +from borax.datasets.fetch import * # noqa: F403 -class Empty(object): - pass - - -EMPTY = Empty() - - -def bget(obj, key, default=Empty): - try: - return getattr(obj, key) - except AttributeError: - pass - - try: - return obj[key] - except KeyError: - pass - if default is not EMPTY: - return default - - raise ValueError('Item %r has no attr or key for %r' % (obj, key)) - - -def ifetch_single(iterable, key, default=EMPTY, getter=None): - """ - getter() g(item, key):pass - """ - - def _getter(item): - if getter: - custom_getter = partial(getter, key=key) - return custom_getter(item) - else: - return partial(bget, key=key, default=default)(item) - - return map(_getter, iterable) - - -def fetch_single(iterable, key, default=EMPTY, getter=None): - return list(ifetch_single(iterable, key, default=default, getter=getter)) - - -def ifetch_multiple(iterable, *keys, defaults=None, getter=None): - defaults = defaults or {} - if len(keys) > 1: - iters = tee(iterable, len(keys)) - else: - iters = (iterable,) - iters = [ifetch_single(it, key, default=defaults.get(key, EMPTY), getter=getter) for it, key in zip(iters, keys)] - return iters - - -def ifetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None): - if len(keys) > 0: - keys = (key,) + keys - return map(list, ifetch_multiple(iterable, *keys, defaults=defaults, getter=getter)) - else: - return ifetch_single(iterable, key, default=default, getter=getter) - - -def fetch(iterable, key, *keys, default=EMPTY, defaults=None, getter=None): - return list(ifetch(iterable, key, *keys, default=default, defaults=defaults, getter=getter)) - - -def fetch_as_dict(data, key_field, value_value): - return dict([(bget(item, key_field), bget(item, value_value)) for item in data]) +warnings.warn( + 'This module is deprecated and will be removed in V3.3.Use borax.datasets.fetch instead.', + category=PendingDeprecationWarning +) diff --git a/borax/finance.py b/borax/finance.py index b2068dd..ef704c4 100644 --- a/borax/finance.py +++ b/borax/finance.py @@ -15,10 +15,18 @@ (r'^元', '零元') ] +MAX_VALUE_LIMIT = 1000000000000 + def financial_amount_capital(num: Union[int, float, Decimal, str]) -> str: units = '仟佰拾亿仟佰拾万仟佰拾元角分' digits = '零壹贰叁肆伍陆柒捌玖' + if isinstance(num, str): + _n = int(num) + else: + _n = num + if _n < 0 or _n >= MAX_VALUE_LIMIT: + raise ValueError('Out of range') num_str = str(num) + '00' dot_pos = num_str.find('.') diff --git a/borax/loader.py b/borax/loader.py deleted file mode 100644 index 4bdc04f..0000000 --- a/borax/loader.py +++ /dev/null @@ -1,14 +0,0 @@ -# coding=utf8 - -import warnings - -from .system import load_class as _load_class - - -def load_class(s): - """Import a class - :param s: the full path of the class - :return: - """ - warnings.warn('This method is deprecated. Use `borax.system.load_class` instead .', DeprecationWarning) - return _load_class(s) diff --git a/borax/strings.py b/borax/strings.py index 7b9428c..dfa56e7 100644 --- a/borax/strings.py +++ b/borax/strings.py @@ -15,3 +15,33 @@ def snake2camel(s): def get_percentage_display(value, places=2): fmt = '{0:. f}%'.replace(' ', str(places)) return fmt.format(value * 100) + + +class FileEndingUtil: + WINDOWS_LINE_ENDING = b'\r\n' + LINUX_LINE_ENDING = b'\n' + + @staticmethod + def windows2linux(content: bytes) -> bytes: + assert isinstance(content, bytes) + return content.replace(FileEndingUtil.WINDOWS_LINE_ENDING, FileEndingUtil.LINUX_LINE_ENDING) + + @staticmethod + def linux2windows(content: bytes) -> bytes: + return content.replace(FileEndingUtil.LINUX_LINE_ENDING, FileEndingUtil.WINDOWS_LINE_ENDING) + + @staticmethod + def convert_to_linux_style_file(file_path): + with open(file_path, 'rb') as f: + content = f.read() + content = FileEndingUtil.windows2linux(content) + with open(file_path, 'wb') as f: + f.write(content) + + @staticmethod + def convert_to_windows_style_file(file_path): + with open(file_path, 'rb') as f: + content = f.read() + content = FileEndingUtil.linux2windows(content) + with open(file_path, 'wb') as f: + f.write(content) diff --git a/borax/structures/lookup.py b/borax/structures/lookup.py index 7482aec..9dbbead 100644 --- a/borax/structures/lookup.py +++ b/borax/structures/lookup.py @@ -1,7 +1,6 @@ # coding=utf8 import collections -import warnings class TableLookup: @@ -26,10 +25,6 @@ def feed(self, table_data): def find(self, key, default=None): return self._dataset.get(key, default) - def data_dict(self, field): - warnings.warn("'data_dict' method is deprecated, use 'select_as_dict' instead.", DeprecationWarning) - return self.select_as_dict(field) - def select_as_dict(self, field): return {k: getattr(v, field) for k, v in self._dataset.items()} diff --git a/docs/_sidebar.md b/docs/_sidebar.md index 5deb7f5..23833ca 100644 --- a/docs/_sidebar.md +++ b/docs/_sidebar.md @@ -2,18 +2,19 @@ - [快速开始](quickstart) - **Borax.Calendar** - [农历](guides/lunardate) - - [节日(新)](guides/festival) - - [生日(新)](guides/birthday) + - [节日](guides/festival) + - [生日](guides/birthday) - **Borax.Pattern** - [单例模式](guides/singleton) - [选项Choices](guides/choices) +- **Borax.Datasets** + - [数据连接(Join)(新)](guides/join) - **文档** - [字典](guides/alias_dictionary) - [树形结构](guides/tree) - - [序列号生成器(新)](guides/serial_generator) + - [序列号生成器](guides/serial_generator) - [百分比](guides/percentage) - [数据拾取](guides/fetch) - - [Django-Admin装饰器](guides/admin_decorators) - [bjson](guides/bjson) - [cjson](guides/cjson) - [财务工具](guides/finance) diff --git a/docs/changelog.md b/docs/changelog.md index b45c0ed..ef03c65 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,31 @@ # 更新日志 +## v3.1.0 + +> 新增 Python3.8构建 + +- `datasets` 包 + - 新增 `borax.datasets.fetch` + - 新增 `borax.datasets.join_` 模块 + - `join_one` 新增 default 参数 +- `calendars.lunardate` 模块 + - 修正农历闰月转平月错误的BUG ([#11](https://github.com/kinegratii/borax/issues/11)) +- `borax.fetch` 模块 + - 本模块被标记为 PendingDeprecationWarning ,将在V3.3移除 + +## v3.0.0 (20191125) + +- `borax.strings` 模块 + - 新增 windows/linux 换行符转换 `FileEndingUtils` +- `borax.structures` 模块 + - 移除 `TableLookup.data_dict` 方法 +- `borax.counters.serials` 模块 + - 新增 `SerialGenerator.generate_next_one` 方法 +- `borax.finance` 模块 + - `financial_amount_capital` 新增上下限检查 +- 移除 `borax.loader` +- 移除 `borax.decorators.admin` + ## v1.4.2 (20190717) - `counters.serials` 模块 diff --git a/docs/guides/admin_decorators.md b/docs/guides/admin_decorators.md deleted file mode 100644 index daa5de9..0000000 --- a/docs/guides/admin_decorators.md +++ /dev/null @@ -1,74 +0,0 @@ -# decorators.admin 模块 - -> 模块: `borax.decorators.admin` - -> 本模块已废弃,将在 v2.0 移除。 - -## attr - -函数签名 - -``` -attr(**kwargs) -``` - -设置函数对象的属性。 - -## display_field - -函数签名 - -``` -display_field(short_description, admin_order_field=None, **kwargs) -``` - -使用装饰器定义回调函数的 [list_display](https://docs.djangoproject.com/en/2.0/ref/contrib/admin/#django.contrib.admin.ModelAdmin.list_display) 。 - - -原始例子: - -```python -def upper_case_name(obj): - return ("%s %s" % (obj.first_name, obj.last_name)).upper() -upper_case_name.short_description = 'Name' - -class PersonAdmin(admin.ModelAdmin): - list_display = (upper_case_name,) -``` - -使用 `@display_field` 改写如下: - -```python -@display_field(short_description='Name') -def upper_case_name(obj): - return ("%s %s" % (obj.first_name, obj.last_name)).upper() - -class PersonAdmin(admin.ModelAdmin): - list_display = (upper_case_name,) -``` - -## action - -函数签名 - -``` -action(short_description=None, allowed_permissions=None, **kwargs) -``` - -使用装饰器定义 [action](https://docs.djangoproject.com/en/2.0/ref/contrib/admin/actions/#writing-action-functions) 函数 。 - -例子: - -```python -def make_published(modeladmin, request, queryset): - queryset.update(status='p') -make_published.short_description = "Mark selected stories as published" -``` - -改写后: - -```python -@action(short_description="Mark selected stories as published") -def make_published(modeladmin, request, queryset): - queryset.update(status='p') -``` \ No newline at end of file diff --git a/docs/guides/fetch.md b/docs/guides/fetch.md index 216dc85..db0922d 100644 --- a/docs/guides/fetch.md +++ b/docs/guides/fetch.md @@ -1,10 +1,12 @@ # Fetch 模块 -> 模块:`borax.fetch` +> 模块:`borax.datasets.fetch` + +> 引用路径:`borax.fetch` ## 函数接口 -`borax.fetch` 模块实现了从数据列表按照指定的一个或多个属性/键选取数据。 +`borax.datasets.fetch` 模块实现了从数据列表按照指定的一个或多个属性/键选取数据。 `fetch` 模块包含了以下几个函数: @@ -31,7 +33,7 @@ 从 `objects` 数据获取 `name` 的数据。 ```python -from borax.fetch import fetch +from borax.datasets.fetch import fetch objects = [ {'id': 282, 'name': 'Alice', 'age': 30}, @@ -54,7 +56,7 @@ print(names) 从 `objects` 数据获取 `name` 和 `age` 的数据。 ```python -from borax.fetch import fetch +from borax.datasets.fetch import fetch objects = [ {'id': 282, 'name': 'Alice', 'age': 30}, @@ -79,7 +81,7 @@ print(ages) 当 `iterable` 数据列表缺少某个属性/键,可以通过指定 `default` 或 `defaults` 参数提供默认值。 ```python -from borax.fetch import fetch +from borax.datasets.fetch import fetch objects = [ {'id': 282, 'name': 'Alice', 'age': 30, 'gender': 'female'}, @@ -113,7 +115,7 @@ Demo for multiple default values 除了上述的键值访问方式,`fetch` 函数还内置属性访问的获取方式。 ```python -from borax.fetch import fetch +from borax.datasets.fetch import fetch class Point: def __init__(self, x, y, z): @@ -163,7 +165,7 @@ getter 需满足下列的几个条件: 例子: ```python -from borax.fetch import fetch +from borax.datasets.fetch import fetch class Point: diff --git a/docs/guides/join.md b/docs/guides/join.md new file mode 100644 index 0000000..cde505c --- /dev/null +++ b/docs/guides/join.md @@ -0,0 +1,103 @@ +# join 模块 + +> 模块 `borax.datasets.join_` + +本模块实现了类似于数据库的 JOIN 数据列表操作,从另一个数据集获取某一个或几个列的值。 + +## 概述 + +本模块示例所用的数据描述如下: + +图书清单 + +```python +books = [ + {'name': 'Python入门教程', 'catalog': 1, 'price': 45}, + {'name': 'Java标准库', 'catalog': 2, 'price': 80}, + {'name': '软件工程(本科教学版)', 'catalog': 3, 'price': 45}, + {'name': 'Django Book', 'catalog': 1, 'price': 45}, + {'name': '系统架构设计教程', 'catalog': 3, 'price': 104}, +] +``` + +类别(字典格式) + +```python +catalogs_dict = { + 1: 'Python', + 2: 'Java', + 3: '软件工程' +} +``` + +类别(列表格式) + +```python +catalogs_list = [ + {'id': 1, 'name': 'Python'}, + {'id': 2, 'name': 'Java'}, + {'id': 3, 'name': '软件工程'}, +] +``` + + + + +## join_one方法 + +*`join_one(data_list, values, from_, as_, default=None)`* + +> V3.1 新增default参数。 + +从字典读取某一列的值。 + +从 `catalogs_dict` 获取类别名称并按照 catalogs.id 分组填充至 `books` 。 + +```python +catalog_books = join_one(books, catalogs_dict, from_='catalog', as_='catalog_name') +``` + +输出 + +```python +[ + {'name': 'Python入门教程', 'catalog': 1, 'price': 45, 'catalog_name': 'Python'}, + {'name': 'Java标准库', 'catalog': 2, 'price': 80, 'catalog_name': 'Java'}, + {'name': '软件工程(本科教学版)', 'catalog': 3, 'price': 45, 'catalog_name': '软件工程'}, + {'name': 'Django Book', 'catalog': 1, 'price': 45, 'catalog_name': 'Python'}, + {'name': '系统架构设计教程', 'catalog': 3, 'price': 104, 'catalog_name': '软件工程'} +] +``` + + + +## join方法 + +*`join(data_list, values, from_, to_, as_args=None, as_kwargs=None):`* + +从字典读取多个列的值。 + +示例1 + +```python +catalog_books = join( + books, + catalogs_list, + from_='catalog', + to_='id', + as_kwargs={'name': 'catalog_name'} +) +``` + +输出 + +```python +[ + {'name': 'Python入门教程', 'catalog': 1, 'price': 45, 'catalog_name': 'Python'}, + {'name': 'Java标准库', 'catalog': 2, 'price': 80, 'catalog_name': 'Java'}, + {'name': '软件工程(本科教学版)', 'catalog': 3, 'price': 45, 'catalog_name': '软件工程'}, + {'name': 'Django Book', 'catalog': 1, 'price': 45, 'catalog_name': 'Python'}, + {'name': '系统架构设计教程', 'catalog': 3, 'price': 104, 'catalog_name': '软件工程'} +] +``` + diff --git a/docs/index.html b/docs/index.html index 19920df..119ebe6 100644 --- a/docs/index.html +++ b/docs/index.html @@ -24,7 +24,7 @@ var footer = [ '
', '' ].join(''); diff --git a/docs/release/release-v300.md b/docs/release/release-v300.md new file mode 100644 index 0000000..cbb495e --- /dev/null +++ b/docs/release/release-v300.md @@ -0,0 +1,21 @@ +# V3.0.0发布日志 + +## 概述 + +2019年11月15日,Borax 正式发布 V3.0.0 。 + +## 1 Borax-Cli + +Borax-V3 新增一系列命令行工具。 + +### windows/unix文件换行符转化 + +转换为 Unix 风格的换行符 + +```shell +// 转换单个文件 +$ borax-cli w2u install.sh + +//转化多个文件,并输出到另外一个目录 +$ borax-cli w2u *.sh --output dist/scripts +``` \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index a5c2dea..ee09d0e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "borax" -version = "1.4.2" +version = "3.0.1" description = "A util collections for Python3." readme = "long_description.rst" authors = ["kinegratii "] diff --git a/requirements_dev.txt b/requirements_dev.txt index 701f33f..ae07189 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,5 +1,5 @@ -nose==1.3.7 -coverage==4.4.1 -flake8==3.6.0 +nose2==0.9.1 +coverage==5.0.1 +flake8==3.7.9 mccabe==0.6.1 -wheel==0.31.1 \ No newline at end of file +wheel==0.33.6 \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index db9f8ef..52563e9 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,5 +5,5 @@ long_description = file: long_description.rst universal = 1 [flake8] -ignore = E743,E501 +ignore = E743,E501,F401 max-complexity = 10 \ No newline at end of file diff --git a/setup.py b/setup.py index fc24814..7507351 100644 --- a/setup.py +++ b/setup.py @@ -15,6 +15,7 @@ "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3 :: Only", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", diff --git a/tests/test_choices3.py b/tests/test_choices3.py new file mode 100644 index 0000000..1d026c5 --- /dev/null +++ b/tests/test_choices3.py @@ -0,0 +1,63 @@ +# coding=utf8 + +import unittest + +from borax import choices3 + + +class BItemTestCase(unittest.TestCase): + def test_choice_item_equal(self): + c = choices3.BItem(4, 'Demo') + self.assertTrue(c == 4) + self.assertTrue(4 == c) + self.assertFalse(c != 4) + self.assertFalse(4 != c) + self.assertEqual(4, c) + + def test_member(self): + members = [ + choices3.BItem(2, 'A'), + choices3.BItem(3, 'B'), + choices3.BItem(4, 'C') + ] + self.assertTrue(2 in members) + + +class Demo1Field(choices3.BChoices): + A = 1, 'VER' + B = 2, 'STE' + c = 3, 'SSS' + D = 5 + _E = 6, 'ES' + F = 8 + G = choices3.BItem(10, 'G') + + +class FieldChoiceTestCase(unittest.TestCase): + def test_get_value(self): + self.assertEqual(1, Demo1Field.A) + self.assertEqual(2, Demo1Field.B) + self.assertEqual(5, Demo1Field.D) + + def test_is_valid(self): + self.assertTrue(1 in Demo1Field) + self.assertTrue(2 in Demo1Field) + self.assertTrue(3 in Demo1Field) + self.assertFalse(4 in Demo1Field) + self.assertTrue(5 in Demo1Field) + self.assertFalse(6 in Demo1Field) + + def test_get_display(self): + self.assertEqual('VER', Demo1Field.get_value_display(1)) + self.assertIsNone(Demo1Field.get_value_display(4)) + self.assertEqual('d', Demo1Field.get_value_display(5)) + self.assertEqual('f', Demo1Field.get_value_display(8)) + self.assertEqual('G', Demo1Field.get_value_display(10)) + + +class FieldChoicesNewAttrTestCase(unittest.TestCase): + def test_text(self): + self.assertListEqual( + [1, 2, 3, 5, 8, 10], + Demo1Field.values + ) diff --git a/tests/test_finance.py b/tests/test_finance.py index 6ac917e..95e4c0f 100644 --- a/tests/test_finance.py +++ b/tests/test_finance.py @@ -18,3 +18,13 @@ def test_amount_capital(self): def test_decimal(self): self.assertEqual('肆元伍角零分', financial_amount_capital(decimal.Decimal(4.50))) self.assertEqual('壹拾万柒仟元伍角叁分', financial_amount_capital(decimal.Decimal('107000.53'))) + + def test_valid_range(self): + with self.assertRaises(ValueError): + financial_amount_capital(332342342341234) + with self.assertRaises(ValueError): + financial_amount_capital(1000000000000) + self.assertIsNotNone(financial_amount_capital(999999999999)) + self.assertIsNotNone(financial_amount_capital(999999999999.99)) + with self.assertRaises(ValueError): + financial_amount_capital('1000000000000') diff --git a/tests/test_join.py b/tests/test_join.py new file mode 100644 index 0000000..d6b6b5e --- /dev/null +++ b/tests/test_join.py @@ -0,0 +1,62 @@ +# coding=utf8 + +import copy +import unittest + +from borax.datasets.join_ import join_one, join + +catalogs_dict = { + 1: 'Python', + 2: 'Java', + 3: '软件工程' +} +catalogs_list = [ + {'id': 1, 'name': 'Python'}, + {'id': 2, 'name': 'Java'}, + {'id': 3, 'name': '软件工程'}, +] +books = [ + {'name': 'Python入门教程', 'catalog': 1, 'price': 45}, + {'name': 'Java标准库', 'catalog': 2, 'price': 80}, + {'name': '软件工程(本科教学版)', 'catalog': 3, 'price': 45}, + {'name': 'Django Book', 'catalog': 1, 'price': 45}, + {'name': '系统架构设计教程', 'catalog': 3, 'price': 104}, +] + + +class JoinOneTestCase(unittest.TestCase): + def test_join_one(self): + book_data = copy.deepcopy(books) + catalog_books = join_one(book_data, catalogs_dict, from_='catalog', as_='catalog_name') + self.assertTrue(all(['catalog_name' in book for book in catalog_books])) + self.assertEqual('Java', catalog_books[1]['catalog_name']) + + def test_join_one_with_default(self): + book_data = copy.deepcopy(books) + cur_catalogs_dict = { + 1: 'Python', + 2: 'Java' + } + + catalog_books = join_one(book_data, cur_catalogs_dict, from_='catalog', as_='catalog_name') + self.assertTrue(all(['catalog_name' in book for book in catalog_books])) + self.assertEqual(None, catalog_books[2]['catalog_name']) + + def test_join_one_with_custom_default(self): + book_data = copy.deepcopy(books) + cur_catalogs_dict = { + 1: 'Python', + 2: 'Java' + } + + catalog_books = join_one(book_data, cur_catalogs_dict, from_='catalog', as_='catalog_name', default='[未知分类]') + self.assertTrue(all(['catalog_name' in book for book in catalog_books])) + self.assertEqual('[未知分类]', catalog_books[2]['catalog_name']) + + +class JoinTestCase(unittest.TestCase): + def test_as_kwargs(self): + book_data = copy.deepcopy(books) + catalog_books = join(book_data, catalogs_list, from_='catalog', to_='id', as_kwargs={'name': 'catalog_name'}) + self.assertTrue(all(['catalog_name' in book for book in catalog_books])) + self.assertEqual('Java', catalog_books[1]['catalog_name']) diff --git a/tests/test_string_convert.py b/tests/test_string_convert.py index a2ba946..f2cff7c 100644 --- a/tests/test_string_convert.py +++ b/tests/test_string_convert.py @@ -4,6 +4,7 @@ import unittest from unittest.mock import Mock, patch +from nose2.tools.params import params from borax.strings import camel2snake, snake2camel, get_percentage_display from borax.system import rotate_filename, SUFFIX_DT_UNDERLINE @@ -18,11 +19,10 @@ class StringConvertTestCase(unittest.TestCase): - def test_all(self): - for cs, ss in FIXTURES: - with self.subTest(cs=cs, ss=ss): - self.assertEqual(cs, snake2camel(ss)) - self.assertEqual(ss, camel2snake(cs)) + @params(*FIXTURES) + def test_all(self, cs, ss): + self.assertEqual(cs, snake2camel(ss)) + self.assertEqual(ss, camel2snake(cs)) class PercentageStringTestCase(unittest.TestCase):