Skip to content

Allow configuration of multiple paths - #615

Open
nmeum wants to merge 1 commit into
pimutils:mainfrom
nmeum:pr/paths-configuration
Open

Allow configuration of multiple paths#615
nmeum wants to merge 1 commit into
pimutils:mainfrom
nmeum:pr/paths-configuration

Conversation

@nmeum

@nmeum nmeum commented Jul 20, 2026

Copy link
Copy Markdown

For my work machine, I want to be able to access both my private and work-related to-dos. Unfortunately, they are stored in two entirely different directories, which I cannot easily match using a single glob(3) expression. For this kind of use case, this PR introduces a new configuration variable called paths which can be set to a list of glob(3) expressions to include multiple directories with .ics files.

The proposed patch is backwards compatible with the previous configuration format as the list's value defaults to a single-element list containing the value of the previous path configuration option, if the paths option is unset.

I intentionally tried to implement this in a way that retains backwards compatibility, I would also be open to other designs. For example, making the existing path configuration option a list of strings instead of a string. I haven't added tests yet but can do so once we agreed on a design.

This introduces a new configuration variable called "paths" which
can be set to a list to include multiple directories of directories
with .ics files. This is useful when, for example, different directories
are used for private and work-related to-do entries, which cannot
be easily matched using a single glob(3) expression.

The proposed patch is backwards compatible with the previous
configuration format as the list's value defaults to a single-element
list containing the value of the previous "path" configuration option,
if the "paths" option is unset.
@nmeum
nmeum force-pushed the pr/paths-configuration branch 3 times, most recently from d89d49b to a3a7fe5 Compare July 20, 2026 16:02
@nmeum

nmeum commented Jul 22, 2026

Copy link
Copy Markdown
Author

Not a Python person myself, but I assume the mypy error on the CI are not caused by my changes?

mypy error output on the build.sr.ht/alpine-py312 CI.
lint: commands[2]> mypy .
todoman/widgets.py:70: error: No return value expected  [return-value]
todoman/widgets.py:70: error: Argument 1 to "keypress" of "Edit" has incompatible type "tuple[int, int] | tuple[int] | tuple[()]"; expected "tuple[int]"  [arg-type]
todoman/widgets.py:123: error: "Widget" has no attribute "_loop"  [attr-defined]
todoman/widgets.py:150: error: Cannot infer value of type parameter "WrappedWidget" of "__init__" of "WidgetWrap"  [misc]
todoman/interactive.py:59: error: Incompatible types in assignment (expression has type "Text", variable has type "str")  [assignment]
todoman/interactive.py:60: error: List item 0 has incompatible type "tuple[int, str]"; expected "Widget | tuple[Literal['pack', WHSettings.PACK] | int, Widget] | tuple[Literal['given', WHSettings.GIVEN], int, Widget] | tuple[Literal['weight', WHSettings.WEIGHT], int | float, Widget]"  [list-item]
todoman/interactive.py:60: error: List item 1 has incompatible type "object"; expected "Widget | tuple[Literal['pack', WHSettings.PACK] | int, Widget] | tuple[Literal['given', WHSettings.GIVEN], int, Widget] | tuple[Literal['weight', WHSettings.WEIGHT], int | float, Widget]"  [list-item]
todoman/interactive.py:63: error: Argument 1 to "Pile" has incompatible type "list[tuple[str, Columns]]"; expected "Iterable[Widget | tuple[Literal['pack', WHSettings.PACK] | int, Widget] | tuple[Literal['given', WHSettings.GIVEN], int, Widget] | tuple[Literal['weight', WHSettings.WEIGHT], int | float, Widget]]"  [arg-type]
todoman/interactive.py:77: error: Argument "parent" to "ExtendedEdit" has incompatible type "TodoEditor"; expected "Widget"  [arg-type]
todoman/interactive.py:81: error: Argument "parent" to "ExtendedEdit" has incompatible type "TodoEditor"; expected "Widget"  [arg-type]
todoman/interactive.py:86: error: Argument "parent" to "ExtendedEdit" has incompatible type "TodoEditor"; expected "Widget"  [arg-type]
todoman/interactive.py:90: error: Argument "parent" to "ExtendedEdit" has incompatible type "TodoEditor"; expected "Widget"  [arg-type]
todoman/interactive.py:94: error: Argument "parent" to "ExtendedEdit" has incompatible type "TodoEditor"; expected "Widget"  [arg-type]
todoman/interactive.py:98: error: Argument "parent" to "ExtendedEdit" has incompatible type "TodoEditor"; expected "Widget"  [arg-type]
todoman/interactive.py:103: error: Argument "parent" to "PrioritySelector" has incompatible type "TodoEditor"; expected "Widget"  [arg-type]
todoman/interactive.py:143: error: "ListWalker" has no attribute "contents"  [attr-defined]
todoman/interactive.py:144: error: "ListWalker" has no attribute "contents"  [attr-defined]
todoman/interactive.py:146: error: "ListWalker" has no attribute "contents"  [attr-defined]
todoman/interactive.py:156: error: Incompatible types in assignment (expression has type "MainLoop", variable has type "None")  [assignment]
todoman/interactive.py:159: error: Argument "unhandled_input" to "MainLoop" has incompatible type "Callable[[str], None]"; expected "Callable[[str | tuple[str, int, int, int]], bool | None] | None"  [arg-type]
tests/test_widgets.py:16: error: Argument 1 to "ExtendedEdit" has incompatible type "None"; expected "Widget"  [arg-type]
tests/test_widgets.py:44: error: Argument 1 to "ExtendedEdit" has incompatible type "None"; expected "Widget"  [arg-type]
tests/test_widgets.py:60: error: Argument 1 to "ExtendedEdit" has incompatible type "None"; expected "Widget"  [arg-type]
tests/test_widgets.py:82: error: Argument 1 to "ExtendedEdit" has incompatible type "None"; expected "Widget"  [arg-type]
tests/test_widgets.py:98: error: Argument 1 to "ExtendedEdit" has incompatible type "None"; expected "Widget"  [arg-type]
tests/test_widgets.py:114: error: Argument 1 to "ExtendedEdit" has incompatible type "None"; expected "Widget"  [arg-type]
tests/test_widgets.py:168: error: Argument 1 to "PrioritySelector" has incompatible type "None"; expected "Widget"  [arg-type]
tests/test_ui.py:120: error: "ListWalker" has no attribute "contents"  [attr-defined]
tests/test_ui.py:124: error: "ListWalker" has no attribute "contents"  [attr-defined]
tests/test_ui.py:155: error: "ListWalker" has no attribute "contents"  [attr-defined]
Found 30 errors in 4 files (checked 24 source files)

With respect to the CI coverage failure, would address this by adding tests if we agree on this API (see the original description for possible alternative designs).

@WhyNotHugo WhyNotHugo left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall idea sounds good. Note a huge fan, but I don't see a cleaner way to support this, and the request sounds reasonable.

I think we should fail with an error when both path and paths are set.

Mostly minor comments, general changes are good.

Comment thread todoman/cli.py
paths = [
path
for path in glob.iglob(path)
if isdir(path) and not path.endswith("__pycache__")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Ignoring __pycache__ specifically is unusual, but acceptable I guess.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

That was part of the existing implementation, hence I retained it.

if isdir(path) and not path.endswith("__pycache__")

Comment thread todoman/cli.py
Comment thread todoman/cli.py
paths = ctx.config["paths"]
if len(paths) == 0:
raise exceptions.NoListsFoundError(ctx.config["path"])
paths = glob_path(ctx.config["path"])

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we update ctx.config["paths"] with this new value?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Is there a specific reason why this should be necessary? The existing implementation does also not update ctx.config["path"] after calling .iglob() on it:

todoman/todoman/cli.py

Lines 363 to 369 in 78e3578

paths = [
path
for path in glob.iglob(ctx.config["path"])
if isdir(path) and not path.endswith("__pycache__")
]
if len(paths) == 0:
raise exceptions.NoListsFoundError(ctx.config["path"])

Comment thread todoman/cli.py Outdated
@nmeum
nmeum force-pushed the pr/paths-configuration branch from a3a7fe5 to 91c21b3 Compare July 23, 2026 09:57
@nmeum

nmeum commented Jul 23, 2026

Copy link
Copy Markdown
Author

Overall idea sounds good. Note a huge fan, but I don't see a cleaner way to support this, and the request sounds reasonable.

Thanks for the feedback! This could get a little cleaner by deprecating the path configuration option in the future. I think having a list of glob expressions instead of a single glob expressions for the path configuration is generally a "cleaner" approach to configuring searched directories.

@nmeum
nmeum force-pushed the pr/paths-configuration branch from 91c21b3 to 89949e6 Compare July 23, 2026 10:03
Comment thread todoman/cli.py
Comment on lines +374 to +378
# XXX: There is no good way to check if an option has been explicitly set.
default_path = expand_path(CONFIG_SPEC[0].default)
if ctx.config["path"] != default_path and len(ctx.config["paths"]) != 0:
raise ConfigurationError("Both 'path' and 'paths' is set, use one.")

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

I think we should fail with an error when both path and paths are set.

Implemented this here, but it's a little hacky as path has a default value and we thus need to determine if it has been explicitly set by the user, for which there doesn't seem to be an API at the moment.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Also can't do this via a configuration validation function as it doesn't have access to the already parsed options.

@nmeum
nmeum force-pushed the pr/paths-configuration branch from 89949e6 to 79e3cf8 Compare July 23, 2026 10:08
@nmeum

nmeum commented Aug 10, 2026

Copy link
Copy Markdown
Author

As an alternative, potentially less hacky design, we could also make path an Any type and then use the old behavior if it is a string, or new one if it is a list.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants