Skip to content

Conversation

@candleindark
Copy link
Contributor

@candleindark candleindark commented Nov 4, 2025

The main goal of this PR is to make the RunConfig model introduced in #164 more succinct and declarative.

To accomplish this goal, it

  1. removes the None type annotations for the bids_directory, cache_directory, and run_id fields. Annotating these fields as None is inconsistent with their behavior since these fields never hold a None value once the containing model object is initialized. The codebase also expects these values to be non-None as in
    self._parent_run_directory = self.cache_directory / "runs"
    . If the None annotations are not removed, the code above would raise a mypy error since None doesn't support the / operator.
  2. removes the literal choice of "auto" for the file_mode field . Similar to situation described in the previous point, annotating the file_mode field with a possible value of "auto" contradicts with the behavior that the file_mode field is never "auto" once the containing object is initialized. Our codebase already depends on this behavior,
    if file_mode == "copy":
    shutil.copy(src=nwbfile_path, dst=session_file_path)
    elif file_mode == "move":
    shutil.move(src=nwbfile_path, dst=session_file_path)
    elif file_mode == "symlink":
    session_file_path.symlink_to(target=nwbfile_path)
    . (The "auto" value is never checked in that code).
  3. redefines the _parent_run_directory and _run_directory as properties. These two "properties" should be more appropriately defined as properties since that are calculated based on other attributes and should be updated when those other attributes change. They should also be read-only.
  4. invokes the processes of setting the default values of different fields by passing them as the default_factory of the respective field instead of invoking them in the model_post_init() hook method. This help make the model more declarative.
  5. modifies _validate_existing_directory_as_bids to be used as a field validator. This help make the model more declarative and allows raising validation errors with proper location.

Aside from accomplishing the main goal, this PR incidentally made the following minor improvements in validation of existing directory as a BIDS directory.

  1. Ensure dataset_description.json exists as a file. Raise proper error otherwise.
  2. Handle the situation in which dataset_description.json doesn't contain proper JSON

… annotations

Redefine `_parent_run_directory` and
`_run_directory` of `RunConfig` as private
properties since they are computed based on
the value of a field of the model and are never
modified. Additionally, `None` type annotations
are removed from these properties for they are
really never `None`
… annotation

Set the default of `run_id` using the default factory
mechanism. This allows a more declarative definition
of the model and `model_post_init` should be reserved
for setting something that requires multi-field access.
Additionally, the `None` type annotation is removed.
`run_id` was never `None` once it was set. The `None`
value was only used as an indication of absence of
user provided value and to signal the setting of a
default. Setting the default of `run_id` using the
default factory mechanism obviates the `None` type
annotation.
…one` type annotation

Set the default of `cache_directory` using the default
factory mechanism. This allows a more declarative
definition of the model and `model_post_init` should be
reserved for setting something that requires multi-field
access. Additionally, the `None` type annotation is removed.
`cache_directory` was never `None` once it was set. The `None`
value was only used as an indication of absence of
user provided value and to signal the setting of a
default. Setting the default of `cache_directory` using the
default factory mechanism obviates the `None` type
annotation.
…ne` type annotation

Set the default of `bids_directory` using the default
factory mechanism. This allows a more declarative
definition of the model and `model_post_init` should be
reserved for setting something that requires multi-field
access. Additionally, the `None` type annotation is removed.
`bids_directory` was never `None` once it was set. The `None`
value was only used as an indication of absence of
user provided value and to signal the setting of a
default. Setting the default of `bids_directory` using the
default factory mechanism obviates the `None` type
annotation.
@codecov
Copy link

codecov bot commented Nov 4, 2025

Codecov Report

❌ Patch coverage is 84.78261% with 7 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.31%. Comparing base (4028e5d) to head (ca5ff77).

Files with missing lines Patch % Lines
src/nwb2bids/_converters/_run_config.py 90.32% 3 Missing ⚠️
src/nwb2bids/_command_line_interface/_main.py 0.00% 2 Missing ⚠️
src/nwb2bids/_core/_file_mode.py 84.61% 2 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@              Coverage Diff               @@
##           add_config     #175      +/-   ##
==============================================
- Coverage       87.56%   87.31%   -0.25%     
==============================================
  Files              27       28       +1     
  Lines             965      962       -3     
==============================================
- Hits              845      840       -5     
- Misses            120      122       +2     
Flag Coverage Δ
unittests 87.31% <84.78%> (-0.25%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/nwb2bids/_command_line_interface/_main.py 0.00% <0.00%> (ø)
src/nwb2bids/_core/_file_mode.py 84.61% <84.61%> (ø)
src/nwb2bids/_converters/_run_config.py 94.54% <90.32%> (+0.10%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@candleindark candleindark changed the title Add config revise config model Revise config model Nov 4, 2025
Comment on lines +61 to +64
@property
def _parent_run_directory(self) -> pathlib.Path:
"""The parent directory where all run-specific directories are stored."""
return self.cache_directory / "runs"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Since it is only being set once, isn't it cleaner to set it when I originally did? (otherwise, this line gets re-called every time you access the property)

Copy link
Collaborator

Choose a reason for hiding this comment

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

Further, marking it as a PrivateAttr with the rest of the fields make it more obvious that is there and has all the other pydantic side effects you would expect

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Since it is only being set once, isn't it cleaner to set it when I originally did? (otherwise, this line gets re-called every time you access the property)

It may be cleaner. However, what I see is that this value along with _run_directory are based on the value of another attribute in the model, and since this RunConfig is mutable (Pydantic models don't have true immutability), defining these two values as private attributes can render them outdated once the containing model object is mutated. Additionally, since these two values are based on the value of another attribute, it seems more appropriate to me that they are read-only.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Further, marking it as a PrivateAttr with the rest of the fields make it more obvious that is there and has all the other pydantic side effects you would expect

I don't know which side effects you are referring to. The main point having a private attribute in a Pydantic model is that you can keep information in the model that behaves very differently than a regular field, https://docs.pydantic.dev/latest/concepts/models/#private-model-attributes.

…to"` value option

Set the default of `file_mode` using the default
factory mechanism. This allows a more declarative
definition of the model and `model_post_init`
should be reserved for setting something that
requires multi-field access. Additionally, the
 `"auto"` value option is removed.
`file_mode` was never `"auto"` once it was set.
The `"auto"` value was only used as an indication
of absence of user provided value and to signal
the setting of a default. Setting the default of
`file_mode` using the default factory mechanism
obviates the `"auto"` value option and allows
the type annotation of the `file_mode` field
to be consistent with the possible values
that it holds in operation.
…dator

Doing it this way makes the definition of the
model more declarative and also avoids using
`model_post_init` which should be preserved
for validations that requires access to multiple
fields of the model
Doing this allows CLI arguments to remain the same
to users while making the `RunConfig` more
succinct. For example, `RunConfig.run_id` can
now be of type `str` instead of `str | None` even
though it doesn't hold a value of `None` in
operation.
When validating a directory as a BIDS directory,
ensure it is a file instead of just ensuring its
mere existence. This rules out the situation
where `dataset_description.json` is a directory
…json file

when validating a directory as a BIDS directory
from .._core._run_id import _generate_run_id


def _validate_existing_directory_as_bids(directory: pathlib.Path) -> pathlib.Path:
Copy link
Collaborator

Choose a reason for hiding this comment

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

Might as well move this out like you did the file mode?

Copy link
Collaborator

Choose a reason for hiding this comment

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

(into a separate file)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can do that. I don't have a strong preference. Do you want to _core?

However, I want you to know that _validate_existing_directory_as_bids is a field validator in the RunConfig now. The argument it takes, the values it returns, and the errors it raises, are dictated by Pydantic. I.e. It may not have any other use, so it may be a good idea to just keep it close the model that uses it, as it is really part of the model in some sense.

Comment on lines -22 to +73
file_mode : one of "move", "copy", "symlink", or "auto", default: "auto"
file_mode : one of "move", "copy", or "symlink"
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yarik specifically requested the "auto" value previously

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I can re-add it if he really wants it, but the it will not be a consistent model.

I preserved "auto" as an option in the CLI, so it is as user friend to the CLI users. Adding it into the config model doesn't make much sense since once an a model object is initialized, the field never hold a value of "auto". This behavior is actually reflected in your code.

if file_mode == "copy":
shutil.copy(src=nwbfile_path, dst=session_file_path)
elif file_mode == "move":
shutil.move(src=nwbfile_path, dst=session_file_path)
elif file_mode == "symlink":
session_file_path.symlink_to(target=nwbfile_path)

There is no checking for the "auto" value in that example, which is appropriate.

@yarikoptic What do you think? It may not be what you want initially, but I think it works better.

@CodyCBakerPhD
Copy link
Collaborator

Cool, looks like you got it working!

Some minor comments above

@candleindark
Copy link
Contributor Author

Cool, looks like you got it working!

Some minor comments above

It is quite a bit of tweaks, but I think it is worth it since this model permeates through out the codebase. It will help simplify the code or/and avoid quite a bit of mypy errors down the road.

@CodyCBakerPhD CodyCBakerPhD added the enhancement New feature or request label Nov 4, 2025
@candleindark candleindark requested a review from Copilot November 4, 2025 22:40
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR refactors the RunConfig class to use Pydantic's field validators and default factories for better validation and initialization, replacing the previous approach that used None defaults with post-initialization logic.

Key changes:

  • Extracted file mode detection and BIDS directory validation into standalone functions
  • Converted optional fields with None defaults to use default_factory with validators
  • Updated command-line interface to filter out unset parameters before validation

Reviewed Changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
src/nwb2bids/_core/_file_mode.py New module containing _determine_file_mode() function extracted from RunConfig
src/nwb2bids/_converters/_run_config.py Refactored to use Pydantic validators and default factories; extracted validation logic to standalone function; converted private attributes to properties
src/nwb2bids/_command_line_interface/_main.py Updated to filter CLI parameters before passing to RunConfig.model_validate()

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@candleindark candleindark marked this pull request as ready for review November 4, 2025 23:11
@CodyCBakerPhD CodyCBakerPhD merged commit e74ecf1 into con:add_config Nov 5, 2025
31 of 44 checks passed
@CodyCBakerPhD
Copy link
Collaborator

🚀 PR was released in v0.7.0 🚀

@CodyCBakerPhD CodyCBakerPhD added the released This issue/pull request has been released. label Dec 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request released This issue/pull request has been released.

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants