Skip to content

Commit 9e62f05

Browse files
authored
fix: Address warnings, error on malformed TSV contents (#2240)
* fix: Opt in to pandas 3.0 behaviors, when possible * fix: Address warning identified with -Werror * fix: Raise error on warnings in custom fence * fix: Bad tabs in TSV examples
1 parent a5aaf93 commit 9e62f05

File tree

9 files changed

+142
-39
lines changed

9 files changed

+142
-39
lines changed

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ dependencies = [
55
"bidsschematools[render]",
66
"mkdocs>=1.1.0",
77
"mkdocs-material>=5.4",
8-
"pymdown-extensions>=7.0.0",
8+
"pymdown-extensions>=9.2.0",
99
"mkdocs-branchcustomization-plugin~=0.1.3",
1010
"mkdocs-macros-plugin",
1111
"mkdocs-redirects",
@@ -57,3 +57,7 @@ replace = """NEXT_VERSION_HERE
5757
- '{new_version}'
5858
- """
5959
include_bumps = ['pre_label']
60+
61+
[[tool.mypy.overrides]]
62+
module = ["pymdownx.*"]
63+
ignore_missing_imports = true

src/derivatives/imaging.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -548,9 +548,9 @@ standard BIDS labels:
548548

549549
```tsv
550550
index name abbreviation
551-
137 pars opercularis IFGop
552-
138 pars triangularis IFGtr
553-
139 pars orbitalis IFGor
551+
137 pars opercularis IFGop
552+
138 pars triangularis IFGtr
553+
139 pars orbitalis IFGor
554554
```
555555

556556
<!-- Link Definitions -->

src/modality-specific-files/microscopy.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -420,8 +420,8 @@ Additional optional columns MAY be used to describe other subjects' attributes.
420420
`participants.tsv` example:
421421
```tsv
422422
participant_id species strain strain_rrid
423-
sub-01 mus musculus C57BL/6J RRID:IMSR_JAX:000664
424-
sub-02 mus musculus C57BL/6J RRID:IMSR_JAX:000664
423+
sub-01 mus musculus C57BL/6J RRID:IMSR_JAX:000664
424+
sub-02 mus musculus C57BL/6J RRID:IMSR_JAX:000664
425425
```
426426

427427
`participants.json` example:

tools/schemacode/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ render = [
4343
"tabulate",
4444
"pandas",
4545
"markdown-it-py",
46+
"pymdown-extensions>=9.2",
4647
]
4748
tests = [
4849
"bidsschematools[validation,render,expressions]",

tools/schemacode/src/bidsschematools/render/tsv.py

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,12 @@
77
from markdown_it import MarkdownIt
88
from tabulate import tabulate
99

10+
from ..utils import filter_warnings, in_context
11+
from .utils import propagate_fence_exception
1012

13+
14+
@in_context(filter_warnings(["error"]))
15+
@propagate_fence_exception
1116
def fence(
1217
source: str,
1318
language: str,
@@ -27,32 +32,26 @@ def fence(
2732

2833
classes[:0] = ["tsv-table", "index" if linenums else "noindex"]
2934

30-
try:
31-
df = pd.read_csv(
32-
io.StringIO(source),
33-
sep="\t",
34-
dtype=str,
35-
index_col=False,
36-
keep_default_na=False,
37-
header=None if "noheader" in attrs else "infer",
38-
)
39-
md_table = tabulate(
40-
df, # type: ignore
41-
tablefmt="github",
42-
showindex=linenums,
43-
headers="keys",
44-
numalign="right",
45-
)
46-
html = MarkdownIt("commonmark").enable("table").render(md_table)
47-
if "noheader" in attrs:
48-
html = re.sub("<thead>.+</thead>", "", html, flags=re.DOTALL)
49-
50-
html = html.replace("<table>", f'<table class="{" ".join(classes)}">')
51-
52-
# Remove newlines from HTML to prevent copy-paste from inserting spaces
53-
return html.replace("\n", "")
54-
except Exception:
55-
import traceback
56-
57-
exc = traceback.format_exc()
58-
return f"<pre>{exc}</pre>"
35+
df = pd.read_csv(
36+
io.StringIO(source),
37+
sep="\t",
38+
dtype=str,
39+
index_col=False,
40+
keep_default_na=False,
41+
header=None if "noheader" in attrs else "infer",
42+
)
43+
md_table = tabulate(
44+
df, # type: ignore
45+
tablefmt="github",
46+
showindex=linenums,
47+
headers="keys",
48+
numalign="right",
49+
)
50+
html = MarkdownIt("commonmark").enable("table").render(md_table)
51+
if "noheader" in attrs:
52+
html = re.sub("<thead>.+</thead>", "", html, flags=re.DOTALL)
53+
54+
html = html.replace("<table>", f'<table class="{" ".join(classes)}">')
55+
56+
# Remove newlines from HTML to prevent copy-paste from inserting spaces
57+
return html.replace("\n", "")

tools/schemacode/src/bidsschematools/render/utils.py

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,47 @@
33
import math
44
import posixpath
55
import re
6+
from functools import cache, wraps
7+
8+
import pandas as pd
9+
10+
from ..utils import in_context
11+
12+
13+
def _pandas_3_0():
14+
"""Silence pandas warnings and opt in to future behavior.
15+
16+
This sets pandas behavior to 3.0+ defaults.
17+
Prior to pandas 3.0, the fillna() and replace() methods would convert
18+
object columns to float64 if the resulting series was all float64.
19+
In 3.0+, you need to use infer_objects() to do this.
20+
21+
This also opts-in to copy-on-write, which previously required `copy=False`
22+
to be set.
23+
"""
24+
if args := _pandas_3_0_options():
25+
return pd.option_context(*args)
26+
27+
import contextlib
28+
29+
return contextlib.nullcontext()
30+
31+
32+
@cache
33+
def _pandas_3_0_options():
34+
options = [
35+
("future.no_silent_downcasting", True),
36+
("mode.copy_on_write", True),
37+
]
38+
39+
args = []
40+
for option in options:
41+
try:
42+
pd.get_option(option[0])
43+
except KeyError:
44+
continue
45+
args.extend(option)
46+
return args
647

748

849
def _link_with_html(string, html_path=None, heading=None, pdf_format=False):
@@ -113,6 +154,7 @@ def combine_extensions(lst, html_path=None, heading_lst=None, pdf_format=True):
113154
return new_lst
114155

115156

157+
@in_context(_pandas_3_0())
116158
def drop_unused_entities(df):
117159
"""Remove columns from a dataframe where all values in the column are NaNs.
118160
@@ -130,8 +172,7 @@ def drop_unused_entities(df):
130172
df : pandas.DataFrame
131173
DataFrame with columns associated with unused entities removed.
132174
"""
133-
df = df.replace("", math.nan).dropna(axis=1, how="all").fillna("")
134-
return df
175+
return df.replace("", math.nan).dropna(axis=1, how="all").fillna("")
135176

136177

137178
def flatten_multiindexed_columns(df):
@@ -412,3 +453,18 @@ def num2words(integer, to="ordinal"):
412453
return mapper[integer]
413454
except KeyError:
414455
raise ValueError(f"Input {integer} is not supported.")
456+
457+
458+
def propagate_fence_exception(func):
459+
"""Decorator to prevent superfences from swallowing exceptions."""
460+
461+
@wraps(func)
462+
def wrapper(*args, **kwargs):
463+
try:
464+
return func(*args, **kwargs)
465+
except Exception as e:
466+
from pymdownx.superfences import SuperFencesException
467+
468+
raise SuperFencesException from e
469+
470+
return wrapper

tools/schemacode/src/bidsschematools/utils.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
import logging
66
import os
77
import sys
8+
import warnings
9+
from contextlib import contextmanager
10+
from functools import wraps
811

912
from . import data
1013

@@ -149,3 +152,41 @@ def jsonschema_validator(
149152
validator_kwargs: ValidatorKwargs
150153
validator_kwargs = {"format_checker": validator_cls.FORMAT_CHECKER} if check_format else {}
151154
return validator_cls(schema, **validator_kwargs) # type: ignore[call-arg]
155+
156+
157+
def in_context(context_manager):
158+
"""Convert a context manager into a function decorator.
159+
160+
Parameters
161+
----------
162+
context_manager : context manager
163+
The context manager to use.
164+
165+
Returns
166+
-------
167+
Callable
168+
The function decorator.
169+
"""
170+
171+
def decorator(func):
172+
@wraps(func)
173+
def wrapper(*args, **kwargs):
174+
with context_manager:
175+
return func(*args, **kwargs)
176+
177+
return wrapper
178+
179+
return decorator
180+
181+
182+
@contextmanager
183+
def filter_warnings(*filters):
184+
"""Context manager to apply warning filters.
185+
186+
Arguments are lists of positional arguments to :func:`warnings.filterwarnings`.
187+
"""
188+
189+
with warnings.catch_warnings():
190+
for filt in filters:
191+
warnings.filterwarnings(*filt)
192+
yield

tools/schemacode/src/bidsschematools/validator.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ def write_report(
495495
"""
496496

497497
report_path = report_path.format(
498-
datetime=datetime.datetime.utcnow().strftime(datetime_format),
498+
datetime=datetime.datetime.now(datetime.timezone.utc).strftime(datetime_format),
499499
pid=os.getpid(),
500500
)
501501
report_path = os.path.abspath(os.path.expanduser(report_path))

uv.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)