Completed
Push — master ( bff2ab...5dc771 )
by Satoru
01:07
created

anyconfig.backend._pre_process_line()   C

Complexity

Conditions 7

Size

Total Lines 31

Duplication

Lines 0
Ratio 0 %
Metric Value
cc 7
dl 0
loc 31
rs 5.5
1
#
2
# Copyright (C) 2012 - 2015 Satoru SATOH <ssato @ redhat.com>
3
# License: MIT
4
#
5
"""
6
Java properties file support.
7
8
.. versionadded:: 0.2
9
   Added native Java properties parser instead of a plugin utilizes
10
   pyjavaproperties module.
11
12
- Format to support: Java Properties file, e.g.
13
  http://docs.oracle.com/javase/1.5.0/docs/api/java/util/Properties.html
14
- Requirements: None (built-in)
15
- Limitations: None
16
- Special options: None
17
"""
18
from __future__ import absolute_import
19
20
import logging
21
import re
22
23
import anyconfig.backend.base
24
import anyconfig.compat
25
26
27
LOGGER = logging.getLogger(__name__)
28
_COMMENT_MARKERS = ("#", "!")
29
30
31
def _parseline(line):
32
    """
33
    Parse a line of Java properties file.
34
35
    :param line:
36
        A string to parse, must not start with ' ', '#' or '!' (comment)
37
    :return: A tuple of (key, value), both key and value may be None
38
39
    >>> _parseline("aaa")
40
    (None, None)
41
    >>> _parseline("calendar.japanese.type: LocalGregorianCalendar")
42
    ('calendar.japanese.type', 'LocalGregorianCalendar')
43
    """
44
    match = re.match(r"^(\S+)(?:\s+)?(?<!\\)[:=](?:\s+)?(.+)", line)
45
    if not match:
46
        LOGGER.warning("Invalid line found: %s", line)
47
        return (None, None)
48
49
    return match.groups()
50
51
52
def _pre_process_line(line, comment_markers=_COMMENT_MARKERS):
53
    """
54
    Preprocess a line in properties; strip comments, etc.
55
56
    :param line:
57
        A string not starting w/ any white spaces and ending w/ line breaks.
58
        It may be empty. see also: :func:`load`.
59
    :param comment_markers: Comment markers, e.g. '#' (hash)
60
61
    >>> _pre_process_line('') is None
62
    True
63
    >>> s0 = "calendar.japanese.type: LocalGregorianCalendar"
64
    >>> _pre_process_line("# " + s0) is None
65
    True
66
    >>> _pre_process_line("! " + s0) is None
67
    True
68
    >>> _pre_process_line(s0 + "# comment")
69
    'calendar.japanese.type: LocalGregorianCalendar'
70
    """
71
    if not line:
72
        return None
73
74
    if any(c in line for c in comment_markers):
75
        if line.startswith(comment_markers):
76
            return None
77
78
        for marker in comment_markers:
79
            if marker in line:  # Then strip the rest starts w/ it.
80
                line = line[:line.find(marker)].rstrip()
81
82
    return line
83
84
85
def unescape(in_s):
86
    """
87
    :param in_s: Input string
88
    """
89
    return re.sub(r"\\(.)", r"\1", in_s)
90
91
92
def _escape_char(in_c):
93
    """
94
    Escape some special characters in java .properties files.
95
96
    :param in_c: Input character
97
98
    >>> "\\:" == _escape_char(':')
99
    True
100
    >>> "\\=" == _escape_char('=')
101
    True
102
    >>> _escape_char('a')
103
    'a'
104
    """
105
    return '\\' + in_c if in_c in (':', '=', '\\') else in_c
106
107
108
def escape(in_s):
109
    """
110
    :param in_s: Input string
111
    """
112
    return ''.join(_escape_char(c) for c in in_s)
113
114
115
def load(stream, to_container=dict, comment_markers=_COMMENT_MARKERS):
116
    """
117
    Load and parse Java properties file given as a fiel or file-like object
118
    `stream`.
119
120
    :param stream: A file or file like object of Java properties files
121
    :param to_container:
122
        Factory function to create a dict-like object to store properties
123
    :param comment_markers: Comment markers, e.g. '#' (hash)
124
    :return: Dict-like object holding properties
125
126
    >>> to_strm = anyconfig.compat.StringIO
127
    >>> s0 = "calendar.japanese.type: LocalGregorianCalendar"
128
    >>> load(to_strm(''))
129
    {}
130
    >>> load(to_strm("# " + s0))
131
    {}
132
    >>> load(to_strm("! " + s0))
133
    {}
134
    >>> load(to_strm("calendar.japanese.type:"))
135
    {}
136
    >>> load(to_strm(s0))
137
    {'calendar.japanese.type': 'LocalGregorianCalendar'}
138
    >>> load(to_strm(s0 + "# ..."))
139
    {'calendar.japanese.type': 'LocalGregorianCalendar'}
140
    >>> s1 = r"key=a\\:b"
141
    >>> load(to_strm(s1))
142
    {'key': 'a:b'}
143
    >>> s2 = '''application/postscript: \\
144
    ...         x=Postscript File;y=.eps,.ps
145
    ... '''
146
    >>> load(to_strm(s2))
147
    {'application/postscript': 'x=Postscript File;y=.eps,.ps'}
148
    """
149
    ret = to_container()
150
    prev = ""
151
152
    for line in stream.readlines():
153
        line = _pre_process_line(prev + line.strip().rstrip(),
154
                                 comment_markers)
155
        # I don't think later case may happen but just in case.
156
        if line is None or not line:
157
            continue
158
159
        if line.endswith("\\"):
160
            prev += line.rstrip(" \\")
161
            continue
162
163
        prev = ""  # re-initialize for later use.
164
165
        (key, val) = _parseline(line)
166
        if key is None:
167
            LOGGER.warning("Failed to parse the line: %s", line)
168
            continue
169
170
        ret[key] = unescape(val)
171
172
    return ret
173
174
175
class Parser(anyconfig.backend.base.FromStreamLoader,
176
             anyconfig.backend.base.ToStreamDumper):
177
    """
178
    Parser for Java properties files.
179
    """
180
    _type = "properties"
181
    _extensions = ["properties"]
182
183
    def load_from_stream(self, stream, to_container, **kwargs):
184
        """
185
        Load config from given file like object `stream`.
186
187
        :param stream: A file or file like object of Java properties files
188
        :param to_container: callble to make a container object
189
        :param kwargs: optional keyword parameters (ignored)
190
191
        :return: Dict-like object holding config parameters
192
        """
193
        return load(stream, to_container=to_container)
194
195
    def dump_to_stream(self, cnf, stream, **kwargs):
196
        """
197
        Dump config `cnf` to a file or file-like object `stream`.
198
199
        :param cnf: Java properties config data to dump
200
        :param stream: Java properties file or file like object
201
        :param kwargs: backend-specific optional keyword parameters :: dict
202
        """
203
        for key, val in anyconfig.compat.iteritems(cnf):
204
            stream.write("%s = %s\n" % (key, escape(val)))
205
206
# vim:sw=4:ts=4:et:
207