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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ jobs:
run: uv pip install -e ".[dev]"
- name: Run ruff
run: uv run ruff check .
- name: Run ruff format check
run: uv run ruff format --check .
- name: Run mypy
run: uv run mypy jsoncsv

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Just use them.

## Requirements

- Python 3.8 or higher
- Python 3.11 or higher

## Quick Start

Expand Down
13 changes: 5 additions & 8 deletions jsoncsv/dumptool.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ def load_headers(
class DumpExcel(Dump, ReadHeadersMixin):
def initialize(self, **kwargs: Any) -> None:
super().initialize(**kwargs)
self._read_row = kwargs.get('read_row')
self._sort_type = kwargs.get('sort_type')
self._read_row = kwargs.get("read_row")
self._sort_type = kwargs.get("sort_type")

def prepare(self) -> None:
headers, datas = self.load_headers(self.fin, self._read_row, self._sort_type)
Expand Down Expand Up @@ -104,10 +104,7 @@ def write_headers(self) -> None:
self.csv_writer.writeheader()

def write_obj(self, obj: dict[str, JsonType]) -> None:
patched_obj: dict[str, str] = {
key: self.patch_value(value)
for key, value in obj.items()
}
patched_obj: dict[str, str] = {key: self.patch_value(value) for key, value in obj.items()}
assert self.csv_writer is not None
self.csv_writer.writerow(patched_obj)

Expand All @@ -121,8 +118,8 @@ class DumpXLS(DumpExcel):
def initialize(self, **kwargs: Any) -> None:
super().initialize(**kwargs)

self.sheet = kwargs.get('sheet', 'Sheet1')
self.wb = xlwt.Workbook(encoding='utf-8')
self.sheet = kwargs.get("sheet", "Sheet1")
self.wb = xlwt.Workbook(encoding="utf-8")
self.ws = self.wb.add_sheet(self.sheet)
self.row = 0
self.cloumn = 0
Expand Down
20 changes: 10 additions & 10 deletions jsoncsv/jsontool.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
)

__all__ = [
'convert_json',
'expand',
'restore',
"convert_json",
"expand",
"restore",
]

# Type alias for the func parameter in convert_json
Expand Down Expand Up @@ -90,7 +90,7 @@ def from_leaf(leafs: Iterable[LeafInputType]) -> JsonType:
return dict(child) # type: ignore[arg-type]


def expand(origin: JsonType, separator: str = '.', safe: bool = False) -> dict[str, JsonType]:
def expand(origin: JsonType, separator: str = ".", safe: bool = False) -> dict[str, JsonType]:
root = origin
leafs = gen_leaf(root)

Expand All @@ -105,15 +105,15 @@ def expand(origin: JsonType, separator: str = '.', safe: bool = False) -> dict[s
return expobj


def restore(expobj: dict[str, JsonType], separator: str = '.', safe: bool = False) -> JsonType:
def restore(expobj: dict[str, JsonType], separator: str = ".", safe: bool = False) -> JsonType:
leafs: list[tuple[DecodedPathType, JsonType]] = []

items = expobj.items()

for key, value in items:
path: DecodedPathType = decode_safe_key(key, separator) if safe else key.split(separator)

if key == '':
if key == "":
path = []

leafs.append((path, value))
Expand All @@ -126,13 +126,13 @@ def convert_json(
fin: io.TextIOBase,
fout: io.TextIOBase,
func: ConvertFunc,
separator: str = '.',
separator: str = ".",
safe: bool = False,
json_array: bool = False,
) -> None:
'''
"""
ensure fin/fout is TextIO
'''
"""

if func not in [expand, restore]:
raise ValueError("unknow convert_json type")
Expand All @@ -158,4 +158,4 @@ def gen_objs_from_array() -> Iterator[JsonType]:
new = func(obj, separator=separator, safe=safe)
content = json.dumps(new, ensure_ascii=False)
fout.write(content)
fout.write('\n')
fout.write("\n")
94 changes: 34 additions & 60 deletions jsoncsv/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,40 +15,22 @@

def separator_type(sep: str) -> str:
if len(sep) != 1:
raise click.BadOptionUsage(option_name='separator',
message='separator can only be a char')
raise click.BadOptionUsage(option_name="separator", message="separator can only be a char")
if sep == unit_char:
raise click.BadOptionUsage(option_name='separator',
message='separator can not be `\\` ')
raise click.BadOptionUsage(option_name="separator", message="separator can not be `\\` ")
return sep


@click.command()
@click.option('-A',
'--array',
'json_array',
is_flag=True,
default=False,
help='read input file as json array')
@click.option('-s',
'--sep',
'separator',
type=separator_type,
default='.',
help='separator')
@click.option('--safe', is_flag=True, help='use safe mode')
@click.option('-r',
'--restore',
'restore',
is_flag=True,
help='restore expanded json')
@click.option('-e',
'--expand',
'expand',
is_flag=True,
help='expand json (default True)')
@click.argument('input', type=click.File('r', encoding='utf-8'), default='-')
@click.argument('output', type=click.File('w', encoding='utf-8'), default='-')
@click.option(
"-A", "--array", "json_array", is_flag=True, default=False, help="read input file as json array"
)
@click.option("-s", "--sep", "separator", type=separator_type, default=".", help="separator")
@click.option("--safe", is_flag=True, help="use safe mode")
@click.option("-r", "--restore", "restore", is_flag=True, help="restore expanded json")
@click.option("-e", "--expand", "expand", is_flag=True, help="expand json (default True)")
@click.argument("input", type=click.File("r", encoding="utf-8"), default="-")
@click.argument("output", type=click.File("w", encoding="utf-8"), default="-")
def jsoncsv(
output: io.TextIOBase,
input: io.TextIOBase,
Expand All @@ -59,42 +41,34 @@ def jsoncsv(
json_array: bool,
) -> None:
if expand and restore:
raise click.UsageError('can not choose both, default is `-e`')
raise click.UsageError("can not choose both, default is `-e`")

func: Callable[..., Any]
func = expand_fn if not restore else restore_fn

convert_json(input,
output,
func,
separator=separator,
safe=safe,
json_array=json_array)
convert_json(input, output, func, separator=separator, safe=safe, json_array=json_array)

input.close()
output.close()


@click.command()
@click.option('-t',
'--type',
'type_',
type=click.Choice(['csv', 'xls']),
default='csv',
help='choose dump format')
@click.option('-r',
'--row',
type=int,
default=None,
help='number of pre-read `row` lines to load `headers`')
@click.option('-s',
'--sort',
'sort_',
is_flag=True,
default=False,
help='enable sort the headers keys')
@click.argument('input', type=click.File('r', encoding='utf-8'), default='-')
@click.argument('output', type=click.Path(), default='-')
@click.option(
"-t",
"--type",
"type_",
type=click.Choice(["csv", "xls"]),
default="csv",
help="choose dump format",
)
@click.option(
"-r", "--row", type=int, default=None, help="number of pre-read `row` lines to load `headers`"
)
@click.option(
"-s", "--sort", "sort_", is_flag=True, default=False, help="enable sort the headers keys"
)
@click.argument("input", type=click.File("r", encoding="utf-8"), default="-")
@click.argument("output", type=click.Path(), default="-")
def mkexcel(
output: str,
input: io.TextIOBase,
Expand All @@ -107,13 +81,13 @@ def mkexcel(
klass = dumptool.DumpXLS

# Open file in appropriate mode based on type
if output == '-':
fout: Any = sys.stdout.buffer if type_ == 'xls' else sys.stdout
if output == "-":
fout: Any = sys.stdout.buffer if type_ == "xls" else sys.stdout
dump_excel(input, fout, klass, read_row=row, sort_type=sort_)
else:
mode = 'wb' if type_ == 'xls' else 'w'
encoding = None if type_ == 'xls' else 'utf-8'
newline = '' if type_ == 'csv' else None
mode = "wb" if type_ == "xls" else "w"
encoding = None if type_ == "xls" else "utf-8"
newline = "" if type_ == "csv" else None
with open(output, mode, encoding=encoding, newline=newline) as fout:
dump_excel(input, fout, klass, read_row=row, sort_type=sort_)

Expand Down
10 changes: 5 additions & 5 deletions jsoncsv/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
# 2016.11.20

# Type aliases for JSON data structures
JsonType = dict[str, 'JsonType'] | list['JsonType'] | str | int | float | bool | None
JsonType = dict[str, "JsonType"] | list["JsonType"] | str | int | float | bool | None
PathType = list[int | str] # Can contain ints (array indices) or strings (dict keys)
DecodedPathType = list[str] # Decoded paths from keys are always strings
LeafType = tuple[PathType, JsonType]
# Type for leafs that can contain either PathType (from gen_leaf) or DecodedPathType (from restore)
LeafInputType = LeafType | tuple[DecodedPathType, JsonType]

unit_char = '\\'
unit_char = "\\"


def encode_safe_key(path: list[str], separator: str) -> str:
Expand All @@ -20,13 +20,13 @@ def encode_safe_key(path: list[str], separator: str) -> str:

def decode_safe_key(key: str, separator: str) -> list[str]:
path: list[str] = []
p = ''
p = ""
escape = False

for char in key:
if escape and char == separator:
path.append(p)
p = ''
p = ""
escape = False
elif escape and char == unit_char:
p += unit_char
Expand All @@ -36,6 +36,6 @@ def decode_safe_key(key: str, separator: str) -> list[str]:
else:
p += char

if p != '':
if p != "":
path.append(p)
return path
45 changes: 30 additions & 15 deletions tests/test_dumptool.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,54 @@


class TestDumpTool(unittest.TestCase):

# FIXME (使用虚拟文件)
def test_dumpexcel_csv(self):
with open('./fixture/files/expand.1.json', encoding='utf-8') as fin, \
open('./fixture/files/tmp.output.1.csv', 'w', encoding='utf-8', newline='') as fout:
with (
open("./fixture/files/expand.1.json", encoding="utf-8") as fin,
open("./fixture/files/tmp.output.1.csv", "w", encoding="utf-8", newline="") as fout,
):
dump_excel(fin, fout, DumpCSV)

with open('./fixture/files/output.1.csv', encoding='utf-8') as output, \
open('./fixture/files/tmp.output.1.csv', encoding='utf-8') as fout:
with (
open("./fixture/files/output.1.csv", encoding="utf-8") as output,
open("./fixture/files/tmp.output.1.csv", encoding="utf-8") as fout,
):
self.assertEqual(output.read(), fout.read())

def test_dumpexcel_csv_with_sort(self):
with open('./fixture/files/expand.1.json', encoding='utf-8') as fin, \
open('./fixture/files/tmp.output.1.sort.csv', 'w', encoding='utf-8', newline='') as fout:
with (
open("./fixture/files/expand.1.json", encoding="utf-8") as fin,
open(
"./fixture/files/tmp.output.1.sort.csv", "w", encoding="utf-8", newline=""
) as fout,
):
dump_excel(fin, fout, DumpCSV, sort_type=True)

with open('./fixture/files/output.1.sort.csv', encoding='utf-8') as output, \
open('./fixture/files/tmp.output.1.sort.csv', encoding='utf-8') as fout:
with (
open("./fixture/files/output.1.sort.csv", encoding="utf-8") as output,
open("./fixture/files/tmp.output.1.sort.csv", encoding="utf-8") as fout,
):
self.assertEqual(output.read(), fout.read())

def test_dumpcexcel_xls(self):
with open('./fixture/files/expand.1.json', encoding='utf-8') as fin, \
open('./fixture/files/tmp.output.1.xls', 'wb') as fout:
with (
open("./fixture/files/expand.1.json", encoding="utf-8") as fin,
open("./fixture/files/tmp.output.1.xls", "wb") as fout,
):
dump_excel(fin, fout, DumpXLS)

def test_dump_csv_with_non_ascii(self):
with open('./fixture/files/expand.2.json', encoding='utf-8') as fin, \
open('./fixture/files/tmp.output.2.csv', 'w', encoding='utf-8', newline='') as fout:
with (
open("./fixture/files/expand.2.json", encoding="utf-8") as fin,
open("./fixture/files/tmp.output.2.csv", "w", encoding="utf-8", newline="") as fout,
):
dump_excel(fin, fout, DumpCSV)

def test_dump_xls_with_non_ascii(self):
with open('./fixture/files/expand.2.json', encoding='utf-8') as fin, \
open('./fixture/files/tmp.output.2.xls', 'wb') as fout:
with (
open("./fixture/files/expand.2.json", encoding="utf-8") as fin,
open("./fixture/files/tmp.output.2.xls", "wb") as fout,
):
dump_excel(fin, fout, DumpXLS)

def test_dump_xls_with_dict(self):
Expand Down
23 changes: 11 additions & 12 deletions tests/test_escape.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,28 @@


class Testescape(unittest.TestCase):

def test_all(self):
path = ['A', 'B', '..', '\\.\\ww']
path = ["A", "B", "..", "\\.\\ww"]

for sep in 'AB.w':
for sep in "AB.w":
key = encode_safe_key(path, sep)
_path = decode_safe_key(key, sep)

self.assertListEqual(path, _path)

def test_encode(self):
path = ['A', 'B', 'C', 'www.xxx.com']
sep = '.'
path = ["A", "B", "C", "www.xxx.com"]
sep = "."
key = encode_safe_key(path, sep)

self.assertEqual(key, 'A\\.B\\.C\\.www.xxx.com')
self.assertEqual(key, "A\\.B\\.C\\.www.xxx.com")

def test_decode(self):
key = 'A\\.B\\.C\\.www.xxx.com'
sep = '.'
key = "A\\.B\\.C\\.www.xxx.com"
sep = "."
path = decode_safe_key(key, sep)

self.assertEqual(path[0], 'A')
self.assertEqual(path[1], 'B')
self.assertEqual(path[2], 'C')
self.assertEqual(path[3], 'www.xxx.com')
self.assertEqual(path[0], "A")
self.assertEqual(path[1], "B")
self.assertEqual(path[2], "C")
self.assertEqual(path[3], "www.xxx.com")
Loading