From 5f5565b6abf89ace749194941a3b6c23b84fa5c2 Mon Sep 17 00:00:00 2001 From: Reimar Bauer Date: Thu, 8 Oct 2026 10:00:39 +0200 Subject: [PATCH 1/2] Add import-linter contracts for the package split and let them check the server blueprints --- ARCHITECTURE.md | 60 ++++++++---- mslib/mscolab/CLAUDE.md | 5 +- mslib/mscolab/blueprints/__init__.py | 25 +++++ mslib/msui/CLAUDE.md | 8 +- mslib/mswms/CLAUDE.md | 5 +- mslib/mswms/blueprints/__init__.py | 25 +++++ mslib/utils/CLAUDE.md | 18 +++- setup.cfg | 132 ++++++++++++++++++++++++++- 8 files changed, 249 insertions(+), 29 deletions(-) create mode 100644 mslib/mscolab/blueprints/__init__.py create mode 100644 mslib/mswms/blueprints/__init__.py diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 5fb522d8c..554856e6d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -15,28 +15,54 @@ Arrows mean "may import". Anything not drawn is forbidden. plotting CLI) └─────┬──────┘ ┌──────────────┼───────────────────────┐ ▼ ▼ ▼ - mslib.mscolab mslib.support mslib.utils - (events/message_ (vendored qt_json_view) (shared: config, constants, - type constants coordinates, units, - ONLY) netCDF, auth, qt helpers) - ▲ + mslib.mscolab.api mslib.support mslib.utils + (typed REST (vendored (shared: config, constants, + contract, socket qt_json_view) coordinates, units, + vocabulary; stdlib netCDF, auth, qt helpers) + only, ONLY this of ▲ + mslib.mscolab) │ + ▲ │ + mslib.mscolab (server) ─────────────────────┤ + (never imports msui/mswms) │ mslib.mswms ────────────────────────────────┘ (WMS server; never imports msui/mscolab) mslib.plugins → mslib.msui.flighttrack (grandfathered), mslib.utils - mslib.msidp → standalone SAML identity provider + mslib.msidp → standalone SAML identity provider: imports no other mslib + package, and no other package imports it ``` +The servers' `blueprints.docs` still import `msui.icons` — a to-do listed below. + +Besides the arrows, the contracts keep third-party stacks where they belong. +"Server stack" means Flask and its extensions, SQLAlchemy, Alembic, Werkzeug +and pysaml2: + +- `mslib.utils` imports no Qt, no keyring and no server stack (`utils-base-layer`); +- `mslib.mscolab.api` imports no other mslib package, no Qt, no numpy and no + server stack (`api-leaf`); +- the servers `mswms`/`mscolab` never import Qt (`servers-without-qt`); +- the client (`msui`, `plugins`, `support`, `autoplot`) never imports the + server stack (`client-without-server-stack`); +- `mslib.msidp` imports no Qt, numpy or Flask (`msidp-standalone`), and no + other package imports it (`msidp-not-imported`). + +This is what lets the conda recipe ship the packages separately +(`plans/PACKAGE_SPLIT_PLAN.md`). + Grandfathered violations (do not add new ones; the lists live in `setup.cfg` under `ignore_imports` and must only shrink): -- `plugins.io.{csv,text,flitestar}` import `msui.flighttrack`. -- the deprecation shim `utils/mssautoplot.py` re-exports `mslib.autoplot` - (delete the shim next release and the entry with it). A twin shim exists at - `msui/constants.py` → `utils/constants.py`. -- `{mscolab,mswms}.blueprints.docs` import `msui.icons` — invisible to the - linter (`blueprints/` are namespace packages without `__init__.py`), but - equally discouraged. +- `plugins.io.{csv,text,flitestar}` import `msui.flighttrack` (all of them + ship in the `mss-msui` package). +- the deprecation shim `msui/constants.py` re-exports `utils/constants.py` + (`docs/samples/plugins/navaid.py` uses it). +- To-dos of the package split, each labelled with the decoupling PR of + `plans/PACKAGE_SPLIT_PLAN.md` that removes it: `{mscolab,mswms}.blueprints.docs` + import `msui.icons`; `utils` modules import Qt, `mslib.support`, Flask and + keyring (`utils-base-layer`, `client-without-server-stack`). import-linter + reports an entry whose import is gone ("No matches for ignored import"), so + the PR that removes an import has to delete its entry. ## Module index @@ -118,10 +144,12 @@ under `ignore_imports` and must only shrink): and `tests/_test_mscolab/test_api_contract.py` (schemas against real routes). 3. All configuration access goes through `mslib.utils.config.config_loader`; never read the settings JSON directly. -4. Qt imports are allowed only in `mslib/msui`, `mslib/utils/{qt,colordialog}.py`, - and `mslib/support`. +4. Qt imports are allowed only in `mslib/msui` and `mslib/support` — and, until + decoupling PR 4 moves them to msui, in the `mslib/utils` modules listed in the + `utils-base-layer` contract (`qt`, `colordialog`, `config`, `airdata`, + `migration/`). 5. New cross-package imports must satisfy the import-linter contracts in - `setup.cfg` — never extend an `ignore_imports` list. + `setup.cfg` — never extend an `ignore_imports` list; its entries only shrink. ## Verification ladder (cheapest first) diff --git a/mslib/mscolab/CLAUDE.md b/mslib/mscolab/CLAUDE.md index ba0fa7fd8..411129ff6 100644 --- a/mslib/mscolab/CLAUDE.md +++ b/mslib/mscolab/CLAUDE.md @@ -38,8 +38,9 @@ Global map: ../../ARCHITECTURE.md ## May import `mslib.utils` only (plus stdlib/Flask stack). Never `mslib.msui` (exception: -`blueprints/docs` icons, grandfathered) or `mslib.mswms`. Enforced by the -`gui-isolation` contract in setup.cfg. +`blueprints/docs` icons, a to-do of `plans/PACKAGE_SPLIT_PLAN.md`) or +`mslib.mswms`, and never Qt. Enforced by the `gui-isolation`, `wms-isolation` +and `servers-without-qt` contracts in setup.cfg. ## Invariants diff --git a/mslib/mscolab/blueprints/__init__.py b/mslib/mscolab/blueprints/__init__.py new file mode 100644 index 000000000..177e12a4d --- /dev/null +++ b/mslib/mscolab/blueprints/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" + + mslib.mscolab.blueprints + ~~~~~~~~~~~~~~~~~~~~~~~~ + + Blueprints of the mscolab server + + This file is part of MSS. + + :copyright: Copyright 2026 Reimar Bauer + :license: APACHE-2.0, see LICENSE for details. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" diff --git a/mslib/msui/CLAUDE.md b/mslib/msui/CLAUDE.md index 4e610b16a..50145034a 100644 --- a/mslib/msui/CLAUDE.md +++ b/mslib/msui/CLAUDE.md @@ -22,10 +22,10 @@ Global map: ../../ARCHITECTURE.md ## May import -`mslib.utils`, `mslib.support`, and ONLY `mslib.mscolab.{events,message_type, -api.schemas,api.endpoints}` from the server package (enforced: -`server-isolation` contract in setup.cfg). Never `mslib.mswms` — WMS is -consumed over HTTP. +`mslib.utils`, `mslib.support`, and ONLY `mslib.mscolab.api.*` from the server +package (enforced: `server-isolation` contract in setup.cfg). Never +`mslib.mswms` — WMS is consumed over HTTP — and never the server stack (Flask, +SQLAlchemy; `client-without-server-stack` contract). ## Invariants diff --git a/mslib/mswms/CLAUDE.md b/mslib/mswms/CLAUDE.md index 73b6f0aca..063bdf33a 100644 --- a/mslib/mswms/CLAUDE.md +++ b/mslib/mswms/CLAUDE.md @@ -21,8 +21,9 @@ Global map: ../../ARCHITECTURE.md ## May import -`mslib.utils` only. Never `mslib.msui` (exception: `blueprints/docs` icons, -grandfathered) or `mslib.mscolab`. Enforced: `wms-isolation` contract. +`mslib.utils` only. Never `mslib.msui` (exception: `blueprints/docs` icons, a +to-do of `plans/PACKAGE_SPLIT_PLAN.md`) or `mslib.mscolab`, and never Qt. +Enforced: `gui-isolation`, `server-isolation`, `servers-without-qt` contracts. ## Invariants diff --git a/mslib/mswms/blueprints/__init__.py b/mslib/mswms/blueprints/__init__.py new file mode 100644 index 000000000..7376ce5f9 --- /dev/null +++ b/mslib/mswms/blueprints/__init__.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +""" + + mslib.mswms.blueprints + ~~~~~~~~~~~~~~~~~~~~~~ + + Blueprints of the mswms server + + This file is part of MSS. + + :copyright: Copyright 2026 Reimar Bauer + :license: APACHE-2.0, see LICENSE for details. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +""" diff --git a/mslib/utils/CLAUDE.md b/mslib/utils/CLAUDE.md index d493b6430..7dd90911d 100644 --- a/mslib/utils/CLAUDE.md +++ b/mslib/utils/CLAUDE.md @@ -23,16 +23,26 @@ Global map: ../../ARCHITECTURE.md ## May import -Nothing from `mslib` outside `utils` — this is the base layer, enforced by -the `gui-isolation` contract in setup.cfg. +Nothing from `mslib` outside `utils`, and no Qt, keyring or server stack +(Flask & co., SQLAlchemy, Werkzeug, pysaml2) — this is the base layer, enforced +by the `gui-isolation` and `utils-base-layer` contracts in setup.cfg. The +imports that still break this are listed there as to-dos of +`plans/PACKAGE_SPLIT_PLAN.md`; don't add more: + +- Qt: `qt`, `colordialog`, `config`, `airdata` and `migration/` import PyQt5 + (`config` and `migration/` also `mslib.support`), so `find_location` and + `ogcwms`, which import `config`, need Qt as well; +- keyring: `auth`, `migration/update_json_file_to_version_eight`; +- Flask: `auth` (`send_email`). ## Invariants - New config keys: add the attribute on `MSUIDefaultConfig` AND, if dict/list shaped, the matching entry in `dict_option_structure`/`list_option_structure` and a line in `config_descriptions`. -- Everything except `qt.py`/`colordialog.py` (Qt) must stay importable without Qt - or a running server. +- Everything except the modules above that need Qt (`qt`, `colordialog`, + `config`, `airdata`, `migration/`, `find_location`, `ogcwms`) must stay + importable without Qt or a running server. ## Verify diff --git a/setup.cfg b/setup.cfg index 64bd5caee..d92ee2909 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,6 +66,9 @@ ignore_imports = mslib.plugins.io.csv -> mslib.msui.flighttrack mslib.plugins.io.text -> mslib.msui.flighttrack mslib.plugins.io.flitestar -> mslib.msui.flighttrack + # To do, decoupling PR 2 (plans/PACKAGE_SPLIT_PLAN.md): the docs blueprints get their own favicon/logo. + mslib.mswms.blueprints.docs -> mslib.msui.icons + mslib.mscolab.blueprints.docs -> mslib.msui.icons [importlinter:contract:server-isolation] name = Only the shared protocol constants of mslib.mscolab may be imported elsewhere @@ -91,10 +94,12 @@ ignore_imports = mslib.msui.* -> mslib.mscolab.api.* [importlinter:contract:api-leaf] -name = mslib.mscolab.api must not pull in the server stack +name = mslib.mscolab.api (the mss-mscolab-api package) needs nothing but the standard library type = forbidden source_modules = mslib.mscolab.api +# the rest of mslib.mscolab is listed module by module: a new module of mslib.mscolab +# has to be added here, see plans/PACKAGE_SPLIT_PLAN.md forbidden_modules = mslib.mscolab.app mslib.mscolab.auth @@ -114,10 +119,25 @@ forbidden_modules = mslib.mscolab.utils mslib.msui mslib.mswms + mslib.utils + mslib.support + mslib.plugins + mslib.autoplot + mslib.msidp + PyQt5 + numpy flask + flask_cors + flask_httpauth + flask_mail + flask_migrate flask_socketio flask_sqlalchemy + flask_wtf + alembic sqlalchemy + werkzeug + saml2 [importlinter:contract:wms-isolation] name = No other mslib package may import the mswms server @@ -132,3 +152,113 @@ source_modules = mslib.autoplot forbidden_modules = mslib.mswms + +[importlinter:contract:utils-base-layer] +name = mslib.utils is the base layer: no other mslib package, no Qt, no keyring, no server stack +type = forbidden +source_modules = + mslib.utils +forbidden_modules = + mslib.support + mslib.autoplot + mslib.plugins + mslib.msidp + PyQt5 + keyring + flask + flask_cors + flask_httpauth + flask_mail + flask_migrate + flask_socketio + flask_sqlalchemy + flask_wtf + alembic + sqlalchemy + werkzeug + saml2 +# Every line below is a to-do of plans/PACKAGE_SPLIT_PLAN.md and has to go with its PR; +# import-linter reports a line whose import is gone, so the list can only shrink. +ignore_imports = + # decoupling PR 3: send_email moves to mslib.mscolab + mslib.utils.auth -> flask + mslib.utils.auth -> flask_mail + # decoupling PR 4: the Qt client modules move to mslib.msui + mslib.utils.airdata -> PyQt5 + mslib.utils.colordialog -> PyQt5 + mslib.utils.config -> PyQt5 + mslib.utils.config -> mslib.support.qt_json_view.datatypes + mslib.utils.migration.config_before_eight -> PyQt5 + mslib.utils.migration.config_before_eight -> mslib.support.qt_json_view.datatypes + mslib.utils.migration.config_before_nine -> PyQt5 + mslib.utils.migration.config_before_nine -> mslib.support.qt_json_view.datatypes + mslib.utils.migration.update_json_file_to_version_eight -> keyring + mslib.utils.qt -> PyQt5 + # decoupling PR 5: the client-only helpers move to mslib.msui + mslib.utils.auth -> keyring + +[importlinter:contract:msidp-standalone] +name = mslib.msidp is standalone: no other mslib package, no Qt, numpy or flask +type = forbidden +source_modules = + mslib.msidp +forbidden_modules = + mslib.utils + mslib.msui + mslib.mswms + mslib.mscolab + mslib.plugins + mslib.support + mslib.autoplot + PyQt5 + numpy + flask + +[importlinter:contract:msidp-not-imported] +name = No other mslib package imports mslib.msidp (mss-msidp is a package of its own) +type = forbidden +source_modules = + mslib.utils + mslib.msui + mslib.mswms + mslib.mscolab + mslib.plugins + mslib.support + mslib.autoplot +forbidden_modules = + mslib.msidp + +[importlinter:contract:servers-without-qt] +name = The servers mswms and mscolab never import Qt +type = forbidden +source_modules = + mslib.mswms + mslib.mscolab +forbidden_modules = + PyQt5 + +[importlinter:contract:client-without-server-stack] +name = The client (msui, plugins, support, autoplot) never imports the server stack +type = forbidden +source_modules = + mslib.msui + mslib.plugins + mslib.support + mslib.autoplot +forbidden_modules = + flask + flask_cors + flask_httpauth + flask_mail + flask_migrate + flask_socketio + flask_sqlalchemy + flask_wtf + alembic + sqlalchemy + werkzeug + saml2 +ignore_imports = + # decoupling PR 3 (plans/PACKAGE_SPLIT_PLAN.md): send_email moves to mslib.mscolab + mslib.utils.auth -> flask + mslib.utils.auth -> flask_mail From 556fe5b5db8f6ab709e1b885067744210e88fb9b Mon Sep 17 00:00:00 2001 From: Reimar Bauer Date: Thu, 8 Oct 2026 13:45:10 +0200 Subject: [PATCH 2/2] Point the package split to-dos to #2307 instead of an untracked plan --- ARCHITECTURE.md | 10 +++++----- mslib/mscolab/CLAUDE.md | 2 +- mslib/mswms/CLAUDE.md | 2 +- mslib/utils/CLAUDE.md | 4 ++-- setup.cfg | 14 +++++++------- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 554856e6d..d3561f8a0 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -47,8 +47,8 @@ and pysaml2: - `mslib.msidp` imports no Qt, numpy or Flask (`msidp-standalone`), and no other package imports it (`msidp-not-imported`). -This is what lets the conda recipe ship the packages separately -(`plans/PACKAGE_SPLIT_PLAN.md`). +This is what lets the conda recipe ship the packages separately (package +split, #2307). Grandfathered violations (do not add new ones; the lists live in `setup.cfg` under `ignore_imports` and must only shrink): @@ -57,8 +57,8 @@ under `ignore_imports` and must only shrink): ship in the `mss-msui` package). - the deprecation shim `msui/constants.py` re-exports `utils/constants.py` (`docs/samples/plugins/navaid.py` uses it). -- To-dos of the package split, each labelled with the decoupling PR of - `plans/PACKAGE_SPLIT_PLAN.md` that removes it: `{mscolab,mswms}.blueprints.docs` +- To-dos of the package split (#2307), each labelled with the change that + removes it: `{mscolab,mswms}.blueprints.docs` import `msui.icons`; `utils` modules import Qt, `mslib.support`, Flask and keyring (`utils-base-layer`, `client-without-server-stack`). import-linter reports an entry whose import is gone ("No matches for ignored import"), so @@ -145,7 +145,7 @@ under `ignore_imports` and must only shrink): 3. All configuration access goes through `mslib.utils.config.config_loader`; never read the settings JSON directly. 4. Qt imports are allowed only in `mslib/msui` and `mslib/support` — and, until - decoupling PR 4 moves them to msui, in the `mslib/utils` modules listed in the + they move to msui, in the `mslib/utils` modules listed in the `utils-base-layer` contract (`qt`, `colordialog`, `config`, `airdata`, `migration/`). 5. New cross-package imports must satisfy the import-linter contracts in diff --git a/mslib/mscolab/CLAUDE.md b/mslib/mscolab/CLAUDE.md index 411129ff6..084a02578 100644 --- a/mslib/mscolab/CLAUDE.md +++ b/mslib/mscolab/CLAUDE.md @@ -38,7 +38,7 @@ Global map: ../../ARCHITECTURE.md ## May import `mslib.utils` only (plus stdlib/Flask stack). Never `mslib.msui` (exception: -`blueprints/docs` icons, a to-do of `plans/PACKAGE_SPLIT_PLAN.md`) or +`blueprints/docs` icons, a to-do of the package split, #2307) or `mslib.mswms`, and never Qt. Enforced by the `gui-isolation`, `wms-isolation` and `servers-without-qt` contracts in setup.cfg. diff --git a/mslib/mswms/CLAUDE.md b/mslib/mswms/CLAUDE.md index 063bdf33a..9f43d9397 100644 --- a/mslib/mswms/CLAUDE.md +++ b/mslib/mswms/CLAUDE.md @@ -22,7 +22,7 @@ Global map: ../../ARCHITECTURE.md ## May import `mslib.utils` only. Never `mslib.msui` (exception: `blueprints/docs` icons, a -to-do of `plans/PACKAGE_SPLIT_PLAN.md`) or `mslib.mscolab`, and never Qt. +to-do of the package split, #2307) or `mslib.mscolab`, and never Qt. Enforced: `gui-isolation`, `server-isolation`, `servers-without-qt` contracts. ## Invariants diff --git a/mslib/utils/CLAUDE.md b/mslib/utils/CLAUDE.md index 7dd90911d..3047da4a2 100644 --- a/mslib/utils/CLAUDE.md +++ b/mslib/utils/CLAUDE.md @@ -26,8 +26,8 @@ Global map: ../../ARCHITECTURE.md Nothing from `mslib` outside `utils`, and no Qt, keyring or server stack (Flask & co., SQLAlchemy, Werkzeug, pysaml2) — this is the base layer, enforced by the `gui-isolation` and `utils-base-layer` contracts in setup.cfg. The -imports that still break this are listed there as to-dos of -`plans/PACKAGE_SPLIT_PLAN.md`; don't add more: +imports that still break this are listed there as to-dos of the package +split (#2307); don't add more: - Qt: `qt`, `colordialog`, `config`, `airdata` and `migration/` import PyQt5 (`config` and `migration/` also `mslib.support`), so `find_location` and diff --git a/setup.cfg b/setup.cfg index d92ee2909..e3959f9e6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,7 +66,7 @@ ignore_imports = mslib.plugins.io.csv -> mslib.msui.flighttrack mslib.plugins.io.text -> mslib.msui.flighttrack mslib.plugins.io.flitestar -> mslib.msui.flighttrack - # To do, decoupling PR 2 (plans/PACKAGE_SPLIT_PLAN.md): the docs blueprints get their own favicon/logo. + # To do (package split, #2307): the docs blueprints get their own favicon/logo. mslib.mswms.blueprints.docs -> mslib.msui.icons mslib.mscolab.blueprints.docs -> mslib.msui.icons @@ -99,7 +99,7 @@ type = forbidden source_modules = mslib.mscolab.api # the rest of mslib.mscolab is listed module by module: a new module of mslib.mscolab -# has to be added here, see plans/PACKAGE_SPLIT_PLAN.md +# has to be added here forbidden_modules = mslib.mscolab.app mslib.mscolab.auth @@ -177,13 +177,13 @@ forbidden_modules = sqlalchemy werkzeug saml2 -# Every line below is a to-do of plans/PACKAGE_SPLIT_PLAN.md and has to go with its PR; +# Every line below is a to-do of the package split (#2307) and goes with the change that removes the import; # import-linter reports a line whose import is gone, so the list can only shrink. ignore_imports = - # decoupling PR 3: send_email moves to mslib.mscolab + # to do: send_email moves to mslib.mscolab mslib.utils.auth -> flask mslib.utils.auth -> flask_mail - # decoupling PR 4: the Qt client modules move to mslib.msui + # to do: the Qt client modules move to mslib.msui mslib.utils.airdata -> PyQt5 mslib.utils.colordialog -> PyQt5 mslib.utils.config -> PyQt5 @@ -194,7 +194,7 @@ ignore_imports = mslib.utils.migration.config_before_nine -> mslib.support.qt_json_view.datatypes mslib.utils.migration.update_json_file_to_version_eight -> keyring mslib.utils.qt -> PyQt5 - # decoupling PR 5: the client-only helpers move to mslib.msui + # to do: the client-only helpers move to mslib.msui mslib.utils.auth -> keyring [importlinter:contract:msidp-standalone] @@ -259,6 +259,6 @@ forbidden_modules = werkzeug saml2 ignore_imports = - # decoupling PR 3 (plans/PACKAGE_SPLIT_PLAN.md): send_email moves to mslib.mscolab + # to do (package split, #2307): send_email moves to mslib.mscolab mslib.utils.auth -> flask mslib.utils.auth -> flask_mail