pyview-web 0.3.0__py3-none-any.whl → 0.8.0a2__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.
Files changed (78) hide show
  1. pyview/__init__.py +16 -6
  2. pyview/assets/js/app.js +1 -0
  3. pyview/assets/js/uploaders.js +221 -0
  4. pyview/assets/package-lock.json +16 -14
  5. pyview/assets/package.json +2 -2
  6. pyview/async_stream_runner.py +2 -1
  7. pyview/auth/__init__.py +3 -1
  8. pyview/auth/provider.py +6 -6
  9. pyview/auth/required.py +7 -10
  10. pyview/binding/__init__.py +47 -0
  11. pyview/binding/binder.py +134 -0
  12. pyview/binding/context.py +33 -0
  13. pyview/binding/converters.py +191 -0
  14. pyview/binding/helpers.py +78 -0
  15. pyview/binding/injectables.py +119 -0
  16. pyview/binding/params.py +105 -0
  17. pyview/binding/result.py +32 -0
  18. pyview/changesets/__init__.py +2 -0
  19. pyview/changesets/changesets.py +8 -3
  20. pyview/cli/commands/create_view.py +4 -3
  21. pyview/cli/main.py +1 -1
  22. pyview/components/__init__.py +72 -0
  23. pyview/components/base.py +212 -0
  24. pyview/components/lifecycle.py +85 -0
  25. pyview/components/manager.py +366 -0
  26. pyview/components/renderer.py +14 -0
  27. pyview/components/slots.py +73 -0
  28. pyview/csrf.py +4 -2
  29. pyview/events/AutoEventDispatch.py +98 -0
  30. pyview/events/BaseEventHandler.py +51 -8
  31. pyview/events/__init__.py +2 -1
  32. pyview/instrumentation/__init__.py +3 -3
  33. pyview/instrumentation/interfaces.py +57 -33
  34. pyview/instrumentation/noop.py +21 -18
  35. pyview/js.py +20 -23
  36. pyview/live_routes.py +5 -3
  37. pyview/live_socket.py +167 -44
  38. pyview/live_view.py +24 -12
  39. pyview/meta.py +14 -2
  40. pyview/phx_message.py +7 -8
  41. pyview/playground/__init__.py +10 -0
  42. pyview/playground/builder.py +118 -0
  43. pyview/playground/favicon.py +39 -0
  44. pyview/pyview.py +54 -20
  45. pyview/session.py +2 -0
  46. pyview/static/assets/app.js +2088 -806
  47. pyview/static/assets/uploaders.js +221 -0
  48. pyview/stream.py +308 -0
  49. pyview/template/__init__.py +11 -1
  50. pyview/template/live_template.py +12 -8
  51. pyview/template/live_view_template.py +338 -0
  52. pyview/template/render_diff.py +33 -7
  53. pyview/template/root_template.py +21 -9
  54. pyview/template/serializer.py +2 -5
  55. pyview/template/template_view.py +170 -0
  56. pyview/template/utils.py +3 -2
  57. pyview/uploads.py +344 -55
  58. pyview/vendor/flet/pubsub/__init__.py +3 -1
  59. pyview/vendor/flet/pubsub/pub_sub.py +10 -18
  60. pyview/vendor/ibis/__init__.py +3 -7
  61. pyview/vendor/ibis/compiler.py +25 -32
  62. pyview/vendor/ibis/context.py +13 -15
  63. pyview/vendor/ibis/errors.py +0 -6
  64. pyview/vendor/ibis/filters.py +70 -76
  65. pyview/vendor/ibis/loaders.py +6 -7
  66. pyview/vendor/ibis/nodes.py +40 -42
  67. pyview/vendor/ibis/template.py +4 -5
  68. pyview/vendor/ibis/tree.py +62 -3
  69. pyview/vendor/ibis/utils.py +14 -15
  70. pyview/ws_handler.py +116 -86
  71. {pyview_web-0.3.0.dist-info → pyview_web-0.8.0a2.dist-info}/METADATA +39 -33
  72. pyview_web-0.8.0a2.dist-info/RECORD +80 -0
  73. pyview_web-0.8.0a2.dist-info/WHEEL +4 -0
  74. pyview_web-0.8.0a2.dist-info/entry_points.txt +3 -0
  75. pyview_web-0.3.0.dist-info/LICENSE +0 -21
  76. pyview_web-0.3.0.dist-info/RECORD +0 -58
  77. pyview_web-0.3.0.dist-info/WHEEL +0 -4
  78. pyview_web-0.3.0.dist-info/entry_points.txt +0 -3
@@ -1,16 +1,14 @@
1
- from . import nodes
2
- from . import errors
3
-
1
+ from . import errors, nodes
4
2
 
5
3
  # Token delimiters.
6
- comment_start = '{#'
7
- comment_end = '#}'
8
- print_start = '{{'
9
- print_end = '}}'
10
- eprint_start = '{$'
11
- eprint_end = '$}'
12
- instruction_start = '{%'
13
- instruction_end = '%}'
4
+ comment_start = "{#"
5
+ comment_end = "#}"
6
+ print_start = "{{"
7
+ print_end = "}}"
8
+ eprint_start = "{$"
9
+ eprint_end = "$}"
10
+ instruction_start = "{%"
11
+ instruction_end = "%}"
14
12
 
15
13
 
16
14
  # Returns the root node of the compiled node tree.
@@ -20,10 +18,9 @@ def compile(template_string, template_id):
20
18
 
21
19
  # Tokens come in four different types: TEXT, PRINT, EPRINT, and INSTRUCTION.
22
20
  class Token:
23
-
24
21
  def __init__(self, token_type, token_text, template_id, line_number):
25
22
  words = token_text.split()
26
- self.keyword = words[0] if words else ''
23
+ self.keyword = words[0] if words else ""
27
24
  self.type = token_type
28
25
  self.text = token_text
29
26
  self.template_id = template_id
@@ -35,7 +32,6 @@ class Token:
35
32
 
36
33
  # The Lexer takes a template string as input and chops it into a list of Tokens.
37
34
  class Lexer:
38
-
39
35
  def __init__(self, template_string, template_id):
40
36
  self.template_string = template_string
41
37
  self.template_id = template_id
@@ -63,7 +59,7 @@ class Lexer:
63
59
  return False
64
60
 
65
61
  def advance(self):
66
- if self.template_string[self.index] == '\n':
62
+ if self.template_string[self.index] == "\n":
67
63
  self.line_number += 1
68
64
  self.index += 1
69
65
 
@@ -84,7 +80,7 @@ class Lexer:
84
80
  start_line_number = self.line_number
85
81
  while self.index < len(self.template_string):
86
82
  if self.match(eprint_end):
87
- text = self.template_string[start_index:self.index].strip()
83
+ text = self.template_string[start_index : self.index].strip()
88
84
  self.tokens.append(Token("EPRINT", text, self.template_id, start_line_number))
89
85
  self.index += len(eprint_end)
90
86
  return
@@ -98,7 +94,7 @@ class Lexer:
98
94
  start_line_number = self.line_number
99
95
  while self.index < len(self.template_string):
100
96
  if self.match(print_end):
101
- text = self.template_string[start_index:self.index].strip()
97
+ text = self.template_string[start_index : self.index].strip()
102
98
  self.tokens.append(Token("PRINT", text, self.template_id, start_line_number))
103
99
  self.index += len(print_end)
104
100
  return
@@ -112,35 +108,33 @@ class Lexer:
112
108
  start_line_number = self.line_number
113
109
  while self.index < len(self.template_string):
114
110
  if self.match(instruction_end):
115
- text = self.template_string[start_index:self.index].strip()
111
+ text = self.template_string[start_index : self.index].strip()
116
112
  self.tokens.append(Token("INSTRUCTION", text, self.template_id, start_line_number))
117
113
  self.index += len(instruction_end)
118
114
  return
119
115
  self.advance()
120
- msg = f"Unclosed instruction tag."
116
+ msg = "Unclosed instruction tag."
121
117
  raise errors.TemplateLexingError(msg, self.template_id, start_line_number)
122
118
 
123
119
  def read_text(self):
124
120
  start_index = self.index
125
121
  start_line_number = self.line_number
126
122
  while self.index < len(self.template_string):
127
- if self.match(comment_start):
128
- break
129
- elif self.match(eprint_start):
130
- break
131
- elif self.match(print_start):
132
- break
133
- elif self.match(instruction_start):
123
+ if (
124
+ self.match(comment_start)
125
+ or self.match(eprint_start)
126
+ or self.match(print_start)
127
+ or self.match(instruction_start)
128
+ ):
134
129
  break
135
130
  self.advance()
136
- text = self.template_string[start_index:self.index]
131
+ text = self.template_string[start_index : self.index]
137
132
  self.tokens.append(Token("TEXT", text, self.template_id, start_line_number))
138
133
 
139
134
 
140
135
  # The Parser takes a template string as input, lexes it into a token stream, then compiles the
141
136
  # token stream into a tree of nodes.
142
137
  class Parser:
143
-
144
138
  def __init__(self, template_string, template_id):
145
139
  self.template_string = template_string
146
140
  self.template_id = template_id
@@ -173,8 +167,8 @@ class Parser:
173
167
  stack[-1].exit_scope()
174
168
  stack.pop()
175
169
  expecting.pop()
176
- elif token.keyword == '':
177
- msg = f"Empty instruction tag."
170
+ elif token.keyword == "":
171
+ msg = "Empty instruction tag."
178
172
  raise errors.TemplateSyntaxError(msg, token)
179
173
  else:
180
174
  msg = f"Unrecognised instruction tag '{token.keyword}'."
@@ -182,10 +176,9 @@ class Parser:
182
176
 
183
177
  if expecting:
184
178
  token = stack[-1].token
185
- msg = f"Unexpected end of template. "
179
+ msg = "Unexpected end of template. "
186
180
  msg += f"Ibis was expecting a closing '{expecting[-1]}' tag to close the "
187
181
  msg += f"'{token.keyword}' tag opened in line {token.line_number}."
188
182
  raise errors.TemplateSyntaxError(msg, token)
189
183
 
190
184
  return stack.pop()
191
-
@@ -1,19 +1,18 @@
1
1
  import datetime
2
- from . import errors
3
2
 
3
+ from . import errors
4
4
 
5
5
  # User-configurable functions and variables available in all contexts.
6
6
  builtins = {
7
- 'now': datetime.datetime.now,
8
- 'range': range,
7
+ "now": datetime.datetime.now,
8
+ "range": range,
9
9
  }
10
10
 
11
11
 
12
12
  # A wrapper around a stack of dictionaries.
13
13
  class DataStack:
14
-
15
14
  def __init__(self):
16
- self.stack = []
15
+ self.stack = []
17
16
 
18
17
  def __getitem__(self, key):
19
18
  for d in reversed(self.stack):
@@ -25,16 +24,17 @@ class DataStack:
25
24
  # A Context object is a wrapper around the user's input data. Its `.resolve()` method contains
26
25
  # the lookup-logic for resolving dotted variable names.
27
26
  class Context:
28
-
29
27
  def __init__(self, data_dict, strict_mode):
30
28
  # Stack of data dictionaries for the .resolve() method.
31
29
  self.data = DataStack()
32
30
 
33
31
  # Standard builtins.
34
- self.data.stack.append({
35
- 'context': self,
36
- 'is_defined': self.is_defined,
37
- })
32
+ self.data.stack.append(
33
+ {
34
+ "context": self,
35
+ "is_defined": self.is_defined,
36
+ }
37
+ )
38
38
 
39
39
  # User-configurable builtins.
40
40
  self.data.stack.append(builtins)
@@ -75,7 +75,7 @@ class Context:
75
75
  def resolve(self, varstring, token):
76
76
  words = []
77
77
  result = self.data
78
- for word in varstring.split('.'):
78
+ for word in varstring.split("."):
79
79
  words.append(word)
80
80
  if hasattr(result, word):
81
81
  result = getattr(result, word)
@@ -95,7 +95,7 @@ class Context:
95
95
 
96
96
  def is_defined(self, varstring):
97
97
  current = self.data
98
- for word in varstring.split('.'):
98
+ for word in varstring.split("."):
99
99
  if hasattr(current, word):
100
100
  current = getattr(current, word)
101
101
  else:
@@ -111,9 +111,8 @@ class Context:
111
111
 
112
112
  # Null type returned when a context lookup fails.
113
113
  class Undefined:
114
-
115
114
  def __str__(self):
116
- return ''
115
+ return ""
117
116
 
118
117
  def __bool__(self):
119
118
  return False
@@ -135,4 +134,3 @@ class Undefined:
135
134
 
136
135
  def __ne__(self, other):
137
136
  return True
138
-
@@ -1,6 +1,5 @@
1
1
  # Base class for all exception types raised by the template engine.
2
2
  class TemplateError(Exception):
3
-
4
3
  def __init__(self, msg):
5
4
  self.msg = msg
6
5
 
@@ -17,7 +16,6 @@ class TemplateLoadError(TemplateError):
17
16
 
18
17
  # This exception type is raised if the lexer cannot tokenize a template string.
19
18
  class TemplateLexingError(TemplateError):
20
-
21
19
  def __init__(self, msg, template_id, line_number):
22
20
  super().__init__(msg)
23
21
  self.template_id = template_id
@@ -29,7 +27,6 @@ class TemplateLexingError(TemplateError):
29
27
 
30
28
  # This exception type may be raised while a template is being compiled.
31
29
  class TemplateSyntaxError(TemplateError):
32
-
33
30
  def __init__(self, msg, token):
34
31
  super().__init__(msg)
35
32
  self.token = token
@@ -37,7 +34,6 @@ class TemplateSyntaxError(TemplateError):
37
34
 
38
35
  # This exception type may be raised while a template is being rendered.
39
36
  class TemplateRenderingError(TemplateError):
40
-
41
37
  def __init__(self, msg, token):
42
38
  super().__init__(msg)
43
39
  self.token = token
@@ -45,8 +41,6 @@ class TemplateRenderingError(TemplateError):
45
41
 
46
42
  # This exception type is raised in strict mode if a variable cannot be resolved.
47
43
  class UndefinedVariable(TemplateError):
48
-
49
44
  def __init__(self, msg, token):
50
45
  super().__init__(msg)
51
46
  self.token = token
52
-
@@ -1,14 +1,14 @@
1
- import re
2
- import pprint
3
1
  import html
2
+ import pprint
4
3
  import random
4
+ import re
5
5
 
6
6
  from . import context
7
7
 
8
8
  try:
9
9
  import pygments
10
- import pygments.lexers
11
10
  import pygments.formatters
11
+ import pygments.lexers
12
12
  except ImportError:
13
13
  pygments = None
14
14
 
@@ -29,7 +29,6 @@ filtermap = {}
29
29
  #
30
30
  # If no name is supplied the function name will be used.
31
31
  def register(nameorfunc=None):
32
-
33
32
  if callable(nameorfunc):
34
33
  filtermap[nameorfunc.__name__] = nameorfunc
35
34
  return nameorfunc
@@ -43,126 +42,126 @@ def register(nameorfunc=None):
43
42
 
44
43
  @register
45
44
  def argtest(*args):
46
- """ Test filter: returns arguments as a concatenated string. """
47
- return '|'.join(str(arg) for arg in args)
45
+ """Test filter: returns arguments as a concatenated string."""
46
+ return "|".join(str(arg) for arg in args)
48
47
 
49
48
 
50
49
  @register
51
50
  def default(obj, fallback):
52
- """ Returns `obj` if `obj` is truthy, otherwise `fallback`. """
51
+ """Returns `obj` if `obj` is truthy, otherwise `fallback`."""
53
52
  return obj or fallback
54
53
 
55
54
 
56
55
  @register
57
- def dtformat(dt, format='%Y-%m-%d %H:%M'):
58
- """ Formats a datetime object using the specified format string. """
56
+ def dtformat(dt, format="%Y-%m-%d %H:%M"):
57
+ """Formats a datetime object using the specified format string."""
59
58
  return dt.strftime(format)
60
59
 
61
60
 
62
61
  @register
63
62
  def endswith(s, suffix):
64
- """ True if the string ends with the specified suffix. """
63
+ """True if the string ends with the specified suffix."""
65
64
  return s.endswith(suffix)
66
65
 
67
66
 
68
67
  @register
69
- @register('e')
70
- @register('esc')
68
+ @register("e")
69
+ @register("esc")
71
70
  def escape(s, quotes=True):
72
- """ Converts html syntax characters to character entities. """
71
+ """Converts html syntax characters to character entities."""
73
72
  return html.escape(s, quotes)
74
73
 
75
74
 
76
75
  @register
77
76
  def first(seq):
78
- """ Returns the first element in the sequence `seq`. """
77
+ """Returns the first element in the sequence `seq`."""
79
78
  return seq[0]
80
79
 
81
80
 
82
81
  @register
83
82
  def firsth(html):
84
- """ Returns the content of the first heading element. """
85
- match = re.search(r'<h(\d)+[^>]*>(.*?)</h\1>', html, flags=re.DOTALL)
86
- return match.group(2) if match else ''
83
+ """Returns the content of the first heading element."""
84
+ match = re.search(r"<h(\d)+[^>]*>(.*?)</h\1>", html, flags=re.DOTALL)
85
+ return match.group(2) if match else ""
87
86
 
88
87
 
89
88
  @register
90
89
  def firsth1(html):
91
- """ Returns the content of the first h1 element. """
92
- match = re.search(r'<h1[^>]*>(.*?)</h1>', html, flags=re.DOTALL)
93
- return match.group(1) if match else ''
90
+ """Returns the content of the first h1 element."""
91
+ match = re.search(r"<h1[^>]*>(.*?)</h1>", html, flags=re.DOTALL)
92
+ return match.group(1) if match else ""
94
93
 
95
94
 
96
95
  @register
97
96
  def firstp(html):
98
- """ Returns the content of the first p element. """
99
- match = re.search(r'<p[^>]*>(.*?)</p>', html, flags=re.DOTALL)
100
- return match.group(1) if match else ''
97
+ """Returns the content of the first p element."""
98
+ match = re.search(r"<p[^>]*>(.*?)</p>", html, flags=re.DOTALL)
99
+ return match.group(1) if match else ""
101
100
 
102
101
 
103
- @register('reversed')
102
+ @register("reversed")
104
103
  def get_reversed(seq):
105
- """ Returns a reverse iterator over the sequence `seq`. """
104
+ """Returns a reverse iterator over the sequence `seq`."""
106
105
  return reversed(seq)
107
106
 
108
107
 
109
108
  @register
110
109
  def index(seq, i):
111
- """ Returns the ith element in the sequence `seq`. """
110
+ """Returns the ith element in the sequence `seq`."""
112
111
  return seq[i]
113
112
 
114
113
 
115
- @register('divisible_by')
114
+ @register("divisible_by")
116
115
  def is_divisible_by(n, d):
117
- """ True if the integer `n` is a multiple of the integer `d`. """
116
+ """True if the integer `n` is a multiple of the integer `d`."""
118
117
  return n % d == 0
119
118
 
120
119
 
121
- @register('even')
120
+ @register("even")
122
121
  def is_even(n):
123
- """ True if the integer `n` is even. """
122
+ """True if the integer `n` is even."""
124
123
  return n % 2 == 0
125
124
 
126
125
 
127
- @register('odd')
126
+ @register("odd")
128
127
  def is_odd(n):
129
- """ True if the integer `n` is odd. """
128
+ """True if the integer `n` is odd."""
130
129
  return n % 2 != 0
131
130
 
132
131
 
133
132
  @register
134
- def join(seq, sep=''):
135
- """ Joins elements of the sequence `seq` with the string `sep`. """
133
+ def join(seq, sep=""):
134
+ """Joins elements of the sequence `seq` with the string `sep`."""
136
135
  return sep.join(str(item) for item in seq)
137
136
 
138
137
 
139
138
  @register
140
139
  def last(seq):
141
- """ Returns the last element in the sequence `seq`. """
140
+ """Returns the last element in the sequence `seq`."""
142
141
  return seq[-1]
143
142
 
144
143
 
145
- @register('len')
144
+ @register("len")
146
145
  def length(seq):
147
- """ Returns the length of the sequence `seq`. """
146
+ """Returns the length of the sequence `seq`."""
148
147
  return len(seq)
149
148
 
150
149
 
151
150
  @register
152
151
  def lower(s):
153
- """ Returns the string `s` converted to lowercase. """
152
+ """Returns the string `s` converted to lowercase."""
154
153
  return s.lower()
155
154
 
156
155
 
157
- @register('pprint')
156
+ @register("pprint")
158
157
  def prettyprint(obj):
159
- """ Returns a pretty-printed representation of `obj`. """
158
+ """Returns a pretty-printed representation of `obj`."""
160
159
  return pprint.pformat(obj)
161
160
 
162
161
 
163
162
  @register
164
163
  def pygmentize(text, lang=None):
165
- """ Applies syntax highlighting using Pygments.
164
+ """Applies syntax highlighting using Pygments.
166
165
 
167
166
  If no language is specified, Pygments will attempt to guess the correct
168
167
  lexer to use. If Pygments is not available or if an appropriate lexer
@@ -192,107 +191,102 @@ def pygmentize(text, lang=None):
192
191
 
193
192
  @register
194
193
  def random(seq):
195
- """ Returns a random element from the sequence `seq`. """
194
+ """Returns a random element from the sequence `seq`."""
196
195
  return random.choice(seq)
197
196
 
198
197
 
199
- @register('repr')
198
+ @register("repr")
200
199
  def to_repr(obj):
201
- """ Returns the result of calling repr() on `obj`. """
200
+ """Returns the result of calling repr() on `obj`."""
202
201
  return repr(obj)
203
202
 
204
203
 
205
204
  @register
206
205
  def slice(seq, start, stop=None, step=None):
207
- """ Returns the start:stop:step slice of the sequence `seq`. """
206
+ """Returns the start:stop:step slice of the sequence `seq`."""
208
207
  return seq[start:stop:step]
209
208
 
210
209
 
211
210
  @register
212
211
  def spaceless(html):
213
- """ Strips all whitespace between html/xml tags. """
214
- return re.sub(r'>\s+<', '><', html)
212
+ """Strips all whitespace between html/xml tags."""
213
+ return re.sub(r">\s+<", "><", html)
215
214
 
216
215
 
217
216
  @register
218
217
  def startswith(s, prefix):
219
- """ True if the string starts with the specified prefix. """
218
+ """True if the string starts with the specified prefix."""
220
219
  return s.startswith(prefix)
221
220
 
222
221
 
223
- @register('str')
222
+ @register("str")
224
223
  def to_str(obj):
225
- """ Returns the result of calling str() on `obj`. """
224
+ """Returns the result of calling str() on `obj`."""
226
225
  return str(obj)
227
226
 
228
227
 
229
228
  @register
230
229
  def striptags(html):
231
- """ Returns the string `html` with all html tags stripped. """
232
- return re.sub(r'<[^>]*>', '', html)
230
+ """Returns the string `html` with all html tags stripped."""
231
+ return re.sub(r"<[^>]*>", "", html)
233
232
 
234
233
 
235
234
  @register
236
- def teaser(s, delimiter='<!-- more -->'):
237
- """ Returns the portion of the string `s` before `delimiter`,
238
- or an empty string if `delimiter` is not found. """
235
+ def teaser(s, delimiter="<!-- more -->"):
236
+ """Returns the portion of the string `s` before `delimiter`,
237
+ or an empty string if `delimiter` is not found."""
239
238
  index = s.find(delimiter)
240
239
  if index == -1:
241
- return ''
240
+ return ""
242
241
  else:
243
242
  return s[:index]
244
243
 
245
244
 
246
245
  @register
247
- @register('title')
246
+ @register("title")
248
247
  def titlecase(s):
249
- """ Returns the string `s` converted to titlecase. """
250
- return re.sub(
251
- r"[A-Za-z]+('[A-Za-z]+)?",
252
- lambda m: m.group(0)[0].upper() + m.group(0)[1:],
253
- s
254
- )
248
+ """Returns the string `s` converted to titlecase."""
249
+ return re.sub(r"[A-Za-z]+('[A-Za-z]+)?", lambda m: m.group(0)[0].upper() + m.group(0)[1:], s)
255
250
 
256
251
 
257
252
  @register
258
- def truncatechars(s, n, ellipsis='...'):
259
- """ Truncates the string `s` to at most `n` characters. """
253
+ def truncatechars(s, n, ellipsis="..."):
254
+ """Truncates the string `s` to at most `n` characters."""
260
255
  if len(s) > n:
261
- return s[:n - 3].rstrip(' .,;:?!') + ellipsis
256
+ return s[: n - 3].rstrip(" .,;:?!") + ellipsis
262
257
  else:
263
258
  return s
264
259
 
265
260
 
266
261
  @register
267
- def truncatewords(s, n, ellipsis=' [...]'):
268
- """ Truncates the string `s` to at most `n` words. """
262
+ def truncatewords(s, n, ellipsis=" [...]"):
263
+ """Truncates the string `s` to at most `n` words."""
269
264
  words = s.split()
270
265
  if len(words) > n:
271
- return ' '.join(words[:n]) + ellipsis
266
+ return " ".join(words[:n]) + ellipsis
272
267
  else:
273
- return ' '.join(words)
268
+ return " ".join(words)
274
269
 
275
270
 
276
271
  @register
277
272
  def upper(s):
278
- """ Returns the string `s` converted to uppercase. """
273
+ """Returns the string `s` converted to uppercase."""
279
274
  return s.upper()
280
275
 
281
276
 
282
277
  @register
283
278
  def wrap(s, tag):
284
- """ Wraps a string in opening and closing tags. """
285
- return '<%s>%s</%s>' % (tag, str(s), tag)
279
+ """Wraps a string in opening and closing tags."""
280
+ return "<%s>%s</%s>" % (tag, str(s), tag)
286
281
 
287
282
 
288
283
  @register
289
284
  def if_undefined(obj, fallback):
290
- """ Returns `obj` if `obj` is defined, otherwise `fallback`. """
285
+ """Returns `obj` if `obj` is defined, otherwise `fallback`."""
291
286
  return fallback if isinstance(obj, context.Undefined) else obj
292
287
 
293
288
 
294
289
  @register
295
290
  def is_defined(obj):
296
- """ Returns true if `obj` is defined, otherwise false. """
291
+ """Returns true if `obj` is defined, otherwise false."""
297
292
  return not isinstance(obj, context.Undefined)
298
-
@@ -1,7 +1,7 @@
1
1
  import os
2
2
 
3
- from .template import Template
4
3
  from .errors import TemplateLoadError
4
+ from .template import Template
5
5
 
6
6
 
7
7
  # Loads templates from the file system. Assumes files are utf-8 encoded. Compiled templates are
@@ -20,7 +20,6 @@ from .errors import TemplateLoadError
20
20
  # template = loader('subdir/foo.txt')
21
21
  #
22
22
  class FileLoader:
23
-
24
23
  def __init__(self, base_dir):
25
24
  self.base_dir = base_dir
26
25
  self.cache = {}
@@ -32,7 +31,7 @@ class FileLoader:
32
31
  path = os.path.join(self.base_dir, filename)
33
32
  if os.path.isfile(path):
34
33
  try:
35
- with open(path, encoding='utf-8') as file:
34
+ with open(path, encoding="utf-8") as file:
36
35
  template_string = file.read()
37
36
  except OSError as err:
38
37
  msg = f"FileLoader cannot load the template file '{filename}'."
@@ -42,14 +41,15 @@ class FileLoader:
42
41
  self.cache[filename] = template
43
42
  return template
44
43
 
45
- msg = f"FileLoader with base '{self.base_dir}' cannot locate the template file '{filename}'."
44
+ msg = (
45
+ f"FileLoader with base '{self.base_dir}' cannot locate the template file '{filename}'."
46
+ )
46
47
  raise TemplateLoadError(msg)
47
48
 
48
49
 
49
50
  # Like FileLoader but templates are automatically recompiled if the underlying template file
50
51
  # is modified.
51
52
  class FileReloader:
52
-
53
53
  def __init__(self, base_dir):
54
54
  self.base_dir = base_dir
55
55
  self.cache = {}
@@ -63,7 +63,7 @@ class FileReloader:
63
63
  return self.cache[filename][1]
64
64
 
65
65
  try:
66
- with open(path, encoding='utf-8') as file:
66
+ with open(path, encoding="utf-8") as file:
67
67
  template_string = file.read()
68
68
  except OSError as err:
69
69
  msg = f"FileReloader cannot load the template file '{filename}'."
@@ -80,7 +80,6 @@ class FileReloader:
80
80
  # Loads templates from a dictionary of template strings. Templates are compiled once and cached for
81
81
  # future use.
82
82
  class DictLoader:
83
-
84
83
  def __init__(self, template_strings):
85
84
  self.templates = {}
86
85
  self.template_strings = template_strings