diff --git a/docs/dev/plans/lint-rule-audit.md b/docs/dev/plans/lint-rule-audit.md new file mode 100644 index 000000000..addefbc0c --- /dev/null +++ b/docs/dev/plans/lint-rule-audit.md @@ -0,0 +1,573 @@ +# Lint-rule audit and adoption roadmap + +Reference: [`AGENTS.md`](../../../AGENTS.md). This plan follows the +project conventions there. One **deliberate exception** to the literal +brief is recorded under [Methodology](#methodology): the per-rule +violation counts were gathered with a single comprehensive +`ruff check --statistics`/`--output-format=json` pass that layers the +disabled rules onto the **unmodified** `pyproject.toml` at the command +line, rather than by literally enabling one rule at a time. In +`ruff check` (no `--fix`) the rules do not interact, so the per-rule +numbers are identical to enabling each rule on its own; the +comprehensive pass is just faster and reproducible. + +## Purpose + +`pyproject.toml` carries a deliberately ambitious `[tool.ruff.lint]` +configuration. Several rule families are still **commented out** in +`select`, and several more are **ignored** — globally and, especially, +for `tests/**` and `docs/**`. This document: + +1. Enumerates every currently-disabled rule. +2. Measures exactly how many violations each would raise today, split by + scope (`src/`, `tests/`, `docs/docs/tutorials/`). +3. Groups the violations by theme and effort. +4. Recommends, per rule, whether to **adopt** (enable + fix) or **keep + disabled** (with rationale). + +It is the **first draft** of a roadmap for tightening code quality — and +it has begun acting on it. This branch lands the tutorial-baseline fix, +this document, the regeneration helper (`tools/lint_rule_audit.py`), the +**Priority 0** cleanup (R1), all of **Priority 1** (P1a `D100`/`D104` +docstrings, P1b `DTZ005` + `TD004`/`TD005`, P1c `I001`/`E501`/`F841`), +and two **Tier-B** adoptions: **D1** (`PLR0402`/`PLR1711`/`PLR6104` + +the `PLW` family in tests, via splitting the `tests/**` PLR ignore) and +**D2** (`T20` — the library's stray `print()` diagnostics routed through +the `Logger`). The remaining Tier-B/C items (R6 `PLC1901`, the +test-complexity rules, R7 `SLF001`) are **recommended to stay disabled** +— see the Adoption roadmap — rather than planned for adoption. + +## ADR + +No new ADR is required **for the audit itself** — it adds a document and +a regeneration helper, not an architectural decision. The audit does +overlap existing lint policy, so two points of coordination apply: + +- **Cross-reference:** the accepted + [`lint-complexity-thresholds.md`](../adrs/accepted/lint-complexity-thresholds.md) + ADR already governs the `PLR` complexity rules — it treats those + thresholds as design guardrails (_refactor; do not raise thresholds or + add `# noqa`_). The `PLR0913`/`PLR0914`/`PLR0915`/`PLR0917` items in + [Tier B](#tier-b--adopt-with-moderate-effort--a-judgment-call) and + [Tier C](#tier-c--keep-disabled-intentional-idioms-policy-or-high-noise) + must be read in that light: in `tests/**` they are currently silenced + by a per-file ignore, which is a _standing exception_ to that ADR. +- **Recommendations are non-binding.** Every "keep disabled" entry in + Tier C (and the "`tests/**` stays permissive" and CIF-aligned + `id`/`type` observations) is a **recommendation pending user/ADR + approval**, not a decision taken by this document. Where a _permanent_ + keep-disabled would conflict with or extend existing policy, it needs + an explicit follow-up ADR before implementation — specifically: + - **`PLR` complexity ignored for + `tests/**`** — conflicts with `lint-complexity-thresholds.md`; + making it permanent needs an ADR amendment (or a new ADR that scopes + the test-file exception). + - **`A` kept disabled for CIF-aligned `id`/`type`** — a short new ADR + ("CIF field names may shadow Python builtins") would record the + rationale. + - **`N812` `MUT` import convention** — either a one-line note in + `AGENTS.md` §Testing or a short ADR. + + Until such an ADR exists, treat these as proposals only. + +## Branch and PR + +- Branch: `lint-rule-audit` (off `develop`); the deferred / Tier-B/C + follow-on work continues on `lint-rule-deferred-adoption` (branched + off `lint-rule-audit`). +- PR target: `develop`. +- This PR bundles two things: the tutorial-baseline refresh that the + merged `auto_estimate` background work (#193) made necessary, and this + audit document. + +## Methodology + +- **Baseline (current `develop` + baseline fix):** all three check tasks + pass clean — `pixi run py-lint-check` → _All checks passed!_, + `pixi run py-format-check` → _591 files already formatted_, + `pixi run docstring-lint-check` (pydoclint) → no findings. So every + violation below is attributable purely to a disabled rule. +- **Audit overlay (no file edits):** the disabled rules are enabled **at + the command line** against the _unmodified, tracked_ `pyproject.toml`, + so the working tree is never touched and no `git checkout`-style + restore is involved: + - `--extend-select A,FIX,SLF,T20,TD,D100,D104,DTZ005` turns on the + commented `select` families (`A`, `FIX`, `SLF`, `T20`, `TD`) plus + the three _Temporary_ global-ignore codes. (`--select` / + `--extend-select` override the global `ignore` list for an exact + code, which is why `D100`/`D104`/`DTZ005` are listed here rather + than removed from `ignore`.) + - `--config "lint.per-file-ignores = { … }"` **replaces** the + per-file-ignore table with the **structural-only** set, dropping the + _Temporary_ `tests/**` and `docs/**` entries while keeping the + documented ones (`*/__init__.py` → `F401`; `tests/**` → `ANN`, `D`, + `DOC`, `INP001`, `RUF012`, `RUF069`, `S101`; `docs/**` → `INP001`, + `RUF001-003`, `T201`; `docs/docs/tutorials/**` → `E402`). A bare + `--select` cannot lift a per-file-ignore, so the table is overridden + via `--config` instead of edited in place. +- **Exact commands** (run from the repo root; these reproduce the + 7144-row inventory and the fix counts exactly — 168 fixes total, of + which 121 are safe (`ruff --fix`) and 47 need `--unsafe-fixes`): + + ``` + # per-rule counts + pixi run ruff check src/ tests/ docs/docs/tutorials/ \ + --extend-select "A,FIX,SLF,T20,TD,D100,D104,DTZ005" \ + --config "lint.per-file-ignores = {'*/__init__.py' = ['F401'], 'tests/**' = ['ANN','D','DOC','INP001','RUF012','RUF069','S101'], 'docs/**' = ['INP001','RUF001','RUF002','RUF003','T201'], 'docs/docs/tutorials/**' = ['E402']}" \ + --statistics + + # full records (group by rule + path for the src/tests/tutorials split) + pixi run ruff check src/ tests/ docs/docs/tutorials/ \ + --extend-select "A,FIX,SLF,T20,TD,D100,D104,DTZ005" \ + --config "lint.per-file-ignores = {'*/__init__.py' = ['F401'], 'tests/**' = ['ANN','D','DOC','INP001','RUF012','RUF069','S101'], 'docs/**' = ['INP001','RUF001','RUF002','RUF003','T201'], 'docs/docs/tutorials/**' = ['E402']}" \ + --output-format=json + ``` + +- **Regeneration helper (added in P1.3):** the JSON-grouping step is + wrapped by a checked-in helper, `tools/lint_rule_audit.py`, so the + whole inventory regenerates with one non-destructive command — + `pixi run python tools/lint_rule_audit.py` — that builds the overlay + above and prints the per-rule/scope/fixability table. It never + modifies the tracked `pyproject.toml`. + +## Full violation inventory + +> **Snapshot.** This inventory is the disabled-rule state at the +> **start** of the audit, before any adoption. Rules enabled since +> (Priority 0; `D100`/`D104` in P1a; `DTZ005`/`TD004`/`TD005` in P1b; +> `I001`/`E501`/`F841` in P1c; `PLR0402`/`PLR1711`/`PLR6104` and the +> `PLW` family in D1; `T20` in D2) are now enforced and would no longer +> appear here — see the Adoption roadmap below. Re-run +> `pixi run python tools/lint_rule_audit.py` for live counts. + +7144 violations total across all disabled rules. Scope columns: `src` = +`src/`, `tst` = `tests/`, `tut` = `docs/docs/tutorials/`. `fix` = fixes +Ruff offers for the rule; the column counts **both** safe and unsafe +fixes. Of the 168 total fixes, **121 are safe** (applied by +`ruff --fix`: `I001` 114, `PLR0402` 5, `PLR1711` 2) and **47 need +`--unsafe-fixes`** (`PLW0108` 18, `T201` 17, `PLW1514` 5, `PLR6104` 4, +`W291` 2, `F841` 1). + +| Rule | Total | src | tst | tut | fix | Disabled via | Meaning | +| ------- | ----: | --: | ---: | --: | --: | --------------------------------- | ----------------------------------- | +| PLC0415 | 1948 | 0 | 1948 | 0 | 0 | tests-ignore | import not at top of file | +| SLF001 | 1830 | 517 | 1313 | 0 | 0 | select(SLF)+tests-ignore | private member accessed | +| PLR6301 | 1114 | 0 | 1114 | 0 | 0 | tests-ignore | method could be static (no `self`) | +| PLR2004 | 505 | 0 | 505 | 0 | 0 | tests-ignore | magic value in comparison | +| W505 | 252 | 0 | 189 | 63 | 0 | tests-ignore(W505)+docs-ignore(W) | doc line too long (>72) | +| ARG005 | 229 | 0 | 229 | 0 | 0 | tests-ignore | unused lambda argument | +| N812 | 152 | 0 | 152 | 0 | 0 | tests-ignore | lowercase imported as non-lowercase | +| ARG002 | 132 | 0 | 132 | 0 | 0 | tests-ignore | unused method argument | +| TD002 | 114 | 112 | 2 | 0 | 0 | select(TD) | missing TODO author | +| TD003 | 114 | 112 | 2 | 0 | 0 | select(TD) | missing TODO issue link | +| FIX002 | 114 | 112 | 2 | 0 | 0 | select(FIX) | line contains TODO | +| I001 | 114 | 0 | 114 | 0 | 114 | tests-ignore | import block unsorted | +| PLC2701 | 112 | 0 | 112 | 0 | 0 | tests-ignore | import of private name | +| ARG001 | 76 | 0 | 76 | 0 | 0 | tests-ignore | unused function argument | +| D100 | 59 | 34 | 0 | 25 | 0 | global-ignore + docs-ignore(D) | missing module docstring | +| PLC1901 | 50 | 0 | 50 | 0 | 0 | tests-ignore | `== ''` simplifiable | +| D104 | 45 | 45 | 0 | 0 | 0 | global-ignore | missing package docstring | +| A002 | 23 | 17 | 6 | 0 | 0 | select(A) | argument shadows builtin | +| ARG004 | 21 | 0 | 21 | 0 | 0 | tests-ignore | unused static-method argument | +| N801 | 21 | 0 | 21 | 0 | 0 | tests-ignore | class name not CapWords | +| PLW0108 | 18 | 0 | 18 | 0 | 18 | tests-ignore | unnecessary lambda | +| T201 | 17 | 14 | 3 | 0 | 17 | select(T20) | `print` found | +| A003 | 16 | 16 | 0 | 0 | 0 | select(A) | builtin shadowed by method | +| A001 | 9 | 0 | 4 | 5 | 0 | select(A) | variable shadows builtin | +| PLR0913 | 7 | 0 | 7 | 0 | 0 | tests-ignore | too many arguments | +| PLR0915 | 7 | 0 | 7 | 0 | 0 | tests-ignore | too many statements | +| PLC2801 | 5 | 0 | 5 | 0 | 0 | tests-ignore | unnecessary dunder call | +| PLR0402 | 5 | 0 | 5 | 0 | 5 | tests-ignore | manual `from` import | +| PLW1514 | 5 | 0 | 5 | 0 | 5 | tests-ignore | `read_text` without encoding | +| PLR6104 | 4 | 0 | 4 | 0 | 4 | tests-ignore | non-augmented assignment | +| TD004 | 4 | 4 | 0 | 0 | 0 | select(TD) | missing TODO colon | +| A006 | 3 | 0 | 3 | 0 | 0 | select(A) | lambda arg shadows builtin | +| E741 | 3 | 0 | 3 | 0 | 0 | tests-ignore | ambiguous variable name `l` | +| B018 | 2 | 0 | 2 | 0 | 0 | tests-ignore | useless expression | +| PLR1711 | 2 | 0 | 2 | 0 | 2 | tests-ignore | useless `return` | +| SIM117 | 2 | 0 | 2 | 0 | 0 | tests-ignore | nested `with` | +| TD005 | 2 | 2 | 0 | 0 | 0 | select(TD) | missing TODO description | +| W291 | 2 | 0 | 0 | 2 | 2 | docs-ignore(W) | trailing whitespace | +| DTZ005 | 1 | 1 | 0 | 0 | 0 | global-ignore | `datetime.now()` without tz | +| E501 | 1 | 0 | 1 | 0 | 0 | tests-ignore | line too long (>99) | +| F841 | 1 | 0 | 1 | 0 | 1 | tests-ignore | unused local variable | +| PLR0914 | 1 | 0 | 1 | 0 | 0 | tests-ignore | too many locals | +| PLR0917 | 1 | 0 | 1 | 0 | 0 | tests-ignore | too many positional args | +| TRY301 | 1 | 0 | 1 | 0 | 0 | tests-ignore | `raise` inside `try` | + +**Listed-but-clean ignores (zero violations today):** `B011`, `B017`, +`N805`, `PLE` are in the `tests/**` _Temporary_ ignore block but flag +nothing. `ANN` is in the `docs/**` _Temporary_ block but flags nothing +(tutorials are linear scripts with almost no annotated function +signatures). These can be dropped from the ignore lists at no cost. + +### By family + +| Family | Total | src | tst | tut | +| --------------- | ----: | --: | ---: | --: | +| PLC | 2115 | 0 | 2115 | 0 | +| SLF | 1830 | 517 | 1313 | 0 | +| PLR | 1646 | 0 | 1646 | 0 | +| ARG | 458 | 0 | 458 | 0 | +| W | 254 | 0 | 189 | 65 | +| TD | 234 | 230 | 4 | 0 | +| N | 173 | 0 | 173 | 0 | +| FIX | 114 | 112 | 2 | 0 | +| I | 114 | 0 | 114 | 0 | +| D | 104 | 79 | 0 | 25 | +| A | 51 | 33 | 13 | 5 | +| PLW | 23 | 0 | 23 | 0 | +| T | 17 | 14 | 3 | 0 | +| E | 4 | 0 | 4 | 0 | +| B/SIM/DTZ/TRY/F | 7 | 1 | 6 | 0 | + +The headline finding: **~92%** of all violations live in `tests/**`, and +most of them reflect deliberate, idiomatic test patterns rather than +defects. + +## Analysis and recommendations + +Recommendations are grouped into three tiers. + +### Tier A — Adopt now (clean wins, low effort, aligns with rigor) + +These are small, mostly mechanical, and improve real quality. Target: +one or two follow-up PRs. + +| Rule(s) | Scope | Count | Why adopt | Work | +| ------------------------------------------------------------------------------------------------------------------------------ | ----- | ----: | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `D100` | src | 34 | Module docstrings match the project's docstring rigor; helps MkDocs/IDE. | Add one-line module docstrings; remove `D100` from global ignore. | +| `D104` | src | 45 | Package (`__init__.py`) docstrings, same rationale. | Add short package docstrings; remove `D104`. | +| `DTZ005` | src | 1 | Single `datetime.now()` in `project/categories/info/default.py:177`. | Use a tz-aware timestamp or a justified inline `# noqa: DTZ005` with reason; remove `DTZ005`. | +| `I001` | tests | 114 | Import sorting is already enforced in `src/`; tests should match. **Auto-fixable.** | `ruff check --fix`; drop `I001` from tests-ignore. | +| `TD004`, `TD005` | src | 6 | Well-formed TODOs (colon + description) without demanding author/link. | Edit 6 TODO comments; enable only the `TD004`/`TD005` subset (not `TD002`/`TD003`). | +| `E501` | tests | 1 | One over-length line in `tests/.../io/test_ascii.py:3`. | Wrap it; drop `E501` from tests-ignore. | +| Fixable tests cluster — **safe:** `PLR0402`(5), `PLR1711`(2); **unsafe:** `PLW0108`(18), `PLW1514`(5), `PLR6104`(4), `F841`(1) | tests | 35 | Small, low-risk hygiene. | `ruff check --fix` clears the 7 safe ones; the 28 unsafe ones need `ruff check --fix --unsafe-fixes` (quick review). (`PLW1514` encoding fixes are a genuine portability win.) Drop all from tests-ignore. | +| Dead ignores: `B011`, `B017`, `N805`, `PLE`, `ANN`(docs) | — | 0 | Listed as _Temporary_ but flag nothing. | Remove from ignore lists — config cleanup, no code change. | + +### Tier B — Adopt with moderate effort / a judgment call + +Worth doing, but each needs review, not a blind `--fix`. + +| Rule(s) | Scope | Count | Notes | +| ------------------------------------------ | ----- | ----: | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `T20` (`T201`) | src | 14 | Mostly debug-style `print`s in calculators/fitting; **but** `display/plotters/ascii.py` legitimately prints chart output to the terminal. Recommend: convert the non-display prints to the project's output mechanism, add a targeted per-file-ignore for `display/plotters/**`, then enable `T20`. | +| `PLC1901` | tests | 50 | `x == ''` → `not x`. Mechanical and readable; enable for tests after fixing. | +| `PLR0913`, `PLR0915`, `PLR0914`, `PLR0917` | tests | 16 | A handful of long/wide test functions. Either refactor (extract helpers) or accept that integration tests are legitimately long and keep ignored. Low priority. | +| `PLC2801` | tests | 5 | `unnecessary-dunder-call`; check each — some dunder calls in tests are intentional behavioural assertions. | + +### Tier C — Keep disabled (intentional idioms, policy, or high noise) + +These are the bulk of the count. Enforcing them would fight deliberate +conventions or project policy. + +| Rule(s) | Scope | Count | Why keep disabled | +| ----------------------------------------- | --------- | ----: | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PLC0415` | tests | 1948 | Tests deliberately import inside functions to isolate state and exercise import-time behaviour. | +| `SLF001` | tests | 1313 | Tests legitimately reach into private members to assert internal state. This is the point of unit tests. | +| `PLR6301` | tests | 1114 | `no-self-use` fires on every `pytest` method that does not touch `self`; idiomatic test classes trip it constantly. Noise. | +| `PLR2004` | tests | 505 | Expected literals in assertions (`assert x == 3.526`) are not "magic values". | +| `ARG001/002/004/005` | tests | 458 | `pytest` fixtures requested for side effects and stub/mock signatures routinely leave arguments unused. | +| `PLC2701` | tests | 112 | Tests import private names of the module under test on purpose. | +| `N812` | tests | 152 | Deliberate convention: the module under test is imported as `MUT` (and helpers as `M`). Consistent and readable. Document the convention rather than fight it. | +| `N801` | tests | 21 | Test-double classes mirror lowercase domain attribute/category names (`_analysis`, `display`, `_fit`) to stand in for them. Intentional. | +| `SLF001` | src | 517 | Large; mostly legitimate intra-package access between cooperating objects. Worth a _separate, focused_ review later, not a blanket enable. | +| `FIX002`, `TD002`, `TD003` | src | 338 | TODOs are intentional tracking; `AGENTS.md` forbids removing unresolved TODOs, and the project does not require author/issue-link annotations. | +| `A` (`A001/2/3/6`, the `id`/`type` cases) | src+tests | ~45 | `id` and `type` are CIF field names; `AGENTS.md` mandates CIF-aligned naming. Shadowing here is deliberate API design. (The few non-CIF cases — `iter`, `range` — can be renamed opportunistically, but the family is not worth enabling project-wide.) | +| `W505`, `D100` | tutorials | 88 | Tutorials carry narration in Markdown cells and use longer prose lines; module docstrings and 72-col doc lines do not fit the notebook-source format. | +| `B018` | tests | 2 | Bare expressions in tests are intentional (asserting attribute access / property side effects). | +| `E741`, `SIM117`, `TRY301` | tests | 6 | Tiny counts; `l` as a loop index, nested `with`, and `raise`-in-`try` are acceptable in test scaffolding. Not worth the churn. | + +## Decisions already made + +These are the only firm decisions; everything in +[Tier C](#tier-c--keep-disabled-intentional-idioms-policy-or-high-noise) +and the policy notes in the [ADR](#adr) section are **recommendations**, +not decisions. + +- **Rule enablement is incremental and reviewed.** This branch has + landed the Priority 0 cleanup (R1), all of Priority 1 — P1a + (`D100`/`D104` docstrings), P1b (`DTZ005`, `TD004`/`TD005`), P1c + (`I001`/`E501`/`F841` test hygiene) — and two Tier-B adoptions: D1 + (`PLR`/`PLW` sub-codes in tests) and D2 (`T20`; src diagnostics routed + through the `Logger`). Each remaining tier is implemented only after + explicit approval, per the roadmap below. +- **Measurement method** is a non-destructive command-line overlay on + the _unmodified_ `pyproject.toml` (see [Methodology](#methodology)), + not literal one-at-a-time toggling — equivalent results because + `ruff check` rules do not interact without `--fix`. +- **Structural ignores stay.** `COM812`, `DOC`, `D200`, and the + documented `tests`/`docs` ignores (`ANN`/`D`/`S101`/…) are out of + scope; they have standing reasons in `pyproject.toml`. + +Recorded as a recommendation (not yet decided): the audit _supports_ +keeping `tests/**` mostly permissive, but that judgement is deferred to +review and, where it conflicts with existing policy, to a follow-up ADR +(see the [ADR](#adr) section). Only the small, clean Tier-A/B items are +proposed for un-ignoring. + +## Open questions (for the reviewer) + +1. **Scope of this PR:** _resolved_ — this branch lands the Priority 0 + cleanup (R1), all of Priority 1 (P1a, P1b, P1c), and two Tier-B + adoptions D1 (`PLR`/`PLW` sub-codes) and D2/R5 (`T20`). The remaining + items (R6 `PLC1901`, R7 src `SLF001`, and the test-complexity rules) + are **recommended to stay disabled** — see the Adoption roadmap and + Tier B/C rationale — pending any final user/ADR sign-off. +2. **`T201` in `display/plotters/ascii.py`:** _resolved_ in D2 — those + prints are the intended terminal-output path and are now + per-file-ignored (`display/plotters/ascii.py`, `project/display.py`); + the other src `print()` diagnostics were routed through the `Logger`. +3. **`N812` `MUT` convention:** keep ignored and document it (in + `AGENTS.md` §Testing), or adopt `flake8-import-conventions` aliases + instead? +4. **src `SLF001` (517):** schedule a dedicated review pass, or accept + as intentional intra-package access and leave disabled? +5. **TODO policy:** _resolved_ — the `TD004`/`TD005` _formatting_ subset + (not author/link) is enabled in P1b; `TD002`/`TD003` stay off, so the + TODOs themselves are kept per `AGENTS.md`. + +## Concrete files likely to change + +- Landed in this branch: `tests/tutorials/baseline.json`, this plan + file, `tools/lint_rule_audit.py` (helper) + its unit test + `tests/unit/tools/test_lint_rule_audit.py`, `pyproject.toml` + (`[tool.ruff.lint]` — Priority 0 ignores removed; `D100`/`D104` and + `TD004`/`TD005` enabled; `DTZ005` un-ignored), the 34 module + 45 + `__init__.py` docstrings (P1a), the `DTZ005` timestamp fix plus four + TODO-comment fixes (P1b), and sorted imports + an `E501`/`F841` fix + across ~60 test files (P1c); the `tests/**` PLR/PLW ignore split plus + the `PLR0402`/`PLR1711`/`PLR6104`/`PLW0108`/`PLW1514` fixes across ~17 + test files (D1); and `T20` enabled, with src `print()` diagnostics + converted to `log` calls (calculators/fitting/singleton/datablocks) + and the two display sinks per-file-ignored (D2). +- Not planned (recommended keep-disabled, see Adoption roadmap): R6 + `PLC1901`, the test-complexity rules, and R7 src `SLF001`. + +## Implementation steps (Phase 1) + +- [x] **P1.1 — Refresh tutorial baselines for `auto_estimate`.** Re-ran + ed-2/ed-6/ed-20, regenerated their entries in + `tests/tutorials/baseline.json`. Commit: + `Update tutorial baselines for auto_estimate background`. +- [x] **P1.2 — Add this audit + roadmap document.** Commit: + `Add lint-rule audit and adoption roadmap`. +- [x] **P1.3 — Add `tools/lint_rule_audit.py` regeneration helper.** + Builds the non-destructive `ruff check` overlay from + [Methodology](#methodology) and prints the + per-rule/scope/fixability table; never modifies the tracked + `pyproject.toml`. **Structure it for testability:** a _pure_ + `aggregate(records)` function (plus a `scope(filename)` helper and + the family rollup) that takes parsed Ruff JSON records and returns + the per-rule/scope/fixability totals, kept separate from the thin + CLI shim that invokes Ruff via `subprocess` and prints. The Phase + 2 unit tests target the pure functions only (no subprocess, no + Ruff run). Commit: `Add lint-rule audit regeneration helper`. +- [x] **P1.4 — Phase 1 review gate.** No code; await review. + +## Adoption roadmap + +Adopted in this branch (reviewed step by step): + +- [x] **R1 / Priority 0 — Config cleanup:** removed the dead ignores + `B011`, `B017`, `N805`, `PLE`, and docs `ANN` (all had 0 + violations). +- [x] **R3 / P1a — Source docstrings:** enabled `D100`/`D104` and added + the 79 missing module and package docstrings. +- [x] **P1b — Misc src wins:** fixed `DTZ005` (naive `datetime.now()` → + UTC-aware, also correcting a latent local-vs-UTC bug) and enabled + `TD004`/`TD005`, fixing the four flagged TODO comments. +- [x] **P1c — Tests clean items:** enabled `I001` (114 imports sorted + via safe `ruff --fix`), `E501` (wrapped one over-long docstring), + and `F841` (removed one dead assignment). `W291` skipped. +- [x] **D1 — PLR/PLW sub-codes:** split the `tests/**` `PLR` family + ignore (kept the `PLR2004`/`PLR6301` idioms and the complexity + rules ignored; enforced the rest, fixing `PLR0402`/`PLR1711`/ + `PLR6104`) and dropped the `PLW` family ignore, fixing `PLW0108` + (incl. two monkeypatch-adapter false positives) and `PLW1514`. +- [x] **D2 / R5 — `T20` (no `print`):** enabled `T20`; routed the src + `print()` diagnostics (calculators, fitting, singleton, + datablocks) through the `Logger` (`log.warning`/`log.debug`), + per-file-ignored the two intentional display sinks + (`display/plotters/ascii.py`, `project/display.py`), and allowed + `print` in tests. + +Remaining — **recommended to stay disabled** (grounded in the Tier-B/C +analysis); revisit only if a future need arises: + +- **R6 — `PLC1901`** (tests): marginal value — `== ''` is explicit and + safe, whereas `not x` conflates with `None`/`0`. Keep disabled. +- **Test complexity** (`PLR0913`/`PLR0914`/`PLR0915`/`PLR0917`): the + complexity ADR targets `src`; forcing the ~16 long _test_ functions to + split usually hurts test locality. Keep the standing test exception. +- **R7 — src `SLF001`** (517): blanket-enabling would force public-API + bloat or `# noqa` noise. A _targeted_ review of genuine + cross-subsystem reach-ins could help someday, but that is a separate + investigation, not a rule adoption. + +Deferred — none outstanding. The `PLR`/`PLW` sub-codes that needed the +`tests/**` family-ignore split (`PLR0402`, `PLR1711`, `PLR6104`, +`PLW0108`, `PLW1514`) were adopted in **D1** above. + +## Phase 2 — Verification + +The **full standard verification set is mandatory** for this PR (per +`AGENTS.md` non-trivial-change workflow) and for every future adoption +batch. It is not optional or narrowed: P1.1 edits +`tests/tutorials/baseline.json`, P1.3 adds a helper script, and P1.2 +adds a developer document, so the whole set runs. + +**Tests to add first.** Per the two-phase workflow, the helper's tests +are written in Phase 2 (not Phase 1). Add +`tests/unit/tools/test_lint_rule_audit.py` — mirroring the existing +`tests/unit/tools/test_bump_vendored_js.py` precedent for testing a +`tools/` script — covering the helper's _pure_ aggregation logic with +representative, synthetic Ruff JSON records (no subprocess, no network, +no real Ruff run, per `AGENTS.md` §Testing): + +- per-rule total counts; +- `scope()` bucketing into `src` / `tests` / `tutorials` from filenames; +- fixability counting (records whose `fix` field is non-null); +- family rollup (leading-alpha-prefix grouping, e.g. `PLC0415` → `PLC`); +- edge cases: an empty record list, and a path outside the three scopes. + +`AGENTS.md` §Testing requires every new module to ship with tests; the +two-phase workflow simply places that test-writing here in Phase 2. +(`tools/` is outside the `test_structure_check.py` mirror, which only +covers `src/easydiffraction/`, so no structure-check entry is needed.) + +After the tests are added, run the mandatory set in order, fixing and +re-running until clean. Use the zsh-safe capture pattern where output +must be saved: + +``` +pixi run fix +pixi run check > /tmp/easydiffraction-check.log 2>&1; check_exit_code=$?; tail -n 200 /tmp/easydiffraction-check.log; exit $check_exit_code +pixi run unit-tests +pixi run integration-tests +pixi run script-tests +``` + +**Additional, baseline-specific checks** — these _supplement_ the +mandatory set above; they do not replace any of it: + +- `pixi run script-tests-checked` — a superset of `script-tests`: it + `depends-on` `script-tests` (so it runs the tutorials first) and then + additionally asserts the refreshed `tests/tutorials/baseline.json`. + Running it satisfies the mandatory `script-tests` step _and_ confirms + the baseline fix; it is the recommended way to cover both for this PR. +- `pixi run python tools/lint_rule_audit.py` — regenerates the inventory + in this document from the current tree (sanity-check after P1.3). +- `pixi run py-lint-check` / `py-format-check` / `docstring-lint-check` + — already inside `pixi run check`; listed only for a quick local + confirmation that nothing introduced lint/format/docstring drift. + +## Suggested Pull Request + +**Title:** Refresh tutorial baselines and draft a lint-rule adoption +roadmap + +**Description:** Updates the saved tutorial fit-result baselines so the +automatic-background feature's slightly different (and correct) results +pass the tutorial checks again. Adds a developer document that +inventories every disabled code-style rule, measures how many issues +each would surface today, and recommends — rule by rule — which to +switch on (with fixes) and which to keep off because they reflect +deliberate, well-reasoned conventions, plus a small helper +(`tools/lint_rule_audit.py`) that regenerates that inventory on demand. +As concrete first steps it also enforces the "Priority 0" cleanup +(removing five ignores that currently flag nothing — `B011`, `B017`, +`N805`, `PLE`, docs `ANN`) and all of "Priority 1": the source-docstring +rules `D100`/`D104` (adding the 79 missing module and package +docstrings), a couple of source fixes (`DTZ005` and the `TD004`/`TD005` +TODO-formatting rules), and test hygiene (`I001` import sorting plus +`E501`/`F841`); plus two Tier-B wins — enforcing more pytest-lint rules +(the `PLR`/`PLW` sub-codes in tests) and routing the library's stray +diagnostic `print()`s through its logging system (`T20`). It sets up a +clear, low-risk path to gradually raise code quality; the remaining +Tier-B/C rules are documented as deliberate keep-disabled +recommendations rather than planned work. + +## Appendix: all rules by priority (quick reference) + +Every rule code that fired in the audit (plus the dead ignores), grouped +by priority. Scope counts are `src` / `tests` / `tut`; **[fix]** = safe +auto-fix (`ruff --fix`), **[fix!]** = needs `--unsafe-fixes`. This +condenses the +[Analysis and recommendations](#analysis-and-recommendations) tiers into +a single lookup. + +### Priority 0 — Free cleanup (0 violations today; just remove from ignore) + +| Rule | Description | Scope | Recommendation | +| -------------- | --------------------------- | -------- | --------------------------------------------------------- | +| `B011` | `assert False` in code | tests | Drop from ignore — costs nothing | +| `B017` | assert on blind `Exception` | tests | Drop from ignore | +| `N805` | first method arg not `self` | tests | Drop from ignore | +| `PLE` (family) | pylint **errors** | tests | Drop from ignore — real errors should never be silenced | +| `ANN` (family) | missing type annotations | docs/tut | Drop from docs ignore (tutorials have ~no annotated defs) | + +### Priority 1 — Adopt now (clean wins, low effort) + +| Rule | Description | Scope (count) | Recommendation | +| --------- | ---------------------------- | --------------------- | ---------------------------------------------------------- | +| `I001` | unsorted import block | tests (114) **[fix]** | Adopt — `ruff --fix` sorts them (safe) | +| `D104` | missing package docstring | src (45) | Adopt — add `__init__.py` docstrings | +| `D100` | missing module docstring | src (34) | Adopt **for src** — add one-liners | +| `PLW0108` | unnecessary lambda | tests (18) **[fix!]** | Adopt — `--unsafe-fixes` (quick review) | +| `TD004` | missing colon in TODO | src (4) | Adopt — TODO formatting subset | +| `PLW1514` | `read_text` without encoding | tests (5) **[fix!]** | Adopt — `--unsafe-fixes`; portability win | +| `PLR0402` | manual `from` import | tests (5) **[fix]** | Adopt — `ruff --fix` (safe) | +| `PLR6104` | non-augmented assignment | tests (4) **[fix!]** | Adopt — `--unsafe-fixes` (quick review) | +| `TD005` | missing TODO description | src (2) | Adopt — TODO formatting subset | +| `PLR1711` | useless `return` | tests (2) **[fix]** | Adopt — `ruff --fix` (safe) | +| `W291` | trailing whitespace | tut (2) **[fix!]** | Adopt — `--unsafe-fixes`; keep `W505` ignored in tutorials | +| `DTZ005` | `datetime.now()` without tz | src (1) | Adopt — fix the one case (or justified `# noqa`) | +| `E501` | line too long (>99) | tests (1) | Adopt — wrap the single line | +| `F841` | unused local variable | tests (1) **[fix!]** | Adopt — `--unsafe-fixes` (quick review) | + +### Priority 2 — Adopt with judgment (review each, not a blind fix) + +| Rule | Description | Scope (count) | Recommendation | +| --------- | ------------------------ | ------------------------------ | ---------------------------------------------------------------------------------------------------- | +| `PLC1901` | `x == ''` simplifiable | tests (50) | Adopt after rewriting to `not x` | +| `T201` | `print` found | src (14), tests (3) **[fix!]** | Adopt **after** per-file-ignoring `display/plotters/ascii.py` (legit terminal output); fix is unsafe | +| `PLR0915` | too many statements | tests (7) | Refactor or keep — governed by complexity ADR | +| `PLR0913` | too many arguments | tests (7) | Refactor or keep — complexity ADR | +| `PLC2801` | unnecessary dunder call | tests (5) | Review each — some are intentional behavioural asserts | +| `PLR0914` | too many locals | tests (1) | Refactor or keep — complexity ADR | +| `PLR0917` | too many positional args | tests (1) | Refactor or keep — complexity ADR | + +### Priority 3 — Keep disabled (intentional idioms, policy, or high noise) + +| Rule | Description | Scope (count) | Recommendation | +| --------- | -------------------------------------- | ----------------------- | --------------------------------------------------------------------------------- | +| `PLC0415` | import not at top level | tests (1948) | Keep — tests import inside functions to isolate state | +| `SLF001` | private member accessed | src (517), tests (1313) | Keep in tests (the point of unit tests); src warrants a _separate_ focused review | +| `PLR6301` | method could be static (`no-self-use`) | tests (1114) | Keep — fires on every `pytest` method; pure noise | +| `PLR2004` | magic value in comparison | tests (505) | Keep — expected literals in assertions aren't "magic" | +| `ARG005` | unused lambda argument | tests (229) | Keep — stub/mock signatures | +| `N812` | lowercase imported as non-lowercase | tests (152) | Keep — deliberate `MUT` (module-under-test) convention; document it | +| `ARG002` | unused method argument | tests (132) | Keep — stub/mock signatures | +| `PLC2701` | import of private name | tests (112) | Keep — tests import internals on purpose | +| `FIX002` | line contains TODO | src (112) | Keep — TODOs are intentional tracking (`AGENTS.md`) | +| `TD002` | missing TODO author | src (112) | Keep — project doesn't require authors | +| `TD003` | missing TODO issue link | src (112) | Keep — project doesn't require links | +| `ARG001` | unused function argument | tests (76) | Keep — `pytest` fixtures used for side effects | +| `ARG004` | unused static-method argument | tests (21) | Keep — stub/mock signatures | +| `N801` | class name not CapWords | tests (21) | Keep — test doubles mirror lowercase domain names (`_analysis`, `display`) | +| `A002` | argument shadows builtin | src (17), tests (6) | Keep — `id`/`type` are CIF field names (CIF-aligned naming) | +| `A003` | builtin shadowed by method | src (16) | Keep — CIF `id`/`type` properties | +| `W505` | doc line too long (>72) | tests (189), tut (63) | Keep — tutorial narration / longer prose lines | +| `A001` | variable shadows builtin | tests (4), tut (5) | Keep — CIF `id` / notebook `display` builtin | +| `D100` | missing module docstring | tut (25) | Keep **for tutorials** — they use Markdown cells | +| `A006` | lambda arg shadows builtin | tests (3) | Keep — CIF `id` | +| `E741` | ambiguous variable name `l` | tests (3) | Keep — trivial test scaffolding | +| `B018` | useless expression | tests (2) | Keep — intentional attribute-access / property tests | +| `SIM117` | nested `with` statements | tests (2) | Keep — not worth the churn | +| `TRY301` | `raise` inside `try` | tests (1) | Keep — trivial | + +Parent families currently commented out in `select`: `A`, `FIX`, `SLF`, +`T20`, `TD` — their subrules appear above. Structural ignores left +untouched (documented reasons, out of scope): `COM812`, `DOC`, `D200`, +and the `tests`/`docs` `ANN`/`D`/`S101`/… set. diff --git a/pyproject.toml b/pyproject.toml index 89e23d7bd..4ef0ad73f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -291,10 +291,12 @@ select = [ 'SIM', # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim #'SLF', # https://docs.astral.sh/ruff/rules/#flake8-self-slf 'SLOT', # https://docs.astral.sh/ruff/rules/#flake8-slots-slot - #'T20', # https://docs.astral.sh/ruff/rules/#flake8-print-t20 - 'TC', # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc + 'T20', # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + 'TC', # https://docs.astral.sh/ruff/rules/#flake8-type-checking-tc #'TD', # https://docs.astral.sh/ruff/rules/#flake8-todos-td - 'TID', # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid + 'TD004', # https://docs.astral.sh/ruff/rules/missing-todo-colon/ (TODO-format subset) + 'TD005', # https://docs.astral.sh/ruff/rules/missing-todo-description/ (TODO-format subset) + 'TID', # https://docs.astral.sh/ruff/rules/#flake8-tidy-imports-tid ] # Exceptions to the linting rules @@ -306,10 +308,6 @@ ignore = [ 'DOC', # https://docs.astral.sh/ruff/rules/#pydoclint-doc # Disable, as [tool.format_docstring] split one-line docstrings into the canonical multi-line layout 'D200', # https://docs.astral.sh/ruff/rules/unnecessary-multiline-docstring/ - # Temporary: - 'D100', # https://docs.astral.sh/ruff/rules/undocumented-public-module/#undocumented-publi-module-d100 - 'D104', # https://docs.astral.sh/ruff/rules/undocumented-public-package/#undocumented-public-package-d104 - 'DTZ005', # https://docs.astral.sh/ruff/rules/call-datetime-now-without-tzinfo/#call-datetime-now-without-tzinfo-dtz005 ] # Ignore specific rules in certain files or directories @@ -325,25 +323,26 @@ ignore = [ 'RUF012', # https://docs.astral.sh/ruff/rules/mutable-class-default/ (test stubs use mutable defaults) 'RUF069', # https://docs.astral.sh/ruff/rules/unreliable-float-equality/ (exact comparisons in assertions) 'S101', # https://docs.astral.sh/ruff/rules/assert/ + 'T20', # tests may print debug/scaffolding output # Temporary: 'ARG001', 'ARG002', 'ARG004', 'ARG005', - 'B011', - 'B017', 'B018', - 'E501', 'E741', - 'F841', - 'I001', 'N801', - 'N805', 'N812', 'PLC', - 'PLE', - 'PLR', - 'PLW', + # Keep the test idioms (PLR2004 magic values, PLR6301 no-self-use) and + # the ADR-governed complexity rules ignored; the rest of PLR (e.g. + # PLR0402/PLR1711/PLR6104) is enforced. PLW is fully enforced. + 'PLR0913', + 'PLR0914', + 'PLR0915', + 'PLR0917', + 'PLR2004', + 'PLR6301', 'SIM117', 'SLF', 'TRY', @@ -356,13 +355,20 @@ ignore = [ 'RUF003', # https://docs.astral.sh/ruff/rules/ambiguous-unicode-character-comment/ (en-dashes in headings) 'T201', # https://docs.astral.sh/ruff/rules/print/ # Temporary: - 'ANN', 'D', 'W', ] 'docs/docs/tutorials/**' = [ 'E402', # https://docs.astral.sh/ruff/rules/module-import-not-at-top-of-file/ ] +# Intentional terminal rendering: these write raw/ASCII output that +# `Console.print` would garble, so `print` is deliberate here. +'src/easydiffraction/display/plotters/ascii.py' = [ + 'T201', # https://docs.astral.sh/ruff/rules/print/ +] +'src/easydiffraction/project/display.py' = [ + 'T201', # https://docs.astral.sh/ruff/rules/print/ +] # Specific options for certain rules diff --git a/src/easydiffraction/__init__.py b/src/easydiffraction/__init__.py index 8b73614c1..8e4569c30 100644 --- a/src/easydiffraction/__init__.py +++ b/src/easydiffraction/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""EasyDiffraction public API for diffraction analysis.""" from __future__ import annotations diff --git a/src/easydiffraction/__main__.py b/src/easydiffraction/__main__.py index bbd8ffc3e..b7d39c87d 100644 --- a/src/easydiffraction/__main__.py +++ b/src/easydiffraction/__main__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Typer command-line interface for the EasyDiffraction library.""" from __future__ import annotations diff --git a/src/easydiffraction/analysis/__init__.py b/src/easydiffraction/analysis/__init__.py index 12e8f82e4..ae480da32 100644 --- a/src/easydiffraction/analysis/__init__.py +++ b/src/easydiffraction/analysis/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Fitting analysis: minimizers, fit parameters, and results.""" from easydiffraction.analysis.analysis import UndoFitOutcome from easydiffraction.analysis.categories.fit_parameter_correlations import ( diff --git a/src/easydiffraction/analysis/analysis.py b/src/easydiffraction/analysis/analysis.py index 38afe0f39..128bb72e9 100644 --- a/src/easydiffraction/analysis/analysis.py +++ b/src/easydiffraction/analysis/analysis.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Analysis orchestration of fitting, parameters, and results.""" from __future__ import annotations diff --git a/src/easydiffraction/analysis/calculators/__init__.py b/src/easydiffraction/analysis/calculators/__init__.py index 38bd1aa02..c6629aee4 100644 --- a/src/easydiffraction/analysis/calculators/__init__.py +++ b/src/easydiffraction/analysis/calculators/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""CrysFML, CrysPy, and PDFfit calculation backends.""" from easydiffraction.analysis.calculators.crysfml import CrysfmlCalculator from easydiffraction.analysis.calculators.cryspy import CryspyCalculator diff --git a/src/easydiffraction/analysis/calculators/base.py b/src/easydiffraction/analysis/calculators/base.py index b0e74ba57..e9c2c6bb8 100644 --- a/src/easydiffraction/analysis/calculators/base.py +++ b/src/easydiffraction/analysis/calculators/base.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Abstract base API for diffraction calculation backends.""" from __future__ import annotations diff --git a/src/easydiffraction/analysis/calculators/crysfml.py b/src/easydiffraction/analysis/calculators/crysfml.py index 72e645f17..34e8d0cb1 100644 --- a/src/easydiffraction/analysis/calculators/crysfml.py +++ b/src/easydiffraction/analysis/calculators/crysfml.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""CrysFML calculation backend for powder diffraction patterns.""" from __future__ import annotations @@ -12,6 +13,7 @@ from easydiffraction.analysis.calculators.factory import CalculatorFactory from easydiffraction.core.metadata import TypeInfo from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum +from easydiffraction.utils.logging import log if TYPE_CHECKING: from easydiffraction.datablocks.experiment.collection import Experiments @@ -137,7 +139,7 @@ def calculate_pattern( try: y = self._calculate_adjusted_pattern(crysfml_dict, experiment) except KeyError: - print('[CrysfmlCalculator] Error: No calculated data') + log.warning('[CrysfmlCalculator] No calculated data') y = [] return np.asarray(y) @@ -164,9 +166,7 @@ def _calculate_raw_pattern( if experiment.type.beam_mode.value == BeamModeEnum.TIME_OF_FLIGHT: _, y = cfml_py_utilities.tof_powder_pattern_from_dict(crysfml_dict) return y - print( - f'[CrysfmlCalculator] Error: Unsupported beam mode {experiment.type.beam_mode.value}' - ) + log.warning(f'[CrysfmlCalculator] Unsupported beam mode {experiment.type.beam_mode.value}') return None def _adjust_pattern_length( # noqa: PLR6301 diff --git a/src/easydiffraction/analysis/calculators/cryspy.py b/src/easydiffraction/analysis/calculators/cryspy.py index fdd4bda46..ff4c33c75 100644 --- a/src/easydiffraction/analysis/calculators/cryspy.py +++ b/src/easydiffraction/analysis/calculators/cryspy.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""CrysPy calculation backend for diffraction patterns.""" from __future__ import annotations @@ -18,6 +19,7 @@ from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum from easydiffraction.datablocks.experiment.item.enums import PeakProfileTypeEnum from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum +from easydiffraction.utils.logging import log from easydiffraction.utils.utils import sin_theta_over_lambda_to_d_spacing if TYPE_CHECKING: @@ -165,7 +167,7 @@ def calculate_structure_factors( y_calc = cryspy_in_out_dict[cryspy_block_name]['intensity_calc'] stol = cryspy_in_out_dict[cryspy_block_name]['sthovl'] except KeyError: - print(f'[CryspyCalculator] Error: No calculated data for {cryspy_block_name}') + log.warning(f'[CryspyCalculator] No calculated data for {cryspy_block_name}') return [], [] return stol, y_calc @@ -255,7 +257,7 @@ def calculate_pattern( if beam_mode in prefixes: cryspy_block_name = f'{prefixes[beam_mode]}_{experiment.name}' else: - print(f'[CryspyCalculator] Error: Unknown beam mode {experiment.type.beam_mode.value}') + log.warning(f'[CryspyCalculator] Unknown beam mode {experiment.type.beam_mode.value}') return [] try: @@ -267,7 +269,7 @@ def calculate_pattern( f'dict_in_out_{structure.name}' ) except KeyError: - print(f'[CryspyCalculator] Error: No calculated data for {cryspy_block_name}') + log.warning(f'[CryspyCalculator] No calculated data for {cryspy_block_name}') return [] return y_calc diff --git a/src/easydiffraction/analysis/calculators/pdffit.py b/src/easydiffraction/analysis/calculators/pdffit.py index 0f0193190..17d7d0f86 100644 --- a/src/easydiffraction/analysis/calculators/pdffit.py +++ b/src/easydiffraction/analysis/calculators/pdffit.py @@ -19,6 +19,7 @@ from easydiffraction.analysis.calculators.base import CalculatorBase from easydiffraction.analysis.calculators.factory import CalculatorFactory from easydiffraction.core.metadata import TypeInfo +from easydiffraction.utils.logging import log if TYPE_CHECKING: from easydiffraction.datablocks.experiment.item.base import ExperimentBase @@ -89,7 +90,7 @@ def calculate_structure_factors( # noqa: PLR6301 # PDF doesn't compute HKL but we keep interface consistent # Intentionally unused, required by public API/signature del structures, experiments - print('[pdffit] Calculating HKLs (not applicable)...') + log.debug('[pdffit] Calculating HKLs (not applicable)') return [] def calculate_pattern( # noqa: PLR6301 diff --git a/src/easydiffraction/analysis/categories/__init__.py b/src/easydiffraction/analysis/categories/__init__.py index 54fea54e5..fa11ec41e 100644 --- a/src/easydiffraction/analysis/categories/__init__.py +++ b/src/easydiffraction/analysis/categories/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""CIF categories for fitting analysis configuration.""" from easydiffraction.analysis.categories.aliases import Alias from easydiffraction.analysis.categories.aliases import Aliases diff --git a/src/easydiffraction/analysis/categories/aliases/__init__.py b/src/easydiffraction/analysis/categories/aliases/__init__.py index 6ca3a8594..ec8c39b7d 100644 --- a/src/easydiffraction/analysis/categories/aliases/__init__.py +++ b/src/easydiffraction/analysis/categories/aliases/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Parameter alias categories for fit expressions.""" from easydiffraction.analysis.categories.aliases.default import Alias from easydiffraction.analysis.categories.aliases.default import Aliases diff --git a/src/easydiffraction/analysis/categories/aliases/default.py b/src/easydiffraction/analysis/categories/aliases/default.py index d54670245..5e5f5f5b9 100644 --- a/src/easydiffraction/analysis/categories/aliases/default.py +++ b/src/easydiffraction/analysis/categories/aliases/default.py @@ -40,7 +40,7 @@ def __init__(self) -> None: name='label', description='Human-readable alias for a parameter.', value_spec=AttributeSpec( - default='_', # TODO, Maybe None? + default='_', # TODO: Maybe None? validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), ), cif_handler=CifHandler( diff --git a/src/easydiffraction/analysis/categories/constraints/__init__.py b/src/easydiffraction/analysis/categories/constraints/__init__.py index 97d8c03c2..36996138f 100644 --- a/src/easydiffraction/analysis/categories/constraints/__init__.py +++ b/src/easydiffraction/analysis/categories/constraints/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Parameter constraint categories for fitting.""" from easydiffraction.analysis.categories.constraints.default import Constraint from easydiffraction.analysis.categories.constraints.default import Constraints diff --git a/src/easydiffraction/analysis/categories/constraints/default.py b/src/easydiffraction/analysis/categories/constraints/default.py index b566ea378..dc64c1776 100644 --- a/src/easydiffraction/analysis/categories/constraints/default.py +++ b/src/easydiffraction/analysis/categories/constraints/default.py @@ -48,7 +48,7 @@ def __init__(self) -> None: name='expression', description='Constraint equation, e.g. "occ_Ba = 1 - occ_La".', value_spec=AttributeSpec( - default='_', # TODO, Maybe None? + default='_', # TODO: Maybe None? validator=RegexValidator(pattern=r'.*'), ), cif_handler=CifHandler( diff --git a/src/easydiffraction/analysis/categories/fit_parameter_correlations/__init__.py b/src/easydiffraction/analysis/categories/fit_parameter_correlations/__init__.py index bbb74736d..a1206a472 100644 --- a/src/easydiffraction/analysis/categories/fit_parameter_correlations/__init__.py +++ b/src/easydiffraction/analysis/categories/fit_parameter_correlations/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Fit parameter correlation categories from fit results.""" from easydiffraction.analysis.categories.fit_parameter_correlations.default import ( FitParameterCorrelationItem, diff --git a/src/easydiffraction/analysis/categories/fit_parameters/__init__.py b/src/easydiffraction/analysis/categories/fit_parameters/__init__.py index 64e72b416..dca45e2ac 100644 --- a/src/easydiffraction/analysis/categories/fit_parameters/__init__.py +++ b/src/easydiffraction/analysis/categories/fit_parameters/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Refined fit parameter categories with values and errors.""" from easydiffraction.analysis.categories.fit_parameters.default import FitParameterItem from easydiffraction.analysis.categories.fit_parameters.default import FitParameters diff --git a/src/easydiffraction/analysis/categories/fit_result/__init__.py b/src/easydiffraction/analysis/categories/fit_result/__init__.py index 6bc137ee8..fa80a2356 100644 --- a/src/easydiffraction/analysis/categories/fit_result/__init__.py +++ b/src/easydiffraction/analysis/categories/fit_result/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Fit-result categories for least-squares and Bayesian fits.""" from easydiffraction.analysis.categories.fit_result.base import FitResultBase from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult diff --git a/src/easydiffraction/analysis/categories/joint_fit/__init__.py b/src/easydiffraction/analysis/categories/joint_fit/__init__.py index 9c7c49f98..2025a0bae 100644 --- a/src/easydiffraction/analysis/categories/joint_fit/__init__.py +++ b/src/easydiffraction/analysis/categories/joint_fit/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Joint-fit categories for fitting multiple experiments together.""" from __future__ import annotations diff --git a/src/easydiffraction/analysis/categories/joint_fit/default.py b/src/easydiffraction/analysis/categories/joint_fit/default.py index f4a904bbf..fbc37e2c2 100644 --- a/src/easydiffraction/analysis/categories/joint_fit/default.py +++ b/src/easydiffraction/analysis/categories/joint_fit/default.py @@ -32,7 +32,7 @@ def __init__(self) -> None: self._experiment_id: StringDescriptor = StringDescriptor( name='experiment_id', - description='Experiment identifier', # TODO + description='Experiment identifier', # TODO: revisit description value_spec=AttributeSpec( default='_', validator=RegexValidator(pattern=r'^[A-Za-z_][A-Za-z0-9_]*$'), @@ -44,7 +44,7 @@ def __init__(self) -> None: ) self._weight: NumericDescriptor = NumericDescriptor( name='weight', - description='Weight factor', # TODO + description='Weight factor', # TODO: revisit description value_spec=AttributeSpec( default=0.0, validator=RangeValidator(), diff --git a/src/easydiffraction/analysis/categories/sequential_fit/__init__.py b/src/easydiffraction/analysis/categories/sequential_fit/__init__.py index 5381b7e1e..2aa97e98b 100644 --- a/src/easydiffraction/analysis/categories/sequential_fit/__init__.py +++ b/src/easydiffraction/analysis/categories/sequential_fit/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Sequential-fit category for fitting experiments one by one.""" from __future__ import annotations diff --git a/src/easydiffraction/analysis/categories/sequential_fit_extract/__init__.py b/src/easydiffraction/analysis/categories/sequential_fit_extract/__init__.py index ff6ba0563..2c88087a5 100644 --- a/src/easydiffraction/analysis/categories/sequential_fit_extract/__init__.py +++ b/src/easydiffraction/analysis/categories/sequential_fit_extract/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Categories for extracted results of a sequential fit.""" from __future__ import annotations diff --git a/src/easydiffraction/analysis/fit_helpers/__init__.py b/src/easydiffraction/analysis/fit_helpers/__init__.py index 18a61e30d..a14cfe307 100644 --- a/src/easydiffraction/analysis/fit_helpers/__init__.py +++ b/src/easydiffraction/analysis/fit_helpers/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Fit-result reporting and Bayesian posterior summaries.""" from easydiffraction.analysis.fit_helpers.bayesian import BayesianFitResults from easydiffraction.analysis.fit_helpers.bayesian import PosteriorPredictiveSummary diff --git a/src/easydiffraction/analysis/fit_helpers/metrics.py b/src/easydiffraction/analysis/fit_helpers/metrics.py index 2de2064cd..af05ba648 100644 --- a/src/easydiffraction/analysis/fit_helpers/metrics.py +++ b/src/easydiffraction/analysis/fit_helpers/metrics.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""R-factor and reduced chi-square fit-quality metrics.""" from __future__ import annotations diff --git a/src/easydiffraction/analysis/fit_helpers/reporting.py b/src/easydiffraction/analysis/fit_helpers/reporting.py index 687060f94..b738c31c5 100644 --- a/src/easydiffraction/analysis/fit_helpers/reporting.py +++ b/src/easydiffraction/analysis/fit_helpers/reporting.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause - +"""Least-squares fit-result container and summary rendering.""" from easydiffraction.analysis.fit_helpers.metrics import calculate_r_factor from easydiffraction.analysis.fit_helpers.metrics import calculate_r_factor_squared diff --git a/src/easydiffraction/analysis/fit_helpers/tracking.py b/src/easydiffraction/analysis/fit_helpers/tracking.py index c24f94359..a736fd0f4 100644 --- a/src/easydiffraction/analysis/fit_helpers/tracking.py +++ b/src/easydiffraction/analysis/fit_helpers/tracking.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Track and display fit and sampler progress during optimization.""" from __future__ import annotations diff --git a/src/easydiffraction/analysis/fitting.py b/src/easydiffraction/analysis/fitting.py index 5b55f34df..1ca3d3898 100644 --- a/src/easydiffraction/analysis/fitting.py +++ b/src/easydiffraction/analysis/fitting.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Fitter orchestrating model refinement via a pluggable minimizer.""" from __future__ import annotations @@ -16,6 +17,7 @@ from easydiffraction.core.variable import Parameter from easydiffraction.datablocks.experiment.item.base import intensity_category_for from easydiffraction.utils.enums import VerbosityEnum +from easydiffraction.utils.logging import log if TYPE_CHECKING: from easydiffraction.analysis.fit_helpers.reporting import FitResults @@ -218,7 +220,7 @@ def fit( analysis._clear_persisted_fit_state() analysis.fit_results = None self.results = None - print('⚠️ No parameters selected for fitting.') + log.warning('No parameters selected for fitting.') return if analysis is not None and not fit_options.resume: diff --git a/src/easydiffraction/analysis/minimizers/__init__.py b/src/easydiffraction/analysis/minimizers/__init__.py index cd3a1c144..c6a1223b7 100644 --- a/src/easydiffraction/analysis/minimizers/__init__.py +++ b/src/easydiffraction/analysis/minimizers/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Minimizer adapters for lmfit, bumps, dfo-ls and emcee.""" from easydiffraction.analysis.minimizers.bumps import BumpsMinimizer from easydiffraction.analysis.minimizers.bumps_amoeba import BumpsAmoebaMinimizer diff --git a/src/easydiffraction/analysis/minimizers/base.py b/src/easydiffraction/analysis/minimizers/base.py index 9be83b6ad..ac2d4e049 100644 --- a/src/easydiffraction/analysis/minimizers/base.py +++ b/src/easydiffraction/analysis/minimizers/base.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Abstract base class for pluggable least-squares minimizers.""" from abc import ABC from abc import abstractmethod diff --git a/src/easydiffraction/analysis/minimizers/dfols.py b/src/easydiffraction/analysis/minimizers/dfols.py index f5dd41394..b8d6851bb 100644 --- a/src/easydiffraction/analysis/minimizers/dfols.py +++ b/src/easydiffraction/analysis/minimizers/dfols.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause - +"""Derivative-free least-squares minimizer built on DFO-LS.""" import numpy as np from dfols import solve diff --git a/src/easydiffraction/analysis/minimizers/lmfit.py b/src/easydiffraction/analysis/minimizers/lmfit.py index 651b79265..dce336617 100644 --- a/src/easydiffraction/analysis/minimizers/lmfit.py +++ b/src/easydiffraction/analysis/minimizers/lmfit.py @@ -1,6 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause - +"""Least-squares minimizer adapter built on lmfit.""" import lmfit diff --git a/src/easydiffraction/core/__init__.py b/src/easydiffraction/core/__init__.py index 5382a8280..7de894567 100644 --- a/src/easydiffraction/core/__init__.py +++ b/src/easydiffraction/core/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Core base classes and shared utilities for EasyDiffraction.""" from easydiffraction.core.display_handler import DisplayHandler from easydiffraction.core.units_vocabulary import VALID_UNITS_CODES diff --git a/src/easydiffraction/core/category.py b/src/easydiffraction/core/category.py index 53b51d9b9..09999c2c6 100644 --- a/src/easydiffraction/core/category.py +++ b/src/easydiffraction/core/category.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Base classes for CIF category items and loop collections.""" from __future__ import annotations diff --git a/src/easydiffraction/core/category_owner.py b/src/easydiffraction/core/category_owner.py index 09e0cf68e..cea098eae 100644 --- a/src/easydiffraction/core/category_owner.py +++ b/src/easydiffraction/core/category_owner.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Base class for objects owning flat CIF-like categories.""" from __future__ import annotations diff --git a/src/easydiffraction/core/datablock.py b/src/easydiffraction/core/datablock.py index 623ab4d90..1ac054478 100644 --- a/src/easydiffraction/core/datablock.py +++ b/src/easydiffraction/core/datablock.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Base classes for CIF datablock items and collections.""" from __future__ import annotations diff --git a/src/easydiffraction/core/guard.py b/src/easydiffraction/core/guard.py index 8176cfa23..51715bdc7 100644 --- a/src/easydiffraction/core/guard.py +++ b/src/easydiffraction/core/guard.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Guarded base class with controlled attribute access.""" from __future__ import annotations diff --git a/src/easydiffraction/core/singleton.py b/src/easydiffraction/core/singleton.py index 95295a9ed..992bcbf83 100644 --- a/src/easydiffraction/core/singleton.py +++ b/src/easydiffraction/core/singleton.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Singleton base and parameter-constraint expression handler.""" from __future__ import annotations @@ -114,7 +115,7 @@ def apply(self) -> None: try: self._apply_one_constraint(ae, lhs_alias, rhs_expr) except (ValueError, TypeError, ArithmeticError, KeyError, AttributeError) as error: - print(f"Failed to apply constraint '{lhs_alias} = {rhs_expr}': {error}") + log.warning(f"Failed to apply constraint '{lhs_alias} = {rhs_expr}': {error}") def _apply_one_constraint( self, diff --git a/src/easydiffraction/core/switchable.py b/src/easydiffraction/core/switchable.py index a260b3ec6..f48d8eb15 100644 --- a/src/easydiffraction/core/switchable.py +++ b/src/easydiffraction/core/switchable.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Mixin for category-owned switchable type selectors.""" from __future__ import annotations diff --git a/src/easydiffraction/core/variable.py b/src/easydiffraction/core/variable.py index 8bcffb872..9fe0e7687 100644 --- a/src/easydiffraction/core/variable.py +++ b/src/easydiffraction/core/variable.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Descriptor and fittable parameter classes for CIF values.""" from __future__ import annotations diff --git a/src/easydiffraction/crystallography/__init__.py b/src/easydiffraction/crystallography/__init__.py index 080e2094d..41a746e00 100644 --- a/src/easydiffraction/crystallography/__init__.py +++ b/src/easydiffraction/crystallography/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Wyckoff position detection and crystallographic helpers.""" from easydiffraction.crystallography.crystallography import WyckoffPosition from easydiffraction.crystallography.crystallography import detect_wyckoff_position diff --git a/src/easydiffraction/crystallography/crystallography.py b/src/easydiffraction/crystallography/crystallography.py index e75569a4b..fbe649d7e 100644 --- a/src/easydiffraction/crystallography/crystallography.py +++ b/src/easydiffraction/crystallography/crystallography.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Space-group symmetry constraints and crystallographic math.""" import itertools import operator diff --git a/src/easydiffraction/datablocks/__init__.py b/src/easydiffraction/datablocks/__init__.py index 4e798e209..d54752e33 100644 --- a/src/easydiffraction/datablocks/__init__.py +++ b/src/easydiffraction/datablocks/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""CIF datablocks for sample structures and experiments.""" diff --git a/src/easydiffraction/datablocks/experiment/__init__.py b/src/easydiffraction/datablocks/experiment/__init__.py index 4e798e209..f417c05f6 100644 --- a/src/easydiffraction/datablocks/experiment/__init__.py +++ b/src/easydiffraction/datablocks/experiment/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Experiment datablock items and their collection.""" diff --git a/src/easydiffraction/datablocks/experiment/categories/__init__.py b/src/easydiffraction/datablocks/experiment/categories/__init__.py index 4e798e209..9c28156e6 100644 --- a/src/easydiffraction/datablocks/experiment/categories/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""CIF categories composing an experiment datablock.""" diff --git a/src/easydiffraction/datablocks/experiment/categories/background/__init__.py b/src/easydiffraction/datablocks/experiment/categories/background/__init__.py index 7ffe8f220..98518e01c 100644 --- a/src/easydiffraction/datablocks/experiment/categories/background/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/background/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Chebyshev and line-segment background categories.""" from easydiffraction.datablocks.experiment.categories.background.chebyshev import ( ChebyshevPolynomialBackground, diff --git a/src/easydiffraction/datablocks/experiment/categories/background/base.py b/src/easydiffraction/datablocks/experiment/categories/background/base.py index 819eca145..1fade4ced 100644 --- a/src/easydiffraction/datablocks/experiment/categories/background/base.py +++ b/src/easydiffraction/datablocks/experiment/categories/background/base.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Abstract base for switchable powder background categories.""" from __future__ import annotations diff --git a/src/easydiffraction/datablocks/experiment/categories/data/__init__.py b/src/easydiffraction/datablocks/experiment/categories/data/__init__.py index 5b2a41923..e19d689f6 100644 --- a/src/easydiffraction/datablocks/experiment/categories/data/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/data/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Measured pattern data categories for powder experiments.""" from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdTofData diff --git a/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py index d31c5db7d..9cdd8df7a 100644 --- a/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py +++ b/src/easydiffraction/datablocks/experiment/categories/data/bragg_pd.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Measured and calculated powder pattern data categories.""" from __future__ import annotations diff --git a/src/easydiffraction/datablocks/experiment/categories/diffrn/__init__.py b/src/easydiffraction/datablocks/experiment/categories/diffrn/__init__.py index 6c11ee7e0..01450f727 100644 --- a/src/easydiffraction/datablocks/experiment/categories/diffrn/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/diffrn/__init__.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Ambient diffraction conditions category for experiments.""" from easydiffraction.datablocks.experiment.categories.diffrn.default import DefaultDiffrn diff --git a/src/easydiffraction/datablocks/experiment/categories/excluded_regions/__init__.py b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/__init__.py index 8d232629c..fca9b9d47 100644 --- a/src/easydiffraction/datablocks/experiment/categories/excluded_regions/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/excluded_regions/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Excluded data regions to omit from pattern fitting.""" from easydiffraction.datablocks.experiment.categories.excluded_regions.default import ( ExcludedRegion, diff --git a/src/easydiffraction/datablocks/experiment/categories/experiment_type/__init__.py b/src/easydiffraction/datablocks/experiment/categories/experiment_type/__init__.py index 197b5510e..b1f40e959 100644 --- a/src/easydiffraction/datablocks/experiment/categories/experiment_type/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/experiment_type/__init__.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Experiment type category defining the diffraction axes.""" from easydiffraction.datablocks.experiment.categories.experiment_type.default import ExperimentType diff --git a/src/easydiffraction/datablocks/experiment/categories/extinction/__init__.py b/src/easydiffraction/datablocks/experiment/categories/extinction/__init__.py index cc259fd8c..dadcd0ab3 100644 --- a/src/easydiffraction/datablocks/experiment/categories/extinction/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/extinction/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Becker-Coppens extinction correction category.""" from easydiffraction.datablocks.experiment.categories.extinction.becker_coppens import ( BeckerCoppensExtinction, diff --git a/src/easydiffraction/datablocks/experiment/categories/instrument/__init__.py b/src/easydiffraction/datablocks/experiment/categories/instrument/__init__.py index d12b40c38..713847025 100644 --- a/src/easydiffraction/datablocks/experiment/categories/instrument/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Instrument categories for CWL and TOF powder and single crystal.""" from easydiffraction.datablocks.experiment.categories.instrument.cwl import CwlPdInstrument from easydiffraction.datablocks.experiment.categories.instrument.cwl import CwlScInstrument diff --git a/src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py b/src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py index 04c9f9a01..51d11f10e 100644 --- a/src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/cwl.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Constant-wavelength powder and single-crystal instruments.""" from easydiffraction.core.display_handler import DisplayHandler from easydiffraction.core.metadata import CalculatorSupport diff --git a/src/easydiffraction/datablocks/experiment/categories/instrument/tof.py b/src/easydiffraction/datablocks/experiment/categories/instrument/tof.py index 00f99e952..06865af21 100644 --- a/src/easydiffraction/datablocks/experiment/categories/instrument/tof.py +++ b/src/easydiffraction/datablocks/experiment/categories/instrument/tof.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Time-of-flight powder and single-crystal instruments.""" from easydiffraction.core.display_handler import DisplayHandler from easydiffraction.core.metadata import CalculatorSupport diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_crystal/__init__.py b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/__init__.py index 4b93121bb..9de40a4f0 100644 --- a/src/easydiffraction/datablocks/experiment/categories/linked_crystal/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/linked_crystal/__init__.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Linked crystal category referencing a sample model by ID.""" from easydiffraction.datablocks.experiment.categories.linked_crystal.default import LinkedCrystal diff --git a/src/easydiffraction/datablocks/experiment/categories/linked_phases/__init__.py b/src/easydiffraction/datablocks/experiment/categories/linked_phases/__init__.py index dda7d445c..d092f39ad 100644 --- a/src/easydiffraction/datablocks/experiment/categories/linked_phases/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/linked_phases/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Linked phases category weighting sample models in a pattern.""" from easydiffraction.datablocks.experiment.categories.linked_phases.default import LinkedPhase from easydiffraction.datablocks.experiment.categories.linked_phases.default import LinkedPhases diff --git a/src/easydiffraction/datablocks/experiment/categories/peak/__init__.py b/src/easydiffraction/datablocks/experiment/categories/peak/__init__.py index 430780c28..7b44e4c12 100644 --- a/src/easydiffraction/datablocks/experiment/categories/peak/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/peak/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Peak profile categories for CWL, TOF, and total scattering.""" from easydiffraction.datablocks.experiment.categories.peak.cwl import CwlPseudoVoigt from easydiffraction.datablocks.experiment.categories.peak.cwl import ( diff --git a/src/easydiffraction/datablocks/experiment/categories/refln/__init__.py b/src/easydiffraction/datablocks/experiment/categories/refln/__init__.py index 8ea2c383b..0f647a1b7 100644 --- a/src/easydiffraction/datablocks/experiment/categories/refln/__init__.py +++ b/src/easydiffraction/datablocks/experiment/categories/refln/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Reflection categories for powder and single-crystal experiments.""" from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlRefln from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlReflnData diff --git a/src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py index 581e00221..a59796545 100644 --- a/src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py +++ b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_pd.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Calculated powder reflection categories for CWL and TOF.""" from __future__ import annotations diff --git a/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py index ca08dcd95..442b7227e 100644 --- a/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py +++ b/src/easydiffraction/datablocks/experiment/categories/refln/bragg_sc.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Single-crystal reflection categories for CWL and TOF.""" from __future__ import annotations diff --git a/src/easydiffraction/datablocks/experiment/item/__init__.py b/src/easydiffraction/datablocks/experiment/item/__init__.py index 35a64fb17..72ac1c6cc 100644 --- a/src/easydiffraction/datablocks/experiment/item/__init__.py +++ b/src/easydiffraction/datablocks/experiment/item/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Experiment datablock items for Bragg and total scattering.""" from easydiffraction.datablocks.experiment.item.base import ExperimentBase from easydiffraction.datablocks.experiment.item.base import PdExperimentBase diff --git a/src/easydiffraction/datablocks/experiment/item/base.py b/src/easydiffraction/datablocks/experiment/item/base.py index e0093b7be..dd9e11e9e 100644 --- a/src/easydiffraction/datablocks/experiment/item/base.py +++ b/src/easydiffraction/datablocks/experiment/item/base.py @@ -575,23 +575,21 @@ def _get_valid_linked_phases( A list of valid linked phases. """ if not self.linked_phases: - print('Warning: No linked phases defined. Returning empty pattern.') + log.warning('No linked phases defined. Returning empty pattern.') return [] valid_linked_phases = [] for linked_phase in self.linked_phases: if linked_phase._identity.category_entry_name not in structures.names: - print( - f"Warning: Linked phase '{linked_phase.id.value}' not " + log.warning( + f"Linked phase '{linked_phase.id.value}' not " f'found in Structures {structures.names}. Skipping it.' ) continue valid_linked_phases.append(linked_phase) if not valid_linked_phases: - print( - 'Warning: None of the linked phases found in Structures. Returning empty pattern.' - ) + log.warning('None of the linked phases found in Structures. Returning empty pattern.') return valid_linked_phases diff --git a/src/easydiffraction/datablocks/experiment/item/bragg_pd.py b/src/easydiffraction/datablocks/experiment/item/bragg_pd.py index f1fbb3372..672a7c4c4 100644 --- a/src/easydiffraction/datablocks/experiment/item/bragg_pd.py +++ b/src/easydiffraction/datablocks/experiment/item/bragg_pd.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Bragg powder diffraction experiment datablock.""" from __future__ import annotations diff --git a/src/easydiffraction/datablocks/experiment/item/bragg_sc.py b/src/easydiffraction/datablocks/experiment/item/bragg_sc.py index 5e0659fae..f831e630b 100644 --- a/src/easydiffraction/datablocks/experiment/item/bragg_sc.py +++ b/src/easydiffraction/datablocks/experiment/item/bragg_sc.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Bragg single-crystal CWL and TOF experiment datablocks.""" from __future__ import annotations diff --git a/src/easydiffraction/datablocks/experiment/item/total_pd.py b/src/easydiffraction/datablocks/experiment/item/total_pd.py index bad95b705..23004adf4 100644 --- a/src/easydiffraction/datablocks/experiment/item/total_pd.py +++ b/src/easydiffraction/datablocks/experiment/item/total_pd.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Total scattering (PDF) powder experiment datablock item.""" from __future__ import annotations diff --git a/src/easydiffraction/datablocks/structure/__init__.py b/src/easydiffraction/datablocks/structure/__init__.py index 4e798e209..a055a3d71 100644 --- a/src/easydiffraction/datablocks/structure/__init__.py +++ b/src/easydiffraction/datablocks/structure/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Crystal structure datablock for diffraction models.""" diff --git a/src/easydiffraction/datablocks/structure/categories/__init__.py b/src/easydiffraction/datablocks/structure/categories/__init__.py index 4e798e209..041976efe 100644 --- a/src/easydiffraction/datablocks/structure/categories/__init__.py +++ b/src/easydiffraction/datablocks/structure/categories/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""CIF categories describing the crystal structure datablock.""" diff --git a/src/easydiffraction/datablocks/structure/categories/atom_site_aniso/__init__.py b/src/easydiffraction/datablocks/structure/categories/atom_site_aniso/__init__.py index fc8393f27..067bbc325 100644 --- a/src/easydiffraction/datablocks/structure/categories/atom_site_aniso/__init__.py +++ b/src/easydiffraction/datablocks/structure/categories/atom_site_aniso/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Anisotropic displacement parameters for atom sites.""" from easydiffraction.datablocks.structure.categories.atom_site_aniso.default import AtomSiteAniso from easydiffraction.datablocks.structure.categories.atom_site_aniso.default import ( diff --git a/src/easydiffraction/datablocks/structure/categories/atom_sites/__init__.py b/src/easydiffraction/datablocks/structure/categories/atom_sites/__init__.py index feb70b52b..1df8035c1 100644 --- a/src/easydiffraction/datablocks/structure/categories/atom_sites/__init__.py +++ b/src/easydiffraction/datablocks/structure/categories/atom_sites/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Atom site category for crystal structure models.""" from easydiffraction.datablocks.structure.categories.atom_sites.default import AtomSite from easydiffraction.datablocks.structure.categories.atom_sites.default import AtomSites diff --git a/src/easydiffraction/datablocks/structure/categories/cell/__init__.py b/src/easydiffraction/datablocks/structure/categories/cell/__init__.py index 16f6cab2a..0f82acfe7 100644 --- a/src/easydiffraction/datablocks/structure/categories/cell/__init__.py +++ b/src/easydiffraction/datablocks/structure/categories/cell/__init__.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Unit cell category for crystal structure models.""" from easydiffraction.datablocks.structure.categories.cell.default import Cell diff --git a/src/easydiffraction/datablocks/structure/categories/geom/__init__.py b/src/easydiffraction/datablocks/structure/categories/geom/__init__.py index 1fcc34dbf..4645bc9ed 100644 --- a/src/easydiffraction/datablocks/structure/categories/geom/__init__.py +++ b/src/easydiffraction/datablocks/structure/categories/geom/__init__.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Bond-geometry cutoff category for crystal structures.""" from easydiffraction.datablocks.structure.categories.geom.default import Geom diff --git a/src/easydiffraction/datablocks/structure/categories/space_group/__init__.py b/src/easydiffraction/datablocks/structure/categories/space_group/__init__.py index a8b33e627..466ff0d21 100644 --- a/src/easydiffraction/datablocks/structure/categories/space_group/__init__.py +++ b/src/easydiffraction/datablocks/structure/categories/space_group/__init__.py @@ -1,4 +1,5 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Space-group category for crystal structures.""" from easydiffraction.datablocks.structure.categories.space_group.default import SpaceGroup diff --git a/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/__init__.py b/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/__init__.py index 3808d3075..9abda2a9e 100644 --- a/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/__init__.py +++ b/src/easydiffraction/datablocks/structure/categories/space_group_wyckoff/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Space-group Wyckoff-position category for structures.""" from easydiffraction.datablocks.structure.categories.space_group_wyckoff.default import ( SpaceGroupWyckoff, diff --git a/src/easydiffraction/datablocks/structure/item/__init__.py b/src/easydiffraction/datablocks/structure/item/__init__.py index 4e798e209..27a793e76 100644 --- a/src/easydiffraction/datablocks/structure/item/__init__.py +++ b/src/easydiffraction/datablocks/structure/item/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Structure datablock item and its construction factory.""" diff --git a/src/easydiffraction/display/utils.py b/src/easydiffraction/display/utils.py index 5a1301647..f03717b43 100644 --- a/src/easydiffraction/display/utils.py +++ b/src/easydiffraction/display/utils.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Jupyter output-cell scroll suppression helper.""" from __future__ import annotations diff --git a/src/easydiffraction/io/__init__.py b/src/easydiffraction/io/__init__.py index 4d0c1560f..d072bd026 100644 --- a/src/easydiffraction/io/__init__.py +++ b/src/easydiffraction/io/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Input/output helpers for data files, CIF, and projects.""" from easydiffraction.io.ascii import extract_data_paths_from_dir from easydiffraction.io.ascii import extract_data_paths_from_zip diff --git a/src/easydiffraction/io/cif/__init__.py b/src/easydiffraction/io/cif/__init__.py index 4e798e209..e5170b763 100644 --- a/src/easydiffraction/io/cif/__init__.py +++ b/src/easydiffraction/io/cif/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""CIF parsing, serialisation, and IUCr export support.""" diff --git a/src/easydiffraction/io/cif/parse.py b/src/easydiffraction/io/cif/parse.py index 089321e7c..0577f0b47 100644 --- a/src/easydiffraction/io/cif/parse.py +++ b/src/easydiffraction/io/cif/parse.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Read CIF documents and tag values via gemmi.""" from __future__ import annotations diff --git a/src/easydiffraction/io/cif/serialize.py b/src/easydiffraction/io/cif/serialize.py index 4adea6aa4..a44b5a62a 100644 --- a/src/easydiffraction/io/cif/serialize.py +++ b/src/easydiffraction/io/cif/serialize.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Serialize and deserialize datablocks to and from CIF text.""" from __future__ import annotations diff --git a/src/easydiffraction/project/__init__.py b/src/easydiffraction/project/__init__.py index 4e798e209..61a6e9ec6 100644 --- a/src/easydiffraction/project/__init__.py +++ b/src/easydiffraction/project/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Project facade grouping models, experiments, and analysis.""" diff --git a/src/easydiffraction/project/categories/__init__.py b/src/easydiffraction/project/categories/__init__.py index 4e798e209..2aeb05cf2 100644 --- a/src/easydiffraction/project/categories/__init__.py +++ b/src/easydiffraction/project/categories/__init__.py @@ -1,2 +1,3 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Project-level categories: info, report, rendering, verbosity.""" diff --git a/src/easydiffraction/project/categories/info/default.py b/src/easydiffraction/project/categories/info/default.py index e18a0632f..61a85fdad 100644 --- a/src/easydiffraction/project/categories/info/default.py +++ b/src/easydiffraction/project/categories/info/default.py @@ -174,7 +174,7 @@ def _set_last_modified(self, value: datetime.datetime | str) -> None: def update_last_modified(self) -> None: """Update the last modified timestamp.""" - self._set_last_modified(datetime.datetime.now()) + self._set_last_modified(datetime.datetime.now(tz=datetime.UTC)) @property def as_cif(self) -> str: diff --git a/src/easydiffraction/utils/__init__.py b/src/easydiffraction/utils/__init__.py index 53fde2c62..cdf987a52 100644 --- a/src/easydiffraction/utils/__init__.py +++ b/src/easydiffraction/utils/__init__.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Shared utilities: versions, downloads, rendering, conversions.""" from easydiffraction.utils.utils import _is_dev_version from easydiffraction.utils.utils import stripped_package_version diff --git a/src/easydiffraction/utils/environment.py b/src/easydiffraction/utils/environment.py index b968e4a6c..0fb65c1f0 100644 --- a/src/easydiffraction/utils/environment.py +++ b/src/easydiffraction/utils/environment.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Runtime environment detection and artifact-path resolution.""" from __future__ import annotations diff --git a/src/easydiffraction/utils/utils.py b/src/easydiffraction/utils/utils.py index 5155bf571..a5e57980a 100644 --- a/src/easydiffraction/utils/utils.py +++ b/src/easydiffraction/utils/utils.py @@ -1,5 +1,6 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +"""Data/tutorial downloads, table rendering, and unit helpers.""" from __future__ import annotations diff --git a/tests/functional/test_background_auto_estimate_corpus.py b/tests/functional/test_background_auto_estimate_corpus.py index eaa07ce65..eca5e3d6b 100644 --- a/tests/functional/test_background_auto_estimate_corpus.py +++ b/tests/functional/test_background_auto_estimate_corpus.py @@ -19,9 +19,10 @@ ``tests/unit/.../categories/background/test_estimate.py``. """ -import easydiffraction as ed import numpy as np +import easydiffraction as ed + # The estimated background may differ from the coarse hand-placed # reference by at most these fractions of the measured signal scale # (the 5-95 percentile range of the measured intensities). They are loose diff --git a/tests/integration/fitting/test_bayesian_tracker_and_base.py b/tests/integration/fitting/test_bayesian_tracker_and_base.py index b120e1023..f149c377a 100644 --- a/tests/integration/fitting/test_bayesian_tracker_and_base.py +++ b/tests/integration/fitting/test_bayesian_tracker_and_base.py @@ -413,14 +413,13 @@ def _check_success(self, raw_result): def test_minimizer_base_applies_physical_limits_and_warns(monkeypatch): - from easydiffraction.analysis.minimizers.base import MinimizerFitOptions - from easydiffraction.analysis.minimizers.base import MinimizerBase + from easydiffraction.analysis.minimizers.base import MinimizerFitOptions warnings: list[str] = [] monkeypatch.setattr( 'easydiffraction.analysis.minimizers.base.log.warning', - lambda message: warnings.append(message), + warnings.append, ) class BoundaryParam(DummyParam): diff --git a/tests/integration/fitting/test_bumps_dream_support.py b/tests/integration/fitting/test_bumps_dream_support.py index a824347e1..0a72e42c4 100644 --- a/tests/integration/fitting/test_bumps_dream_support.py +++ b/tests/integration/fitting/test_bumps_dream_support.py @@ -366,7 +366,7 @@ def test_build_mapper_falls_back_for_spawn_bootstrap_runtime_error(monkeypatch): ) monkeypatch.setattr( 'easydiffraction.analysis.minimizers.bumps_dream.log.warning', - lambda message: warnings.append(message), + warnings.append, ) assert minimizer._build_mapper('problem') is None @@ -408,7 +408,7 @@ def test_build_mapper_falls_back_before_starting_spawn_for_direct_script(monkeyp ) monkeypatch.setattr( 'easydiffraction.analysis.minimizers.bumps_dream.log.warning', - lambda message: warnings.append(message), + warnings.append, ) assert minimizer._build_mapper('problem') is None @@ -697,7 +697,7 @@ def best(): ) monkeypatch.setattr( 'easydiffraction.analysis.minimizers.bumps_dream.log.warning', - lambda message: warnings.append(message), + warnings.append, ) successful = minimizer._build_success_result( diff --git a/tests/integration/scipp-analysis/dream/conftest.py b/tests/integration/scipp-analysis/dream/conftest.py index a0698a4af..f8d1f96e0 100644 --- a/tests/integration/scipp-analysis/dream/conftest.py +++ b/tests/integration/scipp-analysis/dream/conftest.py @@ -38,7 +38,7 @@ def cif_content( cif_path: str, ) -> str: """Read the CIF file content as text.""" - return Path(cif_path).read_text() + return Path(cif_path).read_text(encoding='utf-8') @pytest.fixture(scope='module') diff --git a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py index 4abe6a42b..27d5a3e90 100644 --- a/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py +++ b/tests/integration/scipp-analysis/dream/test_analyze_reduced_data.py @@ -37,7 +37,7 @@ def prepared_cif_path( """Prepare CIF file with experiment type tags for easydiffraction. """ - content = Path(cif_path).read_text() + content = Path(cif_path).read_text(encoding='utf-8') # Add experiment type tags if missing for tag, value in EXPT_TYPE_TAGS.items(): diff --git a/tests/tutorials/baseline.json b/tests/tutorials/baseline.json index 3b37c7f7a..1398c225b 100644 --- a/tests/tutorials/baseline.json +++ b/tests/tutorials/baseline.json @@ -114,12 +114,12 @@ "ed_20_beer_mcstas": { "result_kind": "deterministic", "rtol": 0.02, - "reduced_chi_square": 13.661849, - "R_factor_all": 0.049609, - "wR_factor_all": 0.071233, + "reduced_chi_square": 12.756675, + "R_factor_all": 0.049714, + "wR_factor_all": 0.068833, "parameters": { - "expt_s2.linked_phases.ferrite.scale": 50.36, - "expt_s2.linked_phases.austenite.scale": 11.985 + "expt_s2.linked_phases.ferrite.scale": 50.33, + "expt_s2.linked_phases.austenite.scale": 12.005 } }, "ed_21_lbco_hrpt_bumps_dream": { @@ -183,12 +183,12 @@ "ed_2_lbco_hrpt": { "result_kind": "deterministic", "rtol": 0.02, - "reduced_chi_square": 1.285348, - "R_factor_all": 0.056189, - "wR_factor_all": 0.07197, + "reduced_chi_square": 1.265395, + "R_factor_all": 0.055898, + "wR_factor_all": 0.071409, "parameters": { - "lbco.cell.length_a": 3.890792, - "hrpt.linked_phases.lbco.scale": 9.144 + "lbco.cell.length_a": 3.890758, + "hrpt.linked_phases.lbco.scale": 9.145 } }, "ed_3_lbco_hrpt": { @@ -218,13 +218,13 @@ "ed_6_hs_hrpt": { "result_kind": "deterministic", "rtol": 0.02, - "reduced_chi_square": 1.970653, - "R_factor_all": 0.0403, - "wR_factor_all": 0.050734, + "reduced_chi_square": 1.929924, + "R_factor_all": 0.03972, + "wR_factor_all": 0.050183, "parameters": { - "hs.cell.length_a": 6.86431, - "hs.cell.length_c": 14.1424, - "hrpt.linked_phases.hs.scale": 0.5138 + "hs.cell.length_a": 6.864, + "hs.cell.length_c": 14.1418, + "hrpt.linked_phases.hs.scale": 0.5132 } }, "ed_7_si_sepd": { diff --git a/tests/tutorials/test_tutorial_outputs.py b/tests/tutorials/test_tutorial_outputs.py index 1cb4f1cec..45c40bece 100644 --- a/tests/tutorials/test_tutorial_outputs.py +++ b/tests/tutorials/test_tutorial_outputs.py @@ -21,7 +21,6 @@ from pathlib import Path import pytest - from analysis_cif_reader import read_analysis_cif from generate_baseline import PLATFORM_SENSITIVE from generate_baseline import artifact_root diff --git a/tests/unit/easydiffraction/analysis/calculators/test_pdffit.py b/tests/unit/easydiffraction/analysis/calculators/test_pdffit.py index f317ae4e1..cf3d0b3f0 100644 --- a/tests/unit/easydiffraction/analysis/calculators/test_pdffit.py +++ b/tests/unit/easydiffraction/analysis/calculators/test_pdffit.py @@ -12,17 +12,24 @@ def test_module_import(): assert MUT.__name__ == 'easydiffraction.analysis.calculators.pdffit' -def test_pdffit_engine_flag_and_hkl_message(capsys): +def test_pdffit_engine_flag_and_hkl_message(monkeypatch): + from easydiffraction.analysis.calculators import pdffit as pdffit_mod from easydiffraction.analysis.calculators.pdffit import PdffitCalculator calc = PdffitCalculator() assert isinstance(calc.engine_imported, bool) - # calculate_structure_factors prints fixed message and returns [] by contract + + messages: list[str] = [] + + def fake_debug(*parts): + messages.append(' '.join(str(p) for p in parts)) + + monkeypatch.setattr(pdffit_mod.log, 'debug', fake_debug) + + # calculate_structure_factors logs a not-applicable note and returns [] by contract out = calc.calculate_structure_factors(structures=None, experiments=None) assert out == [] - # The method prints a note - printed = capsys.readouterr().out - assert 'HKLs (not applicable)' in printed + assert any('HKLs (not applicable)' in m for m in messages) # -- Stub classes for test_pdffit_cif_v2_to_v1_regex_behavior ---------- @@ -93,13 +100,12 @@ def parse(self, text): def test_pdffit_cif_v2_to_v1_regex_behavior(monkeypatch): # Exercise the regex conversion path indirectly by providing minimal objects - from easydiffraction.analysis.calculators.pdffit import PdffitCalculator - # Monkeypatch PdfFit and parser to avoid real engine usage import easydiffraction.analysis.calculators.pdffit as mod + from easydiffraction.analysis.calculators.pdffit import PdffitCalculator monkeypatch.setattr(mod, 'PdfFit', _FakePdf) - monkeypatch.setattr(mod, 'pdffit_cif_parser', lambda: _FakeParser()) + monkeypatch.setattr(mod, 'pdffit_cif_parser', _FakeParser) monkeypatch.setattr(mod, 'redirect_stdout', lambda *a, **k: None) monkeypatch.setattr(mod, '_pdffit_devnull', None, raising=False) diff --git a/tests/unit/easydiffraction/analysis/categories/fit_result/test_bayesian.py b/tests/unit/easydiffraction/analysis/categories/fit_result/test_bayesian.py index f636170dd..610c5a0ba 100644 --- a/tests/unit/easydiffraction/analysis/categories/fit_result/test_bayesian.py +++ b/tests/unit/easydiffraction/analysis/categories/fit_result/test_bayesian.py @@ -8,9 +8,7 @@ def test_bayesian_fit_result_defaults_unknown_outputs_to_none(): - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult fit_result = BayesianFitResult() @@ -26,9 +24,7 @@ def test_bayesian_fit_result_defaults_unknown_outputs_to_none(): def test_bayesian_fit_result_round_trips_cif_outputs(): - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult fit_result = BayesianFitResult() fit_result._set_point_estimate_name('posterior_median') @@ -56,9 +52,7 @@ def test_bayesian_fit_result_round_trips_cif_outputs(): def test_bayesian_fit_result_omits_optional_unknown_outputs(): - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult cif_text = BayesianFitResult().as_cif @@ -67,9 +61,7 @@ def test_bayesian_fit_result_omits_optional_unknown_outputs(): def test_bayesian_fit_result_omits_redundant_iterations(): - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult fit_result = BayesianFitResult() fit_result._set_iterations(100) @@ -80,9 +72,7 @@ def test_bayesian_fit_result_omits_redundant_iterations(): def test_bayesian_fit_result_keeps_optional_outputs_when_populated(): - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult fit_result = BayesianFitResult() fit_result._set_resolved_random_seed(12345) diff --git a/tests/unit/easydiffraction/analysis/categories/fit_result/test_factory.py b/tests/unit/easydiffraction/analysis/categories/fit_result/test_factory.py index 7352301fb..c771e8050 100644 --- a/tests/unit/easydiffraction/analysis/categories/fit_result/test_factory.py +++ b/tests/unit/easydiffraction/analysis/categories/fit_result/test_factory.py @@ -7,31 +7,19 @@ def test_fit_result_factory_creates_registered_family_classes(): import easydiffraction.analysis.categories.fit_result # noqa: F401 - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult from easydiffraction.analysis.categories.fit_result.factory import FitResultFactory - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult assert isinstance(FitResultFactory.create('least_squares'), LeastSquaresFitResult) assert isinstance(FitResultFactory.create('bayesian'), BayesianFitResult) def test_minimizer_bases_declare_paired_fit_result_classes(): - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) - from easydiffraction.analysis.categories.minimizer.bayesian_base import ( - BayesianMinimizerBase, - ) - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult + from easydiffraction.analysis.categories.minimizer.bayesian_base import BayesianMinimizerBase + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase assert LeastSquaresMinimizerBase._fit_result_class is LeastSquaresFitResult assert BayesianMinimizerBase._fit_result_class is BayesianFitResult diff --git a/tests/unit/easydiffraction/analysis/categories/fit_result/test_lsq.py b/tests/unit/easydiffraction/analysis/categories/fit_result/test_lsq.py index 59ed59e48..b4ddbf7fc 100644 --- a/tests/unit/easydiffraction/analysis/categories/fit_result/test_lsq.py +++ b/tests/unit/easydiffraction/analysis/categories/fit_result/test_lsq.py @@ -8,9 +8,7 @@ def test_least_squares_fit_result_defaults_unknown_outputs_to_none(): - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult fit_result = LeastSquaresFitResult() @@ -26,9 +24,7 @@ def test_least_squares_fit_result_defaults_unknown_outputs_to_none(): def test_least_squares_fit_result_round_trips_cif_outputs(): - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult fit_result = LeastSquaresFitResult() fit_result._set_objective_name('chi-square') @@ -56,9 +52,7 @@ def test_least_squares_fit_result_round_trips_cif_outputs(): def test_least_squares_fit_result_round_trips_reflection_outputs(): - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult fit_result = LeastSquaresFitResult() fit_result._set_r_factor_all(0.12) @@ -82,9 +76,7 @@ def test_least_squares_fit_result_round_trips_reflection_outputs(): def test_least_squares_fit_result_round_trips_powder_outputs(): - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult fit_result = LeastSquaresFitResult() fit_result._set_prof_r_factor(0.21) @@ -108,9 +100,7 @@ def test_least_squares_fit_result_round_trips_powder_outputs(): def test_least_squares_fit_result_serializes_only_active_families(): - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult fit_result = LeastSquaresFitResult() fit_result._set_number_restraints(0) @@ -142,9 +132,7 @@ def test_least_squares_fit_result_serializes_only_active_families(): def test_least_squares_fit_result_omits_duplicate_exit_reason(): - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult fit_result = LeastSquaresFitResult() fit_result._set_message('Fit succeeded.') @@ -157,9 +145,7 @@ def test_least_squares_fit_result_omits_duplicate_exit_reason(): def test_least_squares_fit_result_keeps_distinct_exit_reason(): - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult fit_result = LeastSquaresFitResult() fit_result._set_message('Fit failed.') diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_base.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_base.py index 4efff614a..3cdda2a08 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_base.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_base.py @@ -6,9 +6,7 @@ def test_descriptor_values_and_native_kwargs_use_descriptor_values(): - from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import ( - LmfitLeastsqMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import LmfitLeastsqMinimizer minimizer = LmfitLeastsqMinimizer() minimizer.max_iterations = 25 diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bayesian_base.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bayesian_base.py index bfffd94b2..27a6f9c6e 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bayesian_base.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bayesian_base.py @@ -9,9 +9,7 @@ def test_bayesian_minimizer_defaults_and_native_kwargs(): - from easydiffraction.analysis.categories.minimizer.bumps_dream import ( - BumpsDreamMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.bumps_dream import BumpsDreamMinimizer minimizer = BumpsDreamMinimizer() @@ -33,9 +31,7 @@ def test_bayesian_minimizer_defaults_and_native_kwargs(): def test_bayesian_minimizer_rejects_unsupported_initialization_method(): - from easydiffraction.analysis.categories.minimizer.bumps_dream import ( - BumpsDreamMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.bumps_dream import BumpsDreamMinimizer minimizer = BumpsDreamMinimizer() @@ -44,9 +40,7 @@ def test_bayesian_minimizer_rejects_unsupported_initialization_method(): def test_bayesian_minimizer_keeps_unset_random_seed_in_cif(): - from easydiffraction.analysis.categories.minimizer.bumps_dream import ( - BumpsDreamMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.bumps_dream import BumpsDreamMinimizer cif_text = BumpsDreamMinimizer().as_cif @@ -54,9 +48,7 @@ def test_bayesian_minimizer_keeps_unset_random_seed_in_cif(): def test_bayesian_minimizer_keeps_configured_random_seed_in_cif(): - from easydiffraction.analysis.categories.minimizer.bumps_dream import ( - BumpsDreamMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.bumps_dream import BumpsDreamMinimizer minimizer = BumpsDreamMinimizer() minimizer.random_seed = 123 @@ -67,9 +59,7 @@ def test_bayesian_minimizer_keeps_configured_random_seed_in_cif(): def test_bayesian_minimizer_reads_cif_unknown_values_as_defaults(): - from easydiffraction.analysis.categories.minimizer.bumps_dream import ( - BumpsDreamMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.bumps_dream import BumpsDreamMinimizer document = gemmi.cif.read_string( """data_minimizer diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps.py index 54a08a6b3..4deff9ada 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps.py @@ -7,9 +7,7 @@ def test_bumps_minimizer_registers_expected_tag(): from easydiffraction.analysis.categories.minimizer.bumps import BumpsMinimizer - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(BumpsMinimizer, LeastSquaresMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_amoeba.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_amoeba.py index 50ac280d9..94e5a676f 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_amoeba.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_amoeba.py @@ -6,12 +6,8 @@ def test_bumps_amoeba_minimizer_registers_expected_tag(): - from easydiffraction.analysis.categories.minimizer.bumps_amoeba import ( - BumpsAmoebaMinimizer, - ) - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.minimizer.bumps_amoeba import BumpsAmoebaMinimizer + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(BumpsAmoebaMinimizer, LeastSquaresMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_de.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_de.py index 750505f42..2c249371f 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_de.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_de.py @@ -7,9 +7,7 @@ def test_bumps_de_minimizer_registers_expected_tag(): from easydiffraction.analysis.categories.minimizer.bumps_de import BumpsDeMinimizer - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(BumpsDeMinimizer, LeastSquaresMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_dream.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_dream.py index 524fdcfa3..6c79ce503 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_dream.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_dream.py @@ -6,12 +6,8 @@ def test_bumps_dream_minimizer_registers_expected_tag(): - from easydiffraction.analysis.categories.minimizer.bayesian_base import ( - BayesianMinimizerBase, - ) - from easydiffraction.analysis.categories.minimizer.bumps_dream import ( - BumpsDreamMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.bayesian_base import BayesianMinimizerBase + from easydiffraction.analysis.categories.minimizer.bumps_dream import BumpsDreamMinimizer from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(BumpsDreamMinimizer, BayesianMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_lm.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_lm.py index 6ac9b727e..966015734 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_lm.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_bumps_lm.py @@ -7,9 +7,7 @@ def test_bumps_lm_minimizer_registers_expected_tag(): from easydiffraction.analysis.categories.minimizer.bumps_lm import BumpsLmMinimizer - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(BumpsLmMinimizer, LeastSquaresMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_dfols.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_dfols.py index 991ad7a0d..e2d4df29b 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_dfols.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_dfols.py @@ -7,9 +7,7 @@ def test_dfols_minimizer_registers_expected_tag(): from easydiffraction.analysis.categories.minimizer.dfols import DfolsMinimizer - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(DfolsMinimizer, LeastSquaresMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_emcee.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_emcee.py index 9f4e904b0..a3b1bfe59 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_emcee.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_emcee.py @@ -27,9 +27,7 @@ def _make_project() -> object: def test_emcee_minimizer_category_defaults_to_max_parallel_workers(): - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_PARALLEL_WORKERS, - ) + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_PARALLEL_WORKERS from easydiffraction.analysis.categories.minimizer.emcee import EmceeMinimizer minimizer = EmceeMinimizer() @@ -40,12 +38,8 @@ def test_emcee_minimizer_category_defaults_to_max_parallel_workers(): def test_emcee_minimizer_category_defaults_to_de_without_thinning(): - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_PROPOSAL_MOVES, - ) - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_THINNING_INTERVAL, - ) + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_PROPOSAL_MOVES + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_THINNING_INTERVAL from easydiffraction.analysis.categories.minimizer.emcee import EmceeMinimizer minimizer = EmceeMinimizer() @@ -59,27 +53,13 @@ def test_emcee_minimizer_category_defaults_to_de_without_thinning(): def test_emcee_minimizer_category_maps_native_kwargs(): - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_BURN_IN_STEPS, - ) - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_INITIALIZATION_METHOD, - ) - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_PARALLEL_WORKERS, - ) - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_POPULATION_SIZE, - ) - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_PROPOSAL_MOVES, - ) - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_SAMPLING_STEPS, - ) - from easydiffraction.analysis.categories.minimizer.emcee import ( - DEFAULT_THINNING_INTERVAL, - ) + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_BURN_IN_STEPS + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_INITIALIZATION_METHOD + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_PARALLEL_WORKERS + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_POPULATION_SIZE + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_PROPOSAL_MOVES + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_SAMPLING_STEPS + from easydiffraction.analysis.categories.minimizer.emcee import DEFAULT_THINNING_INTERVAL from easydiffraction.analysis.categories.minimizer.emcee import EmceeMinimizer native_kwargs = EmceeMinimizer()._native_kwargs() diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_factory.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_factory.py index 233efbd17..2174e4a28 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_factory.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_factory.py @@ -9,12 +9,8 @@ def test_factory_default_creates_lmfit_leastsq_minimizer(): import easydiffraction.analysis.categories.minimizer # noqa: F401 - from easydiffraction.analysis.categories.minimizer.factory import ( - MinimizerCategoryFactory, - ) - from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import ( - LmfitLeastsqMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.factory import MinimizerCategoryFactory + from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import LmfitLeastsqMinimizer minimizer = MinimizerCategoryFactory.create_default_for() @@ -24,9 +20,7 @@ def test_factory_default_creates_lmfit_leastsq_minimizer(): def test_factory_rejects_unsupported_minimizer_type(): import easydiffraction.analysis.categories.minimizer # noqa: F401 - from easydiffraction.analysis.categories.minimizer.factory import ( - MinimizerCategoryFactory, - ) + from easydiffraction.analysis.categories.minimizer.factory import MinimizerCategoryFactory with pytest.raises(ValueError, match='Unsupported type'): MinimizerCategoryFactory.create('missing') diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit.py index 6002689e2..547160b71 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit.py @@ -7,9 +7,7 @@ def test_lmfit_minimizer_registers_expected_tag(): from easydiffraction.analysis.categories.minimizer.lmfit import LmfitMinimizer - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(LmfitMinimizer, LeastSquaresMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit_least_squares.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit_least_squares.py index 5e6e22af6..6fbb5c2ce 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit_least_squares.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit_least_squares.py @@ -9,9 +9,7 @@ def test_lmfit_least_squares_minimizer_registers_expected_tag(): from easydiffraction.analysis.categories.minimizer.lmfit_least_squares import ( LmfitLeastSquaresMinimizer, ) - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(LmfitLeastSquaresMinimizer, LeastSquaresMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit_leastsq.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit_leastsq.py index caf105a5e..cdab00ca6 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit_leastsq.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_lmfit_leastsq.py @@ -6,12 +6,8 @@ def test_lmfit_leastsq_minimizer_registers_expected_tag(): - from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import ( - LmfitLeastsqMinimizer, - ) - from easydiffraction.analysis.categories.minimizer.lsq_base import ( - LeastSquaresMinimizerBase, - ) + from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import LmfitLeastsqMinimizer + from easydiffraction.analysis.categories.minimizer.lsq_base import LeastSquaresMinimizerBase from easydiffraction.analysis.minimizers.enums import MinimizerTypeEnum assert issubclass(LmfitLeastsqMinimizer, LeastSquaresMinimizerBase) diff --git a/tests/unit/easydiffraction/analysis/categories/minimizer/test_lsq_base.py b/tests/unit/easydiffraction/analysis/categories/minimizer/test_lsq_base.py index 011862282..fa1ca3ddf 100644 --- a/tests/unit/easydiffraction/analysis/categories/minimizer/test_lsq_base.py +++ b/tests/unit/easydiffraction/analysis/categories/minimizer/test_lsq_base.py @@ -8,9 +8,7 @@ def test_lsq_minimizer_defaults_to_settings_only(): - from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import ( - LmfitLeastsqMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import LmfitLeastsqMinimizer minimizer = LmfitLeastsqMinimizer() @@ -20,9 +18,7 @@ def test_lsq_minimizer_defaults_to_settings_only(): def test_lsq_minimizer_reads_cif_settings(): - from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import ( - LmfitLeastsqMinimizer, - ) + from easydiffraction.analysis.categories.minimizer.lmfit_leastsq import LmfitLeastsqMinimizer document = gemmi.cif.read_string( """data_minimizer diff --git a/tests/unit/easydiffraction/analysis/categories/test_fit_state.py b/tests/unit/easydiffraction/analysis/categories/test_fit_state.py index a3b4fd7cc..085870671 100644 --- a/tests/unit/easydiffraction/analysis/categories/test_fit_state.py +++ b/tests/unit/easydiffraction/analysis/categories/test_fit_state.py @@ -139,12 +139,8 @@ def test_fit_parameter_posterior_summary_serializes_expected_tags(): def test_dream_sampler_settings_and_diagnostics_use_split_cif_fields(): - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) - from easydiffraction.analysis.categories.minimizer.bumps_dream import ( - BumpsDreamMinimizer, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult + from easydiffraction.analysis.categories.minimizer.bumps_dream import BumpsDreamMinimizer minimizer = BumpsDreamMinimizer() minimizer.sampling_steps = 100 diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_base.py b/tests/unit/easydiffraction/analysis/minimizers/test_base.py index 50a353d69..70904582c 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_base.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_base.py @@ -136,7 +136,6 @@ def _prepare_solver_args(self, parameters): def _run_solver(self, objective_function, **kwargs): del objective_function, kwargs - return def _sync_result_to_parameters(self, parameters, raw_result): del parameters, raw_result diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py b/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py index 830a4f64f..1de7b7a1f 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_bumps.py @@ -234,8 +234,8 @@ def test_fitness_raises_when_max_evaluations_is_reached(): def test_bumps_progress_monitor_reports_evaluation_count(): - from easydiffraction.analysis.minimizers.bumps import _EasyDiffractionFitness from easydiffraction.analysis.minimizers.bumps import _BumpsProgressMonitor + from easydiffraction.analysis.minimizers.bumps import _EasyDiffractionFitness tracker = MagicMock() fitness = _EasyDiffractionFitness([], lambda values: np.array([])) diff --git a/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py b/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py index 7091fdcd4..8286e5121 100644 --- a/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py +++ b/tests/unit/easydiffraction/analysis/minimizers/test_lmfit.py @@ -80,9 +80,8 @@ def __init__(self): def test_lmfit_max_iterations_is_user_facing_iteration_setting(monkeypatch): - from easydiffraction.analysis.minimizers.lmfit import LmfitMinimizer - import easydiffraction.analysis.minimizers.lmfit as lm + from easydiffraction.analysis.minimizers.lmfit import LmfitMinimizer observed_max_nfev = {} diff --git a/tests/unit/easydiffraction/analysis/test_analysis.py b/tests/unit/easydiffraction/analysis/test_analysis.py index 5cda12a5d..cefec3f63 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis.py +++ b/tests/unit/easydiffraction/analysis/test_analysis.py @@ -461,6 +461,9 @@ def test_fit_interrupt_cleans_state_and_prints_message(monkeypatch, capsys): events: list[object] = [] class FakeStopControl: + def __init__(self, *, verbosity: object) -> None: + del verbosity + def __enter__(self) -> object: events.append('enter') return self @@ -478,7 +481,7 @@ def __exit__(self, exc_type, exc_value, traceback) -> None: monkeypatch.setattr( analysis_mod, 'notebook_fit_stop_control', - lambda *, verbosity: FakeStopControl(), + FakeStopControl, ) monkeypatch.setattr( analysis, diff --git a/tests/unit/easydiffraction/analysis/test_analysis_access_params.py b/tests/unit/easydiffraction/analysis/test_analysis_access_params.py index a6e1166c0..94bb70c0d 100644 --- a/tests/unit/easydiffraction/analysis/test_analysis_access_params.py +++ b/tests/unit/easydiffraction/analysis/test_analysis_access_params.py @@ -221,9 +221,7 @@ class FakeTableRenderer: def render(self, df): rendered.append(df) - monkeypatch.setattr( - analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer()) - ) + monkeypatch.setattr(analysis_mod.TableRenderer, 'get', staticmethod(FakeTableRenderer)) Analysis(Project()).display.all_params() assert len(rendered) == 2 @@ -270,9 +268,7 @@ class FakeTableRenderer: def render(self, df): rendered.append(df) - monkeypatch.setattr( - analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer()) - ) + monkeypatch.setattr(analysis_mod.TableRenderer, 'get', staticmethod(FakeTableRenderer)) Analysis(Project()).display.all_params() structure_df = rendered[0] @@ -316,9 +312,7 @@ class FakeTableRenderer: def render(self, df): rendered.append(df) - monkeypatch.setattr( - analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer()) - ) + monkeypatch.setattr(analysis_mod.TableRenderer, 'get', staticmethod(FakeTableRenderer)) Analysis(Project()).display.fittable_params() structure_df = rendered[0] @@ -367,9 +361,7 @@ class FakeTableRenderer: def render(self, df): rendered.append(df) - monkeypatch.setattr( - analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer()) - ) + monkeypatch.setattr(analysis_mod.TableRenderer, 'get', staticmethod(FakeTableRenderer)) Analysis(Project()).display.free_params() free_df = rendered[0] @@ -418,9 +410,7 @@ class FakeTableRenderer: def render(self, df): rendered.append(df) - monkeypatch.setattr( - analysis_mod.TableRenderer, 'get', staticmethod(lambda: FakeTableRenderer()) - ) + monkeypatch.setattr(analysis_mod.TableRenderer, 'get', staticmethod(FakeTableRenderer)) Analysis(Project()).display.all_params() structure_df = rendered[0] diff --git a/tests/unit/easydiffraction/analysis/test_enums.py b/tests/unit/easydiffraction/analysis/test_enums.py index 75e1ca3ec..8f7edc10d 100644 --- a/tests/unit/easydiffraction/analysis/test_enums.py +++ b/tests/unit/easydiffraction/analysis/test_enums.py @@ -3,8 +3,8 @@ """Tests for analysis/enums.py.""" from easydiffraction.analysis.enums import FitCorrelationSourceEnum -from easydiffraction.analysis.enums import FitResultKindEnum from easydiffraction.analysis.enums import FitModeEnum +from easydiffraction.analysis.enums import FitResultKindEnum def test_fit_mode_enum_members(): diff --git a/tests/unit/easydiffraction/analysis/test_fitting.py b/tests/unit/easydiffraction/analysis/test_fitting.py index d5995f5ef..af7c5013e 100644 --- a/tests/unit/easydiffraction/analysis/test_fitting.py +++ b/tests/unit/easydiffraction/analysis/test_fitting.py @@ -259,7 +259,6 @@ def test_residual_function_skips_tracker_for_solver_monitored_minimizer(monkeypa class DummyExperiment: def _update_categories(self, *, called_by_minimizer=False): del called_by_minimizer - return class DummyMin: def __init__(self): diff --git a/tests/unit/easydiffraction/analysis/test_sequential.py b/tests/unit/easydiffraction/analysis/test_sequential.py index ac8932df7..7274e0ad1 100644 --- a/tests/unit/easydiffraction/analysis/test_sequential.py +++ b/tests/unit/easydiffraction/analysis/test_sequential.py @@ -10,8 +10,8 @@ import pytest -from easydiffraction.analysis.sequential import SequentialFitTemplate from easydiffraction.analysis.sequential import _META_COLUMNS +from easydiffraction.analysis.sequential import SequentialFitTemplate from easydiffraction.analysis.sequential import _append_to_csv from easydiffraction.analysis.sequential import _build_csv_header from easydiffraction.analysis.sequential import _chunk_file_range diff --git a/tests/unit/easydiffraction/core/test_factory.py b/tests/unit/easydiffraction/core/test_factory.py index 430d96944..39ba76808 100644 --- a/tests/unit/easydiffraction/core/test_factory.py +++ b/tests/unit/easydiffraction/core/test_factory.py @@ -11,7 +11,6 @@ from easydiffraction.core.metadata import Compatibility from easydiffraction.core.metadata import TypeInfo - # ------------------------------------------------------------------ # Helpers: a fresh factory + stub classes for each test # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/core/test_metadata.py b/tests/unit/easydiffraction/core/test_metadata.py index f8327a3ff..dc605a479 100644 --- a/tests/unit/easydiffraction/core/test_metadata.py +++ b/tests/unit/easydiffraction/core/test_metadata.py @@ -10,7 +10,6 @@ from easydiffraction.core.metadata import Compatibility from easydiffraction.core.metadata import TypeInfo - # ------------------------------------------------------------------ # TypeInfo # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/core/test_switchable.py b/tests/unit/easydiffraction/core/test_switchable.py index 127d4ccb6..1f7718e27 100644 --- a/tests/unit/easydiffraction/core/test_switchable.py +++ b/tests/unit/easydiffraction/core/test_switchable.py @@ -113,7 +113,7 @@ def test_show_supported_renders_active_type_for_supported_shapes( monkeypatch.setattr( switchable_mod.console, 'paragraph', - lambda text: paragraphs.append(text), + paragraphs.append, ) monkeypatch.setattr( switchable_mod, diff --git a/tests/unit/easydiffraction/crystallography/test_crystallography_coverage.py b/tests/unit/easydiffraction/crystallography/test_crystallography_coverage.py index 7b548aca0..4719deb7f 100644 --- a/tests/unit/easydiffraction/crystallography/test_crystallography_coverage.py +++ b/tests/unit/easydiffraction/crystallography/test_crystallography_coverage.py @@ -4,7 +4,6 @@ from easydiffraction.crystallography.crystallography import apply_cell_symmetry_constraints - # ------------------------------------------------------------------ # apply_cell_symmetry_constraints # ------------------------------------------------------------------ @@ -109,9 +108,7 @@ def test_invalid_name_hm_returns_cell_unchanged(self, monkeypatch): class TestCellSymmetryConstrainedFlags: def test_cubic_only_a_is_free(self): - from easydiffraction.crystallography.crystallography import ( - cell_symmetry_constrained_flags, - ) + from easydiffraction.crystallography.crystallography import cell_symmetry_constrained_flags flags = cell_symmetry_constrained_flags('F m -3 m') assert flags == { @@ -124,9 +121,7 @@ def test_cubic_only_a_is_free(self): } def test_monoclinic_b_and_beta_free(self): - from easydiffraction.crystallography.crystallography import ( - cell_symmetry_constrained_flags, - ) + from easydiffraction.crystallography.crystallography import cell_symmetry_constrained_flags flags = cell_symmetry_constrained_flags('P 21/c') assert flags['lattice_a'] is False @@ -137,17 +132,13 @@ def test_monoclinic_b_and_beta_free(self): assert flags['angle_gamma'] is True def test_triclinic_all_free(self): - from easydiffraction.crystallography.crystallography import ( - cell_symmetry_constrained_flags, - ) + from easydiffraction.crystallography.crystallography import cell_symmetry_constrained_flags flags = cell_symmetry_constrained_flags('P 1') assert all(v is False for v in flags.values()) def test_invalid_returns_all_false(self, monkeypatch): - from easydiffraction.crystallography.crystallography import ( - cell_symmetry_constrained_flags, - ) + from easydiffraction.crystallography.crystallography import cell_symmetry_constrained_flags from easydiffraction.utils.logging import Logger monkeypatch.setattr(Logger, '_reaction', Logger.Reaction.WARN, raising=True) diff --git a/tests/unit/easydiffraction/crystallography/test_crystallography_wyckoff.py b/tests/unit/easydiffraction/crystallography/test_crystallography_wyckoff.py index 386b4cb55..e7f2463d2 100644 --- a/tests/unit/easydiffraction/crystallography/test_crystallography_wyckoff.py +++ b/tests/unit/easydiffraction/crystallography/test_crystallography_wyckoff.py @@ -179,10 +179,8 @@ def test_without_coords_has_no_template(self): assert position.coord_template is None def test_selects_nearest_representative_not_first(self): - from easydiffraction.crystallography.crystallography import ( - snap_to_wyckoff_template, - wyckoff_position_info, - ) + from easydiffraction.crystallography.crystallography import snap_to_wyckoff_template + from easydiffraction.crystallography.crystallography import wyckoff_position_info # 'e' first rep is (x,0,0); for a point near the (0,x,0) member the # nearest representative must be chosen so the snap keeps fract_y diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_estimate.py b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_estimate.py index b51f66ef4..3cf5a16b7 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_estimate.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_estimate.py @@ -15,10 +15,10 @@ def _pattern(n=400, slope=0.3, intercept=5.0, peaks=((5.0, 6.0, 0.15),), noise=0 x = np.linspace(0.0, 10.0, n) y = intercept + slope * x for center, amp, width in peaks: - y = y + amp * np.exp(-((x - center) ** 2) / (2.0 * width**2)) + y += amp * np.exp(-((x - center) ** 2) / (2.0 * width**2)) if noise > 0: rng = np.random.default_rng(seed) - y = y + rng.normal(0.0, noise, size=n) + y += rng.normal(0.0, noise, size=n) return x, y.astype(float) @@ -155,9 +155,9 @@ def test_cwl_broadening_keeps_background_off_broad_peaks(): y = 4.0 + 0.0 * x for center in (2.0, 8.0): width = 0.1 + 0.06 * center # broadening with angle - y = y + 7.0 * np.exp(-((x - center) ** 2) / (2.0 * width**2)) + y += 7.0 * np.exp(-((x - center) ** 2) / (2.0 * width**2)) rng = np.random.default_rng(12) - y = y + rng.normal(0.0, 0.05, size=x.size) + y += rng.normal(0.0, 0.05, size=x.size) result = estimate_background_curve(x, y) # Background near the broad peak stays well below the peak top. near_peak = result.curve[np.argmin(np.abs(x - 8.0))] diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_line_segment.py b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_line_segment.py index 4d11a49d0..cd771476e 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_line_segment.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/background/test_line_segment.py @@ -1,11 +1,11 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause +from types import SimpleNamespace + import numpy as np import pytest -from types import SimpleNamespace - from easydiffraction.datablocks.experiment.categories.background import line_segment from easydiffraction.datablocks.experiment.categories.background.line_segment import ( LineSegmentBackground, diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_base.py b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_base.py index 5dc128272..84a34e055 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_base.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/peak/test_base.py @@ -1,9 +1,9 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from easydiffraction.datablocks.experiment.categories.peak.base import PeakBase from easydiffraction.core.metadata import TypeInfo from easydiffraction.core.variable import StringDescriptor +from easydiffraction.datablocks.experiment.categories.peak.base import PeakBase def test_peak_base_identity_code(): diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_pd.py b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_pd.py index 27ab4e533..45bf13728 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_pd.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/refln/test_bragg_pd.py @@ -5,8 +5,8 @@ import pytest from easydiffraction.analysis.calculators.base import PowderReflnRecord -from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum +from easydiffraction.datablocks.experiment.item.factory import ExperimentFactory def test_powder_cwl_refln_defaults(): @@ -24,9 +24,7 @@ def test_powder_cwl_refln_defaults(): def test_powder_cwl_refln_data_replace_from_records_sets_arrays(): - from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import ( - PowderCwlReflnData, - ) + from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlReflnData refln = PowderCwlReflnData() refln._replace_from_records([ @@ -63,9 +61,7 @@ def test_powder_cwl_refln_data_replace_from_records_sets_arrays(): def test_powder_tof_refln_data_replace_from_records_sets_arrays(): - from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import ( - PowderTofReflnData, - ) + from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderTofReflnData refln = PowderTofReflnData() refln._replace_from_records([ @@ -89,21 +85,15 @@ def test_powder_tof_refln_data_replace_from_records_sets_arrays(): def test_powder_refln_is_cryspy_only(): - from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import ( - PowderCwlReflnData, - ) - from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import ( - PowderTofReflnData, - ) + from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlReflnData + from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderTofReflnData assert PowderCwlReflnData.calculator_support.calculators == frozenset({CalculatorEnum.CRYSPY}) assert PowderTofReflnData.calculator_support.calculators == frozenset({CalculatorEnum.CRYSPY}) def test_powder_refln_replace_from_records_rebuilds_index_and_parents(): - from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import ( - PowderCwlReflnData, - ) + from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlReflnData refln = PowderCwlReflnData() refln._replace_from_records([ diff --git a/tests/unit/easydiffraction/datablocks/experiment/categories/test_extinction.py b/tests/unit/easydiffraction/datablocks/experiment/categories/test_extinction.py index 17ae1cfcc..9483f8bd9 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/categories/test_extinction.py +++ b/tests/unit/easydiffraction/datablocks/experiment/categories/test_extinction.py @@ -98,12 +98,12 @@ def test_extinction_factory_registration(): def test_extinction_factory_create(): - from easydiffraction.datablocks.experiment.categories.extinction.factory import ( - ExtinctionFactory, - ) from easydiffraction.datablocks.experiment.categories.extinction.becker_coppens import ( BeckerCoppensExtinction, ) + from easydiffraction.datablocks.experiment.categories.extinction.factory import ( + ExtinctionFactory, + ) ext = ExtinctionFactory.create('becker-coppens') assert isinstance(ext, BeckerCoppensExtinction) diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_base_coverage.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_base_coverage.py index 272781845..8e01fcb8d 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/item/test_base_coverage.py +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_base_coverage.py @@ -10,7 +10,6 @@ from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum - # ------------------------------------------------------------------ # Helpers # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py index b43644b81..1bbd9a916 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_bragg_pd.py @@ -7,12 +7,8 @@ from easydiffraction.analysis.calculators.base import PowderReflnRecord from easydiffraction.datablocks.experiment.categories.background.factory import BackgroundFactory from easydiffraction.datablocks.experiment.categories.experiment_type import ExperimentType -from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import ( - PowderCwlReflnData, -) -from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import ( - PowderTofReflnData, -) +from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlReflnData +from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderTofReflnData from easydiffraction.datablocks.experiment.item.bragg_pd import BraggPdExperiment from easydiffraction.datablocks.experiment.item.enums import BeamModeEnum from easydiffraction.datablocks.experiment.item.enums import CalculatorEnum diff --git a/tests/unit/easydiffraction/datablocks/experiment/item/test_enums_coverage.py b/tests/unit/easydiffraction/datablocks/experiment/item/test_enums_coverage.py index c612b2be5..6bcd7842f 100644 --- a/tests/unit/easydiffraction/datablocks/experiment/item/test_enums_coverage.py +++ b/tests/unit/easydiffraction/datablocks/experiment/item/test_enums_coverage.py @@ -8,7 +8,6 @@ from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum - # ------------------------------------------------------------------ # SampleFormEnum # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/datablocks/structure/categories/test_atom_site_aniso.py b/tests/unit/easydiffraction/datablocks/structure/categories/test_atom_site_aniso.py index 72c15774e..383c6e429 100644 --- a/tests/unit/easydiffraction/datablocks/structure/categories/test_atom_site_aniso.py +++ b/tests/unit/easydiffraction/datablocks/structure/categories/test_atom_site_aniso.py @@ -4,7 +4,6 @@ import math - # ------------------------------------------------------------------ # Module import # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group.py b/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group.py index 4ed17e47b..dc338bb9c 100644 --- a/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group.py +++ b/tests/unit/easydiffraction/datablocks/structure/categories/test_space_group.py @@ -7,7 +7,6 @@ def test_space_group_name_updates_it_code(): sg = SpaceGroup() # default name 'P 1' should set code to the first available - default_code = sg.it_coordinate_system_code.value sg.name_h_m = 'P 1' assert sg.it_coordinate_system_code.value == sg._it_coordinate_system_code_allowed_values[0] # changing name resets the code again diff --git a/tests/unit/easydiffraction/datablocks/structure/item/test_base_coverage.py b/tests/unit/easydiffraction/datablocks/structure/item/test_base_coverage.py index f7f968f01..74d9e9e7e 100644 --- a/tests/unit/easydiffraction/datablocks/structure/item/test_base_coverage.py +++ b/tests/unit/easydiffraction/datablocks/structure/item/test_base_coverage.py @@ -13,7 +13,6 @@ from easydiffraction.datablocks.structure.categories.space_group.factory import SpaceGroupFactory from easydiffraction.datablocks.structure.item.base import Structure - # ------------------------------------------------------------------ # Fixture # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/display/plotters/test_ascii.py b/tests/unit/easydiffraction/display/plotters/test_ascii.py index 16dea0113..e8579a2c9 100644 --- a/tests/unit/easydiffraction/display/plotters/test_ascii.py +++ b/tests/unit/easydiffraction/display/plotters/test_ascii.py @@ -99,8 +99,8 @@ def test_ascii_plotter_plot_single_crystal(capsys): def test_ascii_plotter_single_crystal_marker_uses_paragraph_style(): - from easydiffraction.display.plotters.ascii import AsciiPlotter from easydiffraction.display.plotters.ascii import SINGLE_CRYSTAL_SCATTER_SYMBOL + from easydiffraction.display.plotters.ascii import AsciiPlotter from easydiffraction.utils.logging import CONSOLE_PARAGRAPH_STYLE line = AsciiPlotter._single_crystal_grid_line([ @@ -241,11 +241,10 @@ def fake_plot(series, config): captured['call'] = (series, config) return 'chart' - monkeypatch.setattr( - ascii_mod.shutil, - 'get_terminal_size', - lambda fallback: os.terminal_size(fallback), - ) + def fake_get_terminal_size(fallback): + return os.terminal_size(fallback) + + monkeypatch.setattr(ascii_mod.shutil, 'get_terminal_size', fake_get_terminal_size) monkeypatch.setattr(ascii_mod.asciichartpy, 'plot', fake_plot) AsciiPlotter().plot_powder( diff --git a/tests/unit/easydiffraction/display/plotters/test_plotly.py b/tests/unit/easydiffraction/display/plotters/test_plotly.py index 4f34a4ef9..6c1f6024d 100644 --- a/tests/unit/easydiffraction/display/plotters/test_plotly.py +++ b/tests/unit/easydiffraction/display/plotters/test_plotly.py @@ -593,7 +593,6 @@ def test_get_bragg_tick_trace_includes_peak_metadata(): def test_plot_powder_meas_vs_calc_creates_synced_three_panel_figure(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import BraggTickSet from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec @@ -709,7 +708,6 @@ def fake_show_figure(self, fig): def test_plot_powder_meas_vs_calc_adds_background_curve(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import BraggTickSet from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec @@ -856,7 +854,6 @@ def test_bragg_row_height_pixels_scale_linearly_with_phase_count(): def test_plot_powder_meas_vs_calc_grows_total_height_for_many_phases(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import BraggTickSet from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec @@ -918,7 +915,6 @@ def row_height_pixels(fig, axis_name: str) -> float: def test_plot_powder_meas_vs_calc_uses_explicit_plotly_height_as_pixels(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import BraggTickSet from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec @@ -963,7 +959,6 @@ def fake_show_figure(self, fig): def test_plot_powder_meas_vs_calc_keeps_top_and_bottom_rows_fixed(monkeypatch): """Top and residual rows keep a fixed pixel height.""" import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import BraggTickSet from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec @@ -1025,7 +1020,6 @@ def row_pixels(fig, axis_name: str) -> float: def test_plot_powder_meas_vs_calc_skips_bragg_row_when_no_ticks(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec captured = {} @@ -1069,7 +1063,6 @@ def fake_show_figure(self, fig): def test_plot_powder_meas_vs_calc_styles_predictive_max_posterior_and_band(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec captured = {} @@ -1116,7 +1109,6 @@ def fake_show_figure(self, fig): def test_plot_powder_meas_vs_calc_keeps_exact_residual_scale_match(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec captured = {} @@ -1171,7 +1163,6 @@ def fake_show_figure(self, fig): def test_plot_powder_meas_vs_calc_clips_large_residual_spikes(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec captured = {} @@ -1210,7 +1201,6 @@ def fake_show_figure(self, fig): def test_plot_powder_meas_vs_calc_accepts_empty_filtered_range(monkeypatch): import easydiffraction.display.plotters.plotly as pp - from easydiffraction.display.plotters.base import PowderMeasVsCalcSpec captured = {} @@ -1267,9 +1257,9 @@ def test_typed_arrays_to_float32_transcodes_and_preserves_shape(): def test_serialize_html_shared_is_lazy_placeholder_with_float32(): - import easydiffraction.display.plotters.plotly as pp import plotly.graph_objects as go + import easydiffraction.display.plotters.plotly as pp from easydiffraction.utils.environment import FigureEmbedMode fig = go.Figure(go.Scatter(x=np.arange(3000.0), y=np.arange(3000.0))) @@ -1289,9 +1279,9 @@ def test_serialize_html_shared_is_lazy_placeholder_with_float32(): def test_serialize_html_inline_is_eager_self_contained(): - import easydiffraction.display.plotters.plotly as pp import plotly.graph_objects as go + import easydiffraction.display.plotters.plotly as pp from easydiffraction.utils.environment import FigureEmbedMode fig = go.Figure(go.Scatter(x=np.arange(10.0), y=np.arange(10.0))) @@ -1328,10 +1318,11 @@ def test_typed_arrays_to_float32_leaves_integer_specs_untouched(): def test_typed_arrays_to_float32_roundtrips_through_plotly(): import base64 - import easydiffraction.display.plotters.plotly as pp import plotly.graph_objects as go import plotly.io as pio + import easydiffraction.display.plotters.plotly as pp + expected = np.arange(3000.0) * 1.5 fig = go.Figure(go.Scatter(x=np.arange(3000.0), y=expected)) downcast = pp._typed_arrays_to_float32(fig.to_plotly_json()) diff --git a/tests/unit/easydiffraction/display/structure/renderers/test_base.py b/tests/unit/easydiffraction/display/structure/renderers/test_base.py index 239c1d1e2..c65e9c4fd 100644 --- a/tests/unit/easydiffraction/display/structure/renderers/test_base.py +++ b/tests/unit/easydiffraction/display/structure/renderers/test_base.py @@ -12,7 +12,6 @@ from easydiffraction.display.structure.scene import AtomSphere from easydiffraction.display.structure.scene import StructureScene - # ------------------------------------------------------------------ # Test doubles # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/display/structure/test_viewing.py b/tests/unit/easydiffraction/display/structure/test_viewing.py index ad423b281..3a742c596 100644 --- a/tests/unit/easydiffraction/display/structure/test_viewing.py +++ b/tests/unit/easydiffraction/display/structure/test_viewing.py @@ -17,7 +17,6 @@ from easydiffraction.display.structure.viewing import Viewer from easydiffraction.display.structure.viewing import ViewerFactory - # ------------------------------------------------------------------ # Test doubles and fixtures # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/display/tablers/test_base.py b/tests/unit/easydiffraction/display/tablers/test_base.py index d5ddcf68b..a41302160 100644 --- a/tests/unit/easydiffraction/display/tablers/test_base.py +++ b/tests/unit/easydiffraction/display/tablers/test_base.py @@ -59,8 +59,8 @@ def test_rich_border_color_property(self): assert isinstance(color, str) def test_pandas_border_color_property(self): - from easydiffraction.display.theme import DARK_AXIS_FRAME_COLOR from easydiffraction.display.tablers.rich import RichTableBackend + from easydiffraction.display.theme import DARK_AXIS_FRAME_COLOR backend = RichTableBackend() color = backend._pandas_border_color diff --git a/tests/unit/easydiffraction/display/test_plotting.py b/tests/unit/easydiffraction/display/test_plotting.py index bb8f94a32..286065b78 100644 --- a/tests/unit/easydiffraction/display/test_plotting.py +++ b/tests/unit/easydiffraction/display/test_plotting.py @@ -7,7 +7,6 @@ from types import SimpleNamespace import numpy as np - import pytest @@ -765,10 +764,10 @@ def test_build_posterior_pairs_plot_rejects_unknown_style(): def test_build_param_distribution_plot_returns_plotly_figure(): + from easydiffraction.display.plotting import POSTERIOR_INTERVAL_95_FILL_COLOR from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_FILL_COLOR from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_COLOR from easydiffraction.display.plotting import POSTERIOR_PAIR_MARGINAL_DENSITY_LINE_WIDTH - from easydiffraction.display.plotting import POSTERIOR_INTERVAL_95_FILL_COLOR from easydiffraction.display.plotting import POSTERIOR_POINT_ESTIMATE_LINE_DASH plotter, fit_results, _ = _make_bayesian_plotter_fixture() @@ -898,10 +897,10 @@ def test_plot_param_distribution_routes_ascii_to_marginal_density(monkeypatch): def test_plot_posterior_predictive_summary_uses_consistent_labels_and_styles(monkeypatch): from types import SimpleNamespace + from easydiffraction.display.plotters.plotly import PlotlyPlotter from easydiffraction.display.plotting import POSTERIOR_INTERVAL_95_FILL_COLOR from easydiffraction.display.plotting import POSTERIOR_POINT_ESTIMATE_LINE_DASH from easydiffraction.display.plotting import Plotter - from easydiffraction.display.plotters.plotly import PlotlyPlotter captured: dict[str, object] = {} @@ -1347,7 +1346,7 @@ def test_resolve_posterior_parameter_names_warns_on_ambiguous_label(monkeypatch) monkeypatch.setattr( 'easydiffraction.display.plotting.log.warning', - lambda message: warning_messages.append(message), + warning_messages.append, ) result = Plotter._resolve_posterior_parameter_names( @@ -2047,7 +2046,7 @@ class FakeTabler: def render(self, df): captured['df'] = df - monkeypatch.setattr(TableRenderer, 'get', staticmethod(lambda: FakeTabler())) + monkeypatch.setattr(TableRenderer, 'get', staticmethod(FakeTabler)) class Param: def __init__(self, uid, unique_name): @@ -2094,11 +2093,11 @@ def test_plot_param_correlations_renders_plotly_heatmap(monkeypatch): import easydiffraction.display.plotters.plotly as plotly_mod from easydiffraction.display.plotting import POSTERIOR_PAIR_TITLE_FONT_SIZE - from easydiffraction.display.plotting import Plotter from easydiffraction.display.plotting import SQUARE_MATRIX_AXIS_TITLE_LINE_HEIGHT_PIXELS from easydiffraction.display.plotting import SQUARE_MATRIX_BOTTOM_MARGIN_PIXELS from easydiffraction.display.plotting import SQUARE_MATRIX_TITLE_YSHIFT_PIXELS from easydiffraction.display.plotting import SQUARE_MATRIX_TOP_MARGIN_PIXELS + from easydiffraction.display.plotting import Plotter captured = {} @@ -2284,7 +2283,7 @@ class FakeTabler: def render(self, df): captured['df'] = df - monkeypatch.setattr(TableRenderer, 'get', staticmethod(lambda: FakeTabler())) + monkeypatch.setattr(TableRenderer, 'get', staticmethod(FakeTabler)) class Param: def __init__(self, uid, unique_name): @@ -2386,7 +2385,6 @@ def fake_build(self, *, parameters, style, threshold, max_parameters): def test_plot_posterior_pairs_prints_title_before_ascii_backend_warning(monkeypatch): import easydiffraction.display.plotting as plotting_mod - from easydiffraction.display.plotting import Plotter events: list[tuple[str, str]] = [] @@ -2418,7 +2416,7 @@ class FakeTabler: def render(self, df): captured['df'] = df - monkeypatch.setattr(TableRenderer, 'get', staticmethod(lambda: FakeTabler())) + monkeypatch.setattr(TableRenderer, 'get', staticmethod(FakeTabler)) class Param: def __init__(self, uid, unique_name): @@ -2490,7 +2488,7 @@ class FakeTabler: def render(self, df): captured['df'] = df - monkeypatch.setattr(TableRenderer, 'get', staticmethod(lambda: FakeTabler())) + monkeypatch.setattr(TableRenderer, 'get', staticmethod(FakeTabler)) class Param: def __init__(self, uid, unique_name): diff --git a/tests/unit/easydiffraction/display/test_plotting_coverage.py b/tests/unit/easydiffraction/display/test_plotting_coverage.py index 008881bcf..ed2d9d342 100644 --- a/tests/unit/easydiffraction/display/test_plotting_coverage.py +++ b/tests/unit/easydiffraction/display/test_plotting_coverage.py @@ -4,7 +4,6 @@ import numpy as np - # ------------------------------------------------------------------ # PlotterEngineEnum # ------------------------------------------------------------------ diff --git a/tests/unit/easydiffraction/display/test_theme.py b/tests/unit/easydiffraction/display/test_theme.py index 3a2b3288d..552d3b5b8 100644 --- a/tests/unit/easydiffraction/display/test_theme.py +++ b/tests/unit/easydiffraction/display/test_theme.py @@ -5,7 +5,7 @@ def test_display_theme_colors_returns_light_and_dark_constants(): - import easydiffraction.display.theme as theme + from easydiffraction.display import theme light = theme.display_theme_colors(is_dark_theme=False) dark = theme.display_theme_colors(is_dark_theme=True) @@ -19,7 +19,7 @@ def test_display_theme_colors_returns_light_and_dark_constants(): def test_display_theme_colors_for_template_maps_plotly_templates(): - import easydiffraction.display.theme as theme + from easydiffraction.display import theme assert theme.display_theme_colors_for_template('plotly_white') is theme.LIGHT_THEME_COLORS assert theme.display_theme_colors_for_template('plotly_dark') is theme.DARK_THEME_COLORS @@ -27,7 +27,7 @@ def test_display_theme_colors_for_template_maps_plotly_templates(): def test_plot_backgrounds_opaque_and_paper_transparent(): - import easydiffraction.display.theme as theme + from easydiffraction.display import theme # Inside the axes rectangle is opaque; the figure paper stays # transparent so charts blend into the host page. diff --git a/tests/unit/easydiffraction/io/cif/test_iucr_writer.py b/tests/unit/easydiffraction/io/cif/test_iucr_writer.py index 5aa2deb01..c89708c5f 100644 --- a/tests/unit/easydiffraction/io/cif/test_iucr_writer.py +++ b/tests/unit/easydiffraction/io/cif/test_iucr_writer.py @@ -86,9 +86,7 @@ def _experiment_type(*, sample_form, beam_mode='constant wavelength'): def _fit_result(): - from easydiffraction.analysis.categories.fit_result.lsq import ( - LeastSquaresFitResult, - ) + from easydiffraction.analysis.categories.fit_result.lsq import LeastSquaresFitResult fit_result = LeastSquaresFitResult() fit_result._set_n_parameters(4) @@ -438,9 +436,7 @@ def test_iucr_loop_rows_are_not_padded_to_tag_width(): def test_iucr_atom_site_rows_preserve_parameter_uncertainties(): - from easydiffraction.datablocks.structure.categories.atom_sites.default import ( - AtomSite, - ) + from easydiffraction.datablocks.structure.categories.atom_sites.default import AtomSite from easydiffraction.io.cif.iucr_writer import _atom_site_row from easydiffraction.io.cif.iucr_writer import _atom_site_tags from easydiffraction.io.cif.iucr_writer import _write_loop diff --git a/tests/unit/easydiffraction/io/cif/test_serialize.py b/tests/unit/easydiffraction/io/cif/test_serialize.py index 68e8f9a3f..7d45387c6 100644 --- a/tests/unit/easydiffraction/io/cif/test_serialize.py +++ b/tests/unit/easydiffraction/io/cif/test_serialize.py @@ -32,7 +32,6 @@ def __init__(self): def test_format_param_value_with_uncertainty_uses_two_sig_digits(): import easydiffraction.io.cif.serialize as MUT - from easydiffraction.core.validation import AttributeSpec from easydiffraction.core.variable import Parameter from easydiffraction.io.cif.handler import CifHandler @@ -51,7 +50,6 @@ def test_format_param_value_with_uncertainty_uses_two_sig_digits(): def test_format_param_value_with_large_uncertainty_is_readable(): import easydiffraction.io.cif.serialize as MUT - from easydiffraction.core.validation import AttributeSpec from easydiffraction.core.variable import Parameter from easydiffraction.io.cif.handler import CifHandler @@ -151,7 +149,6 @@ def __init__(self): def test_analysis_from_cif_restores_fit_parameters_without_fit_result(): import easydiffraction.io.cif.serialize as MUT - from easydiffraction.analysis.analysis import Analysis class Project: diff --git a/tests/unit/easydiffraction/io/test_ascii.py b/tests/unit/easydiffraction/io/test_ascii.py index c76e8fcf4..2c48eaeef 100644 --- a/tests/unit/easydiffraction/io/test_ascii.py +++ b/tests/unit/easydiffraction/io/test_ascii.py @@ -1,6 +1,7 @@ # SPDX-FileCopyrightText: 2026 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -"""Tests for load_numeric_block, extract_project_from_zip, extract_data_paths_from_zip and extract_data_paths_from_dir.""" +"""Tests for load_numeric_block, extract_project_from_zip, +extract_data_paths_from_zip, and extract_data_paths_from_dir.""" from __future__ import annotations diff --git a/tests/unit/easydiffraction/io/test_results_sidecar.py b/tests/unit/easydiffraction/io/test_results_sidecar.py index ae95e9532..a750e06db 100644 --- a/tests/unit/easydiffraction/io/test_results_sidecar.py +++ b/tests/unit/easydiffraction/io/test_results_sidecar.py @@ -17,9 +17,7 @@ def _analysis_with_sidecar_payload( include_pair: bool = True, include_predictive: bool = True, ) -> object: - from easydiffraction.analysis.categories.fit_result.bayesian import ( - BayesianFitResult, - ) + from easydiffraction.analysis.categories.fit_result.bayesian import BayesianFitResult fit_result = BayesianFitResult() fit_result._set_result_kind('bayesian') diff --git a/tests/unit/easydiffraction/project/categories/structure_view/test_default.py b/tests/unit/easydiffraction/project/categories/structure_view/test_default.py index 0655ebc28..c933efbba 100644 --- a/tests/unit/easydiffraction/project/categories/structure_view/test_default.py +++ b/tests/unit/easydiffraction/project/categories/structure_view/test_default.py @@ -61,9 +61,7 @@ def test_identity_category_code(view): def test_registered_with_factory(): - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory # The @StructureViewFactory.register decorator in default.py must # register the concrete class under its type_info tag. diff --git a/tests/unit/easydiffraction/project/categories/structure_view/test_factory.py b/tests/unit/easydiffraction/project/categories/structure_view/test_factory.py index aca39d25f..a8e72fa70 100644 --- a/tests/unit/easydiffraction/project/categories/structure_view/test_factory.py +++ b/tests/unit/easydiffraction/project/categories/structure_view/test_factory.py @@ -15,18 +15,14 @@ def test_module_import(): def test_default_rules_universal_fallback(): - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory # The factory declares a single universal-fallback rule. assert StructureViewFactory._default_rules == {frozenset(): 'default'} def test_supported_tags_lists_default(): - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory tags = StructureViewFactory.supported_tags() assert isinstance(tags, list) @@ -34,17 +30,13 @@ def test_supported_tags_lists_default(): def test_default_tag_without_conditions(): - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory assert StructureViewFactory.default_tag() == 'default' def test_default_tag_with_unmatched_conditions_falls_back(): - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory # Extra conditions still match the empty-key universal fallback. assert StructureViewFactory.default_tag(scattering_type='bragg') == 'default' @@ -52,18 +44,14 @@ def test_default_tag_with_unmatched_conditions_falls_back(): def test_create_returns_structure_view(): from easydiffraction.project.categories.structure_view.default import StructureView - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory structure_view = StructureViewFactory.create('default') assert isinstance(structure_view, StructureView) def test_create_rejects_unknown_tag(): - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory with pytest.raises(ValueError, match=r"Unsupported type: 'missing'"): StructureViewFactory.create('missing') @@ -71,9 +59,7 @@ def test_create_rejects_unknown_tag(): def test_create_default_for_returns_structure_view(): from easydiffraction.project.categories.structure_view.default import StructureView - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory structure_view = StructureViewFactory.create_default_for() assert isinstance(structure_view, StructureView) @@ -81,18 +67,14 @@ def test_create_default_for_returns_structure_view(): def test_supported_for_includes_registered_class(): from easydiffraction.project.categories.structure_view.default import StructureView - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory supported = StructureViewFactory.supported_for() assert StructureView in supported def test_show_supported_lists_default(capsys): - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory StructureViewFactory.show_supported() out = capsys.readouterr().out @@ -103,9 +85,7 @@ def test_show_supported_lists_default(capsys): def test_registry_is_independent_from_base(): from easydiffraction.core.factory import FactoryBase from easydiffraction.project.categories.structure_view.default import StructureView - from easydiffraction.project.categories.structure_view.factory import ( - StructureViewFactory, - ) + from easydiffraction.project.categories.structure_view.factory import StructureViewFactory # __init_subclass__ gives each factory its own registry; the # registered concrete class must not leak onto the shared base. diff --git a/tests/unit/easydiffraction/project/test_display.py b/tests/unit/easydiffraction/project/test_display.py index 1dfa513b8..4c862a085 100644 --- a/tests/unit/easydiffraction/project/test_display.py +++ b/tests/unit/easydiffraction/project/test_display.py @@ -12,8 +12,8 @@ from easydiffraction.datablocks.experiment.item.enums import SampleFormEnum from easydiffraction.datablocks.experiment.item.enums import ScatteringTypeEnum from easydiffraction.datablocks.structure.item.base import Structure -from easydiffraction.display.progress import ACTIVITY_LABEL_PROCESSING from easydiffraction.display.plotting import _MeasVsCalcPlotOptions +from easydiffraction.display.progress import ACTIVITY_LABEL_PROCESSING from easydiffraction.display.structure.builder import FeatureAvailability from easydiffraction.project.categories.structure_style.default import StructureStyle from easydiffraction.project.display import PatternOptionStatus diff --git a/tests/unit/easydiffraction/project/test_project.py b/tests/unit/easydiffraction/project/test_project.py index 92b336483..137541b2d 100644 --- a/tests/unit/easydiffraction/project/test_project.py +++ b/tests/unit/easydiffraction/project/test_project.py @@ -1,8 +1,8 @@ # SPDX-FileCopyrightText: 2025 EasyScience contributors # SPDX-License-Identifier: BSD-3-Clause -from collections import UserList import csv +from collections import UserList from types import SimpleNamespace diff --git a/tests/unit/easydiffraction/project/test_project_config.py b/tests/unit/easydiffraction/project/test_project_config.py index 6c9a4beeb..eba88ae2a 100644 --- a/tests/unit/easydiffraction/project/test_project_config.py +++ b/tests/unit/easydiffraction/project/test_project_config.py @@ -9,8 +9,8 @@ def test_project_config_exposes_project_info_chart_and_table_categories(): from easydiffraction.core.category_owner import CategoryOwner from easydiffraction.project.categories.rendering_plot import RenderingPlot - from easydiffraction.project.categories.report import Report from easydiffraction.project.categories.rendering_table import RenderingTable + from easydiffraction.project.categories.report import Report from easydiffraction.project.project_config import ProjectConfig from easydiffraction.project.project_info import ProjectInfo diff --git a/tests/unit/easydiffraction/report/test_data_context.py b/tests/unit/easydiffraction/report/test_data_context.py index 96bb30651..b3d2d24a3 100644 --- a/tests/unit/easydiffraction/report/test_data_context.py +++ b/tests/unit/easydiffraction/report/test_data_context.py @@ -297,9 +297,7 @@ def test_report_data_loop_rows_are_display_truncated(): def test_report_pd_data_columns_use_compact_labels(): - from easydiffraction.datablocks.experiment.categories.data.bragg_pd import ( - PdCwlData, - ) + from easydiffraction.datablocks.experiment.categories.data.bragg_pd import PdCwlData from easydiffraction.report.data_context import _collection_category_context category = PdCwlData() @@ -324,9 +322,7 @@ def test_report_pd_data_columns_use_compact_labels(): def test_report_powder_refln_columns_use_compact_labels(): from easydiffraction.analysis.calculators.base import PowderReflnRecord - from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import ( - PowderCwlReflnData, - ) + from easydiffraction.datablocks.experiment.categories.refln.bragg_pd import PowderCwlReflnData from easydiffraction.report.data_context import _collection_category_context category = PowderCwlReflnData() diff --git a/tests/unit/easydiffraction/report/test_tex_renderer.py b/tests/unit/easydiffraction/report/test_tex_renderer.py index 61efd39a7..c9bec96ec 100644 --- a/tests/unit/easydiffraction/report/test_tex_renderer.py +++ b/tests/unit/easydiffraction/report/test_tex_renderer.py @@ -445,7 +445,6 @@ def test_save_tex_report_removes_stale_managed_bundle_dirs(tmp_path): def test_save_tex_report_writes_structure_figure_png(tmp_path): import easydiffraction as ed - from easydiffraction.report.tex_renderer import save_tex_report project = ed.Project(name='struct_fig') diff --git a/tests/unit/easydiffraction/test___init__.py b/tests/unit/easydiffraction/test___init__.py index 086efa88b..396840e43 100644 --- a/tests/unit/easydiffraction/test___init__.py +++ b/tests/unit/easydiffraction/test___init__.py @@ -32,7 +32,7 @@ def test___getattr__unknown_raises_attribute_error(): def test_lazy_functions_execute_with_monkeypatch(monkeypatch, capsys, tmp_path): import easydiffraction as ed - import easydiffraction.utils.utils as utils + from easydiffraction.utils import utils # 1) list_tutorials uses _fetch_tutorials_index → monkeypatch there fake_tutorial_index = { @@ -49,7 +49,7 @@ def test_lazy_functions_execute_with_monkeypatch(monkeypatch, capsys, tmp_path): assert 'Tutorials available for easydiffraction' in out # 2) download_data should consult index and call pooch.retrieve without network - import easydiffraction.utils.utils as utils + from easydiffraction.utils import utils fake_index = { '12': { diff --git a/tests/unit/easydiffraction/utils/test_environment.py b/tests/unit/easydiffraction/utils/test_environment.py index d74d40025..223a8e4d2 100644 --- a/tests/unit/easydiffraction/utils/test_environment.py +++ b/tests/unit/easydiffraction/utils/test_environment.py @@ -12,11 +12,11 @@ def test_returns_true_in_pytest(self): class TestInWarp: def test_false_by_default(self): - from easydiffraction.utils.environment import in_warp - # Unless running in Warp terminal import os + from easydiffraction.utils.environment import in_warp + if os.getenv('TERM_PROGRAM') != 'WarpTerminal': assert in_warp() is False diff --git a/tests/unit/easydiffraction/utils/test_utils_coverage.py b/tests/unit/easydiffraction/utils/test_utils_coverage.py index d9a1a0f20..b0148a031 100644 --- a/tests/unit/easydiffraction/utils/test_utils_coverage.py +++ b/tests/unit/easydiffraction/utils/test_utils_coverage.py @@ -8,7 +8,6 @@ import numpy as np import pytest - # --- _validate_url ----------------------------------------------------------- @@ -378,7 +377,7 @@ def test_download_data_success(monkeypatch, tmp_path, capsys): def fake_retrieve(url, known_hash, fname, path): import pathlib - pathlib.Path(path, fname).write_text('x y e') + pathlib.Path(path, fname).write_text('x y e', encoding='utf-8') return str(pathlib.Path(path, fname)) monkeypatch.setattr(MUT.pooch, 'retrieve', fake_retrieve) @@ -408,7 +407,7 @@ def test_download_data_overwrite_existing(monkeypatch, tmp_path, capsys): def fake_retrieve(url, known_hash, fname, path): import pathlib - pathlib.Path(path, fname).write_text('new data') + pathlib.Path(path, fname).write_text('new data', encoding='utf-8') return str(pathlib.Path(path, fname)) monkeypatch.setattr(MUT.pooch, 'retrieve', fake_retrieve) @@ -462,7 +461,7 @@ def test_download_data_uses_tutorial_artifact_root_fallback(monkeypatch, tmp_pat def fake_retrieve(url, known_hash, fname, path): import pathlib - pathlib.Path(path, fname).write_text('x y e') + pathlib.Path(path, fname).write_text('x y e', encoding='utf-8') return str(pathlib.Path(path, fname)) monkeypatch.setattr(MUT.pooch, 'retrieve', fake_retrieve) diff --git a/tests/unit/tools/test_lint_rule_audit.py b/tests/unit/tools/test_lint_rule_audit.py new file mode 100644 index 000000000..d16c62e49 --- /dev/null +++ b/tests/unit/tools/test_lint_rule_audit.py @@ -0,0 +1,138 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Tests for tools/lint_rule_audit.py aggregation logic (no Ruff run).""" + +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + + +def _load_audit(): + repo_root = Path(__file__).resolve().parents[3] + module_path = repo_root / 'tools' / 'lint_rule_audit.py' + spec = importlib.util.spec_from_file_location('lint_rule_audit', module_path) + module = importlib.util.module_from_spec(spec) + sys.modules[spec.name] = module + spec.loader.exec_module(module) + return module + + +def _records(): + """Synthetic Ruff JSON records covering every scope and fix kind.""" + return [ + { + 'code': 'D100', + 'message': 'Missing module docstring', + 'filename': 'src/easydiffraction/a.py', + 'fix': None, + }, + { + 'code': 'D100', + 'message': 'Missing module docstring', + 'filename': 'src/easydiffraction/b.py', + 'fix': None, + }, + { + 'code': 'I001', + 'message': 'Import block un-sorted', + 'filename': 'tests/unit/x.py', + 'fix': {'applicability': 'safe'}, + }, + { + 'code': 'T201', + 'message': '`print` found', + 'filename': 'src/easydiffraction/c.py', + 'fix': {'applicability': 'unsafe'}, + }, + { + 'code': 'W505', + 'message': 'Doc line too long', + 'filename': 'docs/docs/tutorials/ed-1.py', + 'fix': None, + }, + { + 'code': 'RUF100', + 'message': 'Unused noqa', + 'filename': 'docs/dev/plans/p.py', + 'fix': {'applicability': 'display'}, + }, + ] + + +def test_scope_classifies_each_tree(): + module = _load_audit() + assert module.scope('src/easydiffraction/x.py') == 'src' + assert module.scope('tests/unit/x.py') == 'tests' + assert module.scope('docs/docs/tutorials/ed-1.py') == 'tutorials' + # Other docs paths are NOT tutorials. + assert module.scope('docs/dev/plans/foo.py') == 'other' + assert module.scope('docs/mkdocs.yml') == 'other' + assert module.scope('tools/x.py') == 'other' + assert module.scope('') == 'other' + + +def test_scope_handles_absolute_paths(): + module = _load_audit() + inside = str(module.REPO_ROOT / 'src' / 'easydiffraction' / 'x.py') + assert module.scope(inside) == 'src' + # Absolute path outside the repo root cannot be relativised. + assert module.scope('/somewhere/else/x.py') == 'other' + + +def test_family_strips_numeric_suffix(): + module = _load_audit() + assert module.family('PLC0415') == 'PLC' + assert module.family('D100') == 'D' + assert module.family('A002') == 'A' + assert module.family('SLF001') == 'SLF' + assert module.family('W505') == 'W' + + +def test_aggregate_counts_totals_scopes_and_fixability(): + module = _load_audit() + summary = module.aggregate(_records()) + + assert summary['D100']['total'] == 2 + assert summary['D100']['src'] == 2 + assert summary['D100']['family'] == 'D' + assert summary['D100']['message'] == 'Missing module docstring' + + assert summary['I001']['tests'] == 1 + assert summary['I001']['fix_safe'] == 1 + assert summary['I001']['fix_unsafe'] == 0 + + assert summary['T201']['src'] == 1 + assert summary['T201']['fix_unsafe'] == 1 + assert summary['T201']['fix_safe'] == 0 + + assert summary['W505']['tutorials'] == 1 + + # 'docs/dev/...' is 'other', and a 'display' fix counts as neither + # safe nor unsafe. + assert summary['RUF100']['other'] == 1 + assert summary['RUF100']['fix_safe'] == 0 + assert summary['RUF100']['fix_unsafe'] == 0 + + +def test_aggregate_handles_empty_records(): + module = _load_audit() + assert module.aggregate([]) == {} + + +def test_format_table_reports_header_rules_and_footer_totals(): + module = _load_audit() + table = module.format_table(module.aggregate(_records())) + assert 'RULE' in table + assert 'D100' in table + # 6 records, 5 distinct rules, 1 safe fix, 1 unsafe fix. + assert 'Total: 6 violations across 5 rules' in table + assert '1 safe-fixable' in table + assert '1 need --unsafe-fixes' in table + + +def test_format_table_handles_empty_summary(): + module = _load_audit() + table = module.format_table({}) + assert 'Total: 0 violations across 0 rules' in table diff --git a/tools/lint_rule_audit.py b/tools/lint_rule_audit.py new file mode 100644 index 000000000..67aaf34eb --- /dev/null +++ b/tools/lint_rule_audit.py @@ -0,0 +1,255 @@ +# SPDX-FileCopyrightText: 2026 EasyScience contributors +# SPDX-License-Identifier: BSD-3-Clause +"""Regenerate the disabled-rule inventory for the lint-rule audit. + +This helper reproduces the table in +``docs/dev/plans/lint-rule-audit.md`` without modifying any tracked +file. It layers the currently-disabled Ruff rules onto the *unmodified* +``pyproject.toml`` at the command line, runs ``ruff check``, and prints +a per-rule breakdown by source scope (``src``/``tests``/``tutorials``) +and auto-fixability:: + + pixi run python tools/lint_rule_audit.py + +The aggregation logic (:func:`scope`, :func:`family`, :func:`aggregate`) +is kept pure and importable so it can be unit-tested without invoking +Ruff; :func:`collect_records` and :func:`main` are the thin shim that +shells out to Ruff and prints. +""" + +from __future__ import annotations + +import json +import shutil +import subprocess # noqa: S404 +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] + +# Trees that the ``py-lint-check`` pixi task covers. +LINT_PATHS = ('src/', 'tests/', 'docs/docs/tutorials/') + +# Disabled rules enabled for the audit. The commented-out ``select`` +# families (``A``, ``FIX``, ``SLF``, ``T20``, ``TD``) plus the three +# *Temporary* global-ignore codes. ``--extend-select`` overrides the +# global ``ignore`` list for an exact code, so the global-ignore codes +# are listed here rather than removed from ``ignore``. +EXTEND_SELECT = ('A', 'FIX', 'SLF', 'T20', 'TD', 'D100', 'D104', 'DTZ005') + +# Per-file-ignore table reduced to the documented (structural) entries +# only, dropping the *Temporary* tests/docs entries so their impact is +# measured. Passed verbatim to ``ruff check --config``. +PER_FILE_IGNORES = ( + "lint.per-file-ignores = {" + "'*/__init__.py' = ['F401'], " + "'tests/**' = ['ANN','D','DOC','INP001','RUF012','RUF069','S101'], " + "'docs/**' = ['INP001','RUF001','RUF002','RUF003','T201'], " + "'docs/docs/tutorials/**' = ['E402']}" +) + +# Scopes reported in the table, in display order. +SCOPES = ('src', 'tests', 'tutorials') + + +def scope(filename: str, root: Path = REPO_ROOT) -> str: + """Return the audit scope that a file belongs to. + + Parameters + ---------- + filename + Path reported by Ruff (absolute or repo-relative). + root + Repository root used to relativise absolute paths. + + Returns + ------- + str + ``'src'``, ``'tests'``, ``'tutorials'``, or ``'other'``. + """ + path = Path(filename) + if path.is_absolute(): + try: + path = path.relative_to(root) + except ValueError: + return 'other' + parts = path.parts + if parts[:3] == ('docs', 'docs', 'tutorials'): + return 'tutorials' + if parts[:1] == ('tests',): + return 'tests' + if parts[:1] == ('src',): + return 'src' + return 'other' + + +def family(code: str) -> str: + """Return the rule family (leading alphabetic prefix) of a code. + + Parameters + ---------- + code + A Ruff rule code such as ``'PLC0415'``. + + Returns + ------- + str + The leading-alphabetic prefix, e.g. ``'PLC'`` for ``'PLC0415'``. + """ + end = 0 + while end < len(code) and code[end].isalpha(): + end += 1 + return code[:end] + + +def aggregate(records: list[dict], root: Path = REPO_ROOT) -> dict[str, dict]: + """Aggregate Ruff JSON records into per-rule totals. + + Parameters + ---------- + records + Parsed ``ruff check --output-format=json`` records. + root + Repository root used to assign each record a scope. + + Returns + ------- + dict + Mapping of rule code to a summary with ``total``, per-scope + counts (``src``/``tests``/``tutorials``/``other``), ``fix_safe`` + (applied by ``ruff --fix``), ``fix_unsafe`` (needs + ``--unsafe-fixes``), ``family``, and a representative + ``message``. + """ + summary: dict[str, dict] = {} + for record in records: + code = record['code'] + entry = summary.get(code) + if entry is None: + entry = { + 'total': 0, + 'src': 0, + 'tests': 0, + 'tutorials': 0, + 'other': 0, + 'fix_safe': 0, + 'fix_unsafe': 0, + 'family': family(code), + 'message': record['message'], + } + summary[code] = entry + entry['total'] += 1 + entry[scope(record['filename'], root)] += 1 + applicability = (record.get('fix') or {}).get('applicability') + if applicability == 'safe': + entry['fix_safe'] += 1 + elif applicability == 'unsafe': + entry['fix_unsafe'] += 1 + return summary + + +def ruff_command(ruff_path: str) -> list[str]: + """Return the non-destructive audit ``ruff check`` command. + + Parameters + ---------- + ruff_path + Absolute path to the ``ruff`` executable. + + Returns + ------- + list of str + Argument vector for :func:`subprocess.run`. + """ + return [ + ruff_path, + 'check', + *LINT_PATHS, + '--extend-select', + ','.join(EXTEND_SELECT), + '--config', + PER_FILE_IGNORES, + '--output-format=json', + '--no-cache', + ] + + +def collect_records(root: Path = REPO_ROOT) -> list[dict]: + """Run the audit ``ruff check`` and return parsed JSON records. + + Parameters + ---------- + root + Repository root; used as the working directory for Ruff. + + Returns + ------- + list of dict + Parsed Ruff JSON records. + + Raises + ------ + SystemExit + If the ``ruff`` executable cannot be located on ``PATH``. + """ + ruff_path = shutil.which('ruff') + if ruff_path is None: + msg = "ruff not found on PATH; run via 'pixi run python tools/lint_rule_audit.py'." + raise SystemExit(msg) + # Ruff exits non-zero when violations exist, which is expected here. + result = subprocess.run( # noqa: S603 + ruff_command(ruff_path), + cwd=str(root), + capture_output=True, + text=True, + encoding='utf-8', + ) + if not result.stdout: + raise SystemExit(result.stderr.strip() or 'ruff produced no output') + return json.loads(result.stdout) + + +def format_table(summary: dict[str, dict]) -> str: + """Render a per-rule table sorted by descending total count. + + Parameters + ---------- + summary + The mapping returned by :func:`aggregate`. + + Returns + ------- + str + A fixed-width, printable table. The ``safe`` column counts fixes + applied by ``ruff --fix``; ``uns`` counts fixes that require + ``--unsafe-fixes``. + """ + header = ( + f'{"RULE":9}{"TOTAL":>7}{"src":>6}{"tests":>7}{"tut":>6}' + f'{"safe":>6}{"uns":>5} description' + ) + lines = [header, '-' * len(header)] + for code in sorted(summary, key=lambda c: -summary[c]['total']): + entry = summary[code] + lines.append( + f'{code:9}{entry["total"]:7}{entry["src"]:6}{entry["tests"]:7}' + f'{entry["tutorials"]:6}{entry["fix_safe"]:6}{entry["fix_unsafe"]:5}' + f' {entry["message"][:44]}' + ) + total = sum(entry['total'] for entry in summary.values()) + safe = sum(entry['fix_safe'] for entry in summary.values()) + unsafe = sum(entry['fix_unsafe'] for entry in summary.values()) + lines.append('-' * len(header)) + lines.append( + f'Total: {total} violations across {len(summary)} rules; ' + f'{safe} safe-fixable (ruff --fix), {unsafe} need --unsafe-fixes.' + ) + return '\n'.join(lines) + + +def main() -> None: + """Print the disabled-rule inventory table.""" + print(format_table(aggregate(collect_records()))) + + +if __name__ == '__main__': + main()