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 autoupdate command #1065

Merged
merged 42 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
dc1b335
First Draft for autoupdate command
lorrainealisha75 Jun 23, 2020
c42db57
Putting files in the right directories
lorrainealisha75 Jun 23, 2020
dcf206a
Making minor changes
lorrainealisha75 Jun 24, 2020
81a8a83
Basic functionalityof autoupdate check-in
lorrainealisha75 Jun 25, 2020
85df40e
Minor changens in autoupdate file
lorrainealisha75 Jul 13, 2020
2b09418
modify autoupdate so that tokens can be updated correctly
simonbray Jul 29, 2020
3988282
working version of autoupdate
simonbray Jul 30, 2020
efc78ee
next commit
simonbray Aug 3, 2020
0b89771
Merge remote-tracking branch 'upstream/master' into autoupdate-sb
simonbray Aug 5, 2020
ce54d0f
next commit
simonbray Aug 6, 2020
f7dc089
minor change to logging
simonbray Aug 7, 2020
cd139a6
add conda flags
simonbray Aug 13, 2020
b572fee
linting
simonbray Aug 13, 2020
f3160e3
small changes to docs
simonbray Aug 13, 2020
b52825d
add initial draft of some documentation
simonbray Aug 13, 2020
3ba0775
some fixes
simonbray Aug 14, 2020
6536192
rewrite code
simonbray Aug 20, 2020
6b44dd8
commit
simonbray Aug 21, 2020
c5e6d5b
autoupdate
simonbray Aug 24, 2020
a1a1e3e
autoupdate
simonbray Aug 24, 2020
2cb8262
do not add +galaxy0 where not already used, requested by @wm75
simonbray Aug 24, 2020
6bd3a8a
restructure autoupdate code, small fixes
simonbray Sep 7, 2020
6185f44
add 2 test cases (w and w/o --dry-run)
simonbray Sep 7, 2020
b479570
docs linting
simonbray Sep 7, 2020
c3b4c0e
autoupdate test passing locally, try and get it to run on the CI as well
simonbray Sep 7, 2020
739f44c
[ci skip] add skiplist option as suggested by @bgruening
simonbray Sep 8, 2020
f27e282
another attempt at fixing the test
simonbray Sep 8, 2020
940b577
create xml file for autoupdate test during the test
simonbray Sep 8, 2020
f5c4711
Merge remote-tracking branch 'upstream/master' into autoupdate-sb
simonbray Sep 25, 2020
08e5988
change tests to check stdout
simonbray Sep 25, 2020
36ac709
ensure conda is installed
simonbray Sep 25, 2020
f13cce1
lint
simonbray Sep 25, 2020
c5af2af
(re)add docs
simonbray Sep 29, 2020
ed5fcc4
Merge remote-tracking branch 'upstream/master' into autoupdate-sb
simonbray Jan 5, 2021
8c1382f
do not modify os-dependent newlines when using autoupdate
simonbray Jan 14, 2021
16e6857
add skip_requirements option to autoupdate
simonbray Jan 14, 2021
5b582b5
fix bug introduced in previous commit
simonbray Mar 12, 2021
a5be98f
add link to autoupdate ci, fix update_test_data
simonbray Mar 12, 2021
bc3ed0c
add default to skip_requirements arg
simonbray Mar 13, 2021
3420eae
fix for testing
simonbray Mar 15, 2021
599cb59
fix autoupdate skip requirements, update docs, some general tidying
simonbray Mar 16, 2021
cbbbc43
update docs with @VERSION_SUFFIX@
simonbray Mar 16, 2021
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
131 changes: 131 additions & 0 deletions planemo/autoupdate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""Autoupdate older conda dependencies in the requirements section."""
from __future__ import absolute_import

import re
import xml.etree.ElementTree as ET
import planemo.conda

from planemo.io import error, info

def autoupdate(tool_path, dry_run=False):
"""
Autoupdate an XML file
"""
xml_tree = ET.parse(tool_path)
requirements = find_requirements(xml_tree)

for macro_import in requirements['imports']:
# recursively check macros
macro_requirements = autoupdate('/'.join(tool_path.split('/')[:-1] + [macro_import]), dry_run)
for requirement in macro_requirements:
if requirement not in requirements:
requirements[requirement] = macro_requirements[requirement]

if not requirements.get('@TOOL_VERSION@'):
# if tool_version is not specified, finish without changes
error("The @TOOL_VERSION@ token is not specified in {}. This is required for autoupdating.".format(tool_path))
return requirements
updated_main_req = get_latest_versions({requirements.get('main_req'): requirements.get('@TOOL_VERSION@')})
if updated_main_req[requirements.get('main_req')] == requirements.get('@TOOL_VERSION@'):
# check main_req is up-to-date; if so, finish without changes
info("No updates required or madeto {}.".format(tool_path))
return requirements

if dry_run:
error("Update required to {}! Tool main requirement has version {}, newest conda version is {}".format(tool_path, requirements.get('@TOOL_VERSION@'), updated_main_req[requirements.get('main_req')]))
return requirements

# if main_req is not up-to-date, update everything
updated_version_dict = get_latest_versions(requirements.get('other_reqs'))
update_requirements(tool_path, xml_tree, updated_version_dict, updated_main_req)
info("Tool {} updated.".format(tool_path))
return requirements

def find_requirements(xml_tree):
"""
Get all requirements with versions as a dictionary of the form
{'main_req': main requirement, 'other_reqs': {req1: version, ... },
'imports: ['macros.xml'], '*VERSION@': '...'}
"""
requirements = {'other_reqs': {}, 'imports': []}

# get tokens
for token in xml_tree.iter("token"):
if 'VERSION@' in token.attrib.get('name', ''):
requirements[token.attrib['name']] = token.text
for macro_import in xml_tree.iter("import"):
requirements['imports'].append(macro_import.text)

# get requirements
for requirement in xml_tree.iter("requirement"):
if requirement.attrib.get('version') == '@TOOL_VERSION@':
requirements['main_req'] = requirement.text
else:
requirements['other_reqs'][requirement.text] = requirement.attrib.get('version')
return requirements


def get_latest_versions(version_dict):
"""
Update a dict with current conda versions for tool requirements
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pretty awesome!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you @jmchilton - I think this is ready for a review

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I take that back, there are still some things I can fix. Though any overall comments about the implementation would be welcome.

for package in version_dict.keys():
target = planemo.conda.conda_util.CondaTarget(package)
search_results = planemo.conda.best_practice_search(target)
version_dict[package] = search_results[0]['version']
return version_dict


def update_requirements(tool_path, xml_tree, updated_version_dict, updated_main_req):
"""
Update requirements to latest conda versions
and update version tokens
"""

tags_to_update = {'tokens': [], 'requirements': []}

for token in xml_tree.iter("token"):
if token.attrib.get('name') == '@TOOL_VERSION@':
# check this
token.text = list(updated_main_req.values())[0]
elif token.attrib.get('name') == '@GALAXY_VERSION@':
token.text = '0'
else:
continue
tags_to_update['tokens'].append(ET.tostring(token, encoding="unicode").strip())

if '@GALAXY_VERSION@' not in [n.attrib.get('name') for n in xml_tree.iter('token')]:
tags_to_update['update_tool'] = True

for requirement in xml_tree.iter("requirement"):
if requirement.text not in updated_main_req:
requirement.set('version', updated_version_dict[requirement.text])
tags_to_update['requirements'].append(ET.tostring(requirement, encoding="unicode").strip())
write_to_xml(tool_path, xml_tree, tags_to_update)
return xml_tree


def write_to_xml(tool_path, xml_tree, tags_to_update):
"""
Write modified XML to tool_path
"""
with open(tool_path, 'r+') as f:
xml_text = f.read()
for token in tags_to_update['tokens']:
xml_text = re.sub('{}>.*<{}'.format(*re.split('>.*<', token)), token, xml_text)

for requirement in tags_to_update['requirements']:
xml_text = re.sub('{}version=".*"{}'.format(*re.split('version=".*"', requirement)), requirement, xml_text)

# if '@GALAXY_VERSION@' not in tags_to_update['tokens']:
if tags_to_update.get('update_tool'):
# update the version directly in the tool tag
xml_text = re.sub('version="@TOOL_VERSION@\+galaxy.*"', 'version="@TOOL_VERSION@+galaxy0"', xml_text)

f.seek(0)
f.truncate()
f.write(xml_text)

__all__ = (
"autoupdate"
)
64 changes: 64 additions & 0 deletions planemo/commands/cmd_autoupdate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"""Module describing the planemo ``autoupdate`` command."""
import click

from planemo.exit_codes import (
EXIT_CODE_GENERIC_FAILURE,
EXIT_CODE_OK
)
from planemo.io import (
coalesce_return_codes,
info
)
from planemo.tools import (
is_tool_load_error,
yield_tool_sources_on_paths
)

from planemo import options, autoupdate
from planemo.cli import command_function
from planemo.config import planemo_option


def dry_run_option():
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure if this should be moved to options.py or not

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is totally fine here, especially if only one command is using it.

"""Perform a dry run autoupdate without modifying the XML files"""
return planemo_option(
"--dry-run",
is_flag=True,
help="Perform a dry run autoupdate without modifying the XML files."
)

@click.command('autoupdate')
@options.optional_tools_arg(multiple=True)
@options.report_level_option()
@options.report_xunit()
@options.fail_level_option()
@options.skip_option()
@options.recursive_option()
@dry_run_option()
@command_function
def cli(ctx, paths, **kwds):
"""Auto-update requirements section if necessary"""
assert_tools = kwds.get("assert_tools", True)
recursive = kwds.get("recursive", False)
exit_codes = []
for (tool_path, tool_xml) in yield_tool_sources_on_paths(ctx, paths, recursive):
info("Auto-updating tool %s" % tool_path)
tool_xml = autoupdate.autoupdate(tool_path, kwds['dry_run'])
if handle_tool_load_error(tool_path, tool_xml):
exit_codes.append(EXIT_CODE_GENERIC_FAILURE)
continue
else:
exit_codes.append(EXIT_CODE_OK)
return coalesce_return_codes(exit_codes, assert_at_least_one=assert_tools)
ctx.exit()


def handle_tool_load_error(tool_path, tool_xml):
""" Return True if tool_xml is tool load error (invalid XML), and
print a helpful error message.
"""
is_error = False
if is_tool_load_error(tool_xml):
info("Could not update %s due to malformed xml." % tool_path)
is_error = True
return is_error