enmerkar-underscore 2.4.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,96 @@
1
+ from importlib.metadata import version
2
+
3
+ import django
4
+ from django.template.base import Lexer
5
+
6
+ if django.VERSION[:2] >= (2, 1):
7
+ from django.template.base import TokenType
8
+ TOKEN_TEXT = TokenType.TEXT
9
+ else: # django < 2.1
10
+ from django.template.base import TOKEN_TEXT
11
+
12
+ from django.utils.encoding import force_str
13
+ from .vendor.enmerkar.enmerkar_extract import extract_django
14
+
15
+ from .vendor.markey import underscore
16
+ from .vendor.markey.machine import parse_arguments, tokenize
17
+ from .vendor.markey.tools import TokenStream
18
+
19
+ __version__ = version("enmerkar-underscore")
20
+
21
+
22
+ def extract(fileobj, keywords, comment_tags, options):
23
+ """Extracts translation messages from underscore template files.
24
+
25
+ This method does also extract django templates. If a template does not
26
+ contain any django translation tags we always fallback to underscore extraction.
27
+
28
+ This is a plugin to Babel, written according to
29
+ http://babel.pocoo.org/docs/messages/#writing-extraction-methods
30
+
31
+ :param fileobj: the file-like object the messages should be extracted
32
+ from
33
+ :param keywords: a list of keywords (i.e. function names) that should
34
+ be recognized as translation functions
35
+ :param comment_tags: a list of translator tags to search for and
36
+ include in the results
37
+ :param options: a dictionary of additional options (optional)
38
+ :return: an iterator over ``(lineno, funcname, message, comments)``
39
+ tuples
40
+ :rtype: ``iterator``
41
+ """
42
+ encoding = options.get('encoding', 'utf-8')
43
+
44
+ original_position = fileobj.tell()
45
+
46
+ text = fileobj.read().decode(encoding)
47
+
48
+ if django.VERSION[:2] >= (1, 9):
49
+ tokens = Lexer(text).tokenize()
50
+ else:
51
+ tokens = Lexer(text, None).tokenize()
52
+
53
+ vars = [token.token_type != TOKEN_TEXT for token in tokens]
54
+
55
+ could_be_django = any(list(vars))
56
+
57
+ if could_be_django:
58
+ fileobj.seek(original_position)
59
+ iterator = extract_django(fileobj, keywords, comment_tags, options)
60
+ for lineno, funcname, message, comments in iterator:
61
+ yield lineno, funcname, message, comments
62
+ else:
63
+ # Underscore template extraction
64
+ comments = []
65
+
66
+ fileobj.seek(original_position)
67
+
68
+ for lineno, line in enumerate(fileobj, 1):
69
+ funcname = None
70
+
71
+ stream = TokenStream.from_tuple_iter(tokenize(line, underscore.rules))
72
+ while not stream.eof:
73
+ if stream.current.type == 'gettext_begin':
74
+ stream.expect('gettext_begin')
75
+ funcname = stream.expect('func_name').value
76
+ args, kwargs = parse_arguments(stream, 'gettext_end')
77
+
78
+ strings = []
79
+
80
+ for arg, argtype in args:
81
+ if argtype == 'func_string_arg':
82
+ strings.append(force_str(arg))
83
+ else:
84
+ strings.append(None)
85
+
86
+ for arg in kwargs:
87
+ strings.append(None)
88
+
89
+ if len(strings) == 1:
90
+ strings = strings[0]
91
+ else:
92
+ strings = tuple(strings)
93
+
94
+ yield lineno, funcname, strings, []
95
+
96
+ stream.next()
File without changes
@@ -0,0 +1,30 @@
1
+ Copyright (C) 2013-2014 django-babel Team
2
+ Copyright (C) 2019 Extracover Holdings Limited
3
+
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions
8
+ are met:
9
+
10
+ 1. Redistributions of source code must retain the above copyright
11
+ notice, this list of conditions and the following disclaimer.
12
+ 2. Redistributions in binary form must reproduce the above copyright
13
+ notice, this list of conditions and the following disclaimer in
14
+ the documentation and/or other materials provided with the
15
+ distribution.
16
+ 3. The name of the author may not be used to endorse or promote
17
+ products derived from this software without specific prior
18
+ written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
21
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
24
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
26
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
28
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
29
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
30
+ IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,174 @@
1
+ # Vendored from https://github.com/Zegocover/enmerkar (enmerkar 0.7.1)
2
+ # enmerkar/__init__.py uses pkg_resources (a setuptools artifact not guaranteed
3
+ # to be present in Python 3.12+ environments). We only need extract_django, and
4
+ # enmerkar/extract.py has no such dependency, so we vendor it here directly.
5
+ from django.template.base import Lexer
6
+ from django.template.base import TokenType
7
+
8
+ from django.utils.translation import trim_whitespace
9
+ from django.utils.encoding import smart_str
10
+
11
+ from django.utils.translation.template import (
12
+ inline_re, block_re, endblock_re, plural_re, constant_re)
13
+
14
+
15
+ TOKEN_TEXT = TokenType.TEXT
16
+ TOKEN_VAR = TokenType.VAR
17
+ TOKEN_BLOCK = TokenType.BLOCK
18
+
19
+
20
+ def join_tokens(tokens, trim=False):
21
+ message = ''.join(tokens)
22
+ if trim:
23
+ message = trim_whitespace(message)
24
+ return message
25
+
26
+
27
+ def strip_quotes(s):
28
+ if (s[0] == s[-1]) and s.startswith(("'", '"')):
29
+ return s[1:-1]
30
+ return s
31
+
32
+
33
+ def extract_django(fileobj, keywords, comment_tags, options):
34
+ """Extract messages from Django template files.
35
+
36
+ :param fileobj: the file-like object the messages should be extracted from
37
+ :param keywords: a list of keywords (i.e. function names) that should
38
+ be recognized as translation functions
39
+ :param comment_tags: a list of translator tags to search for and
40
+ include in the results
41
+ :param options: a dictionary of additional options (optional)
42
+ :return: an iterator over ``(lineno, funcname, message, comments)``
43
+ tuples
44
+ :rtype: ``iterator``
45
+ """
46
+ intrans = False
47
+ inplural = False
48
+ trimmed = False
49
+ message_context = None
50
+ singular = []
51
+ plural = []
52
+ lineno = 1
53
+
54
+ encoding = options.get('encoding', 'utf8')
55
+ text = fileobj.read().decode(encoding)
56
+
57
+ try:
58
+ text_lexer = Lexer(text)
59
+ except TypeError:
60
+ # Django 1.9 changed the way we invoke Lexer; older versions
61
+ # require two parameters.
62
+ text_lexer = Lexer(text, None)
63
+
64
+ for t in text_lexer.tokenize():
65
+ lineno += t.contents.count('\n')
66
+ if intrans:
67
+ if t.token_type == TOKEN_BLOCK:
68
+ endbmatch = endblock_re.match(t.contents)
69
+ pluralmatch = plural_re.match(t.contents)
70
+ if endbmatch:
71
+ if inplural:
72
+ if message_context:
73
+ yield (
74
+ lineno,
75
+ 'npgettext',
76
+ [smart_str(message_context),
77
+ smart_str(join_tokens(singular, trimmed)),
78
+ smart_str(join_tokens(plural, trimmed))],
79
+ [],
80
+ )
81
+ else:
82
+ yield (
83
+ lineno,
84
+ 'ngettext',
85
+ (smart_str(join_tokens(singular, trimmed)),
86
+ smart_str(join_tokens(plural, trimmed))),
87
+ [])
88
+ else:
89
+ if message_context:
90
+ yield (
91
+ lineno,
92
+ 'pgettext',
93
+ [smart_str(message_context),
94
+ smart_str(join_tokens(singular, trimmed))],
95
+ [],
96
+ )
97
+ else:
98
+ yield (
99
+ lineno,
100
+ None,
101
+ smart_str(join_tokens(singular, trimmed)),
102
+ [])
103
+
104
+ intrans = False
105
+ inplural = False
106
+ message_context = None
107
+ singular = []
108
+ plural = []
109
+ elif pluralmatch:
110
+ inplural = True
111
+ else:
112
+ raise SyntaxError('Translation blocks must not include '
113
+ 'other block tags: %s' % t.contents)
114
+ elif t.token_type == TOKEN_VAR:
115
+ if inplural:
116
+ plural.append('%%(%s)s' % t.contents)
117
+ else:
118
+ singular.append('%%(%s)s' % t.contents)
119
+ elif t.token_type == TOKEN_TEXT:
120
+ if inplural:
121
+ plural.append(t.contents)
122
+ else:
123
+ singular.append(t.contents)
124
+ else:
125
+ if t.token_type == TOKEN_BLOCK:
126
+ imatch = inline_re.match(t.contents)
127
+ bmatch = block_re.match(t.contents)
128
+ cmatches = constant_re.findall(t.contents)
129
+ if imatch:
130
+ g = imatch.group(1)
131
+ g = strip_quotes(g)
132
+ message_context = imatch.group(3)
133
+ if message_context:
134
+ # strip quotes
135
+ message_context = message_context[1:-1]
136
+ yield (
137
+ lineno,
138
+ 'pgettext',
139
+ [smart_str(message_context), smart_str(g)],
140
+ [],
141
+ )
142
+ message_context = None
143
+ else:
144
+ yield lineno, None, smart_str(g), []
145
+ elif bmatch:
146
+ if bmatch.group(2):
147
+ message_context = bmatch.group(2)[1:-1]
148
+ for fmatch in constant_re.findall(t.contents):
149
+ stripped_fmatch = strip_quotes(fmatch)
150
+ yield lineno, None, smart_str(stripped_fmatch), []
151
+ intrans = True
152
+ inplural = False
153
+ trimmed = 'trimmed' in t.split_contents()
154
+ singular = []
155
+ plural = []
156
+ elif cmatches:
157
+ for cmatch in cmatches:
158
+ stripped_cmatch = strip_quotes(cmatch)
159
+ yield lineno, None, smart_str(stripped_cmatch), []
160
+ elif t.token_type == TOKEN_VAR:
161
+ parts = t.contents.split('|')
162
+ cmatch = constant_re.match(parts[0])
163
+ if cmatch:
164
+ stripped_cmatch = strip_quotes(cmatch.group(1))
165
+ yield lineno, None, smart_str(stripped_cmatch), []
166
+ for p in parts[1:]:
167
+ if p.find(':_(') >= 0:
168
+ p1 = p.split(':', 1)[1]
169
+ if p1[0] == '_':
170
+ p1 = p1[1:]
171
+ if p1[0] == '(':
172
+ p1 = p1.strip('()')
173
+ p1 = strip_quotes(p1)
174
+ yield lineno, None, smart_str(p1), []
@@ -0,0 +1,28 @@
1
+ Copyright (C) 2014 Christopher Grebs.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions
6
+ are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+ 2. Redistributions in binary form must reproduce the above copyright
11
+ notice, this list of conditions and the following disclaimer in
12
+ the documentation and/or other materials provided with the
13
+ distribution.
14
+ 3. The name of the author may not be used to endorse or promote
15
+ products derived from this software without specific prior
16
+ written permission.
17
+
18
+ THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
19
+ OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
21
+ ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
22
+ DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
23
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
24
+ GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
25
+ INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER
26
+ IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
27
+ OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
28
+ IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,11 @@
1
+ These files were copied here from https://github.com/EnTeQuAk/markey which is a
2
+ project by the same original owner as the project this repo was forked from
3
+ (django-babel-underscore).
4
+
5
+ The original project hasn't had updates in 8 years and while a PR was made to
6
+ make the same fix upstream (https://github.com/EnTeQuAk/markey/pull/6) we can't
7
+ wait for it to get merged before the Redwood release cut (May 2024).
8
+
9
+ To reduce the number of forks we have in play for frontend tech that we have
10
+ already deprecated, we copy the bits we need here and make the updates we need
11
+ to get support for Python 3.11 and 3.12. The content was copied from https://github.com/EnTeQuAk/markey/commit/d9fc042b147824164908f00a24da6ce2c26dcd8e before we added modifications to allow for Python 3.11 support.
File without changes
@@ -0,0 +1,87 @@
1
+ import sys
2
+
3
+ PY2 = sys.version_info[0] == 2
4
+ PY3 = sys.version_info[0] == 3
5
+
6
+
7
+ _identity = lambda x: x
8
+
9
+
10
+ if not PY2:
11
+ text_type = str
12
+ string_types = (str,)
13
+ integer_types = (int, )
14
+ unichr = chr
15
+
16
+ text_to_native = lambda s, enc: s
17
+
18
+ iterkeys = lambda d: iter(d.keys())
19
+ itervalues = lambda d: iter(d.values())
20
+ iteritems = lambda d: iter(d.items())
21
+
22
+ from io import StringIO, BytesIO
23
+ import pickle
24
+
25
+ izip = zip
26
+ imap = map
27
+ range_type = range
28
+
29
+ cmp = lambda a, b: (a > b) - (a < b)
30
+
31
+ else:
32
+ text_type = unicode
33
+ string_types = (str, unicode)
34
+ integer_types = (int, long)
35
+
36
+ text_to_native = lambda s, enc: s.encode(enc)
37
+ unichr = unichr
38
+
39
+ iterkeys = lambda d: d.iterkeys()
40
+ itervalues = lambda d: d.itervalues()
41
+ iteritems = lambda d: d.iteritems()
42
+
43
+ from cStringIO import StringIO as BytesIO
44
+ from StringIO import StringIO
45
+ import cPickle as pickle
46
+
47
+ from itertools import izip, imap
48
+ range_type = xrange
49
+
50
+ cmp = cmp
51
+
52
+
53
+ number_types = integer_types + (float,)
54
+
55
+
56
+ # Mostly taken from Django.
57
+ def force_text(s, encoding='utf-8', errors='strict'):
58
+ # Handle the common case first for performance reasons.
59
+ if isinstance(s, text_type):
60
+ return s
61
+ try:
62
+ if not isinstance(s, string_types):
63
+ if PY3:
64
+ if isinstance(s, bytes):
65
+ s = text_type(s, encoding, errors)
66
+ else:
67
+ s = text_type(s)
68
+ elif hasattr(s, '__unicode__'):
69
+ s = text_type(s)
70
+ else:
71
+ s = text_type(bytes(s), encoding, errors)
72
+ else:
73
+ # Note: We use .decode() here, instead of six.text_type(s, encoding,
74
+ # errors), so that if s is a SafeBytes, it ends up being a
75
+ # SafeText at the end.
76
+ s = s.decode(encoding, errors)
77
+ except UnicodeDecodeError as e:
78
+ if not isinstance(s, Exception):
79
+ raise UnicodeDecodeError(s, *e.args)
80
+ else:
81
+ # If we get to here, the caller has passed in an Exception
82
+ # subclass populated with non-ASCII bytestring data without a
83
+ # working unicode method. Try to handle this without raising a
84
+ # further exception by individually forcing the exception args
85
+ # to unicode.
86
+ s = ' '.join([force_text(arg, encoding, errors) for arg in s])
87
+ return s
@@ -0,0 +1,119 @@
1
+ from .rules import include
2
+ from ._compat import force_text
3
+
4
+
5
+ def iter_rules(x, rules):
6
+ for rule in rules[x]:
7
+ if rule.__class__ is include:
8
+ for item in iter_rules(rule, rules):
9
+ yield item
10
+ else:
11
+ yield rule
12
+
13
+
14
+ def tokenize(string, rules, encoding='utf-8'):
15
+ string = force_text(string)
16
+
17
+ pos = 0
18
+ end = len(string)
19
+ stack = [(None, 'everything')]
20
+ rule_cache = {}
21
+ text_buffer = []
22
+ add_text = text_buffer.append
23
+ push = stack.append
24
+ flatten = ''.join
25
+
26
+ while pos < end:
27
+ state = stack[-1][1]
28
+ if state not in rule_cache:
29
+ rule_cache[state] = list(iter_rules(state, rules))
30
+ for rule in rule_cache[state]:
31
+ m = rule.match(string, pos)
32
+ if m is not None:
33
+ # first flush text that is left in the buffer
34
+ if text_buffer:
35
+ text = flatten(text_buffer)
36
+ if text:
37
+ yield 'text', text
38
+ del text_buffer[:]
39
+
40
+ # now enter the new scopes if entered in a
41
+ # non silent way
42
+ if rule.enter is not None:
43
+ push((rule.enter + '_end', rule.enter))
44
+ yield rule.enter + '_begin', m.group()
45
+
46
+ # now process the data
47
+ if callable(rule.token):
48
+ for item in rule.token(m):
49
+ yield item
50
+ elif rule.token is not None:
51
+ yield rule.token, m.group()
52
+
53
+ # now check if we leave something. if the state was
54
+ # entered non silent, send a close token.
55
+ pos = m.end()
56
+ for x in range(rule.leave):
57
+ announce, item = stack.pop()
58
+ if announce is not None:
59
+ yield announce, m.group()
60
+
61
+ break
62
+ else:
63
+ char = string[pos]
64
+ add_text(char)
65
+ pos += 1
66
+
67
+ # if the text buffer is left filled, we flush it
68
+ if text_buffer:
69
+ text = flatten(text_buffer)
70
+ if text:
71
+ yield 'text', text
72
+
73
+ # if there are things in the stack left we should
74
+ # emit the end tokens
75
+ for announce, item in reversed(stack):
76
+ if announce is not None:
77
+ yield announce, u''
78
+
79
+
80
+ def parse_arguments(stream, end_token):
81
+ """
82
+ Helper function for function argument parsing. Pass it a
83
+ `TokenStream` and the delimiter token for the argument section and
84
+ it will extract all position and keyword arguments as well as
85
+ each argument's type info (string literal or not).
86
+
87
+ Returns a ``(args, kwargs)`` tuple.
88
+ """
89
+ args = []
90
+ kwargs = {}
91
+ keywords = []
92
+ while stream.current.type != end_token:
93
+ if stream.current.type == 'func_string_arg':
94
+ value = stream.current.value.strip()
95
+ for char in ['\'', '"']:
96
+ if value.startswith(char) and value.endswith(char):
97
+ value = value[1: -1]
98
+ break
99
+ stream.next()
100
+ if keywords:
101
+ for keyword in keywords:
102
+ kwargs[keyword] = value
103
+ del keywords[:]
104
+ else:
105
+ args.append((value, 'func_string_arg'))
106
+ elif stream.current.type == 'text':
107
+ args.append((stream.current.value, 'text'))
108
+ stream.next()
109
+ elif stream.current.type == 'func_kwarg':
110
+ keywords.append(stream.current.value)
111
+ stream.next()
112
+ elif stream.current.type == 'func_argument_delimiter':
113
+ stream.next()
114
+ else:
115
+ break
116
+ for keyword in keywords:
117
+ args.append((keyword, 'func_kwarg'))
118
+
119
+ return tuple(args), kwargs
@@ -0,0 +1,34 @@
1
+ import re
2
+ from ._compat import izip
3
+
4
+
5
+ def bygroups(*args):
6
+ """
7
+ Callback creator for bygroup yielding.
8
+ """
9
+ return lambda m: izip(args, m.groups())
10
+
11
+
12
+ class include(str):
13
+ """
14
+ Tells the lexer to include tokens from another set.
15
+ """
16
+ __slots__ = ()
17
+
18
+
19
+ class ruleset(tuple):
20
+ __slots__ = ()
21
+
22
+ def __new__(cls, *args):
23
+ return tuple.__new__(cls, args)
24
+
25
+
26
+ class rule(object):
27
+ """This represents a parsing rule."""
28
+ __slots__ = ('match', 'token', 'enter', 'leave')
29
+
30
+ def __init__(self, regexp, token=None, enter=None, leave=0):
31
+ self.match = re.compile(regexp, re.U).match
32
+ self.token = token
33
+ self.enter = enter
34
+ self.leave = leave
File without changes
@@ -0,0 +1,15 @@
1
+ from enmerkar_underscore.vendor.markey.machine import tokenize, parse_arguments
2
+ from enmerkar_underscore.vendor.markey.tools import TokenStream
3
+ from enmerkar_underscore.vendor.markey.underscore import rules as underscore_rules
4
+
5
+
6
+ def test_parse_arguments():
7
+ line = '<% gettext(varible, "some string", str="foo")'
8
+ stream = TokenStream.from_tuple_iter(tokenize(line, underscore_rules))
9
+ stream.next()
10
+ stream.next()
11
+ stream.expect('gettext_begin')
12
+ stream.expect('func_name').value
13
+ args, kwargs = parse_arguments(stream, 'gettext_end')
14
+ assert args == (('varible', 'text'), ('some string', 'func_string_arg'),)
15
+ assert kwargs == {'str': 'foo'}
@@ -0,0 +1,21 @@
1
+ import re
2
+
3
+ from enmerkar_underscore.vendor.markey.rules import bygroups, rule, ruleset
4
+
5
+
6
+ def test_bygroups():
7
+ match = re.match(r'(1)(2)', '12')
8
+ assert list(bygroups(('one',))(match)) == [(('one',), '1')]
9
+
10
+
11
+ class test_rule():
12
+ r = rule(r'(\d)', 'num', 'do_number')
13
+ assert r.match('1').group() == '1'
14
+ assert r.token == 'num'
15
+ assert r.enter == 'do_number'
16
+ assert r.leave == 0
17
+
18
+
19
+ class test_ruleset():
20
+ rs = ruleset(rule(r'(\d)', 'num', 'do_number'),)
21
+ assert isinstance(rs, tuple)
@@ -0,0 +1,96 @@
1
+ import sys
2
+ from six import StringIO
3
+
4
+ import pytest
5
+
6
+ from enmerkar_underscore.vendor.markey.tools import Token, TokenStream, TokenStreamIterator
7
+
8
+
9
+ def test_tokenstream():
10
+ s = TokenStream(iter((Token('a', 1), Token('b', 2), Token('c', 3))))
11
+ assert s.current == Token('a', 1)
12
+
13
+
14
+ def test_tokenstream_from_tuple_iter():
15
+ # from_tuple_iter
16
+ s = TokenStream.from_tuple_iter(iter((('a', 1), ('b', 2), ('c', 3))))
17
+ assert s.current == Token('a', 1)
18
+
19
+ # iter
20
+ assert isinstance(iter(s), TokenStreamIterator)
21
+ assert tuple(iter(s)) == (Token('a', 1), Token('b', 2), Token('c', 3))
22
+
23
+
24
+ def test_tokenstream_eof():
25
+ # eof
26
+ s = TokenStream(iter((Token('a', 1), Token('b', 2), Token('c', 3))))
27
+ assert not s.eof
28
+ list(s)
29
+ assert s.eof
30
+
31
+
32
+ def test_tokenstream_look_push():
33
+ # look, push
34
+ s = TokenStream(iter((Token('a', 1), Token('b', 2), Token('c', 3))))
35
+ assert s.current == Token('a', 1)
36
+ assert s.look() == Token('b', 2)
37
+ s.next()
38
+ assert s.look() == Token('c', 3)
39
+ s.push(Token('b', 2))
40
+ assert s.look() == Token('b', 2)
41
+ s.push(Token('e', 4), current=True)
42
+ assert s.current == Token('e', 4)
43
+ assert s.look() == Token('b', 2)
44
+
45
+
46
+ def test_tokenstream_skip_next():
47
+ # skip, next
48
+ s = TokenStream(iter((Token('a', 1), Token('b', 2), Token('c', 3))))
49
+ s.skip(1)
50
+ assert s.current == Token('b', 2)
51
+ s.next()
52
+ assert s.current == Token('c', 3)
53
+ s.push(Token('e', 4))
54
+ assert s.current == Token('c', 3)
55
+ s.next()
56
+ assert s.current == Token('e', 4)
57
+ s.next()
58
+ assert s.current == Token('eof', None)
59
+
60
+
61
+ def test_tokenstream_expect():
62
+ # expect
63
+ s = TokenStream(iter((Token('a', 1), Token('b', 2), Token('c', 3))))
64
+ assert s.expect('a') == Token('a', 1)
65
+ assert s.expect('b', 2) == Token('b', 2)
66
+ pytest.raises(AssertionError, s.expect, 'e')
67
+ pytest.raises(AssertionError, s.expect, 'c', 5)
68
+
69
+
70
+ def test_tokenstream_test_shift():
71
+ # test
72
+ s = TokenStream(iter((Token('a', 1), Token('b', 2), Token('c', 3))))
73
+ assert s.test('a')
74
+ s.next()
75
+ assert s.test('b', 2)
76
+
77
+ # shift
78
+ assert s.current == Token('b', 2)
79
+ s.shift(Token('f', 5))
80
+ assert s.current == Token('f', 5)
81
+ s.next()
82
+ assert s.current == Token('b', 2)
83
+
84
+
85
+ def test_tokenstream_debug():
86
+ stream = StringIO()
87
+
88
+ _original_stdout = sys.stdout
89
+ sys.stdout = stream
90
+
91
+ try:
92
+ s = TokenStream(iter((Token('a', 1),)))
93
+ s.debug()
94
+ assert stream.getvalue() == "Token(type='a', value=1)\n"
95
+ finally:
96
+ sys.stdout = _original_stdout
@@ -0,0 +1,130 @@
1
+ import sys
2
+ from collections import namedtuple
3
+
4
+ #: Represents one token.
5
+ Token = namedtuple('Token', ('type', 'value'))
6
+
7
+
8
+ class TokenStreamIterator(object):
9
+ """
10
+ The iterator for tokenstreams. Iterate over the stream
11
+ until the eof token is reached.
12
+ """
13
+
14
+ def __init__(self, stream):
15
+ self._stream = stream
16
+
17
+ def __iter__(self):
18
+ return self
19
+
20
+ def __next__(self):
21
+ token = self._stream.current
22
+ if token.type == 'eof':
23
+ raise StopIteration()
24
+ self._stream.next()
25
+ return token
26
+
27
+ # Python 2.x
28
+ next = __next__
29
+
30
+
31
+ class TokenStream(object):
32
+ """
33
+ A token stream wraps a generator and supports pushing tokens back.
34
+ It also provides some functions to expect tokens and similar stuff.
35
+
36
+ Important note: Do never push more than one token back to the
37
+ stream. Although the stream object won't stop you
38
+ from doing so, the behavior is undefined. Multiple
39
+ pushed tokens are only used internally!
40
+ """
41
+
42
+ def __init__(self, generator):
43
+ self._next = lambda: next(generator)
44
+ self._pushed = []
45
+ self.current = Token('initial', '')
46
+ self.next()
47
+
48
+ @classmethod
49
+ def from_tuple_iter(cls, tupleiter):
50
+ return cls(Token(*a) for a in tupleiter)
51
+
52
+ def __iter__(self):
53
+ return TokenStreamIterator(self)
54
+
55
+ @property
56
+ def eof(self):
57
+ """Are we at the end of the tokenstream?"""
58
+ return not bool(self._pushed) and self.current.type == 'eof'
59
+
60
+ def debug(self, stream=None):
61
+ """Displays the tokenized code on the stream provided or stdout."""
62
+ if stream is None:
63
+ stream = sys.stdout
64
+ for token in self:
65
+ stream.write(repr(token) + '\n')
66
+
67
+ def look(self):
68
+ """See what's the next token."""
69
+ if self._pushed:
70
+ return self._pushed[-1]
71
+ old_token = self.current
72
+ self.next()
73
+ new_token = self.current
74
+ self.current = old_token
75
+ self.push(new_token)
76
+ return new_token
77
+
78
+ def push(self, token, current=False):
79
+ """Push a token back to the stream (only one!)."""
80
+ self._pushed.append(token)
81
+ if current:
82
+ self.next()
83
+
84
+ def skip(self, n):
85
+ """Got n tokens ahead."""
86
+ for x in range(n):
87
+ self.next()
88
+
89
+ def next(self):
90
+ """Go one token ahead."""
91
+ if self._pushed:
92
+ self.current = self._pushed.pop()
93
+ else:
94
+ try:
95
+ self.current = self._next()
96
+ except StopIteration:
97
+ if self.current.type != 'eof':
98
+ self.current = Token('eof', None)
99
+
100
+ def expect(self, type, value=None):
101
+ """expect a given token."""
102
+ assert self.current.type == type, "expect failed (%s, %s)" % (
103
+ self.current.type, type)
104
+ if value is not None:
105
+ assert self.current.value == value or \
106
+ (value.__class__ is tuple and
107
+ self.current.value in value), "%s != %s" % (type, value)
108
+ try:
109
+ return self.current
110
+ finally:
111
+ self.next()
112
+
113
+ def test(self, type, value=Ellipsis):
114
+ """Test the current token."""
115
+ return (
116
+ self.current.type == type and
117
+ (value is Ellipsis or self.current.value == value or
118
+ value.__class__ is tuple and
119
+ self.current.value in value))
120
+
121
+ def shift(self, token):
122
+ """
123
+ Push one token into the stream.
124
+ """
125
+ old_current = self.current
126
+ self.next()
127
+ self.push(self.current)
128
+ self.push(old_current)
129
+ self.push(token)
130
+ self.next()
@@ -0,0 +1,54 @@
1
+ # Full functioning underscore template parser.
2
+
3
+ from .rules import ruleset, include, rule, bygroups
4
+
5
+
6
+ keywords = frozenset((
7
+ 'pluralidx', 'gettext', 'ngettext', 'gettext_noop', 'pgettext',
8
+ 'npgettext', '_'
9
+ ))
10
+
11
+
12
+ rules = {
13
+ 'everything': ruleset(
14
+ include('inline'),
15
+ include('function'),
16
+ ),
17
+ 'inline': ruleset(
18
+ rule(r'<%=', enter='interpolate'),
19
+ rule(r'<%-', enter='escape'),
20
+ rule(r'<%', enter='evaluate'),
21
+ ),
22
+ 'inline_with_func': ruleset(
23
+ include('inline'),
24
+ include('function')
25
+ ),
26
+ 'function': ruleset(
27
+ rule(r'({keywords})\('.format(keywords='|'.join(keywords)),
28
+ bygroups('func_name'), enter='gettext')
29
+ ),
30
+ 'gettext': ruleset(
31
+ rule(r'([\"\'])?\)', leave=1),
32
+ include('function_call')
33
+ ),
34
+ 'evaluate': ruleset(
35
+ rule(r'%>', leave=1),
36
+ include('inline_with_func'),
37
+ ),
38
+ 'interpolate': ruleset(
39
+ rule(r'%>', leave=1),
40
+ include('inline_with_func'),
41
+ ),
42
+ 'escape': ruleset(
43
+ rule(r'%>', leave=1),
44
+ include('inline_with_func'),
45
+ ),
46
+ # function calls (parse string arguments and implicit strings)
47
+ 'function_call': ruleset(
48
+ rule(',', 'func_argument_delimiter'),
49
+ rule('\s+', None),
50
+ rule(r"(?s)('([^'\\]*(?:\\.[^'\\]*)*)'|"
51
+ r'"([^"\\]*(?:\\.[^"\\]*)*)")', 'func_string_arg'),
52
+ rule(r'([\w_]+)\s*=', bygroups('func_kwarg'))
53
+ )
54
+ }
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: enmerkar-underscore
3
+ Version: 2.4.2
4
+ Summary: Implements a underscore extractor for django-babel.
5
+ Author-email: Open edX Project <oscm@openedx.org>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/openedx/enmerkar-underscore
8
+ Project-URL: Repository, https://github.com/openedx/enmerkar-underscore
9
+ Keywords: enmerkar-underscore,Python,edx
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Framework :: Django
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Natural Language :: English
16
+ Classifier: Programming Language :: Python
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Requires-Python: >=3.12
20
+ Description-Content-Type: text/x-rst
21
+ License-File: LICENSE
22
+ Requires-Dist: django
23
+ Requires-Dist: babel>=1.3
24
+ Dynamic: license-file
25
+
26
+ =================================
27
+ Django Babel Underscore Extractor
28
+ =================================
29
+
30
+ .. image:: https://badge.fury.io/py/django-babel-underscore.png
31
+ :target: http://badge.fury.io/py/django-babel-underscore
32
+
33
+ .. image:: https://github.com/openedx/enmerkar-underscore/workflows/Python%20CI/badge.svg?branch=master
34
+ :target: https://github.com/openedx/enmerkar-underscore/actions?query=workflow%3A%22Python+CI%22
35
+
36
+
37
+ Implements a underscore extractor for django-babel.
38
+
39
+ * Free software: BSD license
40
+ * Documentation: https://github.com/openedx/enmerkar-underscore
41
+
42
+ Features
43
+ --------
44
+
45
+ * TODO
46
+
47
+ .. :changelog:
48
+
49
+ History
50
+ -------
51
+
52
+ 2.4.0 (2025-04-22)
53
+ ------------------
54
+
55
+ * Add support for django 5.2
56
+
57
+ 2.0.0 (2020-05-29)
58
+ ------------------
59
+
60
+ * Drop support for Python 3.8
61
+
62
+
63
+ 1.0.0 (2020-05-29)
64
+ ------------------
65
+
66
+ * Rename the repo
67
+ * Using latest version of enmerkar.
68
+ * Release of pypi
69
+
70
+ 0.6.0 (2020-01-07)
71
+ ------------------
72
+
73
+ * Adds support for Python 3.6, 3.7
74
+ * Adds support for Django 2.0, 2.1, 2.2 and 3.0.
75
+ * Drop support for Django < 1.11
76
+
77
+
78
+ 0.5.2 (2018-08-18)
79
+ ------------------
80
+
81
+ * Adds support for Django 1.10 and 1.11.
82
+
83
+
84
+ 0.5.1 (2016-07-15)
85
+ ------------------
86
+
87
+ * Fix release
88
+
89
+
90
+ 0.5.0 (2016-07-15)
91
+ ------------------
92
+
93
+ * Remove support for Python 2.6 and Python 3.3
94
+ * Remove support for Django < 1.8
95
+ * Add support for Python 3.5
96
+ * Add support for Django 1.9
97
+ * Now requires markey 0.8
98
+
99
+ 0.4.2 (2015-08-28)
100
+ ------------------
101
+
102
+ * Update to markey 0.7
103
+
104
+ 0.4.1 (2015-08-28)
105
+ ------------------
106
+
107
+ * Fix release
108
+
109
+ 0.4.0 (2015-08-28)
110
+ ------------------
111
+
112
+ * Add support for Django 1.8
113
+
114
+ 0.3.0 (2015-03-15)
115
+ ------------------
116
+
117
+ * Added `underscore` entry_point for babel.extractors (#1)
118
+
119
+ 0.2.0 (2014-12-24)
120
+ ------------------
121
+
122
+ * Fix dependency names.
123
+
124
+ 0.1.0 (2014-05-25)
125
+ ------------------
126
+
127
+ * First release on PyPI.
@@ -0,0 +1,22 @@
1
+ enmerkar_underscore/__init__.py,sha256=eM7M4vra7qcytA8VXhN6SNj-PgjMI4Lwx1o-sQ1am0o,3322
2
+ enmerkar_underscore/vendor/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ enmerkar_underscore/vendor/enmerkar/LICENSE,sha256=65Z5NCGyb2yADuQ2IAQy0wAPIgQEW7cuXhOwLhY5V34,1465
4
+ enmerkar_underscore/vendor/enmerkar/enmerkar_extract.py,sha256=0cLhMs7ITNLLkwkmqy7taUyjy0YB1OrAqO7HRi7-uvo,6957
5
+ enmerkar_underscore/vendor/markey/COPYING,sha256=zsVkBdUL9ZVl2LNG-wNmF5TKtwiR9qxIgD7zcDbF6PY,1413
6
+ enmerkar_underscore/vendor/markey/README.rst,sha256=BrJFx727YSybIuZUYRUqUGUKeDH7nNvMCovWlOExco8,784
7
+ enmerkar_underscore/vendor/markey/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ enmerkar_underscore/vendor/markey/_compat.py,sha256=51WsDkLYOK99-MZgx0HvpTzPoWixcmoWTPB7mNC3X-A,2409
9
+ enmerkar_underscore/vendor/markey/machine.py,sha256=B8Qe96X4tYimUl8cknjEcFzQKhDPczcPpk9AZR3kLY0,3807
10
+ enmerkar_underscore/vendor/markey/rules.py,sha256=_uHGDO1wczBiKNV4BeG9M4dGI0PRSefxPtnWiT-LXHk,704
11
+ enmerkar_underscore/vendor/markey/tools.py,sha256=zG2sik1k6Jyb83dVv6xoZ0Sh3kbaUA9UrIfqzHflLtw,3735
12
+ enmerkar_underscore/vendor/markey/underscore.py,sha256=aNLTO_iAxobOB6ZWtBOciyNFTiS3rIZ1gShVW7q5TGA,1464
13
+ enmerkar_underscore/vendor/markey/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
14
+ enmerkar_underscore/vendor/markey/tests/test_machine.py,sha256=rNFe7Bmyjkh_CKR_Ih5PMSrlR2bkJ9rvkcUFpw_f4zA,671
15
+ enmerkar_underscore/vendor/markey/tests/test_rules.py,sha256=1_ZA60aow0H5AYLmE8GKXCnQR_3Oz5IYj5GDcp3R2GI,510
16
+ enmerkar_underscore/vendor/markey/tests/test_tools.py,sha256=RFZtkziapmJpzuQ8julXSMI1q34kB9U4WhANHr-yNQ4,2552
17
+ enmerkar_underscore-2.4.2.dist-info/licenses/LICENSE,sha256=OXqUbEDFudXLg2c9yUtD5V5DOJI2aA1Q0_CoAs2DEIY,1498
18
+ enmerkar_underscore-2.4.2.dist-info/METADATA,sha256=iFbEeYO4idwn-_ezXPQOUwh5NVU4FuAwq75VsXXx6wU,2748
19
+ enmerkar_underscore-2.4.2.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
20
+ enmerkar_underscore-2.4.2.dist-info/entry_points.txt,sha256=RrEDKrwUU3vz5RRVRFhECDEcubpmkUvaAaqWkv4HOHM,60
21
+ enmerkar_underscore-2.4.2.dist-info/top_level.txt,sha256=OQ-esflDonOuDESY9Lh7nOhxOpceH_6CI3lbopUABH4,20
22
+ enmerkar_underscore-2.4.2.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [babel.extractors]
2
+ underscore = enmerkar_underscore:extract
@@ -0,0 +1,12 @@
1
+ Copyright (c) 2015, Christopher Grebs
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+
8
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9
+
10
+ * Neither the name of Django Babel Underscore Extractor nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
11
+
12
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1 @@
1
+ enmerkar_underscore