Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions dandi/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"genotype",
"sex",
"species",
"strain",
"subject_id",
)

Expand Down
26 changes: 23 additions & 3 deletions dandi/pytest_plugin.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from dandischema.models import DandiBaseModel
from pytest import Config, Item, Parser

from .tests.fixtures import * # noqa: F401, F403 # lgtm [py/polluting-import]
Expand Down Expand Up @@ -28,9 +29,7 @@
"ai_generated",
]
for marker in markers:
config.addinivalue_line(
"markers", marker
)
config.addinivalue_line("markers", marker)


def pytest_collection_modifyitems(items: list[Item], config: Config) -> None:
Expand All @@ -46,3 +45,24 @@
deselected_items.append(item)
config.hook.pytest_deselected(items=deselected_items)
items[:] = selected_items


def pytest_assertrepr_compare(op, left, right):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@candleindark if you see how to improve this -- the goal is to make it easier to understand diffs on our pydantic models. If you see how -- please try your idea first before suggesting since there could be "gotchas" ;-)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of

ldict, rdict = dict(left), dict(right)

we can

ldict, rdict = left.model_dump(), right.model_dump()

so we can get all the sub-models recursively converted to dictionaries.

Beyond that, we can use library like https://zepworks.com/deepdiff/8.6.1/diff.html and https://github.com/xlwings/jsondiff to locate the exact difference in the nested structure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe model dump doesn't work here, hence I suggested to try first ;-)

Copy link
Member

@candleindark candleindark Oct 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After digging into it a bit more. Here is what I found.

The reason model_dump() doesn't work is because the right operant is not a valid DandiBaseModel instance and contains values such as anys.ANY_AWARE_DATETIME, which is not serializable Pydantic by default. The right operate is constructed by calling the model_construct method to bypass the validation.

One way to allow a meaningful comparison illustration of the two operants is to make model_dump() to work on the right operant as well. This can be accomplished by using a custom serializer to the dandischema.models.DandiBaseModel. This custom serializer is depicted in the simplified model below.

Custom serializer example
from typing import Any

from anys import ANY_AWARE_DATETIME, AnyBase

from datetime import datetime


from pydantic import BaseModel, field_serializer, SerializerFunctionWrapHandler


class Foo(BaseModel):
    date: datetime = datetime.fromisoformat("2000-01-01")

    @field_serializer("*", mode="wrap")
    def ignore_any_types(
        self, value: Any, handler: SerializerFunctionWrapHandler
    ) -> Any:
        if isinstance(value, AnyBase):
            return value
        return handler(value)


foo = Foo.model_construct(date=ANY_AWARE_DATETIME)
print(foo.model_dump())

Once this custom serializer is added to dandischema.models.DandiBaseModel. We can change the line https://github.com/dandi/dandi-cli/blob/28d954c7620f80d96fc749dcd39410acdbbd10d9/tox.ini#L16C5-L16C46

to

coverage run -m pytest -vv {posargs} dandi

to see the detailed difference in the structure of the objects.

Note This solution involves changing the dandi-schema repo, including adding the anys dependency to the dandischema pacakge.

@yarikoptic Please let me know if you want to proceed with this solution. I will send a PR if yes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since you did this analysis already, I think it might well be with adding. But I wonder if we could avoid need too depend on any. Presumably we can get instance only if any already installed, at we can easily just try to import any and if gains to import just not to do that check.

"""Custom comparison representation for your classes."""
if (
isinstance(left, DandiBaseModel)
and isinstance(right, DandiBaseModel)
and op == "=="
):
ldict, rdict = dict(left), dict(right)
if ldict == rdict:
return [
"dict representations of models are equal, but values aren't!",
f"Left: {left!r}",
f"Right: {right!r}",
]
else:
# Rely on pytest just "recursing" into interpreting the dict fails
# TODO: could be further improved by account for ANY values etc
assert ldict == rdict # for easier comprehension of diffs

Check warning

Code scanning / CodeQL

Redundant comparison Warning

Test is always false, because of
this condition
.

Copilot Autofix

AI 6 months ago

To fix this issue, we should remove the redundant and always-failing assertion assert ldict == rdict on line 67. This assertion is unnecessary, because the logic already branches appropriately depending on whether the dictionaries are equal, and the function's documentation/comment notes that pytest will "recurse" into its own comparison machinery if the dicts differ. By deleting this line, we avoid dead code and potential confusion from inevitable, spurious assertion errors. We do not need to add imports or change any other logic. Only remove line 67 from dandi/pytest_plugin.py.


Suggested changeset 1
dandi/pytest_plugin.py

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/dandi/pytest_plugin.py b/dandi/pytest_plugin.py
--- a/dandi/pytest_plugin.py
+++ b/dandi/pytest_plugin.py
@@ -64,5 +64,4 @@
         else:
             # Rely on pytest just "recursing" into interpreting the dict fails
             # TODO: could be further improved by account for ANY values etc
-            assert ldict == rdict  # for easier comprehension of diffs
     return None
EOF
@@ -64,5 +64,4 @@
else:
# Rely on pytest just "recursing" into interpreting the dict fails
# TODO: could be further improved by account for ANY values etc
assert ldict == rdict # for easier comprehension of diffs
return None
Copilot is powered by AI and may make mistakes. Always verify output.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of

ldict, rdict = dict(left), dict(right)

we can

ldict, rdict = left.model_dump(), right.model_dump()

so we can get all the sub-models recursively converted to dictionaries.

Beyond that, we can use library like https://zepworks.com/deepdiff/8.6.1/diff.html and https://github.com/xlwings/jsondiff to locate the exact difference in the nested structure.

return None
1 change: 1 addition & 0 deletions dandi/tests/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def simple2_nwb(
date_of_birth=datetime(2016, 12, 1, tzinfo=tzutc()),
sex="U",
species="Mus musculus",
strain="C57BL/6J",
),
**simple1_nwb_metadata,
)
Expand Down
6 changes: 4 additions & 2 deletions dandi/tests/test_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import shutil
from typing import Any

from anys import ANY_AWARE_DATETIME, AnyFullmatch, AnyIn
from anys import ANY_AWARE_DATETIME, ANY_INT, AnyFullmatch, AnyIn
from dandischema.consts import DANDI_SCHEMA_VERSION
from dandischema.metadata import validate
from dandischema.models import (
Expand All @@ -24,6 +24,7 @@
SexType,
Software,
SpeciesType,
StrainType,
)
from dandischema.models import Dandiset as DandisetMeta
from dateutil.tz import tzutc
Expand Down Expand Up @@ -873,7 +874,7 @@ def test_nwb2asset(simple2_nwb: Path) -> None:
],
),
],
contentSize=ByteSize(19664),
contentSize=ANY_INT,
encodingFormat="application/x-nwb",
digest={DigestType.dandi_etag: "dddddddddddddddddddddddddddddddd-1"},
path=str(simple2_nwb),
Expand All @@ -898,6 +899,7 @@ def test_nwb2asset(simple2_nwb: Path) -> None:
identifier="http://purl.obolibrary.org/obo/NCBITaxon_10090",
name="Mus musculus - House mouse",
),
strain=StrainType(schemaKey="StrainType", name="C57BL/6J"),
),
],
variableMeasured=[],
Expand Down