Skip to content

More path cleanups, and ruff --fix#3587

Merged
HalfWhitt merged 4 commits into
beeware:mainfrom
HalfWhitt:more_path_cleanups
Jun 27, 2025
Merged

More path cleanups, and ruff --fix#3587
HalfWhitt merged 4 commits into
beeware:mainfrom
HalfWhitt:more_path_cleanups

Conversation

@HalfWhitt

@HalfWhitt HalfWhitt commented Jun 26, 2025

Copy link
Copy Markdown
Member

Yet another pass at tidying / shortening paths. While doing so, I noticed that the Ruff pre-commit hook doesn't automatically run --fix, but it can be added as an argument, so I put that in here too.

PR Checklist:

  • All new features have been tested
  • All new features have been documented
  • I have read the CONTRIBUTING.md file
  • I will abide by the code of conduct

Comment thread core/tests/test_paths.py
assert f"app.paths.config={home / 'config' / full_name}" in results
assert f"app.paths.data={home / 'user_data' / full_name}" in results
assert f"app.paths.cache={home / 'cache' / full_name}" in results
assert f"app.paths.logs={home / 'logs' / full_name}" in results

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.

Normally I'd opt for home / f"logs/{full_name}" over home / "logs" / full_name, to avoid creating an unnecessary intermediate Path object. However, in this case, I think it's more important to avoid nested f-strings when feasible.

(Also since these are all pulled off Path.home(), which is already absolute, I don't think there's any need for resolve(). Let me know if I'm missing something.)

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.

My recollection is the resolve() was needed because of how mounted network drives appear on Windows... but I might be misremembering that one. If it's passing CI, that's enough for me.

@rmartin16 rmartin16 Jun 26, 2025

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.

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.

Huh. Any idea why that was needed, but removing it now doesn't reintroduce the error? Has handling of capitalization changed, either in pathlib or in Windows?

Comment thread testbed/tests/test_icons.py Outdated
rf"Unable to load icon from "
rf"{re.escape(str(app.paths.app / 'resources' / 'icons' / 'bad'))}"
r"Unable to load icon from "
+ re.escape(str(app.paths.app / "resources/icons/bad"))

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.

Maybe this is just me, but I hate f-strings that don't contain anything outside of the brackets. Sure, you get automatic concatenation with other string literals, but an operator's fine here. (It also converts to string, of course, but in this case re.escape() already returns one.)

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.

Yeah - this is a corner of Python syntax where there are no good answers. The default black/ruff answer of "split this string and don't even use an operator" is worse, IMHO, because easily mistaken for 2 separate arguments.

A moot point in this case, because you need the operator for the escape.

@rmartin16 rmartin16 Jun 26, 2025

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.

FWIW, AFAIU, f-strings are optimized in cpython...especially with some shenanigans they pull; string addition still requires new memory allocations and memory copies. Not incredibly important....but why I look past 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.

Yeah - and in this case, it's not even f-strings that are the issue - the parser reads it as a single string, so there's only one allocation. A really minor optimisation, especially in this context, but an optimisation nonetheless.

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.

Turns out there is a good answer...

escaped_path = re.escape(str(app.paths.app / "resources/icons/bad"))
with pytest.raises(ValueError, match=rf"Unable to load icon from {escaped_path}"):
    toga.Icon("resources/icons/bad")

No nested f-strings, no more allocated strings than necessary (unless I'm mistaken), and it's fewer lines. 😎

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.

Unless the interpreter does some fancy optimization with strings that aren't assigned to variables, anyway. This and the original each create three strings (str, escape, and the f-string), but this one does assign one of them. (Not that it really matters here either way...)

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.

Not to drag this out too much farther... :) but this is what I mean. The new implementation introduces an additional operation in the bytecode to make one string at runtime. The original implementation had the parser create one string at compile time.

>>> import re
... def implicit(var1, var2):
...     f"asdf asdf {var1}" f"{re.escape(var2)}"
... def addition(var1, var2):
...     f"asdf asdf {var1}" + re.escape(var2)
...
>>>
>>> dis.dis(implicit)
  2           RESUME                   0

  3           LOAD_CONST               1 ('asdf asdf ')
              LOAD_FAST                0 (var1)
              FORMAT_SIMPLE
              LOAD_GLOBAL              0 (re)
              LOAD_ATTR                3 (escape + NULL|self)
              LOAD_FAST                1 (var2)
              CALL                     1
              FORMAT_SIMPLE
              BUILD_STRING             3
              POP_TOP
              RETURN_CONST             0 (None)
>>>
>>> dis.dis(addition)
  4           RESUME                   0

  5           LOAD_CONST               1 ('asdf asdf ')
              LOAD_FAST                0 (var1)
              FORMAT_SIMPLE
              BUILD_STRING             2
              LOAD_GLOBAL              0 (re)
              LOAD_ATTR                3 (escape + NULL|self)
              LOAD_FAST                1 (var2)
              CALL                     1
              BINARY_OP                0 (+)
              POP_TOP
              RETURN_CONST             0 (None)
>>>

All that said, this may not even be faster....but there is at least a tangible difference in the two approaches.

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.

Yep! I understand, although thank you for the thoroughness. 😂

As of my most recent commit, there's no longer a + operator; the output of re.escape() is back inside an f-string (although it's assigned to a variable first).

Comment thread textual/src/toga_textual/paths.py
@HalfWhitt HalfWhitt marked this pull request as ready for review June 26, 2025 03:49
@HalfWhitt HalfWhitt requested a review from freakboy3742 June 26, 2025 03:49

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

LGTM - thanks!

Comment thread core/tests/test_paths.py
assert f"app.paths.config={home / 'config' / full_name}" in results
assert f"app.paths.data={home / 'user_data' / full_name}" in results
assert f"app.paths.cache={home / 'cache' / full_name}" in results
assert f"app.paths.logs={home / 'logs' / full_name}" in results

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.

My recollection is the resolve() was needed because of how mounted network drives appear on Windows... but I might be misremembering that one. If it's passing CI, that's enough for me.

Comment thread testbed/tests/test_icons.py Outdated
rf"Unable to load icon from "
rf"{re.escape(str(app.paths.app / 'resources' / 'icons' / 'bad'))}"
r"Unable to load icon from "
+ re.escape(str(app.paths.app / "resources/icons/bad"))

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.

Yeah - this is a corner of Python syntax where there are no good answers. The default black/ruff answer of "split this string and don't even use an operator" is worse, IMHO, because easily mistaken for 2 separate arguments.

A moot point in this case, because you need the operator for the escape.

@freakboy3742

Copy link
Copy Markdown
Member

Ah - except for the merge conflict :-)

@HalfWhitt

Copy link
Copy Markdown
Member Author

Ah - except for the merge conflict :-)

[shakes fist]

@HalfWhitt HalfWhitt merged commit 89fd23a into beeware:main Jun 27, 2025
52 checks passed
@HalfWhitt HalfWhitt deleted the more_path_cleanups branch June 27, 2025 00:47
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.

3 participants