Added `ini` format support. #36 #40

This commit is contained in:
Fabio Caccamo 2021-05-04 23:22:11 +02:00
parent e5e23e0c05
commit b46a87de04
9 changed files with 328 additions and 3 deletions

View File

@ -14,14 +14,14 @@
[![](https://requires.io/github/fabiocaccamo/python-benedict/requirements.svg?branch=master)](https://requires.io/github/fabiocaccamo/python-benedict/requirements/?branch=master)
# python-benedict
python-benedict is a dict subclass with **keylist/keypath** support, **I/O** shortcuts (`base64`, `csv`, `json`, `pickle`, `plist`, `query-string`, `toml`, `xml`, `yaml`.) and many **utilities**... for humans, obviously.
python-benedict is a dict subclass with **keylist/keypath** support, **I/O** shortcuts (`base64`, `csv`, `ini`, `json`, `pickle`, `plist`, `query-string`, `toml`, `xml`, `yaml`) and many **utilities**... for humans, obviously.
## Features
- 100% **backward-compatible**, you can safely wrap existing dictionaries.
- **Keylist** support using **list of keys** as key.
- **Keypath** support using **keypath-separator** *(dot syntax by default)*.
- Keypath **list-index** support *(also negative)* using the standard `[n]` suffix.
- Easy **I/O operations** with most common formats: `base64`, `csv`, `json`, `pickle`, `plist`, `query-string`, `toml`, `xml`, `yaml`.
- Normalized **I/O operations** with most common formats: `base64`, `csv`, `ini`, `json`, `pickle`, `plist`, `query-string`, `toml`, `xml`, `yaml`.
- Many **utility** and **parse methods** to retrieve data as needed *(check the [API](#api) section)*.
- Well **tested**. ;)
@ -185,6 +185,7 @@ lng = loc.get_decimal('longitude')
- [`from_base64`](#from_base64)
- [`from_csv`](#from_csv)
- [`from_ini`](#from_ini)
- [`from_json`](#from_json)
- [`from_pickle`](#from_pickle)
- [`from_plist`](#from_plist)
@ -194,6 +195,7 @@ lng = loc.get_decimal('longitude')
- [`from_yaml`](#from_yaml)
- [`to_base64`](#to_base64)
- [`to_csv`](#to_csv)
- [`to_ini`](#to_ini)
- [`to_json`](#to_json)
- [`to_pickle`](#to_pickle)
- [`to_plist`](#to_plist)
@ -478,6 +480,17 @@ d = benedict.from_base64(s, subformat='json', encoding='utf-8', **kwargs)
d = benedict.from_csv(s, columns=None, columns_row=True, **kwargs)
```
- #### from_ini
```python
# Try to load/decode a ini encoded data and return it as benedict instance.
# Accept as first argument: url, filepath or data-string.
# It's possible to pass decoder specific options using kwargs:
# https://docs.python.org/3/library/configparser.html
# A ValueError is raised in case of failure.
d = benedict.from_ini(s, **kwargs)
```
- #### from_json
```python
@ -575,6 +588,16 @@ s = d.to_base64(subformat='json', encoding='utf-8', **kwargs)
s = d.to_csv(key='values', columns=None, columns_row=True, **kwargs)
```
- #### to_ini
```python
# Return the dict instance encoded in ini format and optionally save it at the specified filepath.
# It's possible to pass encoder specific options using kwargs:
# https://docs.python.org/3/library/configparser.html
# A ValueError is raised in case of failure.
s = d.to_ini(**kwargs)
```
- #### to_json
```python

View File

@ -81,6 +81,16 @@ class IODict(BaseDict):
kwargs['columns_row'] = columns_row
return cls(s, format='csv', **kwargs)
@classmethod
def from_ini(cls, s, **kwargs):
"""
Load and decode INI data from url, filepath or data-string.
Decoder specific options can be passed using kwargs:
https://docs.python.org/3/library/configparser.html
Return a new dict instance. A ValueError is raised in case of failure.
"""
return cls(s, format='ini', **kwargs)
@classmethod
def from_json(cls, s, **kwargs):
"""
@ -173,6 +183,16 @@ class IODict(BaseDict):
kwargs['columns_row'] = columns_row
return self._encode(self.dict()[key], 'csv', **kwargs)
def to_ini(self, **kwargs):
"""
Encode the current dict instance in INI format.
Encoder specific options can be passed using kwargs:
https://docs.python.org/3/library/configparser.html
Return the encoded string and optionally save it at 'filepath'.
A ValueError is raised in case of failure.
"""
return self._encode(self.dict(), 'ini', **kwargs)
def to_json(self, **kwargs):
"""
Encode the current dict instance in JSON format.

View File

@ -3,6 +3,7 @@
from benedict.serializers.abstract import AbstractSerializer
from benedict.serializers.base64 import Base64Serializer
from benedict.serializers.csv import CSVSerializer
from benedict.serializers.ini import INISerializer
from benedict.serializers.json import JSONSerializer
from benedict.serializers.pickle import PickleSerializer
from benedict.serializers.plist import PListSerializer
@ -16,6 +17,7 @@ import re
_BASE64_SERIALIZER = Base64Serializer()
_CSV_SERIALIZER = CSVSerializer()
_INI_SERIALIZER = INISerializer()
_JSON_SERIALIZER = JSONSerializer()
_PICKLE_SERIALIZER = PickleSerializer()
_PLIST_SERIALIZER = PListSerializer()
@ -28,6 +30,7 @@ _SERIALIZERS = {
'b64': _BASE64_SERIALIZER,
'base64': _BASE64_SERIALIZER,
'csv': _CSV_SERIALIZER,
'ini': _INI_SERIALIZER,
'json': _JSON_SERIALIZER,
'pickle': _PICKLE_SERIALIZER,
'plist': _PLIST_SERIALIZER,

View File

@ -0,0 +1,69 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import
from benedict.serializers.abstract import AbstractSerializer
from benedict.utils import type_util
try:
from configparser import ConfigParser
from configparser import DEFAULTSECT as default_section
except ImportError:
from ConfigParser import ConfigParser
default_section = 'DEFAULT'
from six import PY2, StringIO
class INISerializer(AbstractSerializer):
def __init__(self):
super(INISerializer, self).__init__()
@staticmethod
def _get_section_option_value(parser, section, option):
value = None
funcs = [
parser.getint,
parser.getfloat,
parser.getboolean,
parser.get,
]
for func in funcs:
try:
value = func(section, option)
break
except ValueError:
continue
return value
def decode(self, s, **kwargs):
parser = ConfigParser(**kwargs)
if PY2:
parser.readfp(StringIO(s))
else:
parser.read_string(s)
data = {}
for option, _ in parser.defaults().items():
data[option] = self._get_section_option_value(
parser, default_section, option)
for section in parser.sections():
data[section] = {}
for option, _ in parser.items(section):
data[section][option] = self._get_section_option_value(
parser, section, option)
return data
def encode(self, d, **kwargs):
parser = ConfigParser(**kwargs)
for key, value in d.items():
if not type_util.is_dict(value):
parser.set(default_section, key, '{}'.format(value))
continue
section = key
parser.add_section(section)
for option_key, option_value in value.items():
parser.set(section, option_key, '{}'.format(option_value))
str_data = StringIO()
parser.write(str_data)
return str_data.getvalue()

View File

@ -1,2 +1,2 @@
[metadata]
description-file = README.md
description_file = README.md

View File

@ -37,6 +37,8 @@ setup(
'python', 'dictionary', 'dictionaries', 'dict', 'benedict',
'subclass', 'extended', 'keylist', 'keypath', 'utility', 'io',
'data', 'file', 'url', 'read', 'write', 'parse',
'configparser', 'config', 'cfg', 'pickle', 'plist',
'base64', 'csv', 'ini', 'json', 'query-string', 'toml', 'xml', 'yaml',
'clean', 'clone', 'deepclone', 'deepupdate', 'dump',
'filter', 'flatten', 'groupby', 'invert', 'merge',
'move', 'nest', 'remove', 'rename', 'search', 'standardize',

View File

@ -0,0 +1,2 @@
Lorem ipsum consectetur sint id aute officia sed excepteur consectetur labore laboris dolore in labore consequat ut in eu ut deserunt.
Elit aliqua velit aliquip voluptate consequat reprehenderit occaecat dolor ut esse aute laboris cillum fugiat esse est laborum.

View File

@ -0,0 +1,12 @@
[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes
[bitbucket.org]
User = hg
[topsecret.server.com]
Port = 50022
ForwardX11 = no

View File

@ -0,0 +1,194 @@
# -*- coding: utf-8 -*-
from benedict.dicts.io import IODict
from .test_io_dict import io_dict_test_case
class io_dict_ini_test_case(io_dict_test_case):
def test_from_ini_with_valid_data(self):
s = """
[DEFAULT]
ServerAliveInterval = 45
Compression = yes
CompressionLevel = 9
ForwardX11 = yes
[bitbucket.org]
User = hg
[topsecret.server.com]
Port = 50022
ForwardX11 = no
"""
# static method
r = {
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9,
'forwardx11': True,
'bitbucket.org': {
'user': 'hg',
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9,
'forwardx11': True
},
'topsecret.server.com': {
'port': 50022,
'forwardx11': False,
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9
}
}
d = IODict.from_ini(s)
self.assertTrue(isinstance(d, dict))
self.assertEqual(d, r)
# constructor
d = IODict(s, format='ini')
self.assertTrue(isinstance(d, dict))
self.assertEqual(d, r)
def test_from_ini_with_invalid_data(self):
s = 'Lorem ipsum est in ea occaecat nisi officia.'
# static method
with self.assertRaises(ValueError):
IODict.from_ini(s)
# constructor
with self.assertRaises(ValueError):
IODict(s, format='ini')
def test_from_ini_with_valid_file_valid_content(self):
filepath = self.input_path('valid-content.ini')
# static method
d = IODict.from_ini(filepath)
self.assertTrue(isinstance(d, dict))
# constructor
d = IODict(filepath, format='ini')
self.assertTrue(isinstance(d, dict))
# constructor with format autodetection
d = IODict(filepath)
self.assertTrue(isinstance(d, dict))
def test_from_ini_with_valid_file_valid_content_invalid_format(self):
filepath = self.input_path('valid-content.base64')
with self.assertRaises(ValueError):
d = IODict.from_ini(filepath)
filepath = self.input_path('valid-content.json')
with self.assertRaises(ValueError):
IODict.from_ini(filepath)
filepath = self.input_path('valid-content.plist')
with self.assertRaises(ValueError):
IODict.from_ini(filepath)
filepath = self.input_path('valid-content.qs')
with self.assertRaises(ValueError):
IODict.from_ini(filepath)
filepath = self.input_path('valid-content.toml')
with self.assertRaises(ValueError):
IODict.from_ini(filepath)
filepath = self.input_path('valid-content.xml')
with self.assertRaises(ValueError):
IODict.from_ini(filepath)
filepath = self.input_path('valid-content.yml')
with self.assertRaises(ValueError):
IODict.from_ini(filepath)
def test_from_ini_with_valid_file_invalid_content(self):
filepath = self.input_path('invalid-content.ini')
# static method
with self.assertRaises(ValueError):
IODict.from_ini(filepath)
# constructor
with self.assertRaises(ValueError):
IODict(filepath, format='ini')
def test_from_ini_with_invalid_file(self):
filepath = self.input_path('invalid-file.ini')
# static method
with self.assertRaises(ValueError):
IODict.from_ini(filepath)
# constructor
with self.assertRaises(ValueError):
IODict(filepath, format='ini')
# def test_from_ini_with_valid_url_valid_content(self):
# url = self.input_url('valid-content.ini')
# # static method
# d = IODict.from_ini(url)
# self.assertTrue(isinstance(d, dict))
# # constructor
# d = IODict(url, format='ini')
# self.assertTrue(isinstance(d, dict))
# # constructor with format autodetection
# d = IODict(url)
# self.assertTrue(isinstance(d, dict))
def test_from_ini_with_valid_url_invalid_content(self):
url = 'https://github.com/fabiocaccamo/python-benedict'
# static method
with self.assertRaises(ValueError):
IODict.from_ini(url)
# constructor
with self.assertRaises(ValueError):
IODict(url, format='ini')
def test_from_ini_with_invalid_url(self):
url = 'https://github.com/fabiocaccamo/python-benedict-invalid'
# static method
with self.assertRaises(ValueError):
IODict.from_ini(url)
# constructor
with self.assertRaises(ValueError):
IODict(url, format='ini')
def test_to_ini(self):
d = IODict({
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9,
'forwardx11': True,
'bitbucket.org': {
'user': 'hg',
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9,
'forwardx11': True
},
'topsecret.server.com': {
'port': 50022,
'forwardx11': False,
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9
}
})
s = d.to_ini()
self.assertEqual(d, IODict.from_ini(s))
def test_to_ini_file(self):
d = IODict({
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9,
'forwardx11': True,
'bitbucket.org': {
'user': 'hg',
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9,
'forwardx11': True
},
'topsecret.server.com': {
'port': 50022,
'forwardx11': False,
'serveraliveinterval': 45,
'compression': True,
'compressionlevel': 9
}
})
filepath = self.output_path('test_to_ini_file.ini')
d.to_ini(filepath=filepath)
self.assertFileExists(filepath)
self.assertEqual(d, IODict.from_ini(filepath))