diff --git a/.gitignore b/.gitignore index 505a3b1..b39c597 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ wheels/ # Virtual environments .venv +examples/*.json diff --git a/README.md b/README.md index 3b941d5..855762a 100644 --- a/README.md +++ b/README.md @@ -1,39 +1,119 @@ +# zig-fetch-py + +A Python tool to parse Zig Object Notation (ZON) files and convert them to JSON. + +## Installation + +### Using uv (recommended) + +[uv](https://github.com/astral-sh/uv) is a fast Python package installer and resolver. To install zig-fetch-py using uv: + +```bash +# Install uv if you don't have it +curl -sSf https://astral.sh/uv/install.sh | bash + +# Create and activate a virtual environment +uv venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install the package in development mode +uv pip install -e . + +# Install development dependencies +uv pip install -e ".[dev]" +``` + +### Using pip + +```bash +# Create and activate a virtual environment +python -m venv .venv +source .venv/bin/activate # On Windows: .venv\Scripts\activate + +# Install the package in development mode +pip install -e . + +# Install development dependencies +pip install -e ".[dev]" +``` + +## Usage + +### Command Line + +```bash +# Basic usage +zon2json path/to/file.zon + +# Output to a file +zon2json path/to/file.zon -o output.json + +# Pretty print the JSON +zon2json path/to/file.zon -p + +# Enable verbose logging +zon2json path/to/file.zon -v +``` + +### Python API + +```python +from zig_fetch_py.parser import parse_zon_file, zon_to_json + +# Parse a ZON file +result = parse_zon_file("path/to/file.zon") +print(result) # Python dictionary + +# Convert ZON content to JSON +zon_content = """.{ + .name = "test", + .version = "1.0.0", +}""" +json_str = zon_to_json(zon_content, indent=4) +print(json_str) +``` + +## Development + +### Running Tests + +```bash +# Run all tests +pytest + +# Run tests with coverage +pytest --cov=zig_fetch_py + +# Generate coverage report +pytest --cov=zig_fetch_py --cov-report=html +``` + +## ZON Format + +Zig Object Notation (ZON) is a data format used by the Zig programming language. It's similar to JSON but with some differences in syntax: + +- Objects are defined with `.{ ... }` +- Keys are prefixed with a dot: `.key = value` +- Arrays are defined with `.[ ... ]` +- Special identifiers can be quoted with `@`: `.@"special-name" = value` +- Comments use `//` syntax + +Example: + ```zon .{ - .name = .zls, - // Must match the `zls_version` in `build.zig` - .version = "0.15.0-dev", - // Must be kept in line with the `minimum_build_zig_version` in `build.zig`. - // Should be a Zig version that is downloadable from https://ziglang.org/download/ or a mirror. - .minimum_zig_version = "0.14.0", - // If you do not use Nix, a ZLS maintainer can take care of this. - // Whenever the dependencies are updated, run the following command: - // ```bash - // nix run github:Cloudef/zig2nix#zon2nix -- build.zig.zon > deps.nix - // rm build.zig.zon2json-lock # this file is unnecessary - // ``` + .name = "example", + .version = "1.0.0", .dependencies = .{ - .known_folders = .{ - .url = "https://github.com/ziglibs/known-folders/archive/aa24df42183ad415d10bc0a33e6238c437fc0f59.tar.gz", - .hash = "known_folders-0.0.0-Fy-PJtLDAADGDOwYwMkVydMSTp_aN-nfjCZw6qPQ2ECL", - }, - .diffz = .{ - .url = "https://github.com/ziglibs/diffz/archive/ef45c00d655e5e40faf35afbbde81a1fa5ed7ffb.tar.gz", - .hash = "N-V-__8AABhrAQAQLLLGadghhPsdxTgBk9N9aLVOjXW3ay0V", - }, - .@"lsp-codegen" = .{ - .url = "https://github.com/zigtools/zig-lsp-codegen/archive/063a98c13a2293d8654086140813bdd1de6501bc.tar.gz", - .hash = "lsp_codegen-0.1.0-CMjjo0ZXCQB-rAhPYrlfzzpU0u0u2MeGvUucZ-_g32eg", - }, - .tracy = .{ - .url = "https://github.com/wolfpld/tracy/archive/refs/tags/v0.11.1.tar.gz", - .hash = "N-V-__8AAMeOlQEipHjcyu0TCftdAi9AQe7EXUDJOoVe0k-t", - .lazy = true, + .lib1 = .{ + .url = "https://example.com/lib1.tar.gz", + .hash = "abcdef123456", }, }, - .paths = .{""}, - .fingerprint = 0xa66330b97eb969ae, // Changing this has security and trust implications. + .tags = .["tag1", "tag2"], } ``` -It's called `zon`, the Zig Object Notation. I think You need to parse it first (convert to json) and then parse the json. +## License + +MIT \ No newline at end of file diff --git a/examples/example.zon b/examples/example.zon new file mode 100644 index 0000000..7ca93b8 --- /dev/null +++ b/examples/example.zon @@ -0,0 +1,11 @@ +.{ + .name = "example", + .version = "1.0.0", + .dependencies = .{ + .lib1 = .{ + .url = "https://example.com/lib1.tar.gz", + .hash = "abcdef123456", + }, + }, + .tags = .["tag1", "tag2"], +} \ No newline at end of file diff --git a/examples/simple_example.py b/examples/simple_example.py new file mode 100755 index 0000000..9a48e2e --- /dev/null +++ b/examples/simple_example.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 +""" +Simple example demonstrating how to use the zig-fetch-py library. +""" + +import json +import sys +from pathlib import Path + +from loguru import logger +from zig_fetch_py.parser import parse_zon_file, zon_to_json + +# Configure logging +logger.remove() +logger.add(sys.stderr, level="INFO") + +# Example ZON content +ZON_CONTENT = """.{ + .name = "example", + .version = "1.0.0", + .dependencies = .{ + .lib1 = .{ + .url = "https://example.com/lib1.tar.gz", + .hash = "abcdef123456", + }, + }, + .tags = .["tag1", "tag2"], +}""" + + +def main(): + # Create a temporary ZON file + example_dir = Path(__file__).parent + zon_file = example_dir / "example.zon" + zon_file.write_text(ZON_CONTENT) + + logger.info(f"Created example ZON file: {zon_file}") + + # Parse the ZON file + result = parse_zon_file(str(zon_file)) + logger.info("Parsed ZON file to Python dictionary:") + logger.info(result) + + # Convert to JSON + json_str = zon_to_json(ZON_CONTENT, indent=2) + logger.info("Converted ZON to JSON:") + logger.info(json_str) + + # Save JSON to file + json_file = example_dir / "example.json" + with open(json_file, "w") as f: + f.write(json_str) + + logger.info(f"Saved JSON to file: {json_file}") + + # Access specific values from the parsed data + logger.info(f"Package name: {result['name']}") + logger.info(f"Package version: {result['version']}") + logger.info(f"Dependencies: {list(result['dependencies'].keys())}") + logger.info(f"Tags: {result['tags']}") + + +if __name__ == "__main__": + main() diff --git a/test.zon b/examples/test.zon similarity index 100% rename from test.zon rename to examples/test.zon diff --git a/pyproject.toml b/pyproject.toml index 2e324ff..1d46f41 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,7 @@ version = "0.1.0" description = "A tool to parse Zig Object Notation (ZON) files and convert them to JSON" readme = "README.md" requires-python = ">=3.12" -dependencies = [ - "click>=8.1.8", - "loguru>=0.7.3", -] +dependencies = ["click>=8.1.8", "loguru>=0.7.3"] [project.scripts] zon2json = "zig_fetch_py.main:main" @@ -19,5 +16,14 @@ build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["zig_fetch_py"] +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = "test_*.py" +python_functions = "test_*" +python_classes = "Test*" + [tool.ruff] line-length = 100 + +[project.optional-dependencies] +dev = ["pytest>=8.3.5", "pytest-cov>=6.0.0"] diff --git a/tests/test_parser.py b/tests/test_parser.py new file mode 100644 index 0000000..e2be48b --- /dev/null +++ b/tests/test_parser.py @@ -0,0 +1,189 @@ +""" +Unit tests for the ZON parser. +""" + +import json +import pytest +from pathlib import Path + +from zig_fetch_py.parser import ZonParser, parse_zon_file, zon_to_json + + +class TestZonParser: + """Test cases for the ZonParser class.""" + + def test_parse_empty_object(self): + """Test parsing an empty object.""" + parser = ZonParser(".{}") + result = parser.parse() + assert result == {} + + def test_parse_simple_object(self): + """Test parsing a simple object with string values.""" + parser = ZonParser( + """.{ + .name = "test", + .version = "1.0.0", + }""" + ) + result = parser.parse() + assert result == {"name": "test", "version": "1.0.0"} + + def test_parse_nested_object(self): + """Test parsing a nested object.""" + parser = ZonParser( + """.{ + .metadata = .{ + .name = "test", + .version = "1.0.0", + }, + }""" + ) + result = parser.parse() + assert result == {"metadata": {"name": "test", "version": "1.0.0"}} + + def test_parse_array(self): + """Test parsing an array.""" + parser = ZonParser( + """.{ + .tags = .["tag1", "tag2", "tag3"], + }""" + ) + result = parser.parse() + assert result == {"tags": ["tag1", "tag2", "tag3"]} + + def test_parse_numbers(self): + """Test parsing different number formats.""" + parser = ZonParser( + """.{ + .integer = 42, + .negative = -10, + .float = 3.14, + .hex = 0xDEADBEEF, + }""" + ) + result = parser.parse() + assert result == { + "integer": 42, + "negative": -10, + "float": 3.14, + "hex": 0xDEADBEEF, + } + + def test_parse_boolean(self): + """Test parsing boolean values.""" + parser = ZonParser( + """.{ + .is_true = true, + .is_false = false, + }""" + ) + result = parser.parse() + assert result == {"is_true": True, "is_false": False} + + def test_parse_null(self): + """Test parsing null values.""" + parser = ZonParser( + """.{ + .nothing = null, + }""" + ) + result = parser.parse() + assert result == {"nothing": None} + + def test_parse_comments(self): + """Test parsing with comments.""" + parser = ZonParser( + """.{ + // This is a comment + .name = "test", // Inline comment + // Another comment + .version = "1.0.0", + }""" + ) + result = parser.parse() + assert result == {"name": "test", "version": "1.0.0"} + + def test_parse_special_identifiers(self): + """Test parsing special identifiers with @ symbol.""" + parser = ZonParser( + """.{ + .@"special-name" = "value", + }""" + ) + result = parser.parse() + assert result == {"special-name": "value"} + + def test_parse_shorthand_notation(self): + """Test parsing shorthand notation where key is the same as value.""" + parser = ZonParser( + """.{ + .name, + .version, + }""" + ) + result = parser.parse() + assert result == {"name": "name", "version": "version"} + + def test_parse_escaped_strings(self): + """Test parsing strings with escape sequences.""" + parser = ZonParser( + """.{ + .escaped = "Line 1\\nLine 2\\tTabbed\\r\\n", + }""" + ) + result = parser.parse() + assert result == {"escaped": "Line 1\nLine 2\tTabbed\r\n"} + + def test_parse_error_invalid_syntax(self): + """Test that parser raises an error for invalid syntax.""" + with pytest.raises(ValueError): + parser = ZonParser(".{,}") + parser.parse() + + def test_parse_error_unterminated_string(self): + """Test that parser raises an error for unterminated strings.""" + with pytest.raises(ValueError): + parser = ZonParser( + """.{ + .name = "unterminated, + }""" + ) + parser.parse() + + +class TestZonFileParser: + """Test cases for the file parsing functions.""" + + def test_zon_to_json(self, tmp_path): + """Test converting ZON to JSON.""" + zon_content = """.{ + .name = "test", + .version = "1.0.0", + }""" + + # Test without indentation + json_str = zon_to_json(zon_content) + parsed_json = json.loads(json_str) + assert parsed_json == {"name": "test", "version": "1.0.0"} + + # Test with indentation + json_str_pretty = zon_to_json(zon_content, indent=2) + assert " " in json_str_pretty # Should have indentation + parsed_json = json.loads(json_str_pretty) + assert parsed_json == {"name": "test", "version": "1.0.0"} + + def test_parse_zon_file(self, tmp_path): + """Test parsing a ZON file.""" + # Create a temporary ZON file + zon_file = tmp_path / "test.zon" + zon_file.write_text( + """.{ + .name = "test", + .version = "1.0.0", + }""" + ) + + # Parse the file + result = parse_zon_file(str(zon_file)) + assert result == {"name": "test", "version": "1.0.0"} diff --git a/uv.lock b/uv.lock index 6b6d912..3960c53 100644 --- a/uv.lock +++ b/uv.lock @@ -23,6 +23,54 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, ] +[[package]] +name = "coverage" +version = "7.6.12" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/7f/4af2ed1d06ce6bee7eafc03b2ef748b14132b0bdae04388e451e4b2c529b/coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad", size = 208645 }, + { url = "https://files.pythonhosted.org/packages/dc/60/d19df912989117caa95123524d26fc973f56dc14aecdec5ccd7d0084e131/coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3", size = 208898 }, + { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, + { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, + { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, + { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, + { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, + { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, + { url = "https://files.pythonhosted.org/packages/9e/9d/fa04d9e6c3f6459f4e0b231925277cfc33d72dfab7fa19c312c03e59da99/coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95", size = 211289 }, + { url = "https://files.pythonhosted.org/packages/53/40/53c7ffe3c0c3fff4d708bc99e65f3d78c129110d6629736faf2dbd60ad57/coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288", size = 212079 }, + { url = "https://files.pythonhosted.org/packages/76/89/1adf3e634753c0de3dad2f02aac1e73dba58bc5a3a914ac94a25b2ef418f/coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1", size = 208673 }, + { url = "https://files.pythonhosted.org/packages/ce/64/92a4e239d64d798535c5b45baac6b891c205a8a2e7c9cc8590ad386693dc/coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd", size = 208945 }, + { url = "https://files.pythonhosted.org/packages/b4/d0/4596a3ef3bca20a94539c9b1e10fd250225d1dec57ea78b0867a1cf9742e/coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9", size = 242484 }, + { url = "https://files.pythonhosted.org/packages/1c/ef/6fd0d344695af6718a38d0861408af48a709327335486a7ad7e85936dc6e/coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e", size = 239525 }, + { url = "https://files.pythonhosted.org/packages/0c/4b/373be2be7dd42f2bcd6964059fd8fa307d265a29d2b9bcf1d044bcc156ed/coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4", size = 241545 }, + { url = "https://files.pythonhosted.org/packages/a6/7d/0e83cc2673a7790650851ee92f72a343827ecaaea07960587c8f442b5cd3/coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6", size = 241179 }, + { url = "https://files.pythonhosted.org/packages/ff/8c/566ea92ce2bb7627b0900124e24a99f9244b6c8c92d09ff9f7633eb7c3c8/coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3", size = 239288 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/869a138e50b622f796782d642c15fb5f25a5870c6d0059a663667a201638/coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc", size = 241032 }, + { url = "https://files.pythonhosted.org/packages/ae/28/a52ff5d62a9f9e9fe9c4f17759b98632edd3a3489fce70154c7d66054dd3/coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3", size = 211315 }, + { url = "https://files.pythonhosted.org/packages/bc/17/ab849b7429a639f9722fa5628364c28d675c7ff37ebc3268fe9840dda13c/coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef", size = 212099 }, + { url = "https://files.pythonhosted.org/packages/d2/1c/b9965bf23e171d98505eb5eb4fb4d05c44efd256f2e0f19ad1ba8c3f54b0/coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e", size = 209511 }, + { url = "https://files.pythonhosted.org/packages/57/b3/119c201d3b692d5e17784fee876a9a78e1b3051327de2709392962877ca8/coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703", size = 209729 }, + { url = "https://files.pythonhosted.org/packages/52/4e/a7feb5a56b266304bc59f872ea07b728e14d5a64f1ad3a2cc01a3259c965/coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0", size = 253988 }, + { url = "https://files.pythonhosted.org/packages/65/19/069fec4d6908d0dae98126aa7ad08ce5130a6decc8509da7740d36e8e8d2/coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924", size = 249697 }, + { url = "https://files.pythonhosted.org/packages/1c/da/5b19f09ba39df7c55f77820736bf17bbe2416bbf5216a3100ac019e15839/coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b", size = 252033 }, + { url = "https://files.pythonhosted.org/packages/1e/89/4c2750df7f80a7872267f7c5fe497c69d45f688f7b3afe1297e52e33f791/coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d", size = 251535 }, + { url = "https://files.pythonhosted.org/packages/78/3b/6d3ae3c1cc05f1b0460c51e6f6dcf567598cbd7c6121e5ad06643974703c/coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827", size = 249192 }, + { url = "https://files.pythonhosted.org/packages/6e/8e/c14a79f535ce41af7d436bbad0d3d90c43d9e38ec409b4770c894031422e/coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9", size = 250627 }, + { url = "https://files.pythonhosted.org/packages/cb/79/b7cee656cfb17a7f2c1b9c3cee03dd5d8000ca299ad4038ba64b61a9b044/coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3", size = 212033 }, + { url = "https://files.pythonhosted.org/packages/b6/c3/f7aaa3813f1fa9a4228175a7bd368199659d392897e184435a3b66408dd3/coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f", size = 213240 }, + { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, +] + +[[package]] +name = "iniconfig" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, +] + [[package]] name = "loguru" version = "0.7.3" @@ -36,6 +84,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595 }, ] +[[package]] +name = "packaging" +version = "24.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, +] + +[[package]] +name = "pytest" +version = "8.3.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, +] + +[[package]] +name = "pytest-cov" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "coverage" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/45/9b538de8cef30e17c7b45ef42f538a94889ed6a16f2387a6c89e73220651/pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0", size = 66945 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/3b/48e79f2cd6a61dbbd4807b4ed46cb564b4fd50a76166b1c4ea5c1d9e2371/pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35", size = 22949 }, +] + [[package]] name = "win32-setctime" version = "1.2.0" @@ -54,8 +148,17 @@ dependencies = [ { name = "loguru" }, ] +[package.optional-dependencies] +dev = [ + { name = "pytest" }, + { name = "pytest-cov" }, +] + [package.metadata] requires-dist = [ { name = "click", specifier = ">=8.1.8" }, { name = "loguru", specifier = ">=0.7.3" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.5" }, + { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=6.0.0" }, ] +provides-extras = ["dev"] diff --git a/zig_fetch_py/__init__.py b/zig_fetch_py/__init__.py new file mode 100644 index 0000000..3742613 --- /dev/null +++ b/zig_fetch_py/__init__.py @@ -0,0 +1,5 @@ +""" +zig-fetch-py - A tool to parse Zig Object Notation (ZON) files and convert them to JSON. +""" + +__version__ = "0.1.0" diff --git a/zig_fetch_py/main.py b/zig_fetch_py/main.py new file mode 100644 index 0000000..d60da6d --- /dev/null +++ b/zig_fetch_py/main.py @@ -0,0 +1,60 @@ +""" +Command-line interface for the ZON parser. +""" + +import json +import sys +from pathlib import Path + +import click +from loguru import logger + +from zig_fetch_py.parser import parse_zon_file + + +@click.command() +@click.argument("file", type=click.Path(exists=True, readable=True)) +@click.option( + "-o", + "--output", + type=click.Path(writable=True), + help="Output JSON file path (default: stdout)", +) +@click.option("-p", "--pretty", is_flag=True, help="Pretty print JSON output") +@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging") +def main(file, output, pretty, verbose): + """Parse ZON files and convert to JSON. + + This tool parses Zig Object Notation (ZON) files and converts them to JSON format. + """ + # Configure logging + log_level = "DEBUG" if verbose else "INFO" + logger.remove() # Remove default handler + logger.add(sys.stderr, level=log_level) + + logger.info(f"Processing file: {file}") + + try: + result = parse_zon_file(file) + + indent = 4 if pretty else None + json_str = json.dumps(result, indent=indent) + + if output: + logger.info(f"Writing output to: {output}") + with open(output, "w") as f: + f.write(json_str) + else: + logger.debug("Writing output to stdout") + click.echo(json_str) + + except Exception as e: + logger.error(f"Error: {e}") + sys.exit(1) + + +# This is only executed when the module is run directly +if __name__ == "__main__": + # When imported as a module, click will handle the function call + # When run directly, we need to call it explicitly + main() diff --git a/main.py b/zig_fetch_py/parser.py similarity index 85% rename from main.py rename to zig_fetch_py/parser.py index f25ba34..2604322 100644 --- a/main.py +++ b/zig_fetch_py/parser.py @@ -1,11 +1,11 @@ -#!/usr/bin/env python3 -import json -import re -import sys -from pathlib import Path -from typing import Any, Dict, List, Union, Tuple +""" +ZON parser module - Parses Zig Object Notation (ZON) files. +""" + +import json +from pathlib import Path +from typing import Any, Dict, List, Union, Tuple, Optional -import click from loguru import logger @@ -14,12 +14,13 @@ class ZonParser: A parser for Zig Object Notation (ZON) files. """ - content: str - pos: int - line: int - col: int - def __init__(self, content: str): + """ + Initialize the parser with ZON content. + + Args: + content: The ZON content to parse + """ self.content = content self.pos = 0 self.line = 1 @@ -314,7 +315,15 @@ class ZonParser: def parse_zon_file(file_path: str) -> Dict[str, Any]: - """Parse a ZON file and return a Python dictionary.""" + """ + Parse a ZON file and return a Python dictionary. + + Args: + file_path: Path to the ZON file + + Returns: + Dictionary representation of the ZON file + """ logger.debug(f"Parsing ZON file: {file_path}") with open(file_path, "r") as f: content = f.read() @@ -325,46 +334,17 @@ def parse_zon_file(file_path: str) -> Dict[str, Any]: return result -@click.command() -@click.argument("file", type=click.Path(exists=True, readable=True)) -@click.option( - "-o", - "--output", - type=click.Path(writable=True), - help="Output JSON file path (default: stdout)", -) -@click.option("-p", "--pretty", is_flag=True, help="Pretty print JSON output") -@click.option("-v", "--verbose", is_flag=True, help="Enable verbose logging") -def main(file, output, pretty, verbose): - """Parse ZON files and convert to JSON. - - This tool parses Zig Object Notation (ZON) files and converts them to JSON format. +def zon_to_json(zon_content: str, indent: Optional[int] = None) -> str: """ - # Configure logging - log_level = "DEBUG" if verbose else "INFO" - logger.remove() # Remove default handler - logger.add(sys.stderr, level=log_level) + Convert ZON content to JSON string. - logger.info(f"Processing file: {file}") + Args: + zon_content: ZON content as string + indent: Number of spaces for indentation (None for compact JSON) - try: - result = parse_zon_file(file) - - indent = 4 if pretty else None - json_str = json.dumps(result, indent=indent) - - if output: - logger.info(f"Writing output to: {output}") - with open(output, "w") as f: - f.write(json_str) - else: - logger.debug("Writing output to stdout") - click.echo(json_str) - - except Exception as e: - logger.error(f"Error: {e}") - sys.exit(1) - - -if __name__ == "__main__": - main() # pylint: disable=no-value-for-parameter + Returns: + JSON string + """ + parser = ZonParser(zon_content) + result = parser.parse() + return json.dumps(result, indent=indent)