parent
e5e23e0c05
commit
b46a87de04
27
README.md
27
README.md
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
|
@ -1,2 +1,2 @@
|
|||
[metadata]
|
||||
description-file = README.md
|
||||
description_file = README.md
|
||||
|
|
2
setup.py
2
setup.py
|
@ -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',
|
||||
|
|
|
@ -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.
|
|
@ -0,0 +1,12 @@
|
|||
[DEFAULT]
|
||||
ServerAliveInterval = 45
|
||||
Compression = yes
|
||||
CompressionLevel = 9
|
||||
ForwardX11 = yes
|
||||
|
||||
[bitbucket.org]
|
||||
User = hg
|
||||
|
||||
[topsecret.server.com]
|
||||
Port = 50022
|
||||
ForwardX11 = no
|
|
@ -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))
|
Loading…
Reference in New Issue