Skip to content

Commit 6a6f164

Browse files
committed
Implement conda_lint.
With test cases for every failure mode for linting.
1 parent ca88b0c commit 6a6f164

File tree

42 files changed

+1166
-20
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1166
-20
lines changed

planemo/commands/cmd_conda_lint.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Module describing the planemo ``conda_lint`` command."""
2+
import click
3+
4+
from planemo import options
5+
from planemo import conda_lint
6+
from planemo.cli import command_function
7+
8+
9+
@click.command('conda_lint')
10+
@options.report_level_option()
11+
@options.fail_level_option()
12+
@options.recursive_option(
13+
"Recursively perform command for nested conda directories.",
14+
)
15+
@options.recipe_arg(multiple=True)
16+
@command_function
17+
def cli(ctx, paths, **kwds):
18+
"""Check conda recipe for common issues.
19+
20+
Built in large part on the work from the BSD licensed anaconda-verify
21+
project. For more information on anacoda-verify see:
22+
https://github.com/ContinuumIO/anaconda-verify.
23+
"""
24+
exit_code = conda_lint.lint_recipes_on_paths(ctx, paths, **kwds)
25+
ctx.exit(exit_code)

planemo/conda_lint.py

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
"""Logic for linting conda recipes."""
2+
3+
import os
4+
5+
from functools import wraps
6+
7+
from planemo.conda_verify.recipe import (
8+
check_build_number,
9+
check_dir_content,
10+
check_license_family,
11+
check_name,
12+
check_requirements,
13+
check_source,
14+
check_url,
15+
check_version,
16+
FIELDS,
17+
get_field,
18+
RecipeError,
19+
render_jinja2,
20+
yamlize,
21+
)
22+
from planemo.exit_codes import (
23+
EXIT_CODE_GENERIC_FAILURE,
24+
EXIT_CODE_OK,
25+
)
26+
from planemo.io import (
27+
coalesce_return_codes,
28+
find_matching_directories,
29+
info,
30+
)
31+
from planemo.lint_util import handle_lint_complete, setup_lint
32+
33+
34+
def lint_recipes_on_paths(ctx, paths, **kwds):
35+
"""Apply conda linting procedure to recipes on supplied paths."""
36+
assert_tools = kwds.get("assert_recipes", True)
37+
recursive = kwds.get("recursive", False)
38+
exit_codes = []
39+
for recipe_dir in yield_recipes_on_paths(ctx, paths, recursive):
40+
if lint_conda_recipe(ctx, recipe_dir, **kwds) != 0:
41+
exit_codes.append(EXIT_CODE_GENERIC_FAILURE)
42+
else:
43+
exit_codes.append(EXIT_CODE_OK)
44+
return coalesce_return_codes(exit_codes, assert_at_least_one=assert_tools)
45+
46+
47+
def lint_conda_recipe(ctx, recipe_dir, **kwds):
48+
info("Linting conda recipe %s" % recipe_dir)
49+
lint_args, lint_ctx = setup_lint(ctx, **kwds)
50+
51+
def apply(f):
52+
lint_ctx.lint(f.__name__, f, recipe_dir)
53+
54+
apply(lint_name)
55+
apply(lint_version)
56+
apply(lint_summary)
57+
apply(lint_build_number)
58+
apply(lint_directory_content)
59+
apply(lint_license_family)
60+
apply(lint_about_urls)
61+
apply(lint_source)
62+
apply(lint_fields)
63+
apply(lint_requirements)
64+
65+
return handle_lint_complete(lint_ctx, lint_args)
66+
67+
68+
def wraps_recipe_error(is_error=True):
69+
70+
def outer_wrapper(f):
71+
72+
@wraps(f)
73+
def wrapper(recipe_dir, lint_ctx):
74+
try:
75+
f(recipe_dir, lint_ctx)
76+
except RecipeError as e:
77+
if is_error:
78+
lint_ctx.error(str(e))
79+
else:
80+
lint_ctx.warn(str(e))
81+
except TypeError as e: # Errors in recipe checking code from YAML.
82+
lint_ctx.error(str(e))
83+
84+
return wrapper
85+
86+
return outer_wrapper
87+
88+
89+
def lints_metadata(f):
90+
91+
@wraps(f)
92+
def wrapper(recipe_dir, lint_ctx):
93+
meta_path = os.path.join(recipe_dir, 'meta.yaml')
94+
with open(meta_path, 'rb') as fi:
95+
data = fi.read()
96+
if b'{{' in data:
97+
data = render_jinja2(recipe_dir)
98+
meta = dict(yamlize(data))
99+
f(meta, lint_ctx)
100+
101+
return wrapper
102+
103+
104+
@wraps_recipe_error(is_error=False)
105+
def lint_directory_content(recipe_dir, lint_ctx):
106+
check_dir_content(recipe_dir)
107+
lint_ctx.info("Directory content seems okay.")
108+
109+
110+
@lints_metadata
111+
@wraps_recipe_error(is_error=False)
112+
def lint_license_family(meta, lint_ctx):
113+
check_license_family(meta)
114+
lint_ctx.info("License from vaild license family.")
115+
116+
117+
@lints_metadata
118+
def lint_summary(meta, lint_ctx):
119+
summary = get_field(meta, 'about/summary')
120+
121+
if not summary:
122+
lint_ctx.warn("No summary supplied in about metadata.")
123+
124+
if summary and len(summary) > 80:
125+
msg = "summary exceeds 80 characters"
126+
lint_ctx.warn(msg)
127+
128+
129+
@lints_metadata
130+
@wraps_recipe_error(is_error=False)
131+
def lint_about_urls(meta, lint_ctx):
132+
for field in ('about/home', 'about/dev_url', 'about/doc_url',
133+
'about/license_url'):
134+
url = get_field(meta, field)
135+
if url:
136+
check_url(url)
137+
lint_ctx.info("About urls (if present) are valid")
138+
139+
140+
@lints_metadata
141+
@wraps_recipe_error(is_error=True)
142+
def lint_source(meta, lint_ctx):
143+
check_source(meta)
144+
lint_ctx.info("Source (if present) is valid")
145+
146+
147+
@lints_metadata
148+
@wraps_recipe_error(is_error=True)
149+
def lint_build_number(meta, lint_ctx):
150+
build_number = get_field(meta, 'build/number', 0)
151+
check_build_number(build_number)
152+
lint_ctx.info("Valid build number [%s]" % build_number)
153+
154+
155+
@lints_metadata
156+
@wraps_recipe_error(is_error=True)
157+
def lint_version(meta, lint_ctx):
158+
version = get_field(meta, 'package/version')
159+
check_version(version)
160+
lint_ctx.info("Valid version number [%s]" % version)
161+
162+
163+
@lints_metadata
164+
@wraps_recipe_error(is_error=True)
165+
def lint_name(meta, lint_ctx):
166+
name = get_field(meta, 'package/name')
167+
check_name(name)
168+
lint_ctx.info("Valid recipe name [%s]" % name)
169+
170+
171+
@lints_metadata
172+
@wraps_recipe_error(is_error=False)
173+
def lint_fields(meta, lint_ctx):
174+
# Taken from validate_meta
175+
for section in meta:
176+
if section not in FIELDS:
177+
raise RecipeError("Unknown section: %s" % section)
178+
submeta = meta.get(section)
179+
if submeta is None:
180+
submeta = {}
181+
for key in submeta:
182+
# Next two lines added for planemo since we don't do the
183+
# select lines thing.
184+
if key == "skip":
185+
continue
186+
187+
if key not in FIELDS[section]:
188+
raise RecipeError("in section %r: unknown key %r" %
189+
(section, key))
190+
191+
192+
@lints_metadata
193+
@wraps_recipe_error(is_error=False)
194+
def lint_requirements(meta, lint_ctx):
195+
check_requirements(meta)
196+
lint_ctx.info("Reference recipe files appear valid")
197+
198+
199+
def yield_recipes_on_paths(ctx, paths, recursive):
200+
for path in paths:
201+
recipe_dirs = find_matching_directories(
202+
path, "meta.yaml", recursive=recursive
203+
)
204+
for recipe_dir in recipe_dirs:
205+
yield recipe_dir
206+
207+
208+
__all__ = [
209+
"lint_recipes_on_paths",
210+
]

planemo/conda_verify/LICENSE.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
Copyright (c) 2016, Continuum Analytics, Inc.
2+
All rights reserved.
3+
4+
Redistribution and use in source and binary forms, with or without
5+
modification, are permitted provided that the following conditions are met:
6+
* Redistributions of source code must retain the above copyright
7+
notice, this list of conditions and the following disclaimer.
8+
* Redistributions in binary form must reproduce the above copyright
9+
notice, this list of conditions and the following disclaimer in the
10+
documentation and/or other materials provided with the distribution.
11+
* Neither the name of Continuum Analytics, Inc. nor the
12+
names of its contributors may be used to endorse or promote products
13+
derived from this software without specific prior written permission.
14+
15+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18+
DISCLAIMED. IN NO EVENT SHALL CONTINUUM ANALYTICS BE LIABLE FOR ANY
19+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
22+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

planemo/conda_verify/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
__version__ = '1.2.1'

planemo/conda_verify/const.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
LICENSE_FAMILIES = set("""
2+
AGPL
3+
GPL2
4+
GPL3
5+
LGPL
6+
BSD
7+
MIT
8+
Apache
9+
PSF
10+
Public-Domain
11+
Proprietary
12+
Other
13+
""".split())
14+
15+
FIELDS = {
16+
'package': {'name', 'version'},
17+
'source': {'fn', 'url', 'md5', 'sha1', 'sha256',
18+
'git_url', 'git_tag', 'git_branch',
19+
'patches', 'hg_url', 'hg_tag'},
20+
'build': {'features', 'track_features',
21+
'number', 'entry_points', 'osx_is_app', 'noarch',
22+
'preserve_egg_dir', 'win_has_prefix', 'no_link',
23+
'ignore_prefix_files', 'msvc_compiler',
24+
'detect_binary_files_with_prefix',
25+
'always_include_files'},
26+
'requirements': {'build', 'run'},
27+
'app': {'entry', 'icon', 'summary', 'type', 'cli_opts'},
28+
'test': {'requires', 'commands', 'files', 'imports'},
29+
'about': {'license', 'license_url', 'license_family', 'license_file',
30+
'summary', 'description', 'home', 'doc_url', 'dev_url'},
31+
}
32+
33+
MAGIC_HEADERS = {
34+
'\xca\xfe\xba\xbe': 'MachO-universal',
35+
'\xce\xfa\xed\xfe': 'MachO-i386',
36+
'\xcf\xfa\xed\xfe': 'MachO-x86_64',
37+
'\xfe\xed\xfa\xce': 'MachO-ppc',
38+
'\xfe\xed\xfa\xcf': 'MachO-ppc64',
39+
'MZ\x90\x00': 'DLL',
40+
'\x7fELF': 'ELF',
41+
}
42+
43+
DLL_TYPES = {
44+
0x0: 'UNKNOWN', 0x1d3: 'AM33', 0x8664: 'AMD64', 0x1c0: 'ARM',
45+
0xebc: 'EBC', 0x14c: 'I386', 0x200: 'IA64', 0x9041: 'M32R',
46+
0x266: 'MIPS16', 0x366: 'MIPSFPU', 0x466: 'MIPSFPU16', 0x1f0: 'POWERPC',
47+
0x1f1: 'POWERPCFP', 0x166: 'R4000', 0x1a2: 'SH3', 0x1a3: 'SH3DSP',
48+
0x1a6: 'SH4', 0x1a8: 'SH5', 0x1c2: 'THUMB', 0x169: 'WCEMIPSV2',
49+
}

0 commit comments

Comments
 (0)