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
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "yamlcli"
version = "0.2.0"
version = "1.0.0"
description = "Convert YAML files to JSON and vice versa"
requires-python = ">=3.10,<4.0"
readme = "README.md"
Expand All @@ -9,10 +9,11 @@ authors = [
]
dependencies = [
"pyyaml",
"typer>=0.20.0",
]

[project.scripts]
yamlcli = "yamlcli.cli:main"
yamlcli = "yamlcli:main"

[build-system]
requires = ["uv_build>=0.9.11,<0.10.0"]
Expand Down
3 changes: 3 additions & 0 deletions src/yamlcli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .cli import app, main

__all__ = ["app", "main"]
117 changes: 61 additions & 56 deletions src/yamlcli/cli.py
Original file line number Diff line number Diff line change
@@ -1,72 +1,77 @@
import sys
import yaml
import json
import argparse

from pathlib import Path
import yaml
import typer

class RegmonkeyDumper(yaml.Dumper):
def increase_indent(self, flow=False, indentless=False):
return super(RegmonkeyDumper, self).increase_indent(flow, False)
from yamlcli.yamlcli_core import yaml_to_json, json_to_yaml
from yamlcli.library.version import __version__

app = typer.Typer(
add_completion=False,
help=(
"A simple command-line tool to convert between YAML and JSON formats.\n\n"
"Features:\n"
"- Convert YAML files to JSON\n"
"- Convert JSON files to YAML\n"
"- Support for custom JSON indentation\n"
"- Robust error handling for invalid files or malformed data"
),
)

def yaml_to_json(file_path, indent):
with open(file_path, "r") as f:
data = yaml.safe_load(f)
def version_callback(value: bool):
if value:
print(f"yamlcli {__version__}")
print(f"Python {sys.version.split()[0]}")
raise typer.Exit()

if indent <= 0:
print(json.dumps(data))
else:
print(json.dumps(data, indent=indent))

@app.command()
def converter(
file: str = typer.Argument(..., help="Input file path"),
to_json: bool = typer.Option(False, "--to-json", help="Convert YAML → JSON"),
to_yaml: bool = typer.Option(False, "--to-yaml", help="Convert JSON → YAML"),
indent: int = typer.Option(2, "--indent", help="Indentation level (default: 2)"),
version: bool = typer.Option(
False,
"--version",
"-v",
help="Show version information and exit.",
callback=version_callback,
is_eager=True,
),
):
"""Main entry point for yamlcli"""
# 排他チェック(pytest 要求)
if (not to_json and not to_yaml) or (to_json and to_yaml):
typer.echo("Error: Specify exactly one of --to-json or --to-yaml", err=True)
raise typer.Exit(code=1)

def json_to_yaml(file_path, indent):
with open(file_path, "r") as f:
data = json.load(f)
if not Path(file).exists():
typer.echo(f"Error: File not found - {file}", err=True)
raise typer.Exit(code=1)

if indent <= 0:
print(yaml.safe_dump(data, sort_keys=False))
else:
# Dump YAML with 2-space indentation for lists
print(
yaml.dump(
data,
Dumper=RegmonkeyDumper,
sort_keys=False,
default_flow_style=False,
indent=indent,
)
)
try:
if to_yaml:
json_to_yaml(file, indent)
else:
yaml_to_json(file, indent)

except yaml.YAMLError as e:
typer.echo(f"Error: Parsing failed - {e}", err=True)
raise typer.Exit(code=1)

def main():
parser = argparse.ArgumentParser(description="Convert YAML ↔ JSON")
parser.add_argument("file", help="Input file path")
parser.add_argument("--to-json", action="store_true", help="Convert YAML to JSON")
parser.add_argument("--to-yaml", action="store_true", help="Convert JSON to YAML")
parser.add_argument(
"--indent",
type=int,
default=2,
help="Indentation level for JSON output (default: 2)",
)
args = parser.parse_args()
except json.JSONDecodeError as e:
typer.echo(f"Error: Parsing failed - {e}", err=True)
raise typer.Exit(code=1)

if args.to_json == args.to_yaml:
print("Error: Specify exactly one of --to-json or --to-yaml", file=sys.stderr)
sys.exit(1)

try:
if args.to_yaml:
json_to_yaml(args.file, args.indent)
else:
yaml_to_json(args.file, args.indent)
except FileNotFoundError:
print(f"Error: File not found - {args.file}", file=sys.stderr)
sys.exit(1)
except (yaml.YAMLError, json.JSONDecodeError) as e:
print(f"Error: Parsing failed - {e}", file=sys.stderr)
sys.exit(1)
typer.echo(f"Error: File not found - {file}", err=True)
raise typer.Exit(code=1)


def main():
app()

if __name__ == "__main__":
main()
app()
Empty file added src/yamlcli/library/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions src/yamlcli/library/helper_func.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from pathlib import Path
import tomllib
from importlib.metadata import version, PackageNotFoundError


def get_version() -> str:
"""
Retrieve the version of the installed package.

Priority:
1. Installed metadata (importlib.metadata.version)
2. [DEBUG]: pyproject.toml in development mode
3. [DEBUG]: fallback "0.0.0"
"""

# 1. Installed package metadata
try:
return version("yamlcli")
except PackageNotFoundError:
pass

# 2. [DEBUG]: Development mode: look for pyproject.toml
current = Path(__file__).resolve()
for parent in current.parents:
pyproject = parent / "pyproject.toml"
if pyproject.exists():
try:
with pyproject.open("rb") as f:
data = tomllib.load(f)
project_version = data.get("project", {}).get("version")
if project_version:
return project_version
except Exception:
pass

# 3. [DEBUG]: fallback
return "0.0.0"
3 changes: 3 additions & 0 deletions src/yamlcli/library/version.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from yamlcli.library.helper_func import get_version

__version__ = get_version()
36 changes: 36 additions & 0 deletions src/yamlcli/yamlcli_core.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import yaml # type: ignore[import-untyped]
import json


class RegmonkeyDumper(yaml.Dumper):
def increase_indent(self, flow=False, indentless=False):
return super(RegmonkeyDumper, self).increase_indent(flow, False)


def yaml_to_json(file_path, indent):
with open(file_path, "r") as f:
data = yaml.safe_load(f)

if indent <= 0:
print(json.dumps(data))
else:
print(json.dumps(data, indent=indent))


def json_to_yaml(file_path, indent):
with open(file_path, "r") as f:
data = json.load(f)

if indent <= 0:
print(yaml.safe_dump(data, sort_keys=False))
else:
# Dump YAML with 2-space indentation for lists
print(
yaml.dump(
data,
Dumper=RegmonkeyDumper,
sort_keys=False,
default_flow_style=False,
indent=indent,
)
)
125 changes: 125 additions & 0 deletions tests/test_coremodule.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import pytest
import json
import yaml
import tempfile
import os
from yamlcli.yamlcli_core import yaml_to_json, json_to_yaml

@pytest.fixture
def sample_yaml_file():
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".yaml") as f:
f.write("""
name: John Doe
age: 30
hobbies:
- reading
- coding
address:
street: 123 Main St
city: Example City
""")
yield f.name
os.unlink(f.name)


@pytest.fixture
def sample_json_file():
with tempfile.NamedTemporaryFile(mode="w", delete=False, suffix=".json") as f:
json.dump(
{
"name": "John Doe",
"age": 30,
"hobbies": ["reading", "coding"],
"address": {"street": "123 Main St", "city": "Example City"},
},
f,
indent=2,
)
yield f.name
os.unlink(f.name)


def test_yaml_to_json_basic(sample_yaml_file, capsys):
yaml_to_json(sample_yaml_file, indent=2)
captured = capsys.readouterr()
output = captured.out

# Parse the output back to Python object for comparison
parsed_output = json.loads(output)
expected = {
"name": "John Doe",
"age": 30,
"hobbies": ["reading", "coding"],
"address": {"street": "123 Main St", "city": "Example City"},
}
assert parsed_output == expected


def test_yaml_to_json_no_indent(sample_yaml_file, capsys):
yaml_to_json(sample_yaml_file, indent=0)
captured = capsys.readouterr()
output = captured.out

# Verify it's a single line JSON
assert len(output.strip().split("\n")) == 1

# Verify content is correct
parsed_output = json.loads(output)
assert parsed_output["name"] == "John Doe"
assert parsed_output["age"] == 30


def test_yaml_to_json_file_not_found():
with pytest.raises(FileNotFoundError):
yaml_to_json("nonexistent.yaml", indent=2)


def test_yaml_to_json_invalid_yaml(tmp_path, capsys):
invalid_yaml = tmp_path / "invalid.yaml"
invalid_yaml.write_text(
"""invalid:
- missing
indentation
"""
)

yaml_to_json(invalid_yaml, indent=2)
captured = capsys.readouterr()
output = captured.out
parsed_output = json.loads(output)

# Since PyYAML is lenient, check output instead of expecting exception
assert parsed_output == {"invalid": ["missing indentation"]}


def test_json_to_yaml_basic(sample_json_file, capsys):
json_to_yaml(sample_json_file, indent=0)
output = capsys.readouterr().out

# Parse the output back to Python object for comparison
parsed_output = yaml.safe_load(output)
expected = {
"name": "John Doe",
"age": 30,
"hobbies": ["reading", "coding"],
"address": {"street": "123 Main St", "city": "Example City"},
}
assert parsed_output == expected


def test_json_to_yaml_file_not_found():
with pytest.raises(FileNotFoundError):
json_to_yaml("nonexistent.json", indent=0)


def test_json_to_yaml_invalid_json(tmp_path, capsys):
invalid_json = tmp_path / "invalid.json"
invalid_json.write_text("""
{
"invalid": "json",
missing: quotes
}
""")

with pytest.raises(json.JSONDecodeError):
json_to_yaml(str(invalid_json), indent=0)
Loading