Skip to content

Conversation

@wdhongtw
Copy link
Contributor

@wdhongtw wdhongtw commented Feb 5, 2025

This change will preserve path in it's original letter-case during displaying.

Fixes #6823

Because os.getcwd and os.path.abspath both honors original letter-case,
we should not normalize case again when calculating the relative path.

(Edited) As suggested, we match input path with cwd in normalized-casing,
while returning the relative part in original-casing.
Original path is returned untouched if the match fails.

And this change actually fix the (harmless) bug in Windows that relative path is not shown correctly,
as a side effect.

Before:

PS C:\Users\weida\Projects\pip> pip install luckydep                                      
Requirement already satisfied: luckydep in c:\users\weida\projects\pip\.venv\lib\site-packages (0.2.0)

After:

PS C:\Users\weida\Projects\pip> pip install luckydep
Requirement already satisfied: luckydep in .\.venv\Lib\site-packages (0.2.0)

All the testcase in unit test still passed. (And it should, because it's just a cosmetic change.)


I tried to locate the first commit that use os.path.normcase, but it turns out that it's already there
when this project migrated from svn to git in 2018.

95bcf8c5f6394298035a7332c441868f3b0169f4 `pip\utils\__init__.py`
767d11e49cb916e2d4637421d524efcb8d02ae8d `pip\util.py`
dc6b8393eb13e90edf0f6b9af96516d042b4d1c7 `pip/__init__.py`
ef63f2f48f55ab2e110e07cd069e6c0e6c287a2a `pip.py`
97c152c463713bdaa0c1531a910eeae681035489 `pyinstall.py`

Maybe there are some good reason to normalize case for the path in the past, but it should be ok to remove it now. :D

@wdhongtw
Copy link
Contributor Author

wdhongtw commented Feb 5, 2025

Modified according to review feedbacks.


Since that display_path has implicit dependency on process state. It's not easy to write a
test for this function.
Here is a sample script to validate the change of this PR.

(Edited: synchronize the script content to latest version of this PR)

from typing import NamedTuple
import os


def display_path(path: str) -> str:
    """Gives the display value for a given path, making it relative to cwd
    if possible."""
    rel = os.path.relpath(path)
    if rel[:2] == "..":
        return path
    return os.path.join(".", rel)


class Case(NamedTuple):
    input: str
    expected: str


testCases = [
    Case(
        input=r"C:\Users\weida\Projects\pip\main.py",
        expected=r".\main.py",
    ),
    Case(
        input=r"C:\Users\weida\Projects\pip\main.PY",
        expected=r".\main.PY",
    ),
    Case(
        input=r"C:\Users\weida\Projects\PIP\main.py",
        expected=r".\main.py",
    ),
    Case(
        input=r"C:\Users\WeiDa\Projects\ABC\main.PY",
        expected=r"C:\Users\WeiDa\Projects\ABC\main.PY",
    ),
]

# modify this path if needed
assert os.getcwd() == r"C:\Users\weida\Projects\pip"

for idx, test in enumerate(testCases):
    result = display_path(test.input)
    if result == test.expected:
        print(f"Test case {idx} passed.")
    else:
        print(f"Test case {idx} failed: got {result}, expected {test.expected}.")

@wdhongtw wdhongtw changed the title Skip normalizing case for path during displaying Preserve original letter-casing for displaying Feb 5, 2025
@ichard26
Copy link
Member

ichard26 commented Mar 28, 2025

Is there a reason why we can't use os.path.relpath or pathlib.Path.relative_to? I'm not well versed in path manipulation and I'd rather rely on stdlib functionality if possible. It doesn't seem like we need commonpath as we don't care if the CWD and path share some parent path, but only if the entire CWD is common to both, aka path can be rewritten as relative to CWD.

cc @pfmoore given this primarily impacts Windows.

@wdhongtw
Copy link
Contributor Author

Is there a reason why we can't use os.path.relpath ...

No such reason.
I can change this PR to any equivalent implementation, as long as we can fix this issue together. 🙂

@ichard26
Copy link
Member

ichard26 commented Nov 6, 2025

@uranusjr do you have any opinions on this? (As our other resident Windows expert... or at least more than me)

@pfmoore
Copy link
Member

pfmoore commented Nov 6, 2025

Sorry, it looks like I missed the ping here. IMO, preserving case is a good thing to do, so I support the PR.

rel = os.path.relpath(path)
if rel[:2] == "..":
return path
return os.path.join(".", rel)
Copy link
Member

Choose a reason for hiding this comment

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

What happens if the input path is on a different drive than the CWD? Wouldn’t relpath return an absolute path in that case? We should add a unit test covering that case if there isn’t one already.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's something I didn't noticed.
A quick look from the official doc says that worse thing can happen, e.g. throws ValueError.

Thanks for the reminder. 👍
I will enhance this PR this weekend, and try to include a test for it.

Copy link
Member

Choose a reason for hiding this comment

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

How about using pathlib:

try:
    relpath = Path(path).relative_to(Path.cwd())
except ValueError:
    # If the path isn't relative to the CWD, leave it alone
    return path
return str(Path(".") / relpath)

Unlike os.path.relpath, the Path.relative_to method doesn't try to go up the directory tree, so there's no need for the ".." check if we use pathlib.

We should still have some good unit tests for this. If only to ensure that we have a clear specification of the expected results.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks for the suggestions.

PR modified according to the suggestions.
Please let me know if there is any concern. :D

@wdhongtw wdhongtw marked this pull request as draft November 6, 2025 13:54
@wdhongtw
Copy link
Contributor Author

wdhongtw commented Nov 6, 2025

Change to draft. More modification required.

@wdhongtw wdhongtw force-pushed the fix-path-case branch 2 times, most recently from 1f0ad14 to c38ec03 Compare November 9, 2025 10:55
Simplify input path as a relative path to current working directory,
while preserving the original letter-casing at best effort.
@wdhongtw wdhongtw marked this pull request as ready for review November 9, 2025 11:32
@wdhongtw wdhongtw requested a review from pfmoore November 9, 2025 11:35
Copy link
Member

@pfmoore pfmoore left a comment

Choose a reason for hiding this comment

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

Looks good!

@pfmoore
Copy link
Member

pfmoore commented Nov 9, 2025

@ichard26 do you have anything further to add before I merge this?

Copy link
Member

@ichard26 ichard26 left a comment

Choose a reason for hiding this comment

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

No further comments from me. Thanks @wdhongtw for your patience!

@pfmoore pfmoore merged commit e351bbb into pypa:main Nov 9, 2025
28 checks passed
@pfmoore
Copy link
Member

pfmoore commented Nov 9, 2025

Thanks @wdhongtw for your contribution!

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Report windows paths in their original case

4 participants