Skip to content

Commit e5f09b3

Browse files
committed
Added config file support, 100% coverage
Adds support for config files (.pyup.yml) and pushes test coverage to 100%.
1 parent 625d0fb commit e5f09b3

15 files changed

+813
-249
lines changed

.coveragerc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[run]
2+
include = pyup/*
3+
omit = *migrations*, *tests*, pyup/cli.py

pyup/bot.py

Lines changed: 123 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
# -*- coding: utf-8 -*-
22
from __future__ import absolute_import, print_function, unicode_literals
33
import logging
4+
import yaml
45
from .requirements import RequirementsBundle
56
from .providers.github import Provider as GithubProvider
67
from .errors import NoPermissionError, BranchExistsError
8+
from .config import Config
79

810
logger = logging.getLogger(__name__)
911

1012

1113
class Bot(object):
1214
def __init__(self, repo, user_token, bot_token=None,
13-
provider=GithubProvider, bundle=RequirementsBundle):
15+
provider=GithubProvider, bundle=RequirementsBundle, config=Config):
1416
self.bot_token = bot_token
1517
self.req_bundle = bundle()
1618
self.provider = provider(self.req_bundle)
@@ -23,8 +25,9 @@ def __init__(self, repo, user_token, bot_token=None,
2325
self._user_repo = None
2426
self._bot = None
2527
self._bot_repo = None
28+
self.config = config()
2629

27-
self._pull_requests = None
30+
self._fetched_prs = False
2831

2932
@property
3033
def user_repo(self):
@@ -52,37 +55,54 @@ def bot_repo(self):
5255

5356
@property
5457
def pull_requests(self):
55-
if self._pull_requests is None:
56-
self._pull_requests = [pr for pr in self.provider.iter_issues(
58+
if not self._fetched_prs:
59+
self.req_bundle.pull_requests = [pr for pr in self.provider.iter_issues(
5760
repo=self.user_repo, creator=self.bot if self.bot_token else self.user)]
58-
return self._pull_requests
61+
self._fetched_prs = True
62+
return self.req_bundle.pull_requests
5963

60-
def update(self, branch=None, initial=True, pin_unpinned=False, close_stale_prs=False):
61-
62-
default_branch = self.provider.get_default_branch(repo=self.user_repo)
63-
64-
if branch is None:
65-
branch = default_branch
64+
def get_repo_config(self, repo):
65+
try:
66+
content, _ = self.provider.get_file(repo, "/.pyup.yml", self.config.branch)
67+
if content is not None:
68+
return yaml.load(content)
69+
except yaml.YAMLError:
70+
logger.warning("Unable to parse config file /.pyup.yml", exc_info=True)
71+
return None
6672

67-
self.get_all_requirements(branch=branch)
73+
def configure(self, **kwargs):
74+
# if the branch is not set, get the default branch
75+
if kwargs.get("branch", False) in [None, False]:
76+
self.config.branch = self.provider.get_default_branch(repo=self.user_repo)
77+
# set the config for this update run
78+
self.config.update(kwargs)
79+
repo_config = self.get_repo_config(repo=self.user_repo)
80+
if repo_config:
81+
self.config.update(repo_config)
82+
83+
def update(self, **kwargs):
84+
"""
85+
Main entrypoint to kick off an update run.
86+
:param kwargs:
87+
:return: RequirementsBundle
88+
"""
89+
self.configure(**kwargs)
90+
self.get_all_requirements()
6891
self.apply_updates(
69-
branch,
70-
initial=initial,
71-
pin_unpinned=pin_unpinned,
72-
close_stale_prs=close_stale_prs
92+
initial=kwargs.get("initial", False),
7393
)
7494

7595
return self.req_bundle
7696

77-
def apply_updates(self, branch, initial, pin_unpinned, close_stale_prs=False):
97+
def apply_updates(self, initial):
7898

7999
InitialUpdateClass = self.req_bundle.get_initial_update_class()
80100

81101
if initial:
82102
# get the list of pending updates
83103
try:
84104
_, _, _, updates = list(
85-
self.req_bundle.get_updates(initial=initial, pin_unpinned=pin_unpinned)
105+
self.req_bundle.get_updates(initial=initial, config=self.config)
86106
)[0]
87107
except IndexError:
88108
# need to catch the index error here in case the intial update is completely
@@ -107,13 +127,14 @@ def apply_updates(self, branch, initial, pin_unpinned, close_stale_prs=False):
107127
False
108128
)
109129

110-
for title, body, update_branch, updates in self.iter_updates(initial, pin_unpinned):
130+
# todo: This block needs to be refactored
131+
for title, body, update_branch, updates in self.iter_updates(initial):
111132
if initial_pr:
112133
pull_request = initial_pr
113134
elif title not in [pr.title for pr in self.pull_requests]:
114135
pull_request = self.commit_and_pull(
115136
initial=initial,
116-
base_branch=branch,
137+
base_branch=self.config.branch,
117138
new_branch=update_branch,
118139
title=title,
119140
body=body,
@@ -124,8 +145,8 @@ def apply_updates(self, branch, initial, pin_unpinned, close_stale_prs=False):
124145

125146
for update in updates:
126147
update.requirement.pull_request = pull_request
127-
if close_stale_prs and pull_request and not initial:
128-
self.close_stale_prs(update, pull_request)
148+
if self.config.close_prs and pull_request and not initial:
149+
self.close_stale_prs(update=update, pull_request=pull_request)
129150

130151
def close_stale_prs(self, update, pull_request):
131152
"""
@@ -138,8 +159,10 @@ def close_stale_prs(self, update, pull_request):
138159
:param update:
139160
:param pull_request:
140161
"""
162+
logger.info("Preparing to close stale PRs for {}".format(pull_request.title))
141163
if self.bot_token and pull_request.type != "initial":
142164
for pr in self.pull_requests:
165+
logger.info("Checking PR {}".format(pr.title))
143166
# check that, the pr is an update, is open, the titles are not equal and that
144167
# the requirement matches
145168
if pr.type == "update" and \
@@ -158,53 +181,75 @@ def close_stale_prs(self, update, pull_request):
158181
comment="Closing this in favor of #{}".format(pull_request.number)
159182
)
160183

161-
def commit_and_pull(self, initial, base_branch, new_branch, title, body, updates):
162-
184+
def create_branch(self, base_branch, new_branch, delete_empty=False):
185+
"""
186+
Creates a new branch.
187+
:param base_branch: string name of the base branch
188+
:param new_branch: string name of the new branch
189+
:param delete_empty: bool -- delete the branch if it is empty
190+
:return: bool -- True if successfull
191+
"""
192+
logger.info("Preparing to create branch {} from {}".format(new_branch, base_branch))
163193
try:
164194
# create new branch
165195
self.provider.create_branch(
166196
base_branch=base_branch,
167197
new_branch=new_branch,
168198
repo=self.user_repo
169199
)
200+
logger.info("Created branch {} from {}".format(new_branch, base_branch))
201+
return True
170202
except BranchExistsError:
171-
# instead of failing loud if the branch already exists, we are going to return
172-
# None here and handle this case on a different layer.
173-
return None
174-
175-
updated_files = {}
176-
for update in self.iter_changes(initial, updates):
177-
if update.requirement_file.path in updated_files:
178-
sha = updated_files[update.requirement_file.path]["sha"]
179-
content = updated_files[update.requirement_file.path]["content"]
180-
else:
181-
sha = update.requirement_file.sha
182-
content = update.requirement_file.content
183-
old_content = content
184-
content = update.requirement.update_content(content)
185-
if content != old_content:
186-
new_sha = self.provider.create_commit(
187-
repo=self.user_repo,
188-
path=update.requirement_file.path,
189-
branch=new_branch,
190-
content=content,
191-
commit_message=update.commit_message,
192-
sha=sha,
193-
committer=self.bot if self.bot_token else self.user,
194-
)
195-
updated_files[update.requirement_file.path] = {"sha": new_sha, "content": content}
196-
else:
197-
logger.error("Empty commit at {repo}, unable to update {title}.".format(
198-
repo=self.user_repo.full_name, title=title)
199-
)
203+
logger.info("Branch {} exists.".format(new_branch))
204+
# if the branch exists, is empty and delete_empty is set, delete it and call
205+
# this function again
206+
if delete_empty:
207+
if self.provider.is_empty_branch(self.user_repo, base_branch, new_branch):
208+
self.provider.delete_branch(self.user_repo, new_branch)
209+
logger.info("Branch {} was empty and has been deleted".format(new_branch))
210+
return self.create_branch(base_branch, new_branch, delete_empty=False)
211+
logger.info("Branch {} is not empty".format(new_branch))
212+
return False
200213

201-
if updated_files:
202-
return self.create_pull_request(
203-
title=title,
204-
body=body,
205-
base_branch=base_branch,
206-
new_branch=new_branch
207-
)
214+
def commit_and_pull(self, initial, base_branch, new_branch, title, body, updates):
215+
logger.info("Preparing commit {}".format(title))
216+
if self.create_branch(base_branch, new_branch, delete_empty=True):
217+
updated_files = {}
218+
for update in self.iter_changes(initial, updates):
219+
if update.requirement_file.path in updated_files:
220+
sha = updated_files[update.requirement_file.path]["sha"]
221+
content = updated_files[update.requirement_file.path]["content"]
222+
else:
223+
sha = update.requirement_file.sha
224+
content = update.requirement_file.content
225+
old_content = content
226+
content = update.requirement.update_content(content)
227+
if content != old_content:
228+
new_sha = self.provider.create_commit(
229+
repo=self.user_repo,
230+
path=update.requirement_file.path,
231+
branch=new_branch,
232+
content=content,
233+
commit_message=update.commit_message,
234+
sha=sha,
235+
committer=self.bot if self.bot_token else self.user,
236+
)
237+
updated_files[update.requirement_file.path] = {"sha": new_sha,
238+
"content": content}
239+
else:
240+
logger.error("Empty commit at {repo}, unable to update {title}.".format(
241+
repo=self.user_repo.full_name, title=title)
242+
)
243+
244+
if updated_files:
245+
pr = self.create_pull_request(
246+
title=title,
247+
body=body,
248+
base_branch=base_branch,
249+
new_branch=new_branch
250+
)
251+
self.pull_requests.append(pr)
252+
return pr
208253
return None
209254

210255
def create_issue(self, title, body):
@@ -242,33 +287,39 @@ def create_pull_request(self, title, body, base_branch, new_branch):
242287
def iter_git_tree(self, branch):
243288
return self.provider.iter_git_tree(branch=branch, repo=self.user_repo)
244289

245-
def iter_updates(self, initial, pin_unpinned):
246-
return self.req_bundle.get_updates(initial=initial, pin_unpinned=pin_unpinned)
290+
def iter_updates(self, initial):
291+
return self.req_bundle.get_updates(initial=initial, config=self.config)
247292

248293
def iter_changes(self, initial, updates):
249294
return iter(updates)
250295

251296
# if this function gets updated, the gist at https://gist.github.com/jayfk/45862b05836701b49b01
252297
# needs to be updated too
253-
def get_all_requirements(self, branch):
254-
for file_type, path in self.iter_git_tree(branch):
255-
if file_type == "blob":
256-
if "requirements" in path:
257-
if path.endswith("txt") or path.endswith("pip"):
258-
self.add_requirement_file(path, branch)
298+
def get_all_requirements(self):
299+
if self.config.search:
300+
logger.info("Searching requirement files")
301+
for file_type, path in self.iter_git_tree(self.config.branch):
302+
if file_type == "blob":
303+
if "requirements" in path:
304+
if path.endswith("txt") or path.endswith("pip"):
305+
self.add_requirement_file(path)
306+
for req_file in self.config.requirements:
307+
self.add_requirement_file(req_file.path)
259308

260309
# if this function gets updated, the gist at https://gist.github.com/jayfk/c6509bbaf4429052ca3f
261310
# needs to be updated too
262-
def add_requirement_file(self, path, branch):
311+
def add_requirement_file(self, path):
312+
logger.info("Adding requirement file at {}".format(path))
263313
if not self.req_bundle.has_file_in_path(path):
264314
req_file = self.provider.get_requirement_file(
265-
path=path, repo=self.user_repo, branch=branch)
315+
path=path, repo=self.user_repo, branch=self.config.branch)
266316
if req_file is not None:
267317
self.req_bundle.append(req_file)
268318
for other_file in req_file.other_files:
269-
self.add_requirement_file(other_file, branch)
319+
self.add_requirement_file(other_file)
270320

271321

272322
class DryBot(Bot):
273-
def commit_and_pull(self, base_branch, new_branch, title, body, updates): # pragma: no cover
323+
def commit_and_pull(self, initial, base_branch, new_branch, title, body,
324+
updates): # pragma: no cover
274325
return None

pyup/cli.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
@click.option('--branch', help='', default=None)
1919
@click.option('--initial', help='', default=False, is_flag=True)
2020
@click.option('--pin', help='', default=True)
21-
def main(repo, user_token, bot_token, provider, dry, branch, initial, pin):
21+
@click.option('--close-prs', help='Tell the bot to close stale pull requests', default=True)
22+
def main(repo, user_token, bot_token, provider, dry, branch, initial, pin, close_prs):
2223

2324
if provider == 'github':
2425
ProviderClass = GithubProvider
@@ -37,7 +38,7 @@ def main(repo, user_token, bot_token, provider, dry, branch, initial, pin):
3738
provider=ProviderClass
3839
)
3940

40-
bot.update(branch=branch, initial=initial, pin_unpinned=pin)
41+
bot.update(branch=branch, initial=initial, pin=pin, close_prs=close_prs)
4142

4243
if __name__ == '__main__':
4344
main()

0 commit comments

Comments
 (0)