Skip to content
Open
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: 1 addition & 1 deletion graphtage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
ast, bounds, builder, constraints, dataclasses, edits, expressions, fibonacci, formatter, levenshtein, matching,
object_set, pickle, printer, pydiff, search, sequences, tree, utils
)
from . import csv, json, plist, toml, xml, yaml
from . import csv, ini, json, plist, toml, xml, yaml

import inspect

Expand Down
2 changes: 2 additions & 0 deletions graphtage/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ def printer_type(*pos_args, **kwargs):
mimetypes.add_type('application/json5', '.json5')
if '.toml' not in mimetypes.types_map:
mimetypes.add_type('application/toml', '.toml')
if '.ini' not in mimetypes.types_map:
mimetypes.add_type('text/ini', '.ini')
if '.plist' not in mimetypes.types_map:
mimetypes.add_type('application/x-plist', '.plist')
if '.pkl' not in mimetypes.types_map and '.pickle' not in mimetypes.types_map:
Expand Down
92 changes: 92 additions & 0 deletions graphtage/ini.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""A :class:`graphtage.Filetype` for parsing, diffing, and rendering INI files."""

import configparser
import os
from typing import ClassVar, Optional, Type, Union

from . import json
from .graphtage import BuildOptions, Filetype, KeyValuePairNode, MappingNode, StringFormatter, StringNode
from .printer import Printer
from .tree import GraphtageFormatter, TreeNode


def _parser() -> configparser.ConfigParser:
parser = configparser.ConfigParser(interpolation=None)
parser.optionxform = str
return parser


def build_tree(path: str, options: Optional[BuildOptions]) -> TreeNode:
parser = _parser()
with open(path) as f:
parser.read_file(f)

data = {}
if parser.defaults():
data[parser.default_section] = dict(parser.defaults())
for section in parser.sections():
data[section] = dict(parser[section])
return json.build_tree(data, options)


class INIStringFormatter(StringFormatter):
"""A string formatter for INI keys and values."""
is_partial = True

def escape(self, c: str) -> str:
return c.replace("\n", "\\n")


class INIFormatter(GraphtageFormatter):
"""A formatter for INI files."""
sub_format_types: ClassVar[list[Type[GraphtageFormatter]]] = [INIStringFormatter]

def print_KeyValuePairNode(self, printer: Printer, node: KeyValuePairNode):
if isinstance(node.key, StringNode):
node.key.quoted = False
self.print(printer, node.key)
printer.write(" = ")
if isinstance(node.value, StringNode):
node.value.quoted = False
self.print(printer, node.value)
printer.newline()

def print_MappingNode(self, printer: Printer, node: MappingNode):
for section in node:
if not isinstance(section.value, MappingNode):
continue
printer.write("[")
if isinstance(section.key, StringNode):
section.key.quoted = False
self.print(printer, section.key)
printer.write("]")
printer.newline()
for item in section.value:
self.print(printer, item)
printer.newline()


class INI(Filetype):
"""The INI filetype."""

def __init__(self):
"""Initializes the INI filetype."""
super().__init__(
"ini",
"text/ini",
"application/ini",
"text/x-ini",
)

def build_tree(self, path: str, options: Optional[BuildOptions] = None) -> TreeNode:
"""Equivalent to :func:`build_tree`"""
return build_tree(path, options=options)

def build_tree_handling_errors(self, path: str, options: Optional[BuildOptions] = None) -> Union[str, TreeNode]:
try:
return self.build_tree(path=path, options=options)
except (configparser.Error, OSError) as e:
return f"Error parsing {os.path.basename(path)}: {e}"

def get_default_formatter(self) -> INIFormatter:
return INIFormatter.DEFAULT_INSTANCE
20 changes: 20 additions & 0 deletions test/test_formatting.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import csv
import configparser
import json
import plistlib
import random
Expand Down Expand Up @@ -233,6 +234,25 @@ def test_toml_formatting(self):
except (TypeError, ValueError, IndexError) as e:
self.fail(f"""Invalid random TOML object {orig_obj!r}: {e}""")

@filetype_test(iterations=200)
def test_ini_formatting(self):
config = configparser.ConfigParser(interpolation=None)
config.optionxform = str
excluded = frozenset('\t \\\'"\r:[]{}&\n()`|+%<>#*^$@!~_+-=.,;?/')
orig_obj = {
TestFormatting.make_random_str(exclude_bytes=excluded, allow_empty_strings=False): {
TestFormatting.make_random_str(exclude_bytes=excluded, allow_empty_strings=False):
TestFormatting.make_random_str(exclude_bytes=frozenset('\n\r'), allow_empty_strings=True)
for _ in range(random.randint(1, 5))
}
for _ in range(random.randint(1, 5))
}
for section, options in orig_obj.items():
config[section] = options
s = StringIO()
config.write(s)
return orig_obj, s.getvalue()

@staticmethod
def make_random_xml() -> xml.XMLElementObj:
ret = xml.XMLElementObj('', {})
Expand Down