Skip to content

Commit

Permalink
Merge pull request #301 from anaconda-distribution/0.1.1
Browse files Browse the repository at this point in the history
0.1.1
  • Loading branch information
cbouss authored Oct 18, 2023
2 parents d63d253 + 1101fcd commit 7180004
Show file tree
Hide file tree
Showing 11 changed files with 384 additions and 70 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@
Note: version releases in the 0.x.y range may introduce breaking changes.

## 0.1.1

- Add knowledge of Python build backends to the missing_wheel rule
- Relax host_section_needs_exact_pinnings
- Add cbc_dep_in_run_missing_from_host
- Make uses_setup_py an error
- Add auto-fix for cbc_dep_in_run_missing_from_host, uses_setup_py, pip_install_args
- Update percy to >=0.1.0,<0.2.0
- Add wrong_output_script_key
- Add potentially_bad_ignore_run_exports

## 0.1.0
- Use percy as render backend
Expand Down
2 changes: 1 addition & 1 deletion anaconda_linter/lint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
- The class property ``requires`` may contain a list of other check
classes that are required to have passed before this check is
executed. Use this to avoid duplicate errors presented, or to
ensure that asumptions made by your check are met by the recipe.
ensure that assumptions made by your check are met by the recipe.
- Each class is instantiated once per linting run. Do slow preparation
work in the constructor. E.g. the `recipe_in_blocklist` check
Expand Down
193 changes: 131 additions & 62 deletions anaconda_linter/lint/check_build_help.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import os
import re
from pathlib import Path
from typing import Any

from .. import utils as _utils
Expand Down Expand Up @@ -115,7 +116,8 @@ def recipe_has_patches(recipe):


class host_section_needs_exact_pinnings(LintCheck):
"""Packages in host must have exact version pinnings, except python build tools.
"""Linked libraries host should have exact version pinnings.
Other dependencies are case by case.
Specifically, comparison operators must not be used. The version numbers can be
specified in a conda_build_config.yaml file.
Expand All @@ -131,7 +133,7 @@ def check_recipe(self, recipe):
if constraint == "" or re.search("^[<>!]", constraint) is not None:
path = dep["paths"][c]
output = -1 if not path.startswith("outputs") else int(path.split("/")[1])
self.message(section=path, output=output)
self.message(section=path, severity=WARNING, output=output)

@staticmethod
def is_exception(package):
Expand All @@ -145,7 +147,54 @@ def is_exception(package):
# It doesn't make sense to pin the versions of hatch plugins if we're not pinning
# hatch. We could explicitly enumerate the 15 odd plugins in PYTHON_BUILD_TOOLS, but
# this seemed lower maintenance
return (package in exceptions) or package.startswith("hatch-")
return (package in exceptions) or any(
[package.startswith(f"{pkg}-") for pkg in PYTHON_BUILD_TOOLS]
)


class cbc_dep_in_run_missing_from_host(LintCheck):
"""Run dependencies listed in the cbc should also be present in the host section."""

def check_recipe(self, recipe):
for package in recipe.packages.values():
for dep in package.run:
if dep.pkg in recipe.selector_dict and recipe.selector_dict[dep.pkg]:
if not self.is_exception(dep.pkg) and not package.has_dep("host", dep.pkg):
dep_path = _utils.get_dep_path(recipe, dep)
self.message(
section=dep_path,
data=(recipe, f"{package.path_prefix}requirements/host", dep.pkg),
)

@staticmethod
def is_exception(package):
exceptions = (
"python",
"numpy",
)
return package in exceptions

def fix(self, _message, _data):
(recipe, path, dep) = _data
op = [
{
"op": "add",
"path": path,
"match": f"{dep}.*",
"value": [f"{dep} " + "{{ " + f"{dep}" + " }}"],
},
]
return recipe.patch(op)


class potentially_bad_ignore_run_exports(LintCheck):
"""Ignoring run_export of a host dependency. In some cases it is more appropriate to remove the --error-overdepending flag of conda-build.""" # noqa: E501

def check_recipe(self, recipe):
for package in recipe.packages.values():
for dep in package.host:
if dep.pkg in package.ignore_run_exports:
self.message(section=_utils.get_dep_path(recipe, dep), severity=INFO)


class should_use_compilers(LintCheck):
Expand Down Expand Up @@ -331,42 +380,61 @@ class uses_setup_py(LintCheck):
"""

@staticmethod
def _check_line(line: str) -> bool:
def _check_line(x: str) -> bool:
"""Check a line for a broken call to setup.py"""
if "setup.py install" in line:
return False
if isinstance(x, str):
x = [x]
elif not isinstance(x, list):
return True
for line in x:
if "setup.py install" in line:
return False
return True

def check_deps(self, deps):
if "setuptools" not in deps:
return # no setuptools, no problem

for path in deps["setuptools"]["paths"]:
if path.startswith("output"):
n = path.split("/")[1]
script = f"outputs/{n}/script"
output = int(n)
else:
script = "build/script"
output = -1
if self.recipe.contains(script, "setup.py install", ""):
self.message(section=script)
continue
if self.recipe.dir:
def check_recipe(self, recipe):
for package in recipe.packages.values():
if not self._check_line(recipe.get(f"{package.path_prefix}build/script", None)):
self.message(
section=f"{package.path_prefix}build/script",
data=(recipe, f"{package.path_prefix}build/script"),
)
elif not self._check_line(recipe.get(f"{package.path_prefix}script", None)):
self.message(
section=f"{package.path_prefix}script",
data=(recipe, f"{package.path_prefix}script"),
)
elif self.recipe.dir:
try:
if script == "build/script":
build_file = "build.sh"
else:
build_file = self.recipe.get(script, "")
build_file = self.recipe.get(f"{package.path_prefix}script", "")
if not build_file:
continue
with open(os.path.join(self.recipe.dir, build_file)) as buildsh:
for num, line in enumerate(buildsh):
if not self._check_line(line):
self.message(fname=build_file, line=num, output=output)
build_file = self.recipe.get(
f"{package.path_prefix}build/script", "build.sh"
)
build_file = self.recipe.dir / Path(build_file)
if build_file.exists():
with open(str(build_file)) as buildsh:
for num, line in enumerate(buildsh):
if not self._check_line(line):
if package.path_prefix.startswith("output"):
output = int(package.path_prefix.split("/")[1])
else:
output = -1
self.message(fname=build_file, line=num, output=output)
except FileNotFoundError:
pass

def fix(self, _message, _data):
(recipe, path) = _data
op = [
{
"op": "replace",
"path": path,
"match": ".* setup.py .*",
"value": "{{PYTHON}} -m pip install . --no-deps --no-build-isolation --ignore-installed --no-cache-dir -vv", # noqa: E501
},
]
return recipe.patch(op)


class pip_install_args(LintCheck):
"""`pip install` should be run with --no-deps and --no-build-isolation.
Expand All @@ -393,45 +461,46 @@ def _check_line(x: Any) -> bool:

return True

def check_deps(self, deps):
if "pip" not in deps:
return # no pip, no problem

for path in deps["pip"]["paths"]:
if path.startswith("output"):
n = path.split("/")[1]
script = f"outputs/{n}/script"
output = int(n)
else:
script = "build/script"
output = -1
if not self._check_line(self.recipe.get(script, "")):
self.message(section=script, data=self.recipe)
continue
if self.recipe.dir:
def check_recipe(self, recipe):
for package in recipe.packages.values():
if not self._check_line(recipe.get(f"{package.path_prefix}build/script", None)):
self.message(
section=f"{package.path_prefix}build/script",
data=(recipe, f"{package.path_prefix}build/script"),
)
elif not self._check_line(recipe.get(f"{package.path_prefix}script", None)):
self.message(
section=f"{package.path_prefix}script",
data=(recipe, f"{package.path_prefix}script"),
)
elif self.recipe.dir:
try:
if script == "build/script":
build_file = "build.sh"
else:
build_file = self.recipe.get(script, "")
build_file = self.recipe.get(f"{package.path_prefix}script", "")
if not build_file:
continue
with open(os.path.join(self.recipe.dir, build_file)) as buildsh:
for num, line in enumerate(buildsh):
if not self._check_line(line):
self.message(
fname=build_file, line=num, output=output, data=self.recipe
)
build_file = self.recipe.get(
f"{package.path_prefix}build/script", "build.sh"
)
build_file = self.recipe.dir / Path(build_file)
if build_file.exists():
with open(str(build_file)) as buildsh:
for num, line in enumerate(buildsh):
if not self._check_line(line):
if package.path_prefix.startswith("output"):
output = int(package.path_prefix.split("/")[1])
else:
output = -1
self.message(fname=build_file, line=num, output=output)
except FileNotFoundError:
pass

def fix(self, _message, recipe):
def fix(self, _message, _data):
(recipe, path) = _data
op = [
{
"op": "replace",
"path": "@output/build/script",
"match": "pip install(?!=.*--no-build-isolation).*",
"value": "pip install . --no-deps --no-build-isolation --ignore-installed --no-cache-dir -vv",
"path": path,
"match": r"(.*\s)?pip install(?!=.*--no-build-isolation).*",
"value": "{{ PYTHON }} -m pip install . --no-deps --no-build-isolation --ignore-installed --no-cache-dir -vv", # noqa: E501
},
]
return recipe.patch(op)
Expand Down
23 changes: 23 additions & 0 deletions anaconda_linter/lint/check_completeness.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,3 +391,26 @@ class missing_description(LintCheck):
def check_recipe(self, recipe):
if not recipe.get("about/description", ""):
self.message(section="about", severity=WARNING)


class wrong_output_script_key(LintCheck):
"""It should be `outputs/x/script`, not `outputs/x/build/script`
Please change from::
outputs:
- name: output1
build:
script: build_script.sh
To::
outputs:
- name: output1
script: build_script.sh
"""

def check_recipe(self, recipe):
for package in recipe.packages.values():
if package.path_prefix.startswith("outputs"):
if recipe.get(f"{package.path_prefix}build/script", None):
self.message(section=f"{package.path_prefix}build/script")
6 changes: 6 additions & 0 deletions anaconda_linter/lint_names.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,3 +121,9 @@ yaml_load_failure
missing_package_name

missing_package_version

cbc_dep_in_run_missing_from_host

wrong_output_script_key

potentially_bad_ignore_run_exports
9 changes: 9 additions & 0 deletions anaconda_linter/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,15 @@ def ensure_list(obj):
return [obj]


def get_dep_path(recipe, dep):
for n, spec in enumerate(recipe.get(dep.path, [])):
if spec is None: # Fixme: lint this
continue
if spec == dep.raw_dep:
return f"{dep.path}/{n}"
return dep.path


def get_deps_dict(recipe, sections=None, outputs=True):
if not sections:
sections = ("build", "run", "host")
Expand Down
2 changes: 1 addition & 1 deletion environment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ dependencies:
- setuptools
- pip
# run
- percy ==0.0.5
- percy >=0.1.0,<0.2.0
- ruamel.yaml
- license-expression
- jinja2
Expand Down
2 changes: 1 addition & 1 deletion recipe/meta.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ requirements:
- conda-build
- jsonschema
- networkx
- percy ==0.0.5
- percy >=0.1.0,<0.2.0

test:
source_files:
Expand Down
7 changes: 5 additions & 2 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ def recipe_dir(tmpdir):
return recipe_directory


def check(check_name, recipe_str, arch="linux-64"):
def check(check_name, recipe_str, arch="linux-64", expand_variant=None):
config_file = Path(__file__).parent / "config.yaml"
config = utils.load_config(str(config_file.resolve()))
linter = Linter(config=config)
variant = config[arch]
if expand_variant:
variant.update(expand_variant)
recipe = Recipe.from_string(
recipe_text=recipe_str,
variant_id="dummy",
variant=config[arch],
variant=variant,
renderer=RendererType.RUAMEL,
)
messages = linter.check_instances[check_name].run(recipe=recipe)
Expand Down
Loading

0 comments on commit 7180004

Please sign in to comment.