Skip to content

Commit

Permalink
#1: Feature/filter out backup folders
Browse files Browse the repository at this point in the history
  • Loading branch information
artemy committed Sep 22, 2020
1 parent 17b187a commit 27a4e52
Show file tree
Hide file tree
Showing 6 changed files with 191 additions and 53 deletions.
2 changes: 2 additions & 0 deletions .coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[run]
omit = venv/*
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@
.idea
venv
out
*.alfredworkflow
*.alfredworkflow
htmlcov
.coverage
coverage.xml
35 changes: 27 additions & 8 deletions .gitlab-ci.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,32 @@
image: alpine:3.11

variables:
COVERAGE_REPORT: coverage.xml

stages:
- package
- test
- package

Test:
image: "python:2.7"
stage: test
script:
- pip install -r requirements.txt
- coverage run -m unittest recent_projects_test
- coverage report -m
- coverage xml -o $COVERAGE_REPORT
artifacts:
reports:
cobertura: $COVERAGE_REPORT
coverage: '/TOTAL.*\s+(\d+%)$/'

Package:
stage: package
artifacts:
paths:
- "*.alfredworkflow"
script:
- apk add make zip
- make
stage: package
artifacts:
paths:
- "*.alfredworkflow"
script:
- apk add make zip
- make
only:
- master
94 changes: 50 additions & 44 deletions recent_projects.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,16 @@ def __init__(self, items):
self.items = items


class CustomEncoder(json.JSONEncoder):
def default(self, obj):
return obj.__dict__


def create_json(projects):
return CustomEncoder().encode(
AlfredOutput([AlfredItem(project.name, project.path, project.path) for project in projects]))


class Project:
def __init__(self, path):
self.path = os.path.expanduser(path)
Expand All @@ -32,6 +42,11 @@ def __init__(self, path):
self.name = path.split('/')[-1]
self.abbreviation = self.abbreviate()

def __eq__(self, other):
if isinstance(other, self.__class__):
return self.name == other.name and self.path == other.path and self.abbreviation == other.abbreviation
return False

def abbreviate(self):
previous_was_break = False
abbreviation = self.name[0]
Expand All @@ -55,76 +70,67 @@ def sort_on_match_type(self, query):
return 2


class CustomEncoder(json.JSONEncoder):
def default(self, obj):
if isinstance(obj, AlfredItem) | isinstance(obj, AlfredOutput):
return obj.__dict__
return json.JSONEncoder.default(self, obj)


def create_json(projects):
alfred = AlfredOutput(items=
[AlfredItem(title=project.name, subtitle=project.path, arg=project.path) for project in
projects])
print CustomEncoder().encode(alfred)


def read_app_data(app):
def find_app_data(app):
try:
with open('products.json', 'r') as outfile:
data = json.load(outfile)
return data[app]
except IOError:
print "can't open file"
print "Can't open products file"
except KeyError:
print "App '{}' is not found in the products.json".format(app)
exit(1)


def find_recent_files_xml(application):
def find_recentprojects_file(application):
preferences_path = os.path.expanduser("~/Library/Application Support/JetBrains/")
most_recent_preferences = max(
[x for x in next(os.walk(preferences_path))[1] if application['folder-name'] in x])
most_recent_preferences = max(find_preferences_folders(application))
return '{}{}/options/{}.xml'.format(preferences_path, most_recent_preferences, 'recentProjects')


def read_projects(most_recent_projects_file):
def find_preferences_folders(application):
preferences_path = os.path.expanduser("~/Library/Application Support/JetBrains/")
return [folder_name for folder_name in next(os.walk(preferences_path))[1] if
application["folder-name"] in folder_name and not should_ignore_folder(folder_name)]


def should_ignore_folder(folder_name):
return "backup" in folder_name


def read_projects_from_file(most_recent_projects_file):
tree = ElementTree.parse(most_recent_projects_file)
xpath = ".//option[@name='recentPaths']/list/option"
objects = tree.findall(xpath)
targets = [o.attrib['value'].replace('$USER_HOME$', "~") for o in objects]
return targets
projects = [t.attrib['value'].replace('$USER_HOME$', "~") for t
in tree.findall(".//option[@name='recentPaths']/list/option")]
return projects


def filter_projects(targets):
try:
query = sys.argv[2].strip().lower()
if len(query) < 1:
raise IndexError
projects = map(Project, targets)
results = filter(lambda p: p.matches_query(query), projects)
results.sort(key=lambda p: p.sort_on_match_type(query))
return results
except IndexError:
return map(Project, targets)
def filter_and_sort_projects(query, projects):
if len(query) < 1:
return projects
results = filter(lambda p: p.matches_query(query), projects)
results.sort(key=lambda p: p.sort_on_match_type(query))
return results


def main():
def main(): # pragma: nocover
try:
application = read_app_data(sys.argv[1])
most_recent_projects_file = find_recent_files_xml(application)
app_data = find_app_data(sys.argv[1])
recent_projects_file = find_recentprojects_file(app_data)

query = sys.argv[2].strip().lower()

projects = read_projects(most_recent_projects_file)
projects = filter_projects(projects)
projects = map(Project, read_projects_from_file(recent_projects_file))
projects = filter_and_sort_projects(query, projects)

create_json(projects)
print create_json(projects)
except IndexError:
print "no app specified, exiting"
print "No app specified, exiting"
exit(1)
except ValueError:
print "can't find any preferences for", sys.argv[1]
print "Can't find any preferences for", sys.argv[1]
exit(1)


if __name__ == "__main__":
if __name__ == "__main__": # pragma: nocover
main()
106 changes: 106 additions & 0 deletions recent_projects_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import unittest

import mock

from recent_projects import create_json, Project, find_app_data, find_recentprojects_file, read_projects_from_file, \
filter_and_sort_projects


class Unittests(unittest.TestCase):
def setUp(self):
self.recentProjectsPath = '/Users/JohnSnow/Library/Application Support' \
'/JetBrains/IntelliJIdea2020.2/options/recentProjects.xml'
self.example_projects_paths = ["~/Documents/spring-petclinic", "~/Desktop/trash/My Project (42)"]

with mock.patch("os.path.expanduser") as mock_expanduser:
mock_expanduser.return_value = '/Users/JohnSnow/Documents/spring-petclinic'
self.example_project = Project(self.example_projects_paths[0])

@mock.patch('os.path.isfile')
def test_create_json(self, mock_isfile):
mock_isfile.return_value = False
expected = '{"items": [{"type": "file", ' \
'"arg": "/Users/JohnSnow/Documents/spring-petclinic", ' \
'"subtitle": "/Users/JohnSnow/Documents/spring-petclinic", ' \
'"title": "spring-petclinic"}]}'
self.assertEqual(expected, create_json([self.example_project]))

@mock.patch("os.path.expanduser")
@mock.patch('os.path.isfile')
@mock.patch("__builtin__.open", mock.mock_open(read_data="custom_project_name"))
def test_create_json_from_custom_name(self, mock_isfile, mock_expand_user):
mock_expand_user.return_value = '/Users/JohnSnow/Documents/spring-petclinic'
mock_isfile.return_value = True
expected = '{"items": [{"type": "file", ' \
'"arg": "/Users/JohnSnow/Documents/spring-petclinic", ' \
'"subtitle": "/Users/JohnSnow/Documents/spring-petclinic", ' \
'"title": "custom_project_name"}]}'
self.assertEqual(expected, create_json([Project("~/Documents/spring-petclinic")]))

@mock.patch("__builtin__.open", mock.mock_open(read_data='{"clion": {"folder-name": "CLion","name": "CLion"}}'))
def test_read_app_data(self):
self.assertEqual(find_app_data("clion"), {"folder-name": 'CLion', "name": 'CLion'})

with self.assertRaises(SystemExit) as exitcode:
find_app_data("rider")
self.assertEqual(exitcode.exception.code, 1)

@mock.patch("__builtin__.open")
def test_read_app_data_products_file_missing(self, mock_open):
mock_open.side_effect = IOError()
with self.assertRaises(SystemExit) as exitcode:
find_app_data("clion")
self.assertEqual(exitcode.exception.code, 1)

@mock.patch("os.path.expanduser")
@mock.patch("os.walk")
def test_find_recent_files_xml(self, mock_walk, expand_user):
expand_user.return_value = '/Users/JohnSnow/Library/Application Support/JetBrains/'
mock_walk.return_value = iter([
('/Path',
['IntelliJIdea2020.1',
'IntelliJIdea2020.2',
'IntelliJIdea2020.2-backup',
'GoLand2020.1',
'GoLand2020.2'], []),
])
"""Happy Flow"""
self.assertEqual(find_recentprojects_file({"name": "IntelliJ IDEA", "folder-name": "IntelliJIdea"}),
self.recentProjectsPath)

@mock.patch("__builtin__.open", mock.mock_open(
read_data='<application>'
'<component name="RecentProjectsManager">'
'<option name="recentPaths">'
'<list>'
'<option value="$USER_HOME$/Documents/spring-petclinic" />'
'<option value="$USER_HOME$/Desktop/trash/My Project (42)" />'
'</list>'
'</option>'
'</component>'
'</application>'))
def test_read_projects(self):
self.assertEqual(read_projects_from_file(self.recentProjectsPath), self.example_projects_paths)

def test_filter_projects(self):
projects = map(Project, self.example_projects_paths)
self.assertEqual([Project(self.example_projects_paths[0])], filter_and_sort_projects("petclinic", projects))

def test_filter_projects_no_query(self):
projects = map(Project, self.example_projects_paths)
self.assertEqual(filter_and_sort_projects("", projects), projects)

def test_project_equals(self):
project = Project(self.example_projects_paths[0])
self.assertTrue(project == Project("~/Documents/spring-petclinic"))
self.assertFalse(project == "some-other-object")

def test_project_sort_on_match_type(self):
project = Project(self.example_projects_paths[0])
self.assertEqual(project.sort_on_match_type("sp"), 0)
self.assertEqual(project.sort_on_match_type("spring-petclinic"), 1)
self.assertEqual(project.sort_on_match_type("foobar"), 2)


if __name__ == '__main__': # pragma: nocover
unittest.main()
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
coverage==5.3
mock==3.0.5

0 comments on commit 27a4e52

Please sign in to comment.