GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GH-77609: Add follow_symlinks argument to `pathlib.Path.glob()` by barneygale · Pull Request #102616 · python/cpython

Conversation

@barneygale

@barneygale barneygale commented Mar 12, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor

Add a keyword-only follow_symlinks parameter to pathlib.Path.glob() and rglob().

When follow_symlinks is None (the default), these methods follow symlinks except when evaluating "**" wildcards. When set to true or false, symlinks are always or never followed, respectively.

This allows us to address GH-102613 in future.

  • Issue: pathlib.Path.glob does not follow symlinks #77609

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@barneygale
pythonGH-77609: Support following symlinks in pathlib.Path.glob()
59dcdb1
Add a keyword-only *follow_symlinks* parameter to `pathlib.Path.glob()` and
`rglob()`, defaulting to false. When set to true, symlinks to directories
are followed as if they were directories.

Previously these methods followed symlinks except when evaluating "`**`"
wildcards; on Windows they returned paths in filesystem casing except when
evaluating non-wildcard tokens. Both these problems are solved here. This
will allow us to address pythonGH-102613 and pythonGH-81079 in future commits.
@bedevere-bot bedevere-bot mentioned this pull request Mar 12, 2023
pathlib.Path.glob does not follow symlinks #77609
Closed
@bedevere-bot bedevere-bot added the awaiting review label Mar 12, 2023
barneygale added 2 commits March 12, 2023 04:13
@barneygale
Fix top-level error handling.
e850bde
@barneygale
Always follow top-level symlink
4f4ffd3
@barneygale barneygale mentioned this pull request Mar 12, 2023
Path.rglob performance issues in deeply nested directories compared to glob.glob(recursive=True) #102613
Closed
@barneygale
barneygale marked this pull request as ready for review March 12, 2023 04:53
@barneygale barneygale added the topic-pathlib label Mar 12, 2023
@barneygale barneygale mentioned this pull request Mar 12, 2023
Add parameter @case_sensitive to glob and rglob in pathlib #81079
Closed
@barneygale
Simplify test diff slightly
b6c019e
@barneygale

barneygale commented Mar 13, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

Table showing whether symlinks to directories are followed:

Literal (e.g. foo/) Wildcard (e.g. foo*/) Recursive wildcard (**/)
glob() (previous behaviour) ✓ ✓
glob()
glob(follow_symlinks=True) ✓ ✓ ✓

Table showing whether filesystem case is returned:

Literal (e.g. foo/) Wildcard (e.g. foo*/) Recursive wildcard (**/)
glob() (previous behaviour) ✓ ✓
glob() ✓ ✓ ✓
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zooba

zooba commented Mar 14, 2023

Copy link
Copy Markdown
Member

I wrote a fairly long post about my concerns on the changed behaviour, but then I realised that my main question is whether the symlink following changes applies to the entire path or only the bit in pattern? If symlinks in the base path are going to be followed regardless of the option, I'm far less concerned.

However, I'd like to be able to figure this out from the documentation :) So there's a change required there to clarify (and maybe I'll be more concerned if it clarifies the wrong way)

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented Mar 14, 2023

Copy link
Copy Markdown
Contributor Author

Symlinks in the base path (the p in p.glob(...)) are followed regardless of the option! I'll beef up the docs.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale
Clarify docs for *follow_symlinks* argument
8d657ee
@barneygale

barneygale commented Mar 14, 2023

Copy link
Copy Markdown
Contributor Author

Thanks for the feedback btw!

I'm also nervous about changing behaviour. I could revise this to make follow_symlinks=None the default, and have None mean "follow symlinks except when expanding **" (i.e. current behaviour), but it's a bit icky. Or we could make follow_symlinks=False still follow symlinks when expanding literal or non-recursive wildcards (again preserving existing behaviour), but it feels like misleading naming to me. Or perhaps we introduce a zsh-like *** token that follows symlinks. I'm not totally sure.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zooba

zooba commented Mar 14, 2023

Copy link
Copy Markdown
Member

I don't want to add ***. I think we can live without that.

I think the behaviour of path parts before any wildcard are easily assumed to be equivalent of being part of the base path. That is:

>>> Path("a1/b2").glob("c3/**")
# is the same as
>>> Path("a1/b2/c3").glob("**")

Everything from the first wildcard becomes a match pattern. We need to recurse deep enough to create paths with the same number of segments, and then match all the candidates against the pattern. Since we're now recursing, choosing not to follow symlinks by default is justifiable.

Given the current behaviour is to follow "spam*" but not ** (that is, it's inconsistent), choosing one or the other feels more like a bugfix. I think we can describe it as "directory symlinks matched by using a wildcard will only be followed if the flag is set", whereas currently it depends on context, and a symlink may or may not be followed.

I'm pretty much leaning towards needing a deprecation period though. I think that'll be safest, and probably necessary, so the best way to do that is to support =None for the option right now and plan to change it to False in 3.14. Then we can show a deprecation warning when it's used. Hopefully there's a fairly separate way to implement the mixed behaviour that makes it easy to see what to remove later on.

barneygale reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented Mar 14, 2023

Copy link
Copy Markdown
Contributor Author

Right! I think I agree that leading literals should always be followed.

What about trailing literals, e.g. path.glob('**/.ci/tox.ini')? There's a nice optimization we can do in these cases: we walk the directory tree once when we hit the ** token, and then filter the results through a regex to match the .ci/tox.ini parts. This avoids us needing to scandir() the same directory multiple times, or needing to keep a set to de-duplicate results.

... but that optimization only works if the ** token walks the same directories as the .ci/tox.ini literal part would hit. If we treat symlinks inconsistently the whole thing breaks down D:

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented Mar 14, 2023

Copy link
Copy Markdown
Contributor Author

Also agree on the need to preserve existing behaviour and emit deprecation warnings before we change defaults. I'd argue the default should become true, not false, in future. It's the more useful behaviour. glob.glob() only supports following symlinks!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale
Backwards compatibility
b600363
@barneygale

barneygale commented Mar 14, 2023

Copy link
Copy Markdown
Contributor Author

I've switched the default to follow_symlinks=None, which preserves previous behaviour.

Passing None to a boolean parameter wounds me. Wild idea: introduce new functions with better naming?

  • Path.expand(pattern, *, follow_symlinks=True) eventually replaces glob().
  • Path.search(pattern, *, follow_symlinks=True) eventually replaces rglob()

I mention this only because the names "glob" and "rglob" are a bit opaque unless you've done shell scripting before. The suggested names align with PurePath.match(). But feel free to shoot this right back down!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zooba

zooba commented Mar 14, 2023

Copy link
Copy Markdown
Member

Passing None to a parameter is fine, especially since we're going to document it as deprecated and you shouldn't do it explicitly.

... but that optimization only works if the ** token walks the same directories as the .ci/tox.ini literal part would hit. If we treat symlinks inconsistently the whole thing breaks down D:

Yeah, and so if we have **/.*/tox.ini we won't be able to follow symlinks matching .* because we never followed them in the first place. We can only match real directories that start with a dot.

Maybe we can optimise the case where the ** is followed by a literal? And if there are any other wildcard characters (and an unspecified follow_symlinks) we revert to a slower algorithm?

barneygale reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented Mar 14, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

Maybe we can optimise the case where the ** is followed by a literal? And if there are any other wildcard characters (and an unspecified follow_symlinks) we revert to a slower algorithm?

👍 sounds good. As things stand with this PR, follow_symlinks=False and True could be optimized, whereas follow_symlinks=None cannot. I'll leave the optimizations for a future PR as there's a couple other things I need to land first, e.g. #101398.

This PR still breaks backwards compat in two (minor?) ways:

  • ../ tokens in globs no longer navigate to the parent directory. Instead they're matched like other literals among the directory children, and so the match fails.
  • Filesystem case is returned for literal tokens, so if you have "C:/foo/bar.txt", then Path("C:/").glob("FOO*/BAR.TXT") yields Path("C:/foo/bar.txt") and not Path("C:/foo/BAR.TXT").
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zooba

zooba commented Mar 14, 2023

Copy link
Copy Markdown
Member

Matching filesystem case is an improvement, IMHO, even for back-compat. glob is a query operation, so there should be an expectation that the result comes from the FS and not the arguments.

On the .. case, I assume you mean after a wildcard? Before any wildcard we should be able to interpret it normally, and that is important. But I'm not even sure what behaviour I would expect after a wildcard? Is it meant to mean glob('**/spam/../eggs/*.txt') finds all text files in any eggs directory provided it has a spam directory next to it? That's... clever... I guess? But I don't think we've ever suggested it would work.

I also don't have any real feeling for whether we should normpath before globbing, or split at wildcards and normpath, or do something else. What do other tools do with this kind of pattern? Particularly something that "removes" the recursion marker like glob('**/../')

barneygale reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale
Fix handling of '..' in patterns.
b2766f8
@barneygale

barneygale commented Mar 14, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

I also don't have any real feeling for whether we should normpath before globbing, or split at wildcards and normpath, or do something else. What do other tools do with this kind of pattern? Particularly something that "removes" the recursion marker like glob('**/../')

Pathlib uniformly avoids collapsing .. segments by purely lexical means, as it can change the meaning of a path that involves symlinks. So there's no use of normpath() anywhere!

I've fixed handling of .., so I think this might now conform with your expectations!

An updated table showing whether symlinks are followed:

Literal (e.g. foo/) Wildcard (e.g. foo*/) Recursive wildcard (**/)
glob() (previous behaviour) ✓ ✓
glob() ✓ ✓
glob(follow_symlinks=True) ✓ ✓ ✓
glob(follow_symlinks=False)

Note that, for the moment, literals are handled exactly the same whether they appear before or after recursive wildcards.

zooba reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zooba

zooba commented Mar 14, 2023

Copy link
Copy Markdown
Member

Looks good. We should still add the deprecation warning for follow_symlinks=None (I forget if want to skip the warning for 3.12, but even so, add it in and we can comment it out so that at least it's done). The test that uses the default value should suppress the warning.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

barneygale added 2 commits March 14, 2023 23:44
@barneygale
Deprecate follow_symlinks=None
8704d33
@barneygale
Fix news
6f7a83a
@barneygale

barneygale commented Mar 14, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Contributor Author

Done. But honestly I don't feel great about path.glob('foo') emitting a deprecation warning unless users pass follow_symlinks=False or follow_symlinks=True. It's wordy and annoying and in many cases (e.g. when users don't expect symlinks), the choice doesn't matter. Also makes it harder for users to write backwards-compatible code. I'd love if there's a better way I'm not seeing.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented Mar 14, 2023

Copy link
Copy Markdown
Contributor Author

Oh right, I also need to update the docs examples to pass follow_symlinks. I'll do that tomorrow.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented May 4, 2023

Copy link
Copy Markdown
Contributor Author

Alternative PR that adds support for a *** wildcard:

  • GH-77609: Support following symlinks in pathlib.Path.glob() #104176
All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented May 8, 2023

Copy link
Copy Markdown
Contributor Author

Closing this PR as I believe #104176 is the right way forward.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale barneygale closed this May 8, 2023
@barneygale

barneygale commented May 10, 2023

Copy link
Copy Markdown
Contributor Author

@zooba I'd be much more comfortable with this PR if we delayed deprecating follow_symlinks=None for a couple of releases. Would that be alright? Otherwise I think users would need to either drop support for 3.11 in their code, or suppress deprecation warnings, right?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zooba

zooba commented May 10, 2023

Copy link
Copy Markdown
Member

We can document it as deprecated with no planned removal date (or planned change of default, in this case). Or just strongly recommend the use of True or False and say that None is intended for legacy compatibility only.

If we think (or people start saying) that the default is harmful/a trap, we can deprecate properly at that point (and wait two releases before changing it).

barneygale reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

barneygale added 3 commits May 10, 2023 19:37
@barneygale
Revert "Fix news"
ce7973f
This reverts commit 6f7a83a.
@barneygale
Revert "Deprecate follow_symlinks=None"
2db28d9
This reverts commit 8704d33.
@barneygale
Merge branch 'main' into pythongh-77609-glob-follow-symlinks
d3a3396
@barneygale barneygale reopened this May 10, 2023
barneygale added 2 commits May 11, 2023 01:32
@barneygale
Merge branch 'main' into pythongh-77609-glob-follow-symlinks
6c61822
@barneygale
Merge branch 'main' into pythongh-77609-glob-follow-symlinks
3722b64
@barneygale

barneygale commented May 17, 2023

Copy link
Copy Markdown
Contributor Author

I'll pick this up again once #104512 lands.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

barneygale added 2 commits May 24, 2023 00:03
@barneygale
Merge branch 'main' into pythongh-77609-glob-follow-symlinks
9700dd1
@barneygale
Re-target to 3.13
4f2a492
@barneygale
barneygale marked this pull request as ready for review May 23, 2023 23:11
@bedevere-bot bedevere-bot added the awaiting core review label May 23, 2023
@barneygale

barneygale commented May 23, 2023

Copy link
Copy Markdown
Contributor Author

In fact, it doesn't really matter whether #104512 lands first. Marking as ready for review!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented May 23, 2023

Copy link
Copy Markdown
Contributor Author

On follow_symlinks=None: I don't consider it wrong (after all, Guido considered it correct when he fixed #70200). And at this stage it's no less performant. When I fix #102613 and implement the walk-and-match stategy, I'll add a warning in the docs that follow_symlinks=None can be much less performant. Around the Python 3.15 timeframe I'll deprecate follow_symlinks=None. Hope that sounds alright, @zooba?

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale
barneygale requested a review from zooba May 27, 2023 18:12
@zooba

zooba commented May 29, 2023 •
edited
Loading

Uh oh!

There was an error while loading. Please reload this page.

Copy link
Copy Markdown
Member

after all, Guido considered it correct when he fixed #70200

Interesting.

I think if you can clearly explain the different behaviour ("following symlinks matched by ** but not other wildcard matches" would do) and make it sound useful (not just preserving compatibility), I don't think there's a need to deprecate it at all unless we want to change the default.

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented May 29, 2023

Copy link
Copy Markdown
Contributor Author

I think we're in agreement, then!

The docs in this PR say:

   By default, or when the *follow_symlinks* keyword-only argument is set to
   ``None``, this method follows symlinks except when expanding "``**``"
   wildcards. Set *follow_symlinks* to ``True`` to always follow symlinks, or
   ``False`` to treat all symlinks as files.

Does that sound OK to you?

zooba reacted with thumbs up emoji
All reactions
  • 👍 1 reaction

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@zooba

zooba commented May 29, 2023

Copy link
Copy Markdown
Member

Sounds good to me

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale

barneygale commented May 29, 2023

Copy link
Copy Markdown
Contributor Author

Mega, thank you. Would you be up for reviewing the PR as a whole, if you have the time? No worries if not, and no urgency from my side.

The patch is somewhat repetitive due to the way globbing is currently implemented. If you find it offensive, we might want to simplify things first via #104512

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

zooba
zooba approved these changes May 29, 2023
View reviewed changes
@bedevere-bot bedevere-bot added awaiting merge and removed awaiting core review labels May 29, 2023
@barneygale

barneygale commented May 29, 2023

Copy link
Copy Markdown
Contributor Author

Woot! Thank you Steve!

All reactions

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

@barneygale
barneygale merged commit ace676e into python:main May 29, 2023
@bedevere-bot bedevere-bot removed the awaiting merge label May 29, 2023
@barneygale barneygale mentioned this pull request Jun 24, 2023
GH-105793: Add follow_symlinks argument to pathlib.Path.is_dir() and is_file() #105794
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@zooba zooba zooba approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

topic-pathlib

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

3 participants

@barneygale @zooba @bedevere-bot
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.