diff --git a/.github/workflows/cicd_tests.yml b/.github/workflows/cicd_tests.yml index d48e3a02f22..cd67ce0176f 100644 --- a/.github/workflows/cicd_tests.yml +++ b/.github/workflows/cicd_tests.yml @@ -56,7 +56,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - opt: ["codeformat", "mypy"] # "pytype" omitted for being essentially deprecated, see #8865 + opt: ["codeformat", "pyrefly"] steps: - name: Clean unused tools run: | @@ -80,8 +80,7 @@ jobs: run: | # clean up temporary files $(pwd)/runtests.sh --build --clean - # Github actions have multiple cores, so parallelize pytype - $(pwd)/runtests.sh --build --${{ matrix.opt }} -j $(nproc --all) + $(pwd)/runtests.sh --build --${{ matrix.opt }} min-dep: # Test with minumum dependencies installed for different OS, Python, and PyTorch combinations runs-on: ${{ matrix.os }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index 8ff912f5281..1f4a77f34f4 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -214,7 +214,7 @@ jobs: python -c "import torch; print(torch.__version__); print('{} of GPUs available'.format(torch.cuda.device_count()))" python -c 'import torch; print(torch.rand(5,3, device=torch.device("cuda:0")))' ngc --version - BUILD_MONAI=1 ./runtests.sh --build --coverage --unittests --disttests # unit tests with pytype checks, coverage report + BUILD_MONAI=1 ./runtests.sh --build --coverage --pyrefly --unittests --disttests # unit tests with pyrefly checks, coverage report BUILD_MONAI=1 ./runtests.sh --build --coverage --net # integration tests with coverage report coverage xml --ignore-errors if pgrep python; then pkill python; fi diff --git a/.github/workflows/weekly-preview.yml b/.github/workflows/weekly-preview.yml index 7e7349ec931..4f74ff6bd8e 100644 --- a/.github/workflows/weekly-preview.yml +++ b/.github/workflows/weekly-preview.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - opt: ["codeformat", "mypy"] + opt: ["codeformat", "pyrefly"] steps: - name: Clean unused tools run: | diff --git a/.gitignore b/.gitignore index 76c6ab0d124..d0bdc54018d 100644 --- a/.gitignore +++ b/.gitignore @@ -110,12 +110,17 @@ venv.bak/ # pytype cache .pytype/ +# pyrefly cache +.pyrefly_cache/ + # mypy .mypy_cache/ +.dmypy.json + examples/scd_lvsegs.npz temp/ .idea/ -.dmypy.json +.plans/ *~ diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 50f496af3ff..f3fc994d105 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,7 +38,7 @@ Please note that, as per PyTorch, MONAI uses American English spelling. This mea ### Preparing pull requests To ensure the code quality, MONAI relies on several linting tools ([black](https://github.com/psf/black), [isort](https://github.com/timothycrosley/isort), [ruff](https://github.com/astral-sh/ruff)), -static type analysis tools ([mypy](https://github.com/python/mypy), [pytype](https://github.com/google/pytype)), as well as a set of unit/integration tests. +static type analysis tools ([pyrefly](https://github.com/facebook/pyrefly)), as well as a set of unit/integration tests. This section highlights all the necessary preparation steps required before sending a pull request. To collaborate efficiently, please read through this section and follow them. diff --git a/monai/apps/auto3dseg/auto_runner.py b/monai/apps/auto3dseg/auto_runner.py index 8421893f514..37ccf7f19d6 100644 --- a/monai/apps/auto3dseg/auto_runner.py +++ b/monai/apps/auto3dseg/auto_runner.py @@ -790,6 +790,7 @@ def _train_algo_in_nni(self, history: list[dict[str, Any]]) -> None: nni_config_filename = os.path.abspath(os.path.join(self.work_dir, f"{name}_nni_config.yaml")) ConfigParser.export_config_file(nni_config, nni_config_filename, fmt="yaml", default_flow_style=None) + # pyrefly: ignore [redundant-cast] max_trial = min(self.hpo_tasks, cast(int, default_nni_config["maxTrialNumber"])) cmd = "nnictl create --config " + nni_config_filename + " --port 8088" @@ -805,6 +806,7 @@ def _train_algo_in_nni(self, history: list[dict[str, Any]]) -> None: n_trainings = len(import_bundle_algo_history(self.work_dir, only_trained=True)) cmd = "nnictl stop --all" + # pyrefly: ignore [bad-argument-type] run_cmd(cmd.split(), check=True) logger.info(f"NNI completes HPO on {name}") last_total_tasks = n_trainings diff --git a/monai/apps/deepedit/transforms.py b/monai/apps/deepedit/transforms.py index d2f89d2eea7..d15b2bec3cf 100644 --- a/monai/apps/deepedit/transforms.py +++ b/monai/apps/deepedit/transforms.py @@ -434,6 +434,7 @@ def _randomize(self, d, key_label): else: logger.info(f"Not slice IDs for label: {key_label}") sid = None + # pyrefly: ignore [unsupported-operation] self.sid[key_label] = sid def __call__(self, data: Mapping[Hashable, np.ndarray]) -> dict[Hashable, np.ndarray]: @@ -561,6 +562,7 @@ def __init__( self.guidance: dict[str, list[list[int]]] = {} def randomize(self, data=None): + # pyrefly: ignore [unsupported-operation] probability = data[self.probability] self._will_interact = self.R.choice([True, False], p=[probability, 1.0 - probability]) @@ -885,6 +887,7 @@ def _randomize(self, d, key_label): else: logger.info(f"Not slice IDs for label: {key_label}") sid = None + # pyrefly: ignore [unsupported-operation] self.sid[key_label] = sid def __call__(self, data: Mapping[Hashable, np.ndarray]) -> dict[Hashable, np.ndarray]: diff --git a/monai/apps/deepgrow/dataset.py b/monai/apps/deepgrow/dataset.py index e597188e745..0d1c11b1193 100644 --- a/monai/apps/deepgrow/dataset.py +++ b/monai/apps/deepgrow/dataset.py @@ -175,6 +175,7 @@ def _save_data_2d(vol_idx, vol_image, vol_label, dataset_dir, relative_path): continue # For all Labels + # pyrefly: ignore [missing-attribute] unique_labels = np.unique(label.flatten()) unique_labels = unique_labels[unique_labels != 0] unique_labels_count = max(unique_labels_count, len(unique_labels)) diff --git a/monai/apps/deepgrow/transforms.py b/monai/apps/deepgrow/transforms.py index d92a79a16a5..624eed342da 100644 --- a/monai/apps/deepgrow/transforms.py +++ b/monai/apps/deepgrow/transforms.py @@ -288,6 +288,7 @@ def __init__(self, guidance: str = "guidance", discrepancy: str = "discrepancy", self._will_interact = None def randomize(self, data=None): + # pyrefly: ignore [unsupported-operation] probability = data[self.probability] self._will_interact = self.R.choice([True, False], p=[probability, 1.0 - probability]) diff --git a/monai/apps/detection/networks/retinanet_detector.py b/monai/apps/detection/networks/retinanet_detector.py index 95b29b8285c..9b9bf269113 100644 --- a/monai/apps/detection/networks/retinanet_detector.py +++ b/monai/apps/detection/networks/retinanet_detector.py @@ -525,6 +525,7 @@ def forward( ) # 4. Generate anchors and store it in self.anchors: List[Tensor] + # pyrefly: ignore [bad-argument-type] self.generate_anchors(images, head_outputs) # num_anchor_locs_per_level: List[int], list of HW or HWD for each level num_anchor_locs_per_level = [x.shape[2:].numel() for x in head_outputs[self.cls_key]] @@ -535,6 +536,7 @@ def forward( # reshape to Tensor sized(B, sum(HWA), self.num_classes) for self.cls_key # or (B, sum(HWA), 2* self.spatial_dims) for self.box_reg_key # A = self.num_anchors_per_loc + # pyrefly: ignore [bad-argument-type] head_outputs[key] = self._reshape_maps(head_outputs[key]) # 6(1). If during training, return losses diff --git a/monai/apps/detection/transforms/array.py b/monai/apps/detection/transforms/array.py index 301a636b6cc..635506c08ab 100644 --- a/monai/apps/detection/transforms/array.py +++ b/monai/apps/detection/transforms/array.py @@ -257,10 +257,14 @@ def __call__(self, boxes: NdarrayTensor, src_spatial_size: Sequence[int] | int | diff = od - zd half = abs(diff) // 2 if diff > 0: # need padding (half, diff - half) + # pyrefly: ignore [bad-index, unsupported-operation] zoomed_boxes[:, axis] = zoomed_boxes[:, axis] + half + # pyrefly: ignore [bad-index, unsupported-operation] zoomed_boxes[:, axis + spatial_dims] = zoomed_boxes[:, axis + spatial_dims] + half elif diff < 0: # need slicing (half, half + od) + # pyrefly: ignore [bad-index, unsupported-operation] zoomed_boxes[:, axis] = zoomed_boxes[:, axis] - half + # pyrefly: ignore [bad-index, unsupported-operation] zoomed_boxes[:, axis + spatial_dims] = zoomed_boxes[:, axis + spatial_dims] - half return zoomed_boxes diff --git a/monai/apps/detection/transforms/box_ops.py b/monai/apps/detection/transforms/box_ops.py index fa714daad12..54bbc8fd31b 100644 --- a/monai/apps/detection/transforms/box_ops.py +++ b/monai/apps/detection/transforms/box_ops.py @@ -186,7 +186,9 @@ def flip_boxes( _flip_boxes: NdarrayTensor = boxes.clone() if isinstance(boxes, torch.Tensor) else deepcopy(boxes) # type: ignore[assignment] for axis in flip_axes: + # pyrefly: ignore [bad-index, unsupported-operation] _flip_boxes[:, axis + spatial_dims] = spatial_size[axis] - boxes[:, axis] - TO_REMOVE + # pyrefly: ignore [bad-index, unsupported-operation] _flip_boxes[:, axis] = spatial_size[axis] - boxes[:, axis + spatial_dims] - TO_REMOVE return _flip_boxes diff --git a/monai/apps/generation/maisi/networks/autoencoderkl_maisi.py b/monai/apps/generation/maisi/networks/autoencoderkl_maisi.py index 39f84592246..90ffff6c340 100644 --- a/monai/apps/generation/maisi/networks/autoencoderkl_maisi.py +++ b/monai/apps/generation/maisi/networks/autoencoderkl_maisi.py @@ -246,7 +246,9 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: # update padding length if necessary padding = 3 + # pyrefly: ignore [unsupported-operation] if padding % self.stride > 0: + # pyrefly: ignore [unsupported-operation] padding = (padding // self.stride + 1) * self.stride if self.print_info: logger.info(f"Padding size: {padding}") diff --git a/monai/apps/nnunet/nnunetv2_runner.py b/monai/apps/nnunet/nnunetv2_runner.py index 5d5c82801ac..c18e7bcd055 100644 --- a/monai/apps/nnunet/nnunetv2_runner.py +++ b/monai/apps/nnunet/nnunetv2_runner.py @@ -274,6 +274,7 @@ def convert_dataset(self): modality = [modality] create_new_dataset_json( + # pyrefly: ignore [bad-argument-type] modality=modality, num_foreground_classes=num_foreground_classes, num_input_channels=num_input_channels, diff --git a/monai/apps/pathology/transforms/post/dictionary.py b/monai/apps/pathology/transforms/post/dictionary.py index 1e2540daee0..b1dde44ec1f 100644 --- a/monai/apps/pathology/transforms/post/dictionary.py +++ b/monai/apps/pathology/transforms/post/dictionary.py @@ -403,6 +403,7 @@ def __call__(self, data): d = dict(data) for key in self.key_iterator(d): offset = d[self.offset_key] if self.offset_key else None + # pyrefly: ignore [bad-argument-type] centroid = self.converter(d[key], offset) key_to_add = f"{key}_{self.centroid_key_postfix}" if key_to_add in d: diff --git a/monai/auto3dseg/operations.py b/monai/auto3dseg/operations.py index 404a6d326ee..7b64a04a6cd 100644 --- a/monai/auto3dseg/operations.py +++ b/monai/auto3dseg/operations.py @@ -149,4 +149,5 @@ def evaluate(self, data: Any, **kwargs: Any) -> dict: Args: data: input data """ + # pyrefly: ignore [missing-attribute] return {k: v(data[k], **kwargs).tolist() for k, v in self.data.items() if (callable(v) and k in data)} diff --git a/monai/auto3dseg/seg_summarizer.py b/monai/auto3dseg/seg_summarizer.py index 14a10635df2..8fdd9652455 100644 --- a/monai/auto3dseg/seg_summarizer.py +++ b/monai/auto3dseg/seg_summarizer.py @@ -208,6 +208,7 @@ def summarize(self, data: list[dict]) -> dict[str, dict]: for analyzer in self.summary_analyzers: if callable(analyzer): + # pyrefly: ignore [missing-attribute] report.update({analyzer.stats_name: analyzer(data)}) return report diff --git a/monai/bundle/reference_resolver.py b/monai/bundle/reference_resolver.py index b55c62174b7..27f34ebd5eb 100644 --- a/monai/bundle/reference_resolver.py +++ b/monai/bundle/reference_resolver.py @@ -254,6 +254,7 @@ def iter_subconfigs(cls, id: str, config: Any) -> Iterator[tuple[str, str, Any]] """ for k, v in config.items() if isinstance(config, dict) else enumerate(config): sub_id = f"{id}{cls.sep}{k}" if id != "" else f"{k}" + # pyrefly: ignore [invalid-yield] yield k, sub_id, v @classmethod diff --git a/monai/bundle/scripts.py b/monai/bundle/scripts.py index 63a774bfea5..00d54e4440b 100644 --- a/monai/bundle/scripts.py +++ b/monai/bundle/scripts.py @@ -1966,6 +1966,7 @@ def create_workflow( ) if config_file is not None: + # pyrefly: ignore [unexpected-keyword] workflow_ = workflow_class(config_file=config_file, **_args) else: workflow_ = workflow_class(**_args) diff --git a/monai/bundle/workflows.py b/monai/bundle/workflows.py index 5b95441d51f..0367047b58b 100644 --- a/monai/bundle/workflows.py +++ b/monai/bundle/workflows.py @@ -224,6 +224,7 @@ def add_property(self, name: str, required: str, desc: str | None = None) -> Non desc: descriptions for the property. """ if self.properties is None: + # pyrefly: ignore [bad-assignment] self.properties = {} if name in self.properties: logger.warning(f"property '{name}' already exists in the properties list, overriding it.") @@ -329,6 +330,7 @@ def _get_property(self, name: str, property: dict) -> Any: elif name in self._props_vals: value = self._props_vals[name] elif name in self.parser.config[self.parser.meta_key]: # type: ignore[index] + # pyrefly: ignore [missing-attribute] id = self.properties.get(name, None).get(BundlePropertyConfig.ID, None) value = self.parser[id] else: @@ -621,6 +623,7 @@ def _check_optional_id(self, name: str, property: dict) -> bool: else: ref = self.parser.get(ref_id, None) # for reference IDs that not refer to a property directly but using expressions, skip the check + # pyrefly: ignore [unsupported-operation] if ref is not None and not ref.startswith(EXPR_KEY) and ref != ID_REF_KEY + id: return False return True diff --git a/monai/data/dataset.py b/monai/data/dataset.py index f07699594e3..62aced937dd 100644 --- a/monai/data/dataset.py +++ b/monai/data/dataset.py @@ -1648,6 +1648,7 @@ def _cachecheck(self, item_transformed): item_k = kvikio_numpy.fromfile( f"{hashfile}-{k}-{i}", dtype=meta_i_k["dtype"], like=cp.empty(()) ) + # pyrefly: ignore [missing-attribute] item_k = convert_to_tensor(item[i].reshape(meta_i_k["shape"]), device=f"cuda:{self.device}") item[i].update({k: item_k, f"{k}_meta_dict": meta_i_k}) return item diff --git a/monai/data/grid_dataset.py b/monai/data/grid_dataset.py index 689138179ae..a4860cfcad5 100644 --- a/monai/data/grid_dataset.py +++ b/monai/data/grid_dataset.py @@ -142,6 +142,7 @@ def __call__( self, data: Mapping[Hashable, NdarrayTensor] ) -> Generator[tuple[Mapping[Hashable, NdarrayTensor], np.ndarray], None, None]: d = dict(data) + # pyrefly: ignore [missing-attribute] original_spatial_shape = d[first(self.keys)].shape[1:] for patch in zip(*[self.patch_iter(d[key]) for key in self.keys]): diff --git a/monai/data/image_reader.py b/monai/data/image_reader.py index 4e8f32be306..27ac4d82878 100644 --- a/monai/data/image_reader.py +++ b/monai/data/image_reader.py @@ -22,7 +22,7 @@ from collections.abc import Callable, Iterable, Iterator, Sequence from dataclasses import dataclass from pathlib import Path -from typing import TYPE_CHECKING, Any, TypeAlias +from typing import TYPE_CHECKING, Any, TypeAlias # pyrefly: ignore [missing-module-attribute] import numpy as np from torch.utils.data._utils.collate import np_str_obj_array_pattern diff --git a/monai/data/wsi_datasets.py b/monai/data/wsi_datasets.py index 2ee8c9d3634..b1830018358 100644 --- a/monai/data/wsi_datasets.py +++ b/monai/data/wsi_datasets.py @@ -250,8 +250,10 @@ def __init__( self.offset_limits = None elif isinstance(offset_limits, tuple): if isinstance(offset_limits[0], int): + # pyrefly: ignore [bad-assignment] self.offset_limits = (offset_limits, offset_limits) elif isinstance(offset_limits[0], tuple): + # pyrefly: ignore [bad-assignment] self.offset_limits = offset_limits else: raise ValueError( diff --git a/monai/data/wsi_reader.py b/monai/data/wsi_reader.py index b377234d10d..8a465c11979 100644 --- a/monai/data/wsi_reader.py +++ b/monai/data/wsi_reader.py @@ -319,6 +319,7 @@ def _get_metadata( } return metadata + # pyrefly: ignore [bad-override] def get_data( self, wsi, diff --git a/monai/engines/evaluator.py b/monai/engines/evaluator.py index 62d5f838477..2748ca34508 100644 --- a/monai/engines/evaluator.py +++ b/monai/engines/evaluator.py @@ -489,11 +489,13 @@ def _iteration(self, engine: EnsembleEvaluator, batchdata: dict[str, torch.Tenso if engine.amp: with torch.autocast("cuda", **engine.amp_kwargs): if isinstance(engine.state.output, dict): + # pyrefly: ignore [no-matching-overload] engine.state.output.update( {engine.pred_keys[idx]: engine.inferer(inputs, network, *args, **kwargs)} ) else: if isinstance(engine.state.output, dict): + # pyrefly: ignore [no-matching-overload] engine.state.output.update( {engine.pred_keys[idx]: engine.inferer(inputs, network, *args, **kwargs)} ) diff --git a/monai/engines/trainer.py b/monai/engines/trainer.py index 921d54a59cb..1f0c75620fa 100644 --- a/monai/engines/trainer.py +++ b/monai/engines/trainer.py @@ -774,4 +774,5 @@ def _compute_discriminator_loss() -> None: engine.state.output[AdversarialKeys.DISCRIMINATOR_LOSS].backward() engine.state.d_optimizer.step() + # pyrefly: ignore [bad-return] return engine.state.output diff --git a/monai/fl/client/monai_algo.py b/monai/fl/client/monai_algo.py index 6e9a6fd1fe2..2c2fb872274 100644 --- a/monai/fl/client/monai_algo.py +++ b/monai/fl/client/monai_algo.py @@ -251,6 +251,7 @@ def _get_data_key_stats(self, data, data_key, hist_bins, hist_range, output_path dataroot=self.workflow.dataset_dir, # type: ignore hist_bins=hist_bins, hist_range=hist_range, + # pyrefly: ignore [bad-argument-type] output_path=output_path, histogram_only=self.histogram_only, ) diff --git a/monai/handlers/mlflow_handler.py b/monai/handlers/mlflow_handler.py index 3078d89f97c..2ea54cc06ab 100644 --- a/monai/handlers/mlflow_handler.py +++ b/monai/handlers/mlflow_handler.py @@ -234,6 +234,7 @@ def start(self, engine: Engine) -> None: self._log_params(attrs) if self.dataset_logger: + # pyrefly: ignore [bad-argument-type] self.dataset_logger(self.dataset_dict) else: self._default_dataset_log(self.dataset_dict) @@ -257,6 +258,7 @@ def _set_experiment(self): else: raise e + # pyrefly: ignore [missing-attribute] if experiment.lifecycle_stage != mlflow.entities.LifecycleStage.ACTIVE: raise ValueError(f"Cannot set a deleted experiment '{self.experiment_name}' as the active experiment") self.experiment = experiment diff --git a/monai/inferers/inferer.py b/monai/inferers/inferer.py index ee94b1ebdbe..bc55cef15ca 100644 --- a/monai/inferers/inferer.py +++ b/monai/inferers/inferer.py @@ -841,6 +841,7 @@ def network_wrapper( if isinstance(out, Mapping): for k in out.keys(): + # pyrefly: ignore [unsupported-operation] out[k] = out[k].unsqueeze(dim=self.spatial_dim + 2) return out diff --git a/monai/losses/image_dissimilarity.py b/monai/losses/image_dissimilarity.py index 195ac32b1ff..2b39ab57634 100644 --- a/monai/losses/image_dissimilarity.py +++ b/monai/losses/image_dissimilarity.py @@ -241,6 +241,7 @@ def __init__( if self.kernel_type == "gaussian": self.register_buffer("preterm", 1 / (2 * sigma**2), persistent=False) self.register_buffer("bin_centers", bin_centers[None, None, ...], persistent=False) + self.smooth_nr = float(smooth_nr) self.smooth_dr = float(smooth_dr) diff --git a/monai/metrics/utils.py b/monai/metrics/utils.py index 443be953b07..a5927a0a5be 100644 --- a/monai/metrics/utils.py +++ b/monai/metrics/utils.py @@ -217,6 +217,7 @@ def get_mask_edges( or_vol = seg_pred | seg_gt if not or_vol.any(): pred, gt = lib.zeros(seg_pred.shape, dtype=bool), lib.zeros(seg_gt.shape, dtype=bool) + # pyrefly: ignore [bad-return] return (pred, gt) if spacing is None else (pred, gt, pred, gt) channel_first = [seg_pred[None], seg_gt[None], or_vol[None]] if spacing is None and not use_cucim: # cpu only erosion diff --git a/monai/networks/blocks/hyena.py b/monai/networks/blocks/hyena.py index e056162e8c5..51277cb32f8 100644 --- a/monai/networks/blocks/hyena.py +++ b/monai/networks/blocks/hyena.py @@ -98,21 +98,21 @@ class _DepthwiseFFTForward: _spatial_dims: int # set by subclasses fft_chunk_size: int = 0 # 0 = no chunking; set in subclass __init__ - def forward(self, x: torch.Tensor) -> torch.Tensor: - spatial = x.shape[2:] + def forward(self, input: torch.Tensor) -> torch.Tensor: + spatial = input.shape[2:] kernel_shape = self.weight.shape[2:] # type: ignore[attr-defined] fft_dims = tuple(range(-self._spatial_dims, 0)) fft_size = [s + k - 1 for s, k in zip(spatial, kernel_shape)] - in_dtype = x.dtype + in_dtype = input.dtype slices = (slice(None), slice(None)) + tuple(slice(k // 2, k // 2 + s) for s, k in zip(spatial, kernel_shape)) chunk = getattr(self, "fft_chunk_size", 0) - if chunk > 0 and x.shape[1] > chunk: + if chunk > 0 and input.shape[1] > chunk: parts = [] - for c0 in range(0, x.shape[1], chunk): - c1 = min(c0 + chunk, x.shape[1]) - xc = x[:, c0:c1].float() + for c0 in range(0, input.shape[1], chunk): + c1 = min(c0 + chunk, input.shape[1]) + xc = input[:, c0:c1].float() kc = self.weight[c0:c1].squeeze(1).float() # type: ignore[attr-defined] kc = kc.flip(list(range(1, self._spatial_dims + 1))) xc_fft = torch.fft.rfftn(xc, s=fft_size, dim=fft_dims) @@ -125,7 +125,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor: del out_c return torch.cat(parts, dim=1) - x_f32 = x.float() + x_f32 = input.float() k_f32 = self.weight.squeeze(1).float() # type: ignore[attr-defined] # PyTorch ``F.conv*`` computes cross-correlation; FFT computes convolution. # Flip the kernel so the FFT output matches ``Conv{2,3}d`` exactly. diff --git a/monai/networks/nets/flexible_unet.py b/monai/networks/nets/flexible_unet.py index c27b0fc17b9..bc244ba6e7a 100644 --- a/monai/networks/nets/flexible_unet.py +++ b/monai/networks/nets/flexible_unet.py @@ -61,9 +61,13 @@ def register_class(self, name: type[Any] | str): "or implement all interfaces specified by it." ) + # pyrefly: ignore [missing-attribute] name_string_list = name.get_encoder_names() + # pyrefly: ignore [missing-attribute] feature_number_list = name.num_outputs() + # pyrefly: ignore [missing-attribute] feature_channel_list = name.num_channels_per_output() + # pyrefly: ignore [missing-attribute] parameter_list = name.get_encoder_parameters() assert len(name_string_list) == len(feature_number_list) == len(feature_channel_list) == len(parameter_list) diff --git a/monai/networks/nets/milmodel.py b/monai/networks/nets/milmodel.py index a31f1051106..a0f27008da0 100644 --- a/monai/networks/nets/milmodel.py +++ b/monai/networks/nets/milmodel.py @@ -160,6 +160,7 @@ def hook(module, input, output): ] ) self.transformer = transformer_list + # pyrefly: ignore [unsupported-operation] nfc = nfc + 256 self.attention = nn.Sequential(nn.Linear(nfc, 2048), nn.Tanh(), nn.Linear(2048, 1)) diff --git a/monai/networks/nets/transchex.py b/monai/networks/nets/transchex.py index 6c40cae2aa3..dfe71dd4bad 100644 --- a/monai/networks/nets/transchex.py +++ b/monai/networks/nets/transchex.py @@ -74,6 +74,7 @@ def from_pretrained( return load_tf_weights_in_bert(model, weights_path) old_keys = [] new_keys = [] + # pyrefly: ignore [missing-attribute] for key in state_dict.keys(): new_key = None if "gamma" in key: @@ -84,11 +85,13 @@ def from_pretrained( old_keys.append(key) new_keys.append(new_key) for old_key, new_key in zip(old_keys, new_keys): + # pyrefly: ignore [missing-attribute, unsupported-operation] state_dict[new_key] = state_dict.pop(old_key) missing_keys: list = [] unexpected_keys: list = [] error_msgs: list = [] metadata = getattr(state_dict, "_metadata", None) + # pyrefly: ignore [missing-attribute] state_dict = state_dict.copy() if metadata is not None: state_dict._metadata = metadata diff --git a/monai/networks/nets/vqvae.py b/monai/networks/nets/vqvae.py index 43ba48585c2..690bf48de08 100644 --- a/monai/networks/nets/vqvae.py +++ b/monai/networks/nets/vqvae.py @@ -361,18 +361,22 @@ def __init__( else: downsample_parameters_tuple = downsample_parameters + # pyrefly: ignore [not-iterable] if not all(all(isinstance(value, int) for value in sub_item) for sub_item in downsample_parameters_tuple): raise ValueError("`downsample_parameters` should be a single tuple of integer or a tuple of tuples.") # check if downsample_parameters is a tuple of ints or a tuple of tuples of ints + # pyrefly: ignore [not-iterable] if not all(all(isinstance(value, int) for value in sub_item) for sub_item in upsample_parameters_tuple): raise ValueError("`upsample_parameters` should be a single tuple of integer or a tuple of tuples.") for parameter in downsample_parameters_tuple: + # pyrefly: ignore [bad-argument-type] if len(parameter) != 4: raise ValueError("`downsample_parameters` should be a tuple of tuples with 4 integers.") for parameter in upsample_parameters_tuple: + # pyrefly: ignore [bad-argument-type] if len(parameter) != 5: raise ValueError("`upsample_parameters` should be a tuple of tuples with 5 integers.") @@ -396,6 +400,7 @@ def __init__( channels=channels, num_res_layers=num_res_layers, num_res_channels=num_res_channels, + # pyrefly: ignore [bad-argument-type] downsample_parameters=downsample_parameters_tuple, dropout=dropout, act=act, @@ -408,6 +413,7 @@ def __init__( channels=channels, num_res_layers=num_res_layers, num_res_channels=num_res_channels, + # pyrefly: ignore [bad-argument-type] upsample_parameters=upsample_parameters_tuple, dropout=dropout, act=act, diff --git a/monai/networks/utils.py b/monai/networks/utils.py index f56c39dcd18..61d63544b08 100644 --- a/monai/networks/utils.py +++ b/monai/networks/utils.py @@ -601,6 +601,7 @@ def copy_model_state( dst_dict[dst_key] = val updated_keys.append(dst_key) for s in mapping if mapping else {}: + # pyrefly: ignore [unsupported-operation] dst_key = f"{dst_prefix}{mapping[s]}" if dst_key in dst_dict and dst_key not in to_skip: if dst_dict[dst_key].shape != src_dict[s].shape: diff --git a/monai/optimizers/novograd.py b/monai/optimizers/novograd.py index 9ca612fc564..5d3c5504e17 100644 --- a/monai/optimizers/novograd.py +++ b/monai/optimizers/novograd.py @@ -112,6 +112,7 @@ def step(self, closure: Callable[[], T] | None = None) -> T | None: # type: ign norm = torch.sum(torch.pow(grad, 2)) if exp_avg_sq == 0: + # pyrefly: ignore [missing-attribute] exp_avg_sq.copy_(norm) else: exp_avg_sq.mul_(beta2).add_(norm, alpha=1 - beta2) diff --git a/monai/transforms/croppad/array.py b/monai/transforms/croppad/array.py index 4b18c74b2d4..96175a14998 100644 --- a/monai/transforms/croppad/array.py +++ b/monai/transforms/croppad/array.py @@ -1130,6 +1130,7 @@ def __init__( self.bg_indices = bg_indices self.allow_smaller = allow_smaller + # pyrefly: ignore [bad-override] def randomize( self, label: torch.Tensor | None = None, @@ -1319,6 +1320,7 @@ def __init__( self.warn = warn self.max_samples_per_class = max_samples_per_class + # pyrefly: ignore [bad-override] def randomize( self, label: torch.Tensor | None = None, diff --git a/monai/transforms/croppad/dictionary.py b/monai/transforms/croppad/dictionary.py index 7c82fe065ba..d089cea457b 100644 --- a/monai/transforms/croppad/dictionary.py +++ b/monai/transforms/croppad/dictionary.py @@ -19,7 +19,7 @@ from collections.abc import Callable, Hashable, Mapping, Sequence from copy import deepcopy -from typing import Any, TypeAlias, cast +from typing import Any, TypeAlias, cast # pyrefly: ignore [missing-module-attribute] import numpy as np import torch @@ -1104,6 +1104,7 @@ def set_random_state( self.cropper.set_random_state(seed, state) return self + # pyrefly: ignore [bad-override] def randomize( self, label: torch.Tensor | None = None, @@ -1266,6 +1267,7 @@ def set_random_state( self.cropper.set_random_state(seed, state) return self + # pyrefly: ignore [bad-override] def randomize( self, label: torch.Tensor, indices: list[NdarrayOrTensor] | None = None, image: torch.Tensor | None = None ) -> None: diff --git a/monai/transforms/lazy/utils.py b/monai/transforms/lazy/utils.py index 75f1e3529d0..1f8506031a9 100644 --- a/monai/transforms/lazy/utils.py +++ b/monai/transforms/lazy/utils.py @@ -228,11 +228,13 @@ def resample(data: torch.Tensor, matrix: NdarrayOrTensor, kwargs: dict | None = img.affine = call_kwargs["dst_affine"] img = img.to(torch.float32) # consistent with monai.transforms.spatial.functional.spatial_resample return img + # pyrefly: ignore [bad-argument-type, implicit-import] img = monai.transforms.crop_or_pad_nd(img, matrix_np, out_spatial_size, mode=call_kwargs["padding_mode"]) img = img.to(torch.float32) # consistent with monai.transforms.spatial.functional.spatial_resample img.affine = call_kwargs["dst_affine"] return img + # pyrefly: ignore [bad-argument-type, implicit-import] resampler = monai.transforms.SpatialResample(**init_kwargs) resampler.lazy = False # resampler is a lazytransform with resampler.trace_transform(False): # don't track this transform in `img` diff --git a/monai/transforms/transform.py b/monai/transforms/transform.py index 40f95d47d61..624d1f48e08 100644 --- a/monai/transforms/transform.py +++ b/monai/transforms/transform.py @@ -93,8 +93,10 @@ def _apply_transform( data = apply_pending_transforms_in_order(transform, data, lazy, overrides, logger_name) if isinstance(data, tuple) and unpack_parameters: + # pyrefly: ignore [not-callable] return transform(*data, lazy=lazy) if isinstance(transform, LazyTrait) else transform(*data) + # pyrefly: ignore [not-callable] return transform(data, lazy=lazy) if isinstance(transform, LazyTrait) else transform(data) diff --git a/monai/transforms/utility/dictionary.py b/monai/transforms/utility/dictionary.py index 3deb2f84962..4edf2ce45f4 100644 --- a/monai/transforms/utility/dictionary.py +++ b/monai/transforms/utility/dictionary.py @@ -759,6 +759,7 @@ def __call__(self, data): sub_keys = d[key].keys() if self.sub_keys is None else self.sub_keys # move all the sub-keys to the top level + # pyrefly: ignore [not-iterable] for sk in sub_keys: # set the top-level key for the sub-key sk_top = f"{self.prefix}_{sk}" if self.prefix else sk diff --git a/monai/transforms/utils.py b/monai/transforms/utils.py index 377d2b60e9d..0b8a65b0fb3 100644 --- a/monai/transforms/utils.py +++ b/monai/transforms/utils.py @@ -1284,10 +1284,14 @@ def keep_merge_components_with_points( features_neg, _ = label(img_neg_, connectivity=3, return_num=True) outs = np.zeros_like(img_pos_) + # pyrefly: ignore [missing-attribute] for bs in range(point_coords.shape[0]): + # pyrefly: ignore [bad-index] for i, p in enumerate(point_coords[bs]): + # pyrefly: ignore [bad-index] if point_labels[bs, i] in pos_val: features = features_pos + # pyrefly: ignore [bad-index] elif point_labels[bs, i] in neg_val: features = features_neg else: @@ -1495,8 +1499,10 @@ def remove_small_objects( raise RuntimeError("Skimage required.") if by_measure: + # pyrefly: ignore [missing-attribute] sr = len(img.shape[1:]) if isinstance(img, monai.data.MetaTensor): + # pyrefly: ignore [missing-attribute] _pixdim = img.pixdim elif pixdim is not None: _pixdim = ensure_tuple_rep(pixdim, sr) diff --git a/monai/transforms/utils_pytorch_numpy_unification.py b/monai/transforms/utils_pytorch_numpy_unification.py index 1bc9c206d84..db6ebc26e61 100644 --- a/monai/transforms/utils_pytorch_numpy_unification.py +++ b/monai/transforms/utils_pytorch_numpy_unification.py @@ -478,6 +478,7 @@ def max(x: NdarrayTensor, dim: int | tuple | None = None, **kwargs) -> NdarrayTe else: ret = torch.max(x, int(dim), **kwargs) # type: ignore + # pyrefly: ignore [bad-index] return ret[0] if isinstance(ret, tuple) else ret @@ -544,6 +545,7 @@ def min(x: NdarrayTensor, dim: int | tuple | None = None, **kwargs) -> NdarrayTe else: ret = torch.min(x, int(dim), **kwargs) # type: ignore + # pyrefly: ignore [bad-index] return ret[0] if isinstance(ret, tuple) else ret diff --git a/monai/utils/dist.py b/monai/utils/dist.py index 47da2bee6ed..9c321e464e2 100644 --- a/monai/utils/dist.py +++ b/monai/utils/dist.py @@ -197,5 +197,6 @@ def __init__(self, rank: int | None = None, filter_fn: Callable = lambda rank: r ) self.rank = 0 + # pyrefly: ignore [bad-override] def filter(self, *_args): return self.filter_fn(self.rank) diff --git a/monai/utils/enums.py b/monai/utils/enums.py index be00b27d73e..7be796e6b87 100644 --- a/monai/utils/enums.py +++ b/monai/utils/enums.py @@ -390,6 +390,7 @@ def orig_meta(key: str | None = None) -> str: @staticmethod def transforms(key: str | None = None) -> str: + # pyrefly: ignore [unsupported-operation] return PostFix._get_str(key, TraceKeys.KEY_SUFFIX[1:]) diff --git a/monai/utils/misc.py b/monai/utils/misc.py index ed48d4b37d7..10f9c774439 100644 --- a/monai/utils/misc.py +++ b/monai/utils/misc.py @@ -919,11 +919,13 @@ def is_sqrt(num: Sequence[int] | int) -> bool: def unsqueeze_right(arr: NT, ndim: int) -> NT: """Append 1-sized dimensions to `arr` to create a result with `ndim` dimensions.""" + # pyrefly: ignore [bad-index, missing-attribute] return arr[(...,) + (None,) * (ndim - arr.ndim)] def unsqueeze_left(arr: NT, ndim: int) -> NT: """Prepend 1-sized dimensions to `arr` to create a result with `ndim` dimensions.""" + # pyrefly: ignore [bad-index, missing-attribute] return arr[(None,) * (ndim - arr.ndim)] diff --git a/monai/utils/type_conversion.py b/monai/utils/type_conversion.py index b5dfb580c5a..2d7374c9d03 100644 --- a/monai/utils/type_conversion.py +++ b/monai/utils/type_conversion.py @@ -333,10 +333,12 @@ def convert_data_type( orig_device = data.device if isinstance(data, torch.Tensor) else None + # pyrefly: ignore [bad-assignment] output_type = output_type or orig_type dtype_ = get_equivalent_dtype(dtype, output_type) data_: NdarrayTensor + # pyrefly: ignore [bad-argument-type] if issubclass(output_type, torch.Tensor): track_meta = issubclass(output_type, monai.data.MetaTensor) data_ = convert_to_tensor( diff --git a/monai/visualize/img2tensorboard.py b/monai/visualize/img2tensorboard.py index 30fd4560433..a46b725113d 100644 --- a/monai/visualize/img2tensorboard.py +++ b/monai/visualize/img2tensorboard.py @@ -172,6 +172,7 @@ def plot_2d_or_3d_image( max_frames: if plot 3D RGB image as video in TensorBoardX, set the FPS to `max_frames`. tag: tag of the plotted image on TensorBoard. """ + # pyrefly: ignore [bad-index] data_index = data[index] # as the `d` data has no batch dim, reduce the spatial dim index if positive frame_dim = frame_dim - 1 if frame_dim > 0 else frame_dim diff --git a/monai/visualize/visualizer.py b/monai/visualize/visualizer.py index 023e4444062..1f7c7e3eda0 100644 --- a/monai/visualize/visualizer.py +++ b/monai/visualize/visualizer.py @@ -11,7 +11,7 @@ from __future__ import annotations -from collections.abc import Callable, Sized +from collections.abc import Callable, Sequence import torch import torch.nn.functional as F @@ -21,7 +21,9 @@ __all__ = ["default_upsampler"] -def default_upsampler(spatial_size: Sized, align_corners: bool = False) -> Callable[[torch.Tensor], torch.Tensor]: +def default_upsampler( + spatial_size: Sequence[int], align_corners: bool = False +) -> Callable[[torch.Tensor], torch.Tensor]: """ A linear interpolation method for upsampling the feature map. The output of this function is a callable `func`, @@ -32,6 +34,6 @@ def up(x): linear_mode = [InterpolateMode.LINEAR, InterpolateMode.BILINEAR, InterpolateMode.TRILINEAR] interp_mode = linear_mode[len(spatial_size) - 1] smode = str(interp_mode.value) - return F.interpolate(x, size=spatial_size, mode=smode, align_corners=align_corners) # type: ignore + return F.interpolate(x, size=spatial_size, mode=smode, align_corners=align_corners) return up diff --git a/pyproject.toml b/pyproject.toml index 325622b66a7..03c471832cd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,11 +20,9 @@ exclude = ''' \.eggs | \.git | \.hg - | \.mypy_cache | \.tox | \.venv | venv - | \.pytype | _build | buck-out | build @@ -116,3 +114,57 @@ precise_return = true protocols = true # Experimental: Only load submodules that are explicitly imported. strict_import = false + +[tool.pyrefly] +# Check only the monai package +project-includes = ["monai/"] + +# Exclude auto-generated and vendored files +project-excludes = [ + "**/venv/**", + "**/.venv/**", + "versioneer.py", + "monai/_version.py", +] + +# Match CI environment +python-version = "3.9" +python-platform = "linux" + +# "legacy" preset provides a smooth migration from previous type checkers +preset = "legacy" + +# Check unannotated defs (previously enforced in mypy config) +check-unannotated-defs = true + +[tool.pyrefly.errors] +# Ignore missing imports +missing-import = "ignore" + +# Suppress unused-ignore warnings +unused-ignore = "ignore" + +# Suppress implicit-import globally (MONAI style uses lazy imports) +implicit-import = "ignore" + +# Downgrade errors in unannotated/dynamic code to warnings +# (pre-existing issues, not new — will fix incrementally) +bad-assignment = "warn" +bad-return = "warn" +bad-argument-type = "warn" +invalid-annotation = "ignore" +not-iterable = "warn" +not-callable = "warn" +bad-index = "warn" + +# Pre-existing errors not flagged by previous type checkers +# Suppress for a smooth migration; fix incrementally +missing-attribute = "ignore" +bad-override = "ignore" +no-matching-overload = "ignore" +unsupported-operation = "ignore" +unnecessary-type-conversion = "ignore" +missing-module-attribute = "ignore" +not-a-type = "ignore" +invalid-yield = "ignore" +deprecated = "ignore" diff --git a/requirements-dev.txt b/requirements-dev.txt index b2c36f8de62..c46648006bf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -20,7 +20,7 @@ ruff>=0.14.11,<0.15 pybind11 setuptools<71 # pkg_resources removed in setuptools>=71; needed by MetricsReloaded setup.py types-setuptools -mypy>=1.5.0, <1.12.0 +pyrefly>=1.0.0 ninja torchio torchvision diff --git a/runtests.sh b/runtests.sh index 431e9298c35..d48bc96f417 100755 --- a/runtests.sh +++ b/runtests.sh @@ -49,7 +49,7 @@ doRuffFix=false doClangFormat=false doCopyRight=false doPytypeFormat=false -doMypyFormat=false +doPyreflyFormat=false doCleanup=false doDistTests=false doPrecommit=false @@ -61,7 +61,7 @@ PY_EXE=${MONAI_PY_EXE:-$(which python)} function print_usage { echo "runtests.sh [--codeformat] [--autofix] [--black] [--isort] [--pylint] [--ruff]" - echo " [--clangformat] [--precommit] [--pytype] [-j number] [--mypy]" + echo " [--clangformat] [--precommit] [--pytype] [-j number] [--pyrefly]" echo " [--unittests] [--disttests] [--coverage] [--quick] [--min] [--net] [--build] [--list_tests]" echo " [--dryrun] [--copyright] [--clean] [--help] [--version] [--path] [--formatfix]" echo "" @@ -87,9 +87,9 @@ function print_usage { echo " --precommit : perform source code format check and fix using \"pre-commit\"" echo "" echo "Python type check options:" - echo " --pytype : perform \"pytype\" static type checks" - echo " -j, --jobs : number of parallel jobs to run \"pytype\" (default $NUM_PARALLEL)" - echo " --mypy : perform \"mypy\" static type checks" + echo " --pytype : perform \"pytype\" static type checks (deprecated, may be removed in future)" + echo " -j, --jobs : number of parallel jobs to run \"pytype\" (default $NUM_PARALLEL) (deprecated)" + echo " --pyrefly : perform \"pyrefly\" static type checks" echo "" echo "MONAI unit testing options:" echo " -u, --unittests : perform unit testing" @@ -196,8 +196,8 @@ function clean_py { find ${TO_CLEAN} -depth -maxdepth 1 -type d -name "monai.egg-info" -exec rm -r "{}" + find ${TO_CLEAN} -depth -maxdepth 1 -type d -name "build" -exec rm -r "{}" + find ${TO_CLEAN} -depth -maxdepth 1 -type d -name "dist" -exec rm -r "{}" + - find ${TO_CLEAN} -depth -maxdepth 1 -type d -name ".mypy_cache" -exec rm -r "{}" + find ${TO_CLEAN} -depth -maxdepth 1 -type d -name ".pytype" -exec rm -r "{}" + + find ${TO_CLEAN} -depth -maxdepth 1 -type d -name ".pyrefly_cache" -exec rm -r "{}" + find ${TO_CLEAN} -depth -maxdepth 1 -type d -name ".coverage" -exec rm -r "{}" + find ${TO_CLEAN} -depth -maxdepth 1 -type d -name "__pycache__" -exec rm -r "{}" + } @@ -271,6 +271,7 @@ do doIsortFormat=true # doPylintFormat=true # https://github.com/Project-MONAI/MONAI/issues/7094 doRuffFormat=true + doPyreflyFormat=true doCopyRight=true ;; --disttests) @@ -314,10 +315,11 @@ do doPrecommit=true ;; --pytype) + echo "${yellow}WARNING: --pytype is deprecated and may be removed in a future release.${noColor}" doPytypeFormat=true ;; - --mypy) - doMypyFormat=true + --pyrefly) + doPyreflyFormat=true ;; -j|--jobs) NUM_PARALLEL=$2 @@ -611,7 +613,9 @@ fi if [ $doPytypeFormat = true ] then set +e # disable exit on failure so that diagnostics can be given on failure + echo "${yellow}WARNING: pytype is deprecated and may be removed in a future release.${noColor}" echo "${separator}${blue}pytype${noColor}" + # ensure that the necessary packages for code format testing are installed if ! is_pip_installed pytype then @@ -639,26 +643,27 @@ then fi -if [ $doMypyFormat = true ] +if [ $doPyreflyFormat = true ] then set +e # disable exit on failure so that diagnostics can be given on failure - echo "${separator}${blue}mypy${noColor}" + echo "${separator}${blue}pyrefly${noColor}" # ensure that the necessary packages for code format testing are installed - if ! is_pip_installed mypy + if ! is_pip_installed pyrefly then install_deps fi - ${cmdPrefix}"${PY_EXE}" -m mypy --version - ${cmdPrefix}"${PY_EXE}" -m mypy "$homedir" + ${cmdPrefix}"${PY_EXE}" -m pyrefly --version + # Run without file arguments to respect project-includes/excludes from pyproject.toml + ${cmdPrefix}"${PY_EXE}" -m pyrefly check - mypy_status=$? - if [ ${mypy_status} -ne 0 ] + pyrefly_status=$? + if [ ${pyrefly_status} -ne 0 ] then - : # mypy output already follows format - exit ${mypy_status} + echo "${red}failed!${noColor}" + exit ${pyrefly_status} else - : # mypy output already follows format + echo "${green}passed!${noColor}" fi set -e # enable exit on failure fi