Skip to content

More conversions to pattern matching#4443

Merged
freakboy3742 merged 8 commits into
beeware:mainfrom
HalfWhitt:more-pattern-matching
Jun 4, 2026
Merged

More conversions to pattern matching#4443
freakboy3742 merged 8 commits into
beeware:mainfrom
HalfWhitt:more-pattern-matching

Conversation

@HalfWhitt

@HalfWhitt HalfWhitt commented May 31, 2026

Copy link
Copy Markdown
Member

I decided to give my brain a break from thinking about substantive changes, and do some sprucing up. None of these match statements make particularly involved use of pattern-matching; they're all basically switch statements. I believe most should be self-explanatory, and hopefully read better; I'll drop some notes inline on oddities.

PR Checklist:

  • I will abide by the BeeWare Code of Conduct
  • I have read and have followed the CONTRIBUTING.md file
  • This PR was generated or assisted using an AI tool

Comment thread core/src/toga/hardware/location.py
found = all(
getattr(item, attr) == value for attr, value in data.items()
)
case Iterable() if not isinstance(data, str):

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I know isinstance(data, Iterable) is not, strictly speaking, entirely the same thing as hasattr(data, "__iter__"). But it makes the intent easier to read. If we're particularly worried about, say, performance with runtime-checkable user classes, I can revert this (and the other places like it).

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.

I don't think there was a good reason for this to be an __iter__ check rather than an Iterable check - so this looks good to me.

The "type() if not..." syntax is a new one for me though... not sure how I feel about that... but if that's how match statements work, 🤷

@HalfWhitt HalfWhitt Jun 4, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's called a guard; you can put any conditional you want after a pattern, and it'll be checked if the pattern matches. It even has access to variables bound in the pattern, so you could do something fancy like:

match some_string.split():
    case first_word, "and", second_word if first_word == second_word:

Ideally though, I'd REALLY like it if there were just a NonStringIterable ABC to test against (and a NonStringSequence). Strings getting missed when one assumes a non-string sequence/iterable is one of my pet peeves.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Side note: interestingly, strings (and bytes) are already special-cased... by pattern matching.

first, second, third = "abc"  # Unpacks just fine, of course

match "abc":
    case first, second, third:  # Doesn't match!
        ...

Which is a good thing in my book, since you'd almost never want that on purpose.


@value.setter
def value(self, value: object) -> None:
def value(self, value: datetime.date | str | None) -> None:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm not sure if there might have been a specific reason for this object type hint, and its counterpart in TimeInput. I've updated it to include everything the setter can handle. (datetime isn't explicitly listed because it's a subclass of date.)

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.

Like all good typing questions in this repo, most of them go back to the monstrosity of #2252 🤣

There's a few reasons it is typed as object.

  1. It is typed as object to allow _convert_date() to perform the type narrowing and redefine value as a datetime.date
  • If this setter is updated to use specific types, then I'd probably argue it should be an abstracted type that is used any where a un-normalized date is accepted....but it probably doesn't matter that much
  1. There was a larger issue with typing property methods and their setters in Python

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Hm... should I revert it, then?

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.

eh....i think i'm mostly indifferent. your change is probably better from a user perspective since they would see some real types instead of object (however, i think that mostly depends on how python/mypy#18510 was implemented).

Also, I saw this in Python discourse today....interesting timing: https://discuss.python.org/t/class-date-is-unsafe-in-typed-python/107590

So, I wonder if treating date and datetime the same is actually safe here

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Oh, okay. So maybe just add in datetime?

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.

That's probably unnecessary; but I did find it interesting that comparisons between date and datetime can lead to unexpected results. So, since the Date widget returns a date, users could experience weird outcomes if they are comparing it to a datetime. It might be total overkill for us to try to handle anything here given this is general Python behavior....but useful nugget to keep in mind.

Comment thread core/src/toga/window.py
"""
close_window = True
if self.app.main_window == self:
if self.app.main_window is self:

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Noticed this == comparison, that I'm pretty sure should be is.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hmm... there's no defined equality comparsion on Window, so they should be equivalent. But is reads smoother.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yeah, functionally it's identical. Just a semantic quibble.

@HalfWhitt HalfWhitt marked this pull request as ready for review May 31, 2026 23:12

@johnzhou721 johnzhou721 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since we're already doing minor cleanups, if you're intertested you might as well rewrap the following places that you've also touched (granted, I wrote 3/4 of these comments, so it's mostly my fault that I didn't wrap them when I wrote it.)

Comment thread cocoa/src/toga_cocoa/widgets/dateinput.py Outdated
Comment thread cocoa/src/toga_cocoa/widgets/timeinput.py Outdated
Comment thread dummy/src/toga_dummy/command.py Outdated
Comment thread gtk/src/toga_gtk/libs/gtk.py
@HalfWhitt

Copy link
Copy Markdown
Member Author

These lines have been commented out for years:

def rehint(self):
# print(
# "REHINT",
# self,
# self.native.get_preferred_width(),
# self.native.get_preferred_height(),
# )
# width = self.native.get_allocation().width
# height = self.native.get_allocation().height
width = self.interface._MIN_WIDTH
height = self.interface._MIN_HEIGHT
self.interface.intrinsic.height = at_least(width)
self.interface.intrinsic.width = at_least(height)

@freakboy3742, looks like you were the last one to touch these. Are they fine to remove? If not, I can revert a7c5652.

@johnzhou721

Copy link
Copy Markdown
Contributor

@HalfWhitt May I ask what tool you use to rewrap comments, or do you just do it manually? Is there a way to show the limit for the number of characters? I'm asking this because I feel slightly guilty as it seems like I'm the person who leaves the most number of unwrapped comments in the codebase...

@HalfWhitt

Copy link
Copy Markdown
Member Author

@HalfWhitt May I ask what tool you use to rewrap comments, or do you just do it manually? Is there a way to show the limit for the number of characters?

I code in Sublime Text, and it has options for placing one or more visual rulers (the vertical grey line on the right), as well as auto-wrapping comments to a specified width. There's even a keyboard shortcut for it.

screenshot of Sublime Text showing ruler

@johnzhou721

Copy link
Copy Markdown
Contributor

Thanks for the extra tip! I'll try to see if something is possible in vscode... feeling really guilty for not configuring my environment correctly now.

@HalfWhitt

Copy link
Copy Markdown
Member Author

It's really not a big deal. If you can get it set up to do it, that's great. But perfect comment wrapping isn't something worth agonizing over.

@freakboy3742 freakboy3742 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.

A lot of code churn, but I think I agree these are all net gains in readability. Nice work!

decor_view.setSystemUiVisibility(0)
self.show_actionbar(True)
self._in_presentation_mode = False
self.set_window_state(state)

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.

This is an example match statements are (arguably) not as good as if statements - the commonality of the set_window_state() call has been lost in these last two calls.

On net, I think the overall clarity improvement of the "two states match" overrides that downside - but it's worth keeping an eye on the general pattern.

found = all(
getattr(item, attr) == value for attr, value in data.items()
)
case Iterable() if not isinstance(data, str):

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.

I don't think there was a good reason for this to be an __iter__ check rather than an Iterable check - so this looks good to me.

The "type() if not..." syntax is a new one for me though... not sure how I feel about that... but if that's how match statements work, 🤷

@freakboy3742 freakboy3742 merged commit 7ecb75c into beeware:main Jun 4, 2026
64 checks passed
@HalfWhitt HalfWhitt deleted the more-pattern-matching branch June 4, 2026 14:05
johnzhou721 pushed a commit to johnzhou721/toga that referenced this pull request Jun 5, 2026
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.

4 participants