Skip to content

Modernize project: Python 3.11, pyproject.toml, ruff, uv, drop reques… - #115

Merged
will-fawcett-trillium merged 11 commits into
mainfrom
py311-modernization
May 6, 2026
Merged

Modernize project: Python 3.11, pyproject.toml, ruff, uv, drop reques…#115
will-fawcett-trillium merged 11 commits into
mainfrom
py311-modernization

Conversation

@will-fawcett-trillium

Copy link
Copy Markdown
Contributor

…ts_html

Brings the project onto current Python tooling and packaging conventions so new contributors can install and work with it without fighting deprecated tools. Existing config was inconsistent (Makefile pinned 3.8, setup.py said 3.10, CI ran 3.8) and the legacy black/isort/flake8/pylint/pydocstyle stack plus an unmaintained requests_html dep made the install path brittle.

Packaging

  • New pyproject.toml (PEP 621) replaces setup.py + setup.cfg + requirements.txt
    • pytest.ini. Dynamic version from ml4floods/init.py. Optional extras: dev / tests / docs.
  • Generated uv.lock for reproducible installs (232 packages).

Tooling

  • Ruff replaces black + isort + flake8 + pylint + pydocstyle. Conservative initial rule set (E/F/W/I); UP/B and a number of legacy issue codes are ignored for now and intended to be enabled per-module in follow-ups.
  • mypy + pytest config moved into pyproject.toml.
  • Makefile rewritten for uv + ruff (install-dev, lint, format, type, test, check, build, publish). Conda flow removed — environment.yml never existed.
  • New .pre-commit-config.yaml: ruff, ruff-format, plus basic hygiene hooks.

CI

  • deploy.yml: trigger fixed master -> main, Python 3.8 -> 3.11, uv install, publish_dir corrected (docs/_build/html -> jupyterbook/_build/html), action versions bumped (checkout v2 -> v4, setup-python v1 -> setup-uv v3, actions-gh-pages v3.6.1 -> v4).
  • New test.yml with three jobs: ruff lint+format check, mypy (continue-on-error to surface but not gate), pytest.

Source

  • Drop requests_html (unmaintained since 2020). Replaced HTMLSession + r.html.find / .html.links with requests.Session + BeautifulSoup in copernicusEMS/activations.py and unosat/unosat_download.py. Removed dead HTMLSession import from serve/tileserver/helpers.py.
  • Auto-formatter pass touched ~80 files (whitespace, EOF newlines, quote style, isort). No behavior changes.

Misc

  • .idea/ untracked and added to .gitignore. .venv/ added to .gitignore.

Known follow-ups (out of scope here)

  • 150 ruff lint issues remain in legacy code (F821 undefined names, bugbear B006/B007/B008/B028, pyupgrade UP008/UP031, etc.) — currently ignored. Address per module.
  • Three test files reference symbols that don't exist on main (WorldFloodsImage, create_folder); collection errors are pre-existing, not caused by this work.
  • 24 .ipynb notebooks under notebooks/ and jupyterbook/ still mention requests_html in stale cell output; not in scope.

…ts_html

Brings the project onto current Python tooling and packaging conventions so
new contributors can install and work with it without fighting deprecated
tools. Existing config was inconsistent (Makefile pinned 3.8, setup.py said
3.10, CI ran 3.8) and the legacy black/isort/flake8/pylint/pydocstyle stack
plus an unmaintained requests_html dep made the install path brittle.

Packaging
- New pyproject.toml (PEP 621) replaces setup.py + setup.cfg + requirements.txt
  + pytest.ini. Dynamic version from ml4floods/__init__.py. Optional extras:
  dev / tests / docs.
- Generated uv.lock for reproducible installs (232 packages).

Tooling
- Ruff replaces black + isort + flake8 + pylint + pydocstyle. Conservative
  initial rule set (E/F/W/I); UP/B and a number of legacy issue codes are
  ignored for now and intended to be enabled per-module in follow-ups.
- mypy + pytest config moved into pyproject.toml.
- Makefile rewritten for uv + ruff (install-dev, lint, format, type, test,
  check, build, publish). Conda flow removed — environment.yml never existed.
- New .pre-commit-config.yaml: ruff, ruff-format, plus basic hygiene hooks.

CI
- deploy.yml: trigger fixed master -> main, Python 3.8 -> 3.11, uv install,
  publish_dir corrected (docs/_build/html -> jupyterbook/_build/html), action
  versions bumped (checkout v2 -> v4, setup-python v1 -> setup-uv v3,
  actions-gh-pages v3.6.1 -> v4).
- New test.yml with three jobs: ruff lint+format check, mypy
  (continue-on-error to surface but not gate), pytest.

Source
- Drop requests_html (unmaintained since 2020). Replaced HTMLSession +
  r.html.find / .html.links with requests.Session + BeautifulSoup in
  copernicusEMS/activations.py and unosat/unosat_download.py. Removed dead
  HTMLSession import from serve/tileserver/helpers.py.
- Auto-formatter pass touched ~80 files (whitespace, EOF newlines, quote
  style, isort). No behavior changes.

Misc
- .idea/ untracked and added to .gitignore. .venv/ added to .gitignore.

Known follow-ups (out of scope here)
- 150 ruff lint issues remain in legacy code (F821 undefined names,
  bugbear B006/B007/B008/B028, pyupgrade UP008/UP031, etc.) — currently
  ignored. Address per module.
- Three test files reference symbols that don't exist on main
  (WorldFloodsImage, create_folder); collection errors are pre-existing,
  not caused by this work.
- 24 .ipynb notebooks under notebooks/ and jupyterbook/ still mention
  requests_html in stale cell output; not in scope.
These test modules were testing APIs (create_folder, get_files_in_directory,
WorldFloodsImage, data_download) that were intentionally removed in PR #97
(Tutorial Updates, ~1 year ago). They have not been runnable since, and the
symbols they import don't exist in the package or anywhere else in the repo.

Also drops two zero-byte placeholder test files.

After this, `pytest tests` collects and passes cleanly (5 tests). The CI test
job added in the modernization PR is now actually green.

Removed:
- tests/data/worldfloods/test_dataset.py
- tests/data/worldfloods/test_download.py
- tests/data/worldfloods/test_loader.py (empty)
- tests/data/worldfloods/test_prepare_data.py (empty)
- tests/preprocess/test_tiling.py
Resolves all 16 F821 undefined-name lint errors across the package and
scripts (13 in ml4floods/, 3 in scripts/) so the rule can be removed from
ruff's ignore list. F821 is now enforced going forward.

Three classes of issue, all latent for ~1 year:

unosat_download.py — `os` was used (os.path.basename, os.listdir, os.mkdir,
os.path.join, etc.) but never imported. The download_shapefiles code path
would have NameError'd on first call. Added the missing `import os`.

indexer.py:144 — typo. The CLI flag is `args.log_level` (used on line 142)
but the error message tried to interpolate an undefined `loglevel`. Switched
to f-string with args.log_level.

create_worldfloods_dataset.py — `worldfloods_old_gcp_paths` was dead code
referencing the GCPPath class deleted in PR #97 (Tutorial Updates). The
function used GCPPath, .check_if_file_exists, .full_path, .file_name on
plain strings and would have crashed on any call. Deleted it. The
`generate_item` function had it as the default value for `paths_function`;
switched the default to the working `worldfloods_extra_gcp_paths` (same
5-tuple signature). All real callers in scripts/ already pass paths_function
explicitly.

generate_ground_truth_wf.py — `main_worldlfoods_original` was the same dead
code at script level (also using GCPPath). Reachable only via `--dataset ""`,
which produced a NameError immediately. Removed the function, removed the
empty-string choice from argparse, and collapsed the now-redundant branching
in main().

Verification: ruff check (with F821 enabled), pytest, and pre-commit all
pass.
Removes 4 unused imports and 6 unused local assignments so F401 and F841
can be removed from ruff's ignore list and enforced going forward.

The cleanups:

ee_download.py — drop `cirrus_bit_mask` (used only by code that's been
commented out) and the dead comment that referenced it. Drop unused `pol`
and `area_of_interest_geojson` assignments in two functions where neither
the value nor the side effect of `mapping()` was needed.

unosat_download.py — `get_flood_shapefiles` was issuing one HTTP GET per
map link and discarding the response, then calling
`get_flood_shape_and_meta` which fetches the same page itself. Removing
the wasted request halves traffic in this loop.

tiling.py — `sub_image = dataset.read(indexes=bands, window=...)` was
read into a local that was never used; the actual write a few lines down
calls `dataset.read(window=..., boundless=True, fill_value=0)` (no band
selection). Note: the surrounding code has a separate latent issue —
`window_meta["channels"] = bands` claims fewer bands than the write
actually produces — but that's outside the scope of an F841 cleanup.

unused imports — drop `requests` from serve/__init__.py (the only
references are in commented-out lines), drop `pathlib.Path`,
`write_json_to_gcp`, and `worldfloods_output_files` from the worldfloods
GT script (orphaned by the prior dead-code removal), and drop unused
`fs` in download_images_inference.py.

After this, ruff's only remaining ignored rules are formatter-only
(E501) and judgment-call style codes (E402, E711, E721, E722, E731,
E741, F403, F405). Pyflakes-correctness checks (F-series) are now all
on except for star-import ones which need an evaluation of the actual
re-export contracts in __init__.py files.
Resolves all 29 UP violations and adds the UP rule family to ruff's
select set so future regressions get caught.

UP008 (13 sites, auto-fixed) — `super(ClassName, self).method()` is the
Python 2 form. Modern Python uses bare `super().method()`.

UP031 / UP032 (16 sites, mostly auto-fixed) — converted printf-style `%`
formatting and `.format()` calls to f-strings. The 7 sites ruff couldn't
auto-fix (multi-line `%`, multi-arg tuples, nested format specs) were
done by hand. Examples:

- `"%.1f%%\n%d/%d" % (p, c, s)` → `f"{p:.1f}%\n{c}/{s}"`
- `"Date pre event {} is after date post event {}".format(d1, d2)` →
  f-string with nested `.strftime()` calls

No behavior change in any case — the formatted output is byte-equivalent.

Tests, ruff, and pre-commit all pass. The `B` (bugbear) ruleset is the
last opt-in tier left to enable; ~50 sites remain there but most are
judgment calls (mutable defaults, raise-without-from, etc.) that need
per-site review rather than mechanical fixes.
Mechanical fix of 11 E711 violations across three files. Identity
comparison is the correct way to test for None — `==` only happens to
work when no `__eq__` is defined to override it. Behavior is identical
for all current call sites, but `is`/`is not` is unambiguous and ~3x
faster (no method dispatch).

Enables E711 in ruff so any future regression is caught at lint time.
Resolves the remaining mid-tier lint debt across six rules and removes
each from ruff's ignore list, leaving only E501 (formatter-managed) and
E731 (lambda assignment, judgment call) ignored.

E402 (24 sites) — Module-level imports placed below `logging.basicConfig`
calls in three serve modules (REST_mosaic, ingest, modelserver/app), and
one orphaned import buried mid-file in viewer/serve.py. The
basicConfig-then-imports pattern is unintentional; moving the
basicConfig call below the imports is the standard order and equivalent
in behavior. The viewer import was merged into the existing
`from shapely.geometry import shape` at the top.

E721 (1 site) — `type(x) == gpd.GeoDataFrame` → `isinstance(x, gpd.GeoDataFrame)`.
Subclasses now match correctly; no current call site has a subclass but
this is the standard idiom.

E722 (3 sites) — Bare `except:` clauses replaced with `except Exception:`
(or `except FileNotFoundError:` where the failure mode is specific). Bare
except catches BaseException including KeyboardInterrupt and SystemExit,
which is almost never intended. The ingest.py site additionally chains
the original via `raise ValueError(...) from e` to preserve the
underlying cause.

E741 (3 sites) — Renamed loop variable `l` → `label` in three identical
metric helpers (calculate_iou/recall/precision). `l` is hard to
distinguish from `1` and `I` in many fonts.

F403 + F405 (1 + 2 sites) — `helpers.py` had
`from ml4floods.data.copernicusEMS.activations import *` solely to
re-import `geopandas as gpd` indirectly. No other symbols from
`activations` were actually used in the file. Replaced the star import
with a direct `import geopandas as gpd`.

modelserver/app.py also had a `from ml4floods.models.model_setup import
get_model` planted between two top-level statements; merged it into the
existing import line above.
Six lambdas-bound-to-names in model_setup.py converted to proper `def`
functions. Each lambda would have shown up as `<lambda>` in stack traces
and lacked a meaningful `__name__` for debugger / decorator
introspection. Behavior is unchanged — same single-expression bodies,
same closures over `model`, `device`, `activation_fun`.

In `get_pred_function` the `pred_fun = lambda ti: activation_fun(model(...))`
appeared on both branches of an if/else (with the if branch wrapping it
in `padded_predict(...)`). Hoisted the def out of the conditional and
let `padded_predict` rebind `pred_fun` only when `module_shape > 1`, so
the function isn't redefined unnecessarily.

E731 is now enforced. Only E501 remains in ruff's ignore list, and that
one is delegated to ruff-format. Every other rule in `select`
(E, F, W, I, UP) is fully on.
Smoke-tested the package on torch 2.11 / numpy 2.4.4 / pytorch-lightning
2.6.1 / Python 3.12. Surfaces and fixes the real API-compat issues that
were lurking on this branch.

Real bugs fixed:

metrics.py:420 — `thresholds_water: np.array | None = None`. `np.array`
is the array-constructor function, not a type, so PEP 604's `X | None`
union evaluation hit `TypeError: unsupported operand type(s) for |`
*at module import time*. Cascaded to break four downstream modules
(metrics, model_setup, worldfloods_model, plot_utils). Replaced with
`np.ndarray`.

ml4floods/data/worldfloods/download.py — Removed entirely. Imported
`download_data_from_bucket` and `save_file_from_bucket` from
ml4floods.data.utils, both of which were deleted in PR #97 a year ago.
Zero callers remained anywhere in the codebase or notebooks.

Missing core dependencies — `pyproject.toml` had been pinning the
package as installable but several of its modules import packages that
were never declared:

  scikit-image  used by create_gt, postprocess, scripts/inference,
                 serve/tileserver/ingest
  fs            (pyfilesystem2) used by data/index/indexer and
                 data/unosat/unosat_download
  geojson       used by serve/tileserver/{app,helpers}
  lxml          used transitively via pandas.read_html in
                 copernicusEMS/activations.table_floods_ems

Added all four to `dependencies`.

Optional extras introduced:

  serve = [flask, gunicorn]              — pulls in the Flask web servers
  cloudmasks = [segmentation-models-pytorch, s2cloudless]
                                          — opt-in alternative cloud masks
  kappazeta = [tensorflow]               — separate because of TF's size

Lightning + ML stack smoke test: 4/4 architectures (UNet, UNet_dropout,
SimpleCNN, SimpleLinear) round-trip a (2,13,128,128) tensor; WorldFloodsModel
runs forward + training_step + backward and produces non-zero gradients.

Remaining import failures (3 of 66 modules) are *environment-dependent*,
not API breakage:

  serve/tileserver/app.py and serve/modelserver/app.py both make GCS
  fetches at module-import time (table_floods_ems(), get_default_config).
  They require GOOGLE_APPLICATION_CREDENTIALS + an internet route to
  emergency.copernicus.eu and gs://ml4cc_data_lake. This was true on
  main as well.

  data/cmkappazeta needs tensorflow (intentionally excluded from default
  install; available via `pip install ml4floods[kappazeta]`).
The existing `pypi/pyversions/ml4floods` badge is dynamic — it reads the
classifiers/`requires-python` from whatever's currently on PyPI, so it
won't reflect the new 3.11 floor until a release is cut after this work
lands. Adding a static badge alongside it gives readers an immediate,
unambiguous signal of the support floor declared in pyproject.toml.
Cleanup: tests, lint debt, Python 3.11 compat, missing deps
@will-fawcett-trillium
will-fawcett-trillium merged commit 18e4797 into main May 6, 2026
3 checks passed
@will-fawcett-trillium
will-fawcett-trillium deleted the py311-modernization branch May 6, 2026 17:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants