diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..5f8a2d4a5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Developing conda-smithy + +## Set up Development Environment + +To install conda-smithy from source: + +* Install `conda` +* Fork and clone this repository: `git clone https://github.com/YOUR-USERNAME/conda-smithy.git`. Change to it: `cd conda-smithy`. +* Create a new conda environment with all requirements based on [environment.yml](environment.yml): `conda env create -f environment.yml`. +* Activate the environment: `conda activate conda-smithy`. +* Install conda-smithy: `pip install --no-deps --editable .` + +To run all tests: + +```sh +$ pytest +``` + +To run all code checks: + +```sh +# staged changes +$ pre-commit run +# all files +$ pre-commit run --all-files +``` + +To run pyrefly code checks: + +```sh +$ pyrefly check --python-interpreter-path $(which python) --output-format min-text --count-errors=1 --search-path . +``` + +If you encounter pyrefly issues that you don't agree with, feel free to add a `# pyrefly: ignore[]` comment to that line. +An ignored `pyrefly` error is insufficient reason to block the merge of a pull request. diff --git a/bootstrap-obvious-ci-and-miniconda.py b/bootstrap-obvious-ci-and-miniconda.py index 8b380692c..b42d9b174 100644 --- a/bootstrap-obvious-ci-and-miniconda.py +++ b/bootstrap-obvious-ci-and-miniconda.py @@ -11,11 +11,7 @@ import os import platform import subprocess - -try: - from urllib.request import urlretrieve -except ImportError: - from urllib import urlretrieve +from urllib.request import urlretrieve MINICONDA_URL_TEMPLATE = ( "https://repo.continuum.io/miniconda/Miniconda{major_py_version}-" diff --git a/conda_smithy/anaconda_token_rotation.py b/conda_smithy/anaconda_token_rotation.py index b10772061..4d0dc433f 100644 --- a/conda_smithy/anaconda_token_rotation.py +++ b/conda_smithy/anaconda_token_rotation.py @@ -62,9 +62,6 @@ def rotate_anaconda_token( anaconda_token = _get_anaconda_token() - if github_actions: - gh = Github(gh_token()) - # capture stdout, stderr and suppress all exceptions so we don't # spill tokens failed = False @@ -144,8 +141,7 @@ def rotate_anaconda_token( raise e else: err_msg = ( - f"Failed to rotate token for {user}/{project}" - " on azure!" + f"Failed to rotate token for {user}/{project} on azure!" ) failed = True raise RuntimeError(err_msg) @@ -167,6 +163,7 @@ def rotate_anaconda_token( raise RuntimeError(err_msg) if github_actions: + gh = Github(gh_token()) try: rotate_token_in_github_actions( user, project, anaconda_token, token_name, gh diff --git a/conda_smithy/ci_register.py b/conda_smithy/ci_register.py index 4e5262d47..ddd81480b 100755 --- a/conda_smithy/ci_register.py +++ b/conda_smithy/ci_register.py @@ -84,11 +84,11 @@ class LiveServerSession(requests.Session): """Utility class to avoid typing out urls all the time""" - def __init__(self, prefix_url=None, *args, **kwargs): + def __init__(self, prefix_url: str = "", *args, **kwargs): super().__init__(*args, **kwargs) self.prefix_url = prefix_url - def request(self, method, url, *args, **kwargs): + def request(self, method, url: str, *args, **kwargs): from urllib.parse import urljoin url = urljoin(self.prefix_url, url) diff --git a/conda_smithy/configure_feedstock.py b/conda_smithy/configure_feedstock.py index a50fd6395..fd867a423 100644 --- a/conda_smithy/configure_feedstock.py +++ b/conda_smithy/configure_feedstock.py @@ -68,6 +68,8 @@ validate_json_schema, ) +JSONDecodeError = json.JSONDecodeError + conda_forge_content = os.path.abspath(os.path.dirname(__file__)) logger = logging.getLogger(__name__) @@ -2358,7 +2360,7 @@ def render_readme(jinja_env, forge_config, forge_dir, render_info=None): "Azure build_id can't be retrieved using the Azure token. Exception: %s", err, ) - except json.decoder.JSONDecodeError: + except JSONDecodeError: azure_build_id_from_token(forge_config) logger.debug("README") diff --git a/conda_smithy/deprecations.py b/conda_smithy/deprecations.py index b3449f4ff..0c410aa9d 100644 --- a/conda_smithy/deprecations.py +++ b/conda_smithy/deprecations.py @@ -442,7 +442,8 @@ def _generate_message( raise ValueError( "'deprecate_in' version needs at least three components" ) - next_version = datetime(*deprecate_in_tuple[:3]) + remove_in + year, month, day = deprecate_in_tuple[:3] + next_version = datetime(year, month, day) + remove_in remove_in = f"{next_version.year}.{next_version.month}.{next_version.day}" if self._version_less_than(deprecate_in): category = PendingDeprecationWarning diff --git a/conda_smithy/feedstock_io.py b/conda_smithy/feedstock_io.py index 9299b2524..b9fb2d638 100644 --- a/conda_smithy/feedstock_io.py +++ b/conda_smithy/feedstock_io.py @@ -9,7 +9,10 @@ def get_repo(path, search_parent_directories=True): repo = None try: import pygit2 + except ImportError: + return None + try: if search_parent_directories: path = pygit2.discover_repository(path) if path is not None: @@ -19,8 +22,6 @@ def get_repo(path, search_parent_directories=True): no_search = pygit2.GIT_REPOSITORY_OPEN_NO_SEARCH repo = pygit2.Repository(path, no_search) - except ImportError: - pass except pygit2.GitError: pass @@ -28,10 +29,9 @@ def get_repo(path, search_parent_directories=True): def get_repo_root(path): - try: - return get_repo(path).workdir.rstrip(os.path.sep) - except AttributeError: + if (repo := get_repo(path)) is None: return None + return repo.workdir.rstrip(os.path.sep) def set_exe_file(filename, set_exe=True): diff --git a/conda_smithy/github.py b/conda_smithy/github.py index 7da676cab..b82836e33 100644 --- a/conda_smithy/github.py +++ b/conda_smithy/github.py @@ -598,7 +598,7 @@ def configure_github_team( def configure_github_app( org: str, repo: str, - app_slug_or_installation_id: str | int = None, + app_slug_or_installation_id: str | int, remove: bool = False, ) -> None: """ diff --git a/conda_smithy/linter/hints.py b/conda_smithy/linter/hints.py index 8572b405c..92a26f035 100644 --- a/conda_smithy/linter/hints.py +++ b/conda_smithy/linter/hints.py @@ -70,20 +70,19 @@ def hint_suggest_noarch( ) else: with open(recipe_fname, encoding="utf-8") as fh: - in_runreqs = False + runreqs_spacing = None no_arch_possible = True for line in fh: line_s = line.strip() if line_s == "host:" or line_s == "run:": - in_runreqs = True runreqs_spacing = line[: -len(line.lstrip())] continue if line_s.startswith("skip:") and is_selector_line(line): no_arch_possible = False break - if in_runreqs: + if runreqs_spacing is not None: if runreqs_spacing == line[: -len(line.lstrip())]: - in_runreqs = False + runreqs_spacing = None continue if is_selector_line(line): no_arch_possible = False diff --git a/conda_smithy/linter/lints.py b/conda_smithy/linter/lints.py index b3daa7ab8..b9e816b5a 100644 --- a/conda_smithy/linter/lints.py +++ b/conda_smithy/linter/lints.py @@ -327,11 +327,10 @@ def lint_noarch_and_runtime_dependencies( return noarch_platforms = len(forge_yaml.get("noarch_platforms", [])) > 1 with open(meta_fname, encoding="utf-8") as fh: - in_runreqs = False + runreqs_spacing = None for line_number, line in enumerate(fh, 1): line_s = line.strip() if line_s == "host:" or line_s == "run:": - in_runreqs = True runreqs_spacing = line[: -len(line.lstrip())] continue if line_s.startswith("skip:") and is_selector_line(line): @@ -344,9 +343,9 @@ def lint_noarch_and_runtime_dependencies( ).as_string() ) break - if in_runreqs: + if runreqs_spacing is not None: if runreqs_spacing == line[: -len(line.lstrip())]: - in_runreqs = False + runreqs_spacing = None continue if is_selector_line( line, diff --git a/conda_smithy/plugin.py b/conda_smithy/plugin.py index 886bb9b60..fdf678a23 100644 --- a/conda_smithy/plugin.py +++ b/conda_smithy/plugin.py @@ -6,11 +6,13 @@ from __future__ import annotations +from argparse import Namespace + from conda.plugins import hookimpl from conda.plugins.types import CondaSubcommand -def _execute(args: tuple[str, ...]) -> int | None: +def _execute(args: Namespace | tuple[str, ...]) -> int | None: """Dispatch plugin arguments to the smithy CLI. Lazy import to avoid import-time side effects when not using conda-smithy. diff --git a/conda_smithy/validate_schema.py b/conda_smithy/validate_schema.py index c68edc677..d38e54ed5 100644 --- a/conda_smithy/validate_schema.py +++ b/conda_smithy/validate_schema.py @@ -19,7 +19,7 @@ class DeprecatedValidator: def __init__(self): - self.hints = [] + self.hints: list[str] = [] def __call__(self, validator, value, instance, schema): if value and instance is not None: @@ -38,8 +38,9 @@ def get_validator_class(deprecated_validator): def validate_json_schema( - config, schema_file: str = None -) -> tuple[list[ValidationError], list[ValidationError]]: + config, + schema_file: str | Path | None = None, +) -> tuple[list[ValidationError], list[str]]: # Validate the merged configuration against a JSON schema if not schema_file: schema_file = CONDA_FORGE_YAML_SCHEMA_FILE diff --git a/environment.yml b/environment.yml index 6a7535901..46ff0a54c 100644 --- a/environment.yml +++ b/environment.yml @@ -13,6 +13,7 @@ dependencies: - mock - pytest - pytest-cov + - pyrefly # # Part of some optional lint tests - conda-recipe-manager>=0.9 - conda-souschef diff --git a/news/2593.rst b/news/2593.rst new file mode 100644 index 000000000..ac2dc35a9 --- /dev/null +++ b/news/2593.rst @@ -0,0 +1,3 @@ +**Fixed:** + +* Fixed several small typing issues reported by Pyrefly. (#2593) diff --git a/pyproject.toml b/pyproject.toml index e8d882755..880500615 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,6 +31,10 @@ include-package-data = true write_to = "conda_smithy/_version.py" write_to_template = "__version__ = '{version}'" +[tool.pyrefly] +project-includes = ["conda_smithy/**/*.py", "tests/**/*.py"] +search-path = [".", "tests"] + [tool.black] # matches black's default value line-length = 88 diff --git a/tests/test_configure_feedstock.py b/tests/test_configure_feedstock.py index 298ff5470..c899fb92c 100644 --- a/tests/test_configure_feedstock.py +++ b/tests/test_configure_feedstock.py @@ -1041,9 +1041,8 @@ def load_forge_config(forge_yml): def test_cos7_env_render(py_recipe, jinja_env): forge_config = copy.deepcopy(py_recipe.config) forge_config["os_version"] = {"linux_64": "cos7"} - has_env = "DEFAULT_LINUX_VERSION" in os.environ - if has_env: - old_val = os.environ["DEFAULT_LINUX_VERSION"] + old_val = os.environ.get("DEFAULT_LINUX_VERSION") + if old_val is not None: del os.environ["DEFAULT_LINUX_VERSION"] try: @@ -1065,7 +1064,7 @@ def test_cos7_env_render(py_recipe, jinja_env): assert len(os.listdir(matrix_dir)) == 6 finally: - if has_env: + if old_val is not None: os.environ["DEFAULT_LINUX_VERSION"] = old_val else: if "DEFAULT_LINUX_VERSION" in os.environ: @@ -1074,9 +1073,8 @@ def test_cos7_env_render(py_recipe, jinja_env): def test_cuda_enabled_render(cuda_enabled_recipe, jinja_env): forge_config = copy.deepcopy(cuda_enabled_recipe.config) - has_env = "CF_CUDA_ENABLED" in os.environ - if has_env: - old_val = os.environ["CF_CUDA_ENABLED"] + old_val = os.environ.get("CF_CUDA_ENABLED") + if old_val is not None: del os.environ["CF_CUDA_ENABLED"] try: @@ -1099,7 +1097,7 @@ def test_cuda_enabled_render(cuda_enabled_recipe, jinja_env): assert len(os.listdir(matrix_dir)) == 6 finally: - if has_env: + if old_val is not None: os.environ["CF_CUDA_ENABLED"] = old_val else: if "CF_CUDA_ENABLED" in os.environ: diff --git a/tests/test_lint_recipe.py b/tests/test_lint_recipe.py index b7ef7a3d9..dcabf01f4 100644 --- a/tests/test_lint_recipe.py +++ b/tests/test_lint_recipe.py @@ -9,6 +9,7 @@ import textwrap import unittest from collections import OrderedDict +from collections.abc import Iterator from contextlib import contextmanager from itertools import count from pathlib import Path @@ -29,7 +30,7 @@ @contextmanager -def get_recipe_in_dir(recipe_name: str) -> Path: +def get_recipe_in_dir(recipe_name: str) -> Iterator[Path]: base_dir = Path(__file__).parent recipe_path = base_dir / "recipes" / recipe_name assert recipe_path.exists(), f"Recipe {recipe_name} does not exist"