Skip to content

Commit

Permalink
Merge pull request #42 from pelson/moving_ahead
Browse files Browse the repository at this point in the history
Added a recipe linter for common problems with recipes.
  • Loading branch information
pelson committed Jan 12, 2016
2 parents 7524765 + 063bb93 commit c90f49d
Show file tree
Hide file tree
Showing 3 changed files with 183 additions and 0 deletions.
20 changes: 20 additions & 0 deletions conda_smithy/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

from . import ci_register
from . import configure_feedstock
from . import lint_recipe


def generate_feedstock_content(target_directory, recipe_dir):
Expand Down Expand Up @@ -153,6 +154,25 @@ def main():
args.subcommand_func(args)


class RecipeLint(Subcommand):
subcommand = 'recipe-lint'
def __init__(self, parser):
subcommand_parser = Subcommand.__init__(self, parser)
subcommand_parser.add_argument("recipe_directory", default=[os.getcwd()], nargs='*')

def __call__(self, args):
all_good = True
for recipe in args.recipe_directory:
lint = lint_recipe.main(os.path.join(recipe))
if lint:
all_good = False
print('{} has some lint:\n {}'.format(recipe, '\n '.join(lint)))
else:
print('{} is in fine form'.format(recipe))
# Exit code 1 for some lint, 0 for no lint.
sys.exit(int(not all_good))


class Rerender(Subcommand):
subcommand = 'rerender'
def __init__(self, parser):
Expand Down
46 changes: 46 additions & 0 deletions conda_smithy/lint_recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import os

import ruamel.yaml


EXPECTED_SECTION_ORDER = ['package', 'source', 'build', 'requirements', 'test', 'app', 'about', 'extra']


def lintify(meta):
lints = []

major_sections = list(meta.keys())

# 1: Top level meta.yaml keys should have a specific order.
section_order_sorted = sorted(major_sections, key=EXPECTED_SECTION_ORDER.index)
if major_sections != section_order_sorted:
lints.append('The top level meta keys are in an unexpected order. Expecting {}.'.format(section_order_sorted))

# 2: The about section should have a home, license and summary.
for about_item in ['home', 'license', 'summary']:
about_section = meta.get('about', {}) or {}
# if the section doesn't exist, or is just empty, lint it.
if not about_section.get(about_item, ''):
lints.append('The {} item is expected in the about section.'.format(about_item))

# 3: The recipe should have some maintainers.
extra_section = meta.get('extra', {}) or {}
if not extra_section.get('recipe-maintainers', []):
lints.append('The recipe could do with some maintainers listed in the "extra/recipe-maintainers" section.')

# 4: The recipe should have some tests.
if 'test' not in major_sections:
lints.append('The recipe must have some tests.')

return lints


def main(recipe_dir):
recipe_dir = os.path.abspath(recipe_dir)
recipe_meta = os.path.join(recipe_dir, 'meta.yaml')
if not os.path.exists(recipe_dir):
raise IOError('Feedstock has no recipe/meta.yaml.')
with open(recipe_meta, 'r') as fh:
meta = ruamel.yaml.load(fh, ruamel.yaml.RoundTripLoader)
results = lintify(meta)
return results
117 changes: 117 additions & 0 deletions conda_smithy/tests/test_lint_recipe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from __future__ import print_function
from collections import OrderedDict
import os
import shutil
import subprocess
import tempfile
import textwrap
import unittest

import conda_smithy.lint_recipe as linter


class Test_linter(unittest.TestCase):
def test_bad_order(self):
meta = OrderedDict([['package', []],
['build', []],
['source', []]])
lints = linter.lintify(meta)
expected_message = "The top level meta keys are in an unexpected order. Expecting ['package', 'source', 'build']."
self.assertIn(expected_message, lints)

def test_missing_about_license_and_summary(self):
meta = {'about': {'home': 'a URL'}}
lints = linter.lintify(meta)
expected_message = "The license item is expected in the about section."
self.assertIn(expected_message, lints)

expected_message = "The summary item is expected in the about section."
self.assertIn(expected_message, lints)

def test_missing_about_home(self):
meta = {'about': {'license': 'BSD',
'summary': 'A test summary'}}
lints = linter.lintify(meta)
expected_message = "The home item is expected in the about section."
self.assertIn(expected_message, lints)

def test_missing_about_home_empty(self):
meta = {'about': {'home': '',
'summary': '',
'license': ''}}
lints = linter.lintify(meta)
expected_message = "The home item is expected in the about section."
self.assertIn(expected_message, lints)

expected_message = "The license item is expected in the about section."
self.assertIn(expected_message, lints)

expected_message = "The summary item is expected in the about section."
self.assertIn(expected_message, lints)

def test_maintainers_section(self):
expected_message = 'The recipe could do with some maintainers listed in the "extra/recipe-maintainers" section.'

lints = linter.lintify({'extra': {'recipe-maintainers': []}})
self.assertIn(expected_message, lints)

# No extra section at all.
lints = linter.lintify({})
self.assertIn(expected_message, lints)

lints = linter.lintify({'extra': {'recipe-maintainers': ['a']}})
self.assertNotIn(expected_message, lints)

def test_test_section(self):
expected_message = 'The recipe must have some tests.'

lints = linter.lintify({})
self.assertIn(expected_message, lints)

lints = linter.lintify({'test': {'imports': 'sys'}})
self.assertNotIn(expected_message, lints)


class TestCLI_recipe_lint(unittest.TestCase):
def setUp(self):
self.tmp_dir = tempfile.mkdtemp('recipe_')

def tearDown(self):
shutil.rmtree(self.tmp_dir)

def test_cli_fail(self):
with open(os.path.join(self.tmp_dir, 'meta.yaml'), 'w') as fh:
fh.write(textwrap.dedent("""
package:
name: 'test_package'
build: []
requirements: []
"""))
child = subprocess.Popen(['conda-smithy', 'recipe-lint', self.tmp_dir],
stdout=subprocess.PIPE)
child.communicate()
self.assertEqual(child.returncode, 1)

def test_cli_success(self):
with open(os.path.join(self.tmp_dir, 'meta.yaml'), 'w') as fh:
fh.write(textwrap.dedent("""
package:
name: 'test_package'
test: []
about:
home: something
license: something else
summary: a test recipe
extra:
recipe-maintainers:
- a
- b
"""))
child = subprocess.Popen(['conda-smithy', 'recipe-lint', self.tmp_dir],
stdout=subprocess.PIPE)
child.communicate()
self.assertEqual(child.returncode, 0)


if __name__ == '__main__':
unittest.main()

0 comments on commit c90f49d

Please sign in to comment.