Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add type annotations for screens.components subpackage #19

Open
wants to merge 61 commits into
base: order-files-by-size-diff-screen
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
ba54ff9
Retrieve size information when crawling file system
pbugnion Apr 23, 2018
6d407e3
Flake8 fixes
pbugnion Apr 25, 2018
fcc1e18
Encode differences in a named tuple
pbugnion Apr 25, 2018
c1598ed
Pass full file object when rendering
pbugnion Apr 25, 2018
03f3da9
Add table component
pbugnion May 1, 2018
b9a4bf1
Raise error if the number of rows is inconsistent
pbugnion May 1, 2018
656029f
Handle case where there are no rows
pbugnion May 1, 2018
7e2f531
Test case where there are no columns
pbugnion May 1, 2018
d2b5c79
Set table preferred width
pbugnion May 27, 2018
d6f4ef8
Implement table preferred height
pbugnion May 27, 2018
21fed5f
Implement missing container methods
pbugnion May 27, 2018
eb4a3dc
Use table to represent differences
pbugnion May 27, 2018
d35f38d
Change semantics of focus to be Up/Down
pbugnion May 27, 2018
3951b55
Render up/down details
pbugnion May 27, 2018
80b7e8b
Ignore emacs dir-locals
pbugnion May 27, 2018
361ac26
Render local and remote sizes
pbugnion May 27, 2018
c37496d
Human friendly information for attributes
pbugnion May 27, 2018
c9fc685
Clarify naturalsize docstring
pbugnion May 27, 2018
e265bfa
Improve copy of actions
pbugnion May 27, 2018
4873326
Remove unused default for calculating action
pbugnion May 27, 2018
346b907
Place summary container as sidebar
pbugnion May 28, 2018
980fe0f
Factor menu out into separate component
pbugnion May 28, 2018
d993849
Unused import
pbugnion May 28, 2018
a7cf013
Initial tests for vertical menu
pbugnion May 28, 2018
f1ab1ee
Factor out common test fixtures
pbugnion May 28, 2018
e320b3b
Test both up and down key behaviour
pbugnion May 28, 2018
4447ed5
Test that key numbering wraps round
pbugnion May 28, 2018
f869a26
Verify that callbacks are called
pbugnion May 28, 2018
bc04f3e
Handle menu with no entries
pbugnion May 28, 2018
a4f80f0
Test for menu with a single entry
pbugnion May 28, 2018
2d50541
Add help text for up and down sync
pbugnion May 28, 2018
52bee8e
Make sure app rerenders after re-drawing details
pbugnion May 28, 2018
52a6dd9
Allow setting menu width
pbugnion May 28, 2018
5c15661
Test passing explicit width to menu
pbugnion May 28, 2018
7b36d67
Add entry for entering `watch` mode
pbugnion May 28, 2018
d8ad7df
Render help boxes using text area
pbugnion May 28, 2018
a503dfd
Remove extra empty line
pbugnion May 28, 2018
23eee43
Only perform sync when in correct mode
pbugnion May 29, 2018
d7b74ec
Handle fully synchronized case
pbugnion May 29, 2018
0361d55
Allow passing custom separator to table
pbugnion May 29, 2018
d0eb9d0
Use two spaces in columns
pbugnion May 29, 2018
673a54a
Use - to indicate absent table entry
pbugnion May 29, 2018
a6f9017
Ability to specify how columns should be aligned in table
pbugnion May 30, 2018
8bede9a
Right align modification time and date columns
pbugnion May 30, 2018
cb3962a
Unsubscribe from exchange when stopping diff screen
pbugnion Jun 1, 2018
2d2f2b3
Sort entries in action table
pbugnion Jun 1, 2018
95b074f
Order files by size after action
pbugnion Jun 1, 2018
27257e4
Use VerticalLine widget instead of custom
pbugnion Jun 1, 2018
a86669b
Details screen shouldn't gain focus in WATCH state
pbugnion Jun 1, 2018
2b881a4
Replace hard-coded strings with enum values
pbugnion Jun 2, 2018
586d1ea
Docstring explaining sort order
pbugnion Jun 2, 2018
618a2ad
Refactor table component into smaller private methods
pbugnion Jun 2, 2018
67b9037
Update help text for new layout
pbugnion Jun 2, 2018
4e44e18
Update toolbar text
pbugnion Jun 2, 2018
a12afef
Avoid long lines
pbugnion Jun 2, 2018
b3c2e83
Remove dependency on inflect
pbugnion Jun 2, 2018
5a1747b
Add missing package in setup.py
pbugnion Jun 2, 2018
6db9504
Release 0.3.0-alpha2.
pbugnion Jun 2, 2018
f6378ff
Initial commit for mypy checks
pbugnion Jun 3, 2018
33e2131
Remove unnecessary methods in Table
pbugnion Jun 3, 2018
da50be5
Explicit typechecks on screens.components package
pbugnion Jun 3, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@
.mypy_cache/
__pycache__
.pytest_cache/
.dir-locals.el
32 changes: 32 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[mypy]
mypy_path = stubs

[mypy-sml_sync.screens.components.*]
disallow_any_unimported = True
disallow_untyped_defs = True

[mypy-sml_sync.screens.components.tests.*]
# Don't typecheck tests yet
ignore_errors = True

# Dependencies that don't have stubs yet
[mypy-sml.*]
ignore_missing_imports = True

[mypy-watchdog.*]
ignore_missing_imports = True

[mypy-semantic-versions.*]
ignore_missing_imports = True

[mypy-daiquiri]
ignore_missing_imports = True

[mypy-semantic_version]
ignore_missing_imports = True

[mypy-pytest]
ignore_missing_imports = True

[mypy-paramiko]
ignore_missing_imports = True
1 change: 0 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
sml
daiquiri
paramiko
inflect
watchdog
semantic_version
git+https://github.com/jonathanslenders/[email protected]
10 changes: 8 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,19 @@
with open(os.path.join(here, 'sml_sync', 'version.py')) as f:
exec(f.read(), {}, version_ns)

packages = [
'sml_sync',
'sml_sync.screens',
'sml_sync.cli',
'sml_sync.screens.components'
]

setup(
name='sml_sync',
version=version_ns['version'],
description='SherlockML file synchronizer',
author='ASI Data Science',
packages=['sml_sync', 'sml_sync.screens', 'sml_sync.cli'],
packages=packages,
entry_points={
'console_scripts': ['sml-sync=sml_sync:run']
},
Expand All @@ -24,7 +31,6 @@
'sml',
'daiquiri',
'paramiko',
'inflect',
'watchdog',
'semantic_version',
'prompt_toolkit>=2.0'
Expand Down
19 changes: 9 additions & 10 deletions sml_sync/file_trees.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@

import logging
import os
import stat
from datetime import datetime

from .models import DirectoryAttrs, FileAttrs, FsObject, FsObjectType
from .models import FsObjectType, Difference, DifferenceType


def get_remote_mtime(path, sftp):
Expand Down Expand Up @@ -37,18 +36,18 @@ def compare_file_trees(left, right):
right_file_paths = {obj.path: obj for obj in right}
left_only = [obj for obj in left if obj.path not in right_file_paths]
for obj in left_only:
yield ('LEFT_ONLY', obj)
yield Difference(DifferenceType.LEFT_ONLY, left=obj, right=None)
right_only = [obj for obj in right if obj.path not in left_file_paths]
for obj in right_only:
yield ('RIGHT_ONLY', obj)
yield Difference(DifferenceType.RIGHT_ONLY, left=None, right=obj)

for left_obj in left:
if left_obj.path in right_file_paths:
right_obj = right_file_paths[left_obj.path]
if left_obj.obj_type != right_obj.obj_type:
yield ('TYPE_DIFFERENT', left_obj, right_obj)
elif (
left_obj.attrs != right_obj.attrs and
left_obj.obj_type == FsObjectType.FILE
):
yield ('ATTRS_DIFFERENT', left_obj, right_obj)
yield Difference(
DifferenceType.TYPE_DIFFERENT, left_obj, right_obj)
elif (left_obj.attrs != right_obj.attrs and
left_obj.obj_type == FsObjectType.FILE):
yield Difference(
DifferenceType.ATTRS_DIFFERENT, left_obj, right_obj)
26 changes: 25 additions & 1 deletion sml_sync/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,14 @@ def without_path_prefix(self, prefix):
self.attrs
)

def is_file(self):
return self.obj_type == FsObjectType.FILE

FileAttrs = collections.namedtuple('FileAttrs', ['last_modified'])
def is_directory(self):
return self.obj_type == FsObjectType.DIRECTORY


FileAttrs = collections.namedtuple('FileAttrs', ['last_modified', 'size'])
DirectoryAttrs = collections.namedtuple('DirectoryAttrs', ['last_modified'])


Expand All @@ -41,3 +47,21 @@ class ChangeEventType(Enum):
'FsChangeEvent',
['event_type', 'is_directory', 'path', 'extra_args']
)


class DifferenceType(Enum):
# path exists only in left tree
LEFT_ONLY = 'LEFT_ONLY'

# path exists only in right tree
RIGHT_ONLY = 'RIGHT_ONLY'

# path exists in both, but is a file in one and a directory in the other
TYPE_DIFFERENT = 'TYPE_DIFFERENT'

# path exists in both, but they have different attributes
ATTRS_DIFFERENT = 'ATTRS_DIFFERENT'


Difference = collections.namedtuple(
'Difference', ['difference_type', 'left', 'right'])
6 changes: 2 additions & 4 deletions sml_sync/screens/choose_remote_dir.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,9 @@
from enum import Enum
from queue import Empty, Queue

from prompt_toolkit.application.current import get_app
from prompt_toolkit.application import get_app
from prompt_toolkit.key_binding import KeyBindings
from prompt_toolkit.layout import HSplit, VSplit
from prompt_toolkit.layout.containers import Window
from prompt_toolkit.layout.controls import FormattedTextControl
from prompt_toolkit.layout import HSplit, VSplit, Window, FormattedTextControl
from prompt_toolkit.widgets import TextArea

from ..pubsub import Messages
Expand Down
3 changes: 3 additions & 0 deletions sml_sync/screens/components/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@

from .table import Table, TableColumn, ColumnSettings, Alignment # noqa
from .vertical_menu import MenuEntry, VerticalMenu # noqa
112 changes: 112 additions & 0 deletions sml_sync/screens/components/table.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
from collections import namedtuple
import itertools
from enum import Enum
from typing import List, Optional

from prompt_toolkit.buffer import Buffer
from prompt_toolkit.document import Document
from prompt_toolkit.layout import (
Window, HSplit, FormattedTextControl, BufferControl, ScrollbarMargin,
Container
)


class Alignment(Enum):
RIGHT = 'RIGHT'
LEFT = 'LEFT'


class ColumnSettings(object):

def __init__(self, alignment: Alignment = Alignment.LEFT) -> None:
self.alignment = alignment


class TableColumn(
namedtuple('Column', ['rows', 'header', 'settings'])):

def __new__(
cls, rows: List[str],
header: str,
settings: Optional[ColumnSettings] = None
) -> 'TableColumn':
if settings is None:
settings = ColumnSettings()
return super(TableColumn, cls).__new__(cls, rows, header, settings)


class Table(object):

def __init__(self, columns: List[TableColumn], sep: str = ' ') -> None:
if len(set(len(column.rows) for column in columns)) not in {0, 1}:
raise ValueError('All columns must have the same number of rows.')

self._sep = sep

formatted_headers = []
formatted_columns = []
for column in columns:
width = self._get_column_width(column)
formatted_rows = [
self._format_cell(row, column.settings, width)
for row in column.rows
]
formatted_headers.append(column.header.ljust(width, ' '))
formatted_columns.append(formatted_rows)

self.window = HSplit(
self._header_windows(formatted_headers) +
self._body_windows(formatted_columns)
)

def _get_column_width(self, column: TableColumn) -> int:
width = max(
len(column.header),
max((len(row) for row in column.rows), default=0)
)
return width

def _format_cell(
self,
content: str,
column_settings: ColumnSettings,
width: int
) -> str:
if column_settings.alignment == Alignment.LEFT:
return content.ljust(width)
else:
return content.rjust(width)

def _header_windows(self, formatted_headers: List[str]) -> List[Window]:
if len(formatted_headers):
header_control = FormattedTextControl(
self._sep.join(formatted_headers))
header_windows = [Window(header_control, height=1)]
else:
header_windows = [Window(height=1, width=0)]
return header_windows

def _body_windows(
self,
formatted_columns: List[List[str]]
) -> List[Window]:
rows = list(itertools.zip_longest(*formatted_columns, fillvalue=''))
if rows:
rows_string = [self._sep.join(row) for row in rows]
table_body = '\n'.join(rows_string)

document = Document(table_body, cursor_position=0)
_buffer = Buffer(document=document, read_only=True)
self._body_control = BufferControl(_buffer)
body_windows = [
Window(
self._body_control,
right_margins=[ScrollbarMargin(display_arrows=True)]
)
]
else:
body_windows = []
return body_windows

def __pt_container__(self) -> Container:
return self.window
Empty file.
Loading