Total Complexity | 111 |
Total Lines | 505 |
Duplicated Lines | 0 % |
Complex classes like neovim_gui.GtkUI often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
1 | """Neovim Gtk+ UI.""" |
||
68 | r.x, r.y, r.width, r.height = x, y, w, h |
||
69 | return r |
||
70 | |||
71 | |||
72 | class GtkUI(object): |
||
73 | |||
74 | """Gtk+ UI class.""" |
||
75 | |||
76 | def __init__(self, font): |
||
77 | """Initialize the UI instance.""" |
||
78 | self._redraw_arg = None |
||
79 | self._foreground = -1 |
||
80 | self._background = -1 |
||
81 | self._font_name = font[0] |
||
82 | self._font_size = font[1] |
||
83 | self._screen = None |
||
84 | self._attrs = None |
||
85 | self._busy = False |
||
86 | self._mouse_enabled = False |
||
87 | self._insert_cursor = False |
||
88 | self._blink = False |
||
89 | self._blink_timer_id = None |
||
90 | self._resize_timer_id = None |
||
91 | self._pressed = None |
||
92 | self._invalid = None |
||
93 | self._pending = [0, 0, 0] |
||
94 | self._reset_cache() |
||
95 | |||
96 | def start(self, bridge): |
||
97 | """Start the UI event loop.""" |
||
98 | bridge.attach(80, 24, True) |
||
99 | drawing_area = Gtk.DrawingArea() |
||
100 | drawing_area.connect('draw', self._gtk_draw) |
||
101 | window = Gtk.Window() |
||
102 | window.add(drawing_area) |
||
103 | window.set_events(window.get_events() | |
||
104 | Gdk.EventMask.BUTTON_PRESS_MASK | |
||
105 | Gdk.EventMask.BUTTON_RELEASE_MASK | |
||
106 | Gdk.EventMask.POINTER_MOTION_MASK | |
||
107 | Gdk.EventMask.SCROLL_MASK) |
||
108 | window.connect('configure-event', self._gtk_configure) |
||
109 | window.connect('delete-event', self._gtk_quit) |
||
110 | window.connect('key-press-event', self._gtk_key) |
||
111 | window.connect('key-release-event', self._gtk_key_release) |
||
112 | window.connect('button-press-event', self._gtk_button_press) |
||
113 | window.connect('button-release-event', self._gtk_button_release) |
||
114 | window.connect('motion-notify-event', self._gtk_motion_notify) |
||
115 | window.connect('scroll-event', self._gtk_scroll) |
||
116 | window.connect('focus-in-event', self._gtk_focus_in) |
||
117 | window.connect('focus-out-event', self._gtk_focus_out) |
||
118 | window.show_all() |
||
119 | im_context = Gtk.IMMulticontext() |
||
120 | im_context.set_client_window(drawing_area.get_window()) |
||
121 | im_context.set_use_preedit(False) # TODO: preedit at cursor position |
||
122 | im_context.connect('commit', self._gtk_input) |
||
123 | self._pango_context = drawing_area.create_pango_context() |
||
124 | self._drawing_area = drawing_area |
||
125 | self._window = window |
||
126 | self._im_context = im_context |
||
127 | self._bridge = bridge |
||
128 | Gtk.main() |
||
129 | |||
130 | def quit(self): |
||
131 | """Exit the UI event loop.""" |
||
132 | GObject.idle_add(Gtk.main_quit) |
||
133 | |||
134 | def schedule_screen_update(self, apply_updates): |
||
135 | """Schedule screen updates to run in the UI event loop.""" |
||
136 | def wrapper(): |
||
137 | apply_updates() |
||
138 | self._flush() |
||
139 | self._start_blinking() |
||
140 | self._screen_invalid() |
||
141 | GObject.idle_add(wrapper) |
||
142 | |||
143 | def _screen_invalid(self): |
||
144 | self._drawing_area.queue_draw() |
||
145 | |||
146 | def _nvim_resize(self, columns, rows): |
||
147 | da = self._drawing_area |
||
148 | # create FontDescription object for the selected font/size |
||
149 | font_str = '{0} {1}'.format(self._font_name, self._font_size) |
||
150 | self._font, pixels, normal_width, bold_width = _parse_font(font_str) |
||
151 | # calculate the letter_spacing required to make bold have the same |
||
152 | # width as normal |
||
153 | self._bold_spacing = normal_width - bold_width |
||
154 | cell_pixel_width, cell_pixel_height = pixels |
||
155 | # calculate the total pixel width/height of the drawing area |
||
156 | pixel_width = cell_pixel_width * columns |
||
157 | pixel_height = cell_pixel_height * rows |
||
158 | gdkwin = da.get_window() |
||
159 | content = cairo.CONTENT_COLOR |
||
160 | self._cairo_surface = gdkwin.create_similar_surface(content, |
||
161 | pixel_width, |
||
162 | pixel_height) |
||
163 | self._cairo_context = cairo.Context(self._cairo_surface) |
||
164 | self._pango_layout = PangoCairo.create_layout(self._cairo_context) |
||
165 | self._pango_layout.set_alignment(Pango.Alignment.LEFT) |
||
166 | self._pango_layout.set_font_description(self._font) |
||
167 | self._pixel_width, self._pixel_height = pixel_width, pixel_height |
||
168 | self._cell_pixel_width = cell_pixel_width |
||
169 | self._cell_pixel_height = cell_pixel_height |
||
170 | self._screen = Screen(columns, rows) |
||
171 | self._window.resize(pixel_width, pixel_height) |
||
172 | |||
173 | def _nvim_clear(self): |
||
174 | self._clear_region(self._screen.top, self._screen.bot + 1, |
||
175 | self._screen.left, self._screen.right + 1) |
||
176 | self._screen.clear() |
||
177 | |||
178 | def _nvim_eol_clear(self): |
||
179 | row, col = self._screen.row, self._screen.col |
||
180 | self._clear_region(row, row + 1, col, self._screen.right + 1) |
||
181 | self._screen.eol_clear() |
||
182 | |||
183 | def _nvim_cursor_goto(self, row, col): |
||
184 | self._screen.cursor_goto(row, col) |
||
185 | |||
186 | def _nvim_busy_start(self): |
||
187 | self._busy = True |
||
188 | |||
189 | def _nvim_busy_stop(self): |
||
190 | self._busy = False |
||
191 | |||
192 | def _nvim_mouse_on(self): |
||
193 | self._mouse_enabled = True |
||
194 | |||
195 | def _nvim_mouse_off(self): |
||
196 | self._mouse_enabled = False |
||
197 | |||
198 | def _nvim_mode_change(self, mode): |
||
199 | self._insert_cursor = mode == 'insert' |
||
200 | |||
201 | def _nvim_set_scroll_region(self, top, bot, left, right): |
||
202 | self._screen.set_scroll_region(top, bot, left, right) |
||
203 | |||
204 | def _nvim_scroll(self, count): |
||
205 | self._flush() |
||
206 | top, bot = self._screen.top, self._screen.bot + 1 |
||
207 | left, right = self._screen.left, self._screen.right + 1 |
||
208 | # The diagrams below illustrate what will happen, depending on the |
||
209 | # scroll direction. "=" is used to represent the SR(scroll region) |
||
210 | # boundaries and "-" the moved rectangles. note that dst and src share |
||
211 | # a common region |
||
212 | if count > 0: |
||
213 | # move an rectangle in the SR up, this can happen while scrolling |
||
214 | # down |
||
215 | # +-------------------------+ |
||
216 | # | (clipped above SR) | ^ |
||
217 | # |=========================| dst_top | |
||
218 | # | dst (still in SR) | | |
||
219 | # +-------------------------+ src_top | |
||
220 | # | src (moved up) and dst | | |
||
221 | # |-------------------------| dst_bot | |
||
222 | # | src (cleared) | | |
||
223 | # +=========================+ src_bot |
||
224 | src_top, src_bot = top + count, bot |
||
225 | dst_top, dst_bot = top, bot - count |
||
226 | clr_top, clr_bot = dst_bot, src_bot |
||
227 | else: |
||
228 | # move a rectangle in the SR down, this can happen while scrolling |
||
229 | # up |
||
230 | # +=========================+ src_top |
||
231 | # | src (cleared) | | |
||
232 | # |------------------------ | dst_top | |
||
233 | # | src (moved down) and dst| | |
||
234 | # +-------------------------+ src_bot | |
||
235 | # | dst (still in SR) | | |
||
236 | # |=========================| dst_bot | |
||
237 | # | (clipped below SR) | v |
||
238 | # +-------------------------+ |
||
239 | src_top, src_bot = top, bot + count |
||
240 | dst_top, dst_bot = top - count, bot |
||
241 | clr_top, clr_bot = src_top, dst_top |
||
242 | self._cairo_surface.flush() |
||
243 | self._cairo_context.save() |
||
244 | # The move is performed by setting the source surface to itself, but |
||
245 | # with a coordinate transformation. |
||
246 | _, y = self._get_coords(dst_top - src_top, 0) |
||
247 | self._cairo_context.set_source_surface(self._cairo_surface, 0, y) |
||
248 | # Clip to ensure only dst is affected by the change |
||
249 | self._mask_region(dst_top, dst_bot, left, right) |
||
250 | # Do the move |
||
251 | self._cairo_context.paint() |
||
252 | self._cairo_context.restore() |
||
253 | # Clear the emptied region |
||
254 | self._clear_region(clr_top, clr_bot, left, right) |
||
255 | self._screen.scroll(count) |
||
256 | |||
257 | def _nvim_highlight_set(self, attrs): |
||
258 | self._attrs = self._get_pango_attrs(attrs) |
||
259 | |||
260 | def _nvim_put(self, text): |
||
261 | if self._screen.row != self._pending[0]: |
||
262 | # flush pending text if jumped to a different row |
||
263 | self._flush() |
||
264 | # work around some redraw glitches that can happen |
||
265 | self._redraw_glitch_fix() |
||
266 | # Update internal screen |
||
267 | self._screen.put(self._get_pango_text(text), self._attrs) |
||
268 | self._pending[1] = min(self._screen.col - 1, self._pending[1]) |
||
269 | self._pending[2] = max(self._screen.col, self._pending[2]) |
||
270 | |||
271 | def _nvim_bell(self): |
||
272 | self._window.get_window().beep() |
||
273 | |||
274 | def _nvim_visual_bell(self): |
||
275 | pass |
||
276 | |||
277 | def _nvim_update_fg(self, fg): |
||
278 | self._foreground = fg |
||
279 | self._reset_cache() |
||
280 | |||
281 | def _nvim_update_bg(self, bg): |
||
282 | self._background = bg |
||
283 | self._reset_cache() |
||
284 | |||
285 | def _nvim_suspend(self): |
||
286 | self._window.iconify() |
||
287 | |||
288 | def _nvim_set_title(self, title): |
||
289 | self._window.set_title(title) |
||
290 | |||
291 | def _nvim_set_icon(self, icon): |
||
292 | self._window.set_icon_name(icon) |
||
293 | |||
294 | def _gtk_draw(self, wid, cr): |
||
295 | if not self._screen: |
||
296 | return |
||
297 | # from random import random |
||
298 | # cr.rectangle(0, 0, self._pixel_width, self._pixel_height) |
||
299 | # cr.set_source_rgb(random(), random(), random()) |
||
300 | # cr.fill() |
||
301 | self._cairo_surface.flush() |
||
302 | cr.save() |
||
303 | cr.rectangle(0, 0, self._pixel_width, self._pixel_height) |
||
304 | cr.clip() |
||
305 | cr.set_source_surface(self._cairo_surface, 0, 0) |
||
306 | cr.paint() |
||
307 | cr.restore() |
||
308 | if not self._busy and self._blink: |
||
309 | # Cursor is drawn separately in the window. This approach is |
||
310 | # simpler because it doesn't taint the internal cairo surface, |
||
311 | # which is used for scrolling |
||
312 | row, col = self._screen.row, self._screen.col |
||
313 | text, attrs = self._screen.get_cursor() |
||
314 | self._pango_draw(row, col, [(text, attrs,)], cr=cr, cursor=True) |
||
315 | x, y = self._get_coords(row, col) |
||
316 | currect = Rectangle(x, y, self._cell_pixel_width, |
||
317 | self._cell_pixel_height) |
||
318 | self._im_context.set_cursor_location(currect) |
||
319 | |||
320 | def _gtk_configure(self, widget, event): |
||
321 | def resize(*args): |
||
322 | self._resize_timer_id = None |
||
323 | width, height = self._window.get_size() |
||
324 | columns = width // self._cell_pixel_width |
||
325 | rows = height // self._cell_pixel_height |
||
326 | if self._screen.columns == columns and self._screen.rows == rows: |
||
327 | return |
||
328 | self._bridge.resize(columns, rows) |
||
329 | |||
330 | if not self._screen: |
||
331 | return |
||
332 | if event.width == self._pixel_width and \ |
||
333 | event.height == self._pixel_height: |
||
334 | return |
||
335 | if self._resize_timer_id is not None: |
||
336 | GLib.source_remove(self._resize_timer_id) |
||
337 | self._resize_timer_id = GLib.timeout_add(250, resize) |
||
338 | |||
339 | def _gtk_quit(self, *args): |
||
340 | self._bridge.exit() |
||
341 | |||
342 | def _gtk_key(self, widget, event, *args): |
||
343 | # This function was adapted from pangoterm source code |
||
344 | keyval = event.keyval |
||
345 | state = event.state |
||
346 | # GtkIMContext will eat a Shift-Space and not tell us about shift. |
||
347 | # Also don't let IME eat any GDK_KEY_KP_ events |
||
348 | done = (False if state & SHIFT and keyval == ord(' ') else |
||
349 | False if Gdk.KEY_KP_Space <= keyval <= Gdk.KEY_KP_Divide else |
||
350 | self._im_context.filter_keypress(event)) |
||
351 | if done: |
||
352 | # input method handled keypress |
||
353 | return True |
||
354 | if event.is_modifier: |
||
355 | # We don't need to track the state of modifier bits |
||
356 | return |
||
357 | # translate keyval to nvim key |
||
358 | key_name = Gdk.keyval_name(keyval) |
||
359 | if key_name.startswith('KP_'): |
||
360 | key_name = key_name[3:] |
||
361 | input_str = _stringify_key(KEY_TABLE.get(key_name, key_name), state) |
||
362 | self._bridge.input(input_str) |
||
363 | |||
364 | def _gtk_key_release(self, widget, event, *args): |
||
365 | self._im_context.filter_keypress(event) |
||
366 | |||
367 | def _gtk_button_press(self, widget, event, *args): |
||
368 | if not self._mouse_enabled or event.type != Gdk.EventType.BUTTON_PRESS: |
||
369 | return |
||
370 | View Code Duplication | button = 'Left' |
|
371 | if event.button == 2: |
||
372 | button = 'Middle' |
||
373 | elif event.button == 3: |
||
374 | button = 'Right' |
||
375 | col = int(math.floor(event.x / self._cell_pixel_width)) |
||
376 | row = int(math.floor(event.y / self._cell_pixel_height)) |
||
377 | input_str = _stringify_key(button + 'Mouse', event.state) |
||
378 | input_str += '<{0},{1}>'.format(col, row) |
||
379 | self._bridge.input(input_str) |
||
380 | self._pressed = button |
||
381 | |||
382 | def _gtk_button_release(self, widget, event, *args): |
||
383 | self._pressed = None |
||
384 | |||
385 | def _gtk_motion_notify(self, widget, event, *args): |
||
386 | if not self._mouse_enabled or not self._pressed: |
||
387 | return |
||
388 | col = int(math.floor(event.x / self._cell_pixel_width)) |
||
389 | row = int(math.floor(event.y / self._cell_pixel_height)) |
||
390 | input_str = _stringify_key(self._pressed + 'Drag', event.state) |
||
391 | input_str += '<{0},{1}>'.format(col, row) |
||
392 | self._bridge.input(input_str) |
||
393 | |||
394 | def _gtk_scroll(self, widget, event, *args): |
||
395 | if not self._mouse_enabled: |
||
396 | return |
||
397 | View Code Duplication | col = int(math.floor(event.x / self._cell_pixel_width)) |
|
398 | row = int(math.floor(event.y / self._cell_pixel_height)) |
||
399 | if event.direction == Gdk.ScrollDirection.UP: |
||
400 | key = 'ScrollWheelUp' |
||
401 | elif event.direction == Gdk.ScrollDirection.DOWN: |
||
402 | key = 'ScrollWheelDown' |
||
403 | else: |
||
404 | return |
||
405 | input_str = _stringify_key(key, event.state) |
||
406 | input_str += '<{0},{1}>'.format(col, row) |
||
407 | self._bridge.input(input_str) |
||
408 | |||
409 | def _gtk_focus_in(self, *a): |
||
410 | self._im_context.focus_in() |
||
411 | |||
412 | def _gtk_focus_out(self, *a): |
||
413 | self._im_context.focus_out() |
||
414 | |||
415 | def _gtk_input(self, widget, input_str, *args): |
||
416 | self._bridge.input(input_str.replace('<', '<lt>')) |
||
417 | |||
418 | def _start_blinking(self): |
||
419 | def blink(*args): |
||
420 | self._blink = not self._blink |
||
421 | self._screen_invalid() |
||
422 | self._blink_timer_id = GLib.timeout_add(500, blink) |
||
423 | if self._blink_timer_id: |
||
424 | GLib.source_remove(self._blink_timer_id) |
||
425 | self._blink = False |
||
426 | blink() |
||
427 | |||
428 | def _clear_region(self, top, bot, left, right): |
||
429 | self._flush() |
||
430 | self._cairo_context.save() |
||
431 | self._mask_region(top, bot, left, right) |
||
432 | r, g, b = _split_color(self._background) |
||
433 | r, g, b = r / 255.0, g / 255.0, b / 255.0 |
||
434 | self._cairo_context.set_source_rgb(r, g, b) |
||
435 | self._cairo_context.paint() |
||
436 | self._cairo_context.restore() |
||
437 | |||
438 | def _mask_region(self, top, bot, left, right, cr=None): |
||
439 | if not cr: |
||
440 | cr = self._cairo_context |
||
441 | x1, y1, x2, y2 = self._get_rect(top, bot, left, right) |
||
442 | cr.rectangle(x1, y1, x2 - x1, y2 - y1) |
||
443 | cr.clip() |
||
444 | |||
445 | def _get_rect(self, top, bot, left, right): |
||
446 | x1, y1 = self._get_coords(top, left) |
||
447 | x2, y2 = self._get_coords(bot, right) |
||
448 | return x1, y1, x2, y2 |
||
449 | |||
450 | def _get_coords(self, row, col): |
||
451 | x = col * self._cell_pixel_width |
||
452 | y = row * self._cell_pixel_height |
||
453 | return x, y |
||
454 | |||
455 | def _flush(self): |
||
456 | row, startcol, endcol = self._pending |
||
457 | self._pending[0] = self._screen.row |
||
458 | self._pending[1] = self._screen.col |
||
459 | self._pending[2] = self._screen.col |
||
460 | if startcol == endcol: |
||
461 | return |
||
462 | self._cairo_context.save() |
||
463 | ccol = startcol |
||
464 | buf = [] |
||
465 | bold = False |
||
466 | for _, col, text, attrs in self._screen.iter(row, row, startcol, |
||
467 | endcol - 1): |
||
468 | newbold = attrs and 'bold' in attrs[0] |
||
469 | if newbold != bold or not text: |
||
470 | if buf: |
||
471 | self._pango_draw(row, ccol, buf) |
||
472 | bold = newbold |
||
473 | buf = [(text, attrs,)] |
||
474 | ccol = col |
||
475 | else: |
||
476 | buf.append((text, attrs,)) |
||
477 | if buf: |
||
478 | self._pango_draw(row, ccol, buf) |
||
479 | self._cairo_context.restore() |
||
480 | |||
481 | def _pango_draw(self, row, col, data, cr=None, cursor=False): |
||
482 | markup = [] |
||
483 | for text, attrs in data: |
||
484 | if not attrs: |
||
485 | attrs = self._get_pango_attrs(None) |
||
486 | attrs = attrs[1] if cursor else attrs[0] |
||
487 | markup.append('<span {0}>{1}</span>'.format(attrs, text)) |
||
488 | markup = ''.join(markup) |
||
489 | self._pango_layout.set_markup(markup, -1) |
||
490 | # Draw the text |
||
491 | if not cr: |
||
492 | cr = self._cairo_context |
||
493 | x, y = self._get_coords(row, col) |
||
494 | if cursor and self._insert_cursor: |
||
495 | cr.rectangle(x, y, self._cell_pixel_width / 4, |
||
496 | self._cell_pixel_height) |
||
497 | cr.clip() |
||
498 | cr.move_to(x, y) |
||
499 | PangoCairo.update_layout(cr, self._pango_layout) |
||
500 | PangoCairo.show_layout(cr, self._pango_layout) |
||
501 | _, r = self._pango_layout.get_pixel_extents() |
||
502 | |||
503 | def _get_pango_text(self, text): |
||
504 | rv = self._pango_text_cache.get(text, None) |
||
505 | if rv is None: |
||
506 | rv = GLib.markup_escape_text(text or '') |
||
507 | self._pango_text_cache[text] = rv |
||
508 | return rv |
||
509 | |||
510 | def _get_pango_attrs(self, attrs): |
||
511 | key = tuple(sorted((k, v,) for k, v in (attrs or {}).items())) |
||
512 | rv = self._pango_attrs_cache.get(key, None) |
||
513 | if rv is None: |
||
514 | fg = self._foreground if self._foreground != -1 else 0 |
||
515 | bg = self._background if self._background != -1 else 0xffffff |
||
516 | n = { |
||
517 | 'foreground': _split_color(fg), |
||
518 | 'background': _split_color(bg), |
||
519 | } |
||
520 | if attrs: |
||
521 | # make sure that foreground and background are assigned first |
||
522 | for k in ['foreground', 'background']: |
||
523 | if k in attrs: |
||
524 | n[k] = _split_color(attrs[k]) |
||
525 | for k, v in attrs.items(): |
||
526 | if k == 'reverse': |
||
527 | n['foreground'], n['background'] = \ |
||
528 | n['background'], n['foreground'] |
||
529 | elif k == 'italic': |
||
530 | n['font_style'] = 'italic' |
||
531 | elif k == 'bold': |
||
532 | n['font_weight'] = 'bold' |
||
533 | if self._bold_spacing: |
||
534 | n['letter_spacing'] = str(self._bold_spacing) |
||
535 | elif k == 'underline': |
||
536 | n['underline'] = 'single' |
||
537 | c = dict(n) |
||
538 | c['foreground'] = _invert_color(*_split_color(fg)) |
||
539 | c['background'] = _invert_color(*_split_color(bg)) |
||
540 | c['foreground'] = _stringify_color(*c['foreground']) |
||
541 | c['background'] = _stringify_color(*c['background']) |
||
542 | n['foreground'] = _stringify_color(*n['foreground']) |
||
543 | n['background'] = _stringify_color(*n['background']) |
||
544 | n = ' '.join(['{0}="{1}"'.format(k, v) for k, v in n.items()]) |
||
545 | c = ' '.join(['{0}="{1}"'.format(k, v) for k, v in c.items()]) |
||
546 | rv = (n, c,) |
||
547 | self._pango_attrs_cache[key] = rv |
||
548 | return rv |
||
549 | |||
550 | def _reset_cache(self): |
||
551 | self._pango_text_cache = {} |
||
552 | self._pango_attrs_cache = {} |
||
553 | |||
554 | def _redraw_glitch_fix(self): |
||
555 | row, col = self._screen.row, self._screen.col |
||
556 | text, attrs = self._screen.get_cursor() |
||
557 | # when updating cells in italic or bold words, the result can become |
||
558 | # messy(characters can be clipped or leave remains when removed). To |
||
559 | # prevent that, always update non empty sequences of cells and the |
||
560 | # surrounding space. |
||
561 | # find the start of the sequence |
||
562 | lcol = col - 1 |
||
563 | while lcol >= 0: |
||
564 | text, _ = self._screen.get_cell(row, lcol) |
||
565 | lcol -= 1 |
||
566 | if text == ' ': |
||
567 | break |
||
568 | self._pending[1] = min(lcol + 1, self._pending[1]) |
||
569 | # find the end of the sequence |
||
570 | rcol = col + 1 |
||
571 | while rcol < self._screen.columns: |
||
572 | text, _ = self._screen.get_cell(row, rcol) |
||
573 | rcol += 1 |
||
617 |
This can be caused by one of the following:
1. Missing Dependencies
This error could indicate a configuration issue of Pylint. Make sure that your libraries are available by adding the necessary commands.
2. Missing __init__.py files
This error could also result from missing
__init__.py
files in your module folders. Make sure that you place one file in each sub-folder.