Repository: isce-framework/snaphu-py (language: python)

Issue:
Improve Pylance support for Raster
@scottstanie pointed out that [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) was failing to find the definition of `snaphu.io.Raster`, as evidenced by VSCode mouseover tooltips implicitly treating the type as equivalent to `Any`.

![image](https://github.com/isce-framework/snaphu-py/assets/12984092/6d4c9bc2-3d49-4b7f-aba5-1db351782f07)

This is presumably a consequence of the heterodox way in which `Raster` is exported (using [PEP 562](https://peps.python.org/pep-0562/)-style module-level `__getattr__()` and `__dir__()` functions).

This PR tries to update the way that `Raster` is exported to improve compatibility with Pylance while still preserving the same runtime behavior. In particular:

1. If the `rasterio` package is missing, importing `snaphu.io` should not fail. `Raster` should not be a member of `snaphu.io` in this case. Attempting to import `snaphu.io.Raster` should raise an `ImportError`.
2. If the `rasterio` package *is* available, `snaphu.io.Raster` should be a valid type and `'Raster'` should be contained in `dir(snaphu.io)`.

Since Pylance statically analyzes the code, the trick is to initially add `Raster` to `snaphu.io.__all__` and then dynamically remove it from the list if it could not be imported at runtime.
Improve Pylance support for Raster
@scottstanie pointed out that [Pylance](https://marketplace.visualstudio.com/items?itemName=ms-python.vscode-pylance) was failing to find the definition of `snaphu.io.Raster`, as evidenced by VSCode mouseover tooltips implicitly treating the type as equivalent to `Any`.

![image](https://github.com/isce-framework/snaphu-py/assets/12984092/6d4c9bc2-3d49-4b7f-aba5-1db351782f07)

This is presumably a consequence of the heterodox way in which `Raster` is exported (using [PEP 562](https://peps.python.org/pep-0562/)-style module-level `__getattr__()` and `__dir__()` functions).

This PR tries to update the way that `Raster` is exported to improve compatibility with Pylance while still preserving the same runtime behavior. In particular:

1. If the `rasterio` package is missing, importing `snaphu.io` should not fail. `Raster` should not be a member of `snaphu.io` in this case. Attempting to import `snaphu.io.Raster` should raise an `ImportError`.
2. If the `rasterio` package *is* available, `snaphu.io.Raster` should be a valid type and `'Raster'` should be contained in `dir(snaphu.io)`.

Since Pylance statically analyzes the code, the trick is to initially add `Raster` to `snaphu.io.__all__` and then dynamically remove it from the list if it could not be imported at runtime.


Discussion / hints:
## [Codecov](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework) Report
Attention: Patch coverage is `50.00000%` with `2 lines` in your changes are missing coverage. Please review.
> Project coverage is 94.14%. Comparing base [(`c7780bb`)](https://app.codecov.io/gh/isce-framework/snaphu-py/commit/c7780bbe1f909b30a08c30ac4749fb6a12f78cdd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework) to head [(`8061d97`)](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework).

| [Files](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework) | Patch % | Lines |
|---|---|---|
| [src/snaphu/io/\_\_init\_\_.py](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?src=pr&el=tree&filepath=src%2Fsnaphu%2Fio%2F__init__.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework#diff-c3JjL3NuYXBodS9pby9fX2luaXRfXy5weQ==) | 50.00% | [2 Missing :warning: ](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #66      +/-   ##
==========================================
+ Coverage   92.79%   94.14%   +1.34%     
==========================================
  Files           9        9              
  Lines         472      461      -11     
==========================================
- Hits          438      434       -4     
+ Misses         34       27       -7     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework).

## [Codecov](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework) Report
Attention: Patch coverage is `50.00000%` with `2 lines` in your changes are missing coverage. Please review.
> Project coverage is 94.14%. Comparing base [(`c7780bb`)](https://app.codecov.io/gh/isce-framework/snaphu-py/commit/c7780bbe1f909b30a08c30ac4749fb6a12f78cdd?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework) to head [(`8061d97`)](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework).

| [Files](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework) | Patch % | Lines |
|---|---|---|
| [src/snaphu/io/\_\_init\_\_.py](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?src=pr&el=tree&filepath=src%2Fsnaphu%2Fio%2F__init__.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework#diff-c3JjL3NuYXBodS9pby9fX2luaXRfXy5weQ==) | 50.00% | [2 Missing :warning: ](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main      #66      +/-   ##
==========================================
+ Coverage   92.79%   94.14%   +1.34%     
==========================================
  Files           9        9              
  Lines         472      461      -11     
==========================================
- Hits          438      434       -4     
+ Misses         34       27       -7     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/isce-framework/snaphu-py/pull/66?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=isce-framework).


Fix patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index e7680d4..3d10197 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -38,6 +38,7 @@ repos:
     rev: "v1.10.0"
     hooks:
       - id: mypy
+        exclude: snaphu/io/__init__\.py$
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
     rev: "v0.4.4"
diff --git a/src/snaphu/io/__init__.pyi b/src/snaphu/io/__init__.pyi
new file mode 100644
index 0000000..525fbf6
--- /dev/null
+++ b/src/snaphu/io/__init__.pyi
@@ -0,0 +1,8 @@
+from ._dataset import InputDataset, OutputDataset
+from ._raster import Raster
+
+__all__ = [
+    "InputDataset",
+    "OutputDataset",
+    "Raster",
+]


Test patch:
diff --git a/test/io/test_raster.py b/test/io/test_raster.py
index 6c3ef48..468d5cb 100644
--- a/test/io/test_raster.py
+++ b/test/io/test_raster.py
@@ -97,6 +97,19 @@ def make_temp_geotiff_raster() -> Generator[snaphu.io.Raster, None, None]:
         yield raster
 
 
+def test_public_raster():
+    if has_rasterio():
+        assert "Raster" in dir(snaphu.io)
+    else:
+        assert "Raster" not in dir(snaphu.io)
+
+
+@pytest.mark.skipif(has_rasterio(), reason="")
+def test_bad_raster_import():
+    with pytest.raises(ImportError, match=r"^No module named 'rasterio'$"):
+        from snaphu.io import Raster  # noqa: F401
+
+
 @pytest.mark.skipif(not has_rasterio(), reason="requires rasterio package")
 class TestRaster:
     @pytest.fixture(scope="class")