diff env/lib/python3.9/site-packages/planemo/conda_verify/recipe.py @ 0:4f3585e2f14b draft default tip

"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
author shellac
date Mon, 22 Mar 2021 18:12:50 +0000
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/env/lib/python3.9/site-packages/planemo/conda_verify/recipe.py	Mon Mar 22 18:12:50 2021 +0000
@@ -0,0 +1,337 @@
+from __future__ import (
+    absolute_import,
+    division,
+    print_function,
+)
+
+import os
+import re
+from os.path import (
+    basename,
+    getsize,
+    isfile,
+    join,
+)
+
+import yaml
+
+from planemo.conda_verify.const import (
+    FIELDS,
+    LICENSE_FAMILIES,
+)
+from planemo.conda_verify.utils import (
+    all_ascii,
+    get_bad_seq,
+    memoized,
+)
+
+PEDANTIC = True
+sel_pat = re.compile(r'(.+?)\s*\[(.+)\]$')
+name_pat = re.compile(r'[a-z0-9_][a-z0-9_\-\.]*$')
+version_pat = re.compile(r'[\w\.]+$')
+url_pat = re.compile(r'(ftp|http(s)?)://')
+
+
+class RecipeError(Exception):
+    pass
+
+
+def ns_cfg(cfg):
+    plat = cfg['plat']
+    py = cfg['PY']
+    np = cfg['NPY']
+    for x in py, np:
+        assert isinstance(x, int), x
+    return dict(
+        nomkl=False,
+        debug=False,
+        linux=plat.startswith('linux-'),
+        linux32=bool(plat == 'linux-32'),
+        linux64=bool(plat == 'linux-64'),
+        armv7l=False,
+        arm=False,
+        ppc64le=False,
+        osx=plat.startswith('osx-'),
+        unix=plat.startswith(('linux-', 'osx-')),
+        win=plat.startswith('win-'),
+        win32=bool(plat == 'win-32'),
+        win64=bool(plat == 'win-64'),
+        x86=plat.endswith(('-32', '-64')),
+        x86_64=plat.endswith('-64'),
+        py=py,
+        py3k=bool(30 <= py < 40),
+        py2k=bool(20 <= py < 30),
+        py26=bool(py == 26),
+        py27=bool(py == 27),
+        py33=bool(py == 33),
+        py34=bool(py == 34),
+        py35=bool(py == 35),
+        np=np,
+    )
+
+
+def select_lines(data, namespace):
+    lines = []
+    for line in data.splitlines():
+        line = line.rstrip()
+        m = sel_pat.match(line)
+        if m:
+            if PEDANTIC:
+                x = m.group(1).strip()
+                # error on comment, unless the whole line is a comment
+                if '#' in x and not x.startswith('#'):
+                    raise RecipeError("found commented selector: %s" % line)
+            cond = m.group(2)
+            if eval(cond, namespace, {}):
+                lines.append(m.group(1))
+            continue
+        lines.append(line)
+    return '\n'.join(lines) + '\n'
+
+
+@memoized
+def yamlize(data):
+    res = yaml.safe_load(data)
+    # ensure the result is a dict
+    if res is None:
+        res = {}
+    return res
+
+
+def parse(data, cfg):
+    if cfg is not None:
+        data = select_lines(data, ns_cfg(cfg))
+    # ensure we create new object, because yamlize is memoized
+    return dict(yamlize(data))
+
+
+def get_field(meta, field, default=None):
+    section, key = field.split('/')
+    submeta = meta.get(section)
+    if submeta is None:
+        submeta = {}
+    res = submeta.get(key)
+    if res is None:
+        res = default
+    return res
+
+
+def check_name(name):
+    if name:
+        name = str(name)
+    else:
+        raise RecipeError("package name missing")
+    if not name_pat.match(name) or name.endswith(('.', '-', '_')):
+        raise RecipeError("invalid package name '%s'" % name)
+    seq = get_bad_seq(name)
+    if seq:
+        raise RecipeError("'%s' is not allowed in "
+                          "package name: '%s'" % (seq, name))
+
+
+def check_version(ver):
+    if ver:
+        ver = str(ver)
+    else:
+        raise RecipeError("package version missing")
+    if not version_pat.match(ver):
+        raise RecipeError("invalid version '%s'" % ver)
+    if ver.startswith(('_', '.')) or ver.endswith(('_', '.')):
+        raise RecipeError("version cannot start or end with '_' or '.': %s" %
+                          ver)
+    seq = get_bad_seq(ver)
+    if seq:
+        raise RecipeError("'%s' not allowed in version '%s'" % (seq, ver))
+
+
+def check_build_number(bn):
+    if not (isinstance(bn, int) and bn >= 0):
+        raise RecipeError("build/number '%s' (not a positive interger)" % bn)
+
+
+def check_requirements(meta):
+    for req in get_field(meta, 'requirements/run', []):
+        name = req.split()[0]
+        if not name_pat.match(name):
+            raise RecipeError("invalid run requirement name '%s'" % name)
+
+
+def check_license_family(meta):
+    if not PEDANTIC:
+        return
+    lf = get_field(meta, 'about/license_family',
+                   get_field(meta, 'about/license'))
+    if lf not in LICENSE_FAMILIES:
+        print("""\
+Error: license_family is invalid: %s
+Note that about/license_family falls back to about/license.
+Allowed license families are:""" % lf)
+        for x in LICENSE_FAMILIES:
+            print("  - %s" % x)
+        raise RecipeError("wrong license family")
+
+
+def check_url(url):
+    if not url_pat.match(url):
+        raise RecipeError("not a valid URL: %s" % url)
+
+
+def check_about(meta):
+    summary = get_field(meta, 'about/summary')
+    if summary and len(summary) > 80:
+        msg = "summary exceeds 80 characters"
+        if PEDANTIC:
+            raise RecipeError(msg)
+        else:
+            print("Warning: %s" % msg)
+
+    for field in ('about/home', 'about/dev_url', 'about/doc_url',
+                  'about/license_url'):
+        url = get_field(meta, field)
+        if url:
+            check_url(url)
+
+    check_license_family(meta)
+
+
+hash_pat = {'md5': re.compile(r'[a-f0-9]{32}$'),
+            'sha1': re.compile(r'[a-f0-9]{40}$'),
+            'sha256': re.compile(r'[a-f0-9]{64}$')}
+
+
+def check_source(meta):
+    src = meta.get('source')
+    if not src:
+        return
+    fn = src.get('fn')
+    if fn:
+        for ht in 'md5', 'sha1', 'sha256':
+            hexgigest = src.get(ht)
+            if hexgigest and not hash_pat[ht].match(hexgigest):
+                raise RecipeError("invalid hash: %s" % hexgigest)
+        url = src.get('url')
+        if url:
+            check_url(url)
+
+    git_url = src.get('git_url')
+    if git_url and (src.get('git_tag') and src.get('git_branch')):
+        raise RecipeError("cannot specify both git_branch and git_tag")
+
+
+def validate_meta(meta):
+    for section in meta:
+        if PEDANTIC and section not in FIELDS:
+            raise RecipeError("Unknown section: %s" % section)
+        submeta = meta.get(section)
+        if submeta is None:
+            submeta = {}
+        for key in submeta:
+            if PEDANTIC and key not in FIELDS[section]:
+                raise RecipeError("in section %r: unknown key %r" %
+                                  (section, key))
+
+    check_name(get_field(meta, 'package/name'))
+    check_version(get_field(meta, 'package/version'))
+    check_build_number(get_field(meta, 'build/number', 0))
+    check_requirements(meta)
+    check_about(meta)
+    check_source(meta)
+
+
+def validate_files(recipe_dir, meta):
+    for field in 'test/files', 'source/patches':
+        flst = get_field(meta, field)
+        if not flst:
+            continue
+        for fn in flst:
+            if PEDANTIC and fn.startswith('..'):
+                raise RecipeError("path outsite recipe: %s" % fn)
+            path = join(recipe_dir, fn)
+            if isfile(path):
+                continue
+            raise RecipeError("no such file '%s'" % path)
+
+
+def iter_cfgs():
+    for py in 27, 34, 35:
+        for plat in 'linux-64', 'linux-32', 'osx-64', 'win-32', 'win-64':
+            yield dict(plat=plat, PY=py, NPY=111)
+
+
+def dir_size(dir_path):
+    return sum(sum(getsize(join(root, fn)) for fn in files)
+               for root, unused_dirs, files in os.walk(dir_path))
+
+
+def check_dir_content(recipe_dir):
+    disallowed_extensions = (
+        '.tar', '.tar.gz', '.tar.bz2', '.tar.xz',
+        '.so', '.dylib', '.la', '.a', '.dll', '.pyd',
+    )
+    for root, unused_dirs, files in os.walk(recipe_dir):
+        for fn in files:
+            fn_lower = fn.lower()
+            if fn_lower.endswith(disallowed_extensions):
+                if PEDANTIC:
+                    raise RecipeError("found: %s" % fn)
+                else:
+                    print("Warning: found: %s" % fn)
+            path = join(root, fn)
+            # only allow small archives for testing
+            if (PEDANTIC and fn_lower.endswith(('.bz2', '.gz')) and getsize(path) > 512):
+                raise RecipeError("found: %s (too large)" % fn)
+
+    if basename(recipe_dir) == 'icu':
+        return
+
+    # check total size od recipe directory (recursively)
+    kb_size = dir_size(recipe_dir) / 1024
+    kb_limit = 512
+    if PEDANTIC and kb_size > kb_limit:
+        raise RecipeError("recipe too large: %d KB (limit %d KB)" %
+                          (kb_size, kb_limit))
+
+    if PEDANTIC:
+        try:
+            with open(join(recipe_dir, 'build.sh'), 'rb') as fi:
+                data = fi.read()
+            if data and not data.decode('utf-8').startswith(('#!/bin/bash\n',
+                                                             '#!/bin/sh\n')):
+                raise RecipeError("not a bash script: build.sh")
+        except IOError:
+            pass
+
+
+def render_jinja2(recipe_dir):
+    import jinja2
+
+    loaders = [jinja2.FileSystemLoader(recipe_dir)]
+    env = jinja2.Environment(loader=jinja2.ChoiceLoader(loaders))
+    template = env.get_or_select_template('meta.yaml')
+    return template.render(environment=env)
+
+
+def validate_recipe(recipe_dir, pedantic=True):
+    global PEDANTIC
+    PEDANTIC = bool(pedantic)
+
+    meta_path = join(recipe_dir, 'meta.yaml')
+    with open(meta_path, 'rb') as fi:
+        data = fi.read()
+    if PEDANTIC and not all_ascii(data):
+        raise RecipeError("non-ASCII in: %s" % meta_path)
+    if b'{{' in data:
+        if PEDANTIC:
+            raise RecipeError("found {{ in %s (Jinja templating not allowed)" %
+                              meta_path)
+        else:
+            data = render_jinja2(recipe_dir)
+    else:
+        data = data.decode('utf-8')
+
+    check_dir_content(recipe_dir)
+
+    for cfg in iter_cfgs():
+        meta = parse(data, cfg)
+        validate_meta(meta)
+        validate_files(recipe_dir, meta)