Skip to content

Commit be989f0

Browse files
authored
feat: add linting (#11)
* feat: add linting * test: add test w/ more than one recipe * test: missed some changes
1 parent 2505fa1 commit be989f0

26 files changed

+1313
-0
lines changed

.github/workflows/tests.yml

+2
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ jobs:
6565
- name: run tests
6666
run: |
6767
pytest -vvs tests
68+
env:
69+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6870

6971
- name: ensure cli runs
7072
run: |

conda_forge_feedstock_ops/__main__.py

+35
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,31 @@ def _parse_package_and_feedstock_names():
259259
}
260260

261261

262+
def _lint():
263+
from conda_forge_feedstock_ops.lint import lint
264+
from conda_forge_feedstock_ops.os_utils import sync_dirs
265+
266+
logger = logging.getLogger("conda_forge_feedstock_ops.container")
267+
268+
with tempfile.TemporaryDirectory() as tmpdir:
269+
input_fs_dir = "/cf_feedstock_ops_dir"
270+
logger.debug(
271+
"input container feedstock dir %s: %s",
272+
input_fs_dir,
273+
os.listdir(input_fs_dir),
274+
)
275+
276+
fs_dir = os.path.join(tmpdir, os.path.basename(input_fs_dir))
277+
sync_dirs(input_fs_dir, fs_dir, ignore_dot_git=True, update_git=False)
278+
logger.debug(
279+
"copied container feedstock dir %s: %s", fs_dir, os.listdir(fs_dir)
280+
)
281+
282+
lints, hints = lint(fs_dir, use_container=False)
283+
284+
return {"lints": lints, "hints": hints}
285+
286+
262287
@click.group()
263288
def main_container():
264289
pass
@@ -284,3 +309,13 @@ def main_parse_package_and_feedstock_names(log_level):
284309
log_level=log_level,
285310
existing_feedstock_node_attrs=None,
286311
)
312+
313+
314+
@main_container.command(name="lint")
315+
@log_level_option
316+
def main_lint(log_level):
317+
return _run_bot_task(
318+
_lint,
319+
log_level=log_level,
320+
existing_feedstock_node_attrs=None,
321+
)

conda_forge_feedstock_ops/container_utils.py

+6
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def run_container_operation(
103103
input: Optional[str] = None,
104104
mount_dir: Optional[str] = None,
105105
mount_readonly: bool = True,
106+
extra_container_args: Optional[Iterable[str]] = None,
106107
):
107108
"""Run a feedstock operation in a container.
108109
@@ -120,6 +121,8 @@ def run_container_operation(
120121
The directory to mount to the container at `/cf_feedstock_ops_dir`, by default None.
121122
mount_readonly
122123
Whether to mount the directory as read-only, by default True.
124+
extra_container_args
125+
Extra arguments to pass to the container, by default None.
123126
124127
Returns
125128
-------
@@ -137,9 +140,12 @@ def run_container_operation(
137140
else:
138141
mnt_args = []
139142

143+
extra_container_args = extra_container_args or []
144+
140145
cmd = [
141146
*get_default_container_run_args(tmpfs_size_mb=tmpfs_size_mb),
142147
*mnt_args,
148+
*extra_container_args,
143149
get_default_container_name(),
144150
*args,
145151
]

conda_forge_feedstock_ops/lint.py

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import logging
2+
import os
3+
import shutil
4+
import tempfile
5+
from collections import defaultdict
6+
from pathlib import Path
7+
8+
import conda_smithy.lint_recipe
9+
10+
from conda_forge_feedstock_ops.container_utils import (
11+
get_default_log_level_args,
12+
run_container_operation,
13+
should_use_container,
14+
)
15+
from conda_forge_feedstock_ops.json import loads
16+
from conda_forge_feedstock_ops.os_utils import chmod_plus_rwX, sync_dirs
17+
18+
logger = logging.getLogger(__name__)
19+
20+
21+
def lint(feedstock_dir, use_container=None):
22+
"""Lint all of the recipes in a feedstock.
23+
24+
**WARNING: This function will inject the GH_TOKEN env var into
25+
the container in order to run conda-forge-specific linting.
26+
Make sure that token is read-only.**
27+
28+
Parameters
29+
----------
30+
feedstock_dir : str
31+
The path to the feedstock directory.
32+
use_container
33+
Whether to use a container to run the parse.
34+
If None, the function will use a container if the environment
35+
variable `CF_FEEDSTOCK_OPS_IN_CONTAINER` is 'false'. This feature can be
36+
used to avoid container in container calls.
37+
38+
Returns
39+
-------
40+
lints : dict
41+
Dictionary mapping relative recipe path to its lints.
42+
hints : dict
43+
Dictionary mapping relative recipe path to its hints.
44+
"""
45+
if should_use_container(use_container=use_container):
46+
return _lint_containerized(feedstock_dir)
47+
else:
48+
return _lint_local(feedstock_dir)
49+
50+
51+
def _lint_containerized(feedstock_dir):
52+
args = [
53+
"conda-forge-feedstock-ops-container",
54+
"lint",
55+
] + get_default_log_level_args(logger)
56+
57+
with tempfile.TemporaryDirectory() as tmpdir:
58+
sync_dirs(feedstock_dir, tmpdir, ignore_dot_git=True, update_git=False)
59+
chmod_plus_rwX(tmpdir, recursive=True)
60+
61+
logger.debug(
62+
"host feedstock dir %s: %r",
63+
feedstock_dir,
64+
os.listdir(feedstock_dir),
65+
)
66+
logger.debug(
67+
"copied host feedstock dir %s: %r",
68+
tmpdir,
69+
os.listdir(tmpdir),
70+
)
71+
72+
data = run_container_operation(
73+
args,
74+
mount_readonly=True,
75+
mount_dir=tmpdir,
76+
json_loads=loads,
77+
extra_container_args=["-e", "GH_TOKEN"],
78+
)
79+
80+
# When tempfile removes tempdir, it tries to reset permissions on subdirs.
81+
# This causes a permission error since the subdirs were made by the user
82+
# in the container. So we remove the subdir we made before cleaning up.
83+
shutil.rmtree(tmpdir)
84+
85+
return data["lints"], data["hints"]
86+
87+
88+
#############################################################
89+
# This code is from conda-forge-webservices w/ modifications
90+
91+
92+
def _find_recipes(path: Path) -> list[Path]:
93+
"""Returns all `meta.yaml` and `recipe.yaml` files in the given path."""
94+
meta_yamls = path.rglob("meta.yaml")
95+
recipe_yamls = path.rglob("recipe.yaml")
96+
97+
return sorted(set([x for x in (list(meta_yamls) + list(recipe_yamls))]))
98+
99+
100+
def _lint_local(feedstock_dir):
101+
recipes = _find_recipes(Path(feedstock_dir))
102+
103+
lints = defaultdict(list)
104+
hints = defaultdict(list)
105+
106+
for recipe in recipes:
107+
recipe_dir = recipe.parent
108+
rel_path = str(recipe.relative_to(feedstock_dir))
109+
110+
_lints, _hints = conda_smithy.lint_recipe.main(
111+
str(recipe_dir), conda_forge=True, return_hints=True
112+
)
113+
114+
lints[rel_path] = _lints
115+
hints[rel_path] = _hints
116+
117+
return dict(lints), dict(hints)
118+
119+
120+
# end of code from conda-forge-webservices w/ modifications
121+
#############################################################

tests/conftest.py

+4
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@
4242
not (HAVE_CONTAINERS and HAVE_TEST_IMAGE), reason="containers not available"
4343
)
4444

45+
skipif_no_github_token = pytest.mark.skipif(
46+
"GH_TOKEN" not in os.environ, reason="no GH_TOKEN in environment"
47+
)
48+
4549

4650
@pytest.fixture(autouse=True, scope="session")
4751
def set_cf_feedstock_ops_container_tag_to_test():

tests/data/ngmix-blah/.azure-pipelines/azure-pipelines-linux.yml

+80
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/data/ngmix-blah/.azure-pipelines/azure-pipelines-osx.yml

+64
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/data/ngmix-blah/.ci_support/.keepme

Whitespace-only changes.

tests/data/ngmix-blah/.circleci/config.yml

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)