Completed
Push — master ( fe0a9f...aea9af )
by Satoru
10s
created

_pre_process_line()   B

Complexity

Conditions 5

Size

Total Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

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