add a .with_stem() method to pathlib objects

This is obviously a small thing, but it would be nice to be able to say:
Path(r"C:\x.txt").with_stem("y") WindowsPath('C:/y.txt')
Rather than having to do some variation of:
Or:
Or (god forbid):
(old_path := Path(r"C:\x.txt")).with_name("y"+old_path.suffix) WindowsPath('C:/y.txt')
There are already .with_suffix() and with_name() methods. Including with_stem() seems obvious, no? --- Ricky. "I've never met a Kentucky man who wasn't either thinking about going home or actually going home." - Happy Chandler

On Tue, 29 Oct 2019 at 15:29, Ricky Teachey <ricky@teachey.org> wrote:
There are already .with_suffix() and with_name() methods. Including with_stem() seems obvious, no?
As you say, it's a minor thing (either way) but I've never had the need for a `with_stem` method. Do you have a real-life use case (as opposed to just wanting it for completeness)? Having a real example would help decide the appropriate behaviour for corner cases like x.with_stem('name.ext'). Your two suggested equivalents behave differently in that case:

On Oct 29, 2019, at 09:05, Paul Moore <p.f.moore@gmail.com> wrote:
I have a real-life use case, which I’ll simplify here: def safe_create(path): try: return open(path, 'x') except FileExistsError: for i in itertools.count(1): path2 = path.with_stem(f"{path.stem} ({i})") try: return open(path2, 'x') except FileExistsError: pass

On Oct 29, 2019, at 09:05, Paul Moore <p.f.moore@gmail.com> wrote:
Having a real example would help decide the appropriate behaviour for corner cases like x.with_stem('name.ext').
I just realized that my use case doesn’t help answer your corner case. Hopefully the OP’s will have one. But I can imagine one that does. If you wanted my safe_create function to do Mac-like “copy 3 of spam.eggs.txt” instead of Windows-style “spam.eggs (3).txt”, then you would end up calling path.with_stem("copy 3 of spam.eggs"), and you would definitely want the ".eggs" to be added to rather than replaced. So, the OP’s first implementation would be the right one, not his second. Hopefully that doesn’t contradict what you’d want from the OP’s use case. :)

I feel like the semantics of PurePath.suffix (https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix) and PurePath.stem define this pretty unambiguously. I.e., it should be: p = Path("foo.bar.txt") p.with_stem("quux") "quux.txt" p.with_stem("baz.quux") "baz.quux.txt"

On 10/29/2019 10:17 AM, Brian Skinn wrote:
I feel like the semantics of PurePath.suffix (https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix) and PurePath.stem define this pretty unambiguously.
It does indeed -- it declares the last bit to be the suffix. So correspondingly, .with_stem() should be everything before the last bit, or: [corrected examples]
p.with_stem("baz.quux") "baz.quux.bar.txt"
-- ~Ethan~

On Tue, 29 Oct 2019 at 17:08, Andrew Barnert <abarnert@yahoo.com> wrote:
If you wanted my safe_create function to do Mac-like “copy 3 of spam.eggs.txt” instead of Windows-style “spam.eggs (3).txt”, then you would end up calling path.with_stem("copy 3 of spam.eggs"), and you would definitely want the ".eggs" to be added to rather than replaced. So, the OP’s first implementation would be the right one, not his second.
On Tue, 29 Oct 2019 at 17:19, Brian Skinn <brian.skinn@gmail.com> wrote:
I feel like the semantics of PurePath.suffix (https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix) and PurePath.stem define this pretty unambiguously.
[...]
p.with_stem("baz.quux") "baz.quux.txt"
Cool. Sounds like def with_stem(self, new_stem): self.with_name(new_stem + self.suffix) is the preferred solution. I agree it seems like the right option. As I said, I don't think it's a big deal either way. A bugs.python.org issue with an attached PR implementing (and documenting) this seems like the way to go - no big need to debate it. Although it's good that we caught the difference in behaviour between the two options - any patch adding this should include tests for this particular corner case (and any others that you can think of ;-)) Paul

*(Apologies to Paul who is receiving this a second time)* Seems like the right implementation to me. Does anybody disagree that a bug report with a PR is the way to move forward on this? Also: I've filed bug reports before, but I don't think I've ever filed a bug report with a PR and with tests against core python. Is there a link that might help with learning the process, with an eye to how to setup and run the test correctly for the project...? I believe I've already signed the usage agreement and that sort of thing.

On Tue, Oct 29, 2019 at 12:29 PM Ricky Teachey <ricky@teachey.org> wrote:
Depends if you're okay with the PR potentially being rejected because the idea gets rejected. If you're fine with that then do both, if you're not then I would open the issue to make sure other core devs don't object.
https://devguide.python.org/ should have everything you need (and if it doesn't then please submit a PR to fix it 😉). -Brett

I hadn't considered that case. Worth discussing. Not just "for completeness", I had a real-life use case this morning; reached for it, wasn't there, frustrated. The relevant chunk of the code below is here, in which you can see I would have like to use this feature twice: for ym in moduli: update_soup(soup, moduli) new_name = filename_formatter.format(ym=ym) new_path = source_file_path.with_name(new_name).with_suffix(source_file_path.suffix) new_path.write_text(soup.prettify()) run_model(new_path) deflection = get_deflection() discard_data() # completed files are very large; close program and don't save finished file deflection_stem = deflection_formatter.format(deflection=deflection) new_path.rename(new_path.with_name(deflection_stem).with_suffix(source_file_path.suffix) Would be nice to say: for ym in moduli: update_soup(soup, moduli) new_name = filename_formatter.format(ym=ym) new_path = source_file_path.with_stem(new_name) # yay! new_path.write_text(soup.prettify()) run_model(new_path) deflection = get_deflection() discard_data() # completed files are very large; close program and don't save finished file deflection_stem = deflection_formatter.format(deflection=deflection) new_path.rename(new_path.with_stem(deflection_stem) # yay! *Details* I have a file named like so: *5340 ksi 69% plastic E 90% plastic TIG - 25.02 mm.liml* It is an XML format input file (for a finite element analysis program). The file name is my own convention; it has the YM as part of the file name (with some other info I'm not changing), and the deflection of a model of a wall in mm at the end. I'm doing a sensitivity analysis so the goal is to make a copy of the input file changing a single input parameter for one of the materials (YM) and name the new input file appropriately (with XX as an intial placeholder for the final deflection), run the model, obtain the model deflection, and finally rename the file with the final deflection. I'm parsing the file name (using pent) and content (using beautifulsoup to get the XML element), replacing the YM in the relevant XML attribute, and copying the file to a new filename. In the end I'll have a directory of files like this: 5340 ksi 69% plastic E 90% plastic TIG - 25.02 mm.liml 3041 ksi 69% plastic E 90% plastic TIG - XX mm.liml 2087 ksi 69% plastic E 90% plastic TIG - XX mm.liml 1693 ksi 69% plastic E 90% plastic TIG - XX mm.liml (the XX of each will be replaced after the files have been run) The code I am using to do much of this is, roughly: # define starting file and some list of moduli i am interested in (strings) moduli = [ "3041 ksi", "2087 ksi", "1693 ksi", ] source_file_path = Path("5340 ksi 69% plastic E 90% plastic TIG - 25.02 mm.liml") # parse the filename captured = pent.Parser(body="#.+i &. ~! #.+d @.mm").parse_body(source_file_path.stem)[0][0] captured_string = " ".join(captured) filename_formatter = f"{{ym}} {captured_string} XX mm" deflection_formatter = f"{{ym}} {captured_string} {{deflection:.2f} mm}" # load the XML soup with source_file_path.open() as doc: soup = bs4.bs.BeautifulSoup(doc, 'html.parser') # create a file for each young's modulus, run it, and rename it for ym in moduli: update_soup(soup, moduli) new_name = filename_formatter.format(ym=ym) new_path = source_file_path.with_stem(new_name) # shorter code: yay! new_path.write_text(soup.prettify()) run_model(new_path) deflection = get_deflection() discard_data() # completed files are very large; close program and don't save finished file deflection_stem = deflection_formatter.format(deflection=deflection, ym=ym) new_path.rename(new_path.with_stem(deflection_stem) # shorter code: yay!

On Tue, 29 Oct 2019 at 15:29, Ricky Teachey <ricky@teachey.org> wrote:
There are already .with_suffix() and with_name() methods. Including with_stem() seems obvious, no?
As you say, it's a minor thing (either way) but I've never had the need for a `with_stem` method. Do you have a real-life use case (as opposed to just wanting it for completeness)? Having a real example would help decide the appropriate behaviour for corner cases like x.with_stem('name.ext'). Your two suggested equivalents behave differently in that case:

On Oct 29, 2019, at 09:05, Paul Moore <p.f.moore@gmail.com> wrote:
I have a real-life use case, which I’ll simplify here: def safe_create(path): try: return open(path, 'x') except FileExistsError: for i in itertools.count(1): path2 = path.with_stem(f"{path.stem} ({i})") try: return open(path2, 'x') except FileExistsError: pass

On Oct 29, 2019, at 09:05, Paul Moore <p.f.moore@gmail.com> wrote:
Having a real example would help decide the appropriate behaviour for corner cases like x.with_stem('name.ext').
I just realized that my use case doesn’t help answer your corner case. Hopefully the OP’s will have one. But I can imagine one that does. If you wanted my safe_create function to do Mac-like “copy 3 of spam.eggs.txt” instead of Windows-style “spam.eggs (3).txt”, then you would end up calling path.with_stem("copy 3 of spam.eggs"), and you would definitely want the ".eggs" to be added to rather than replaced. So, the OP’s first implementation would be the right one, not his second. Hopefully that doesn’t contradict what you’d want from the OP’s use case. :)

I feel like the semantics of PurePath.suffix (https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix) and PurePath.stem define this pretty unambiguously. I.e., it should be: p = Path("foo.bar.txt") p.with_stem("quux") "quux.txt" p.with_stem("baz.quux") "baz.quux.txt"

On 10/29/2019 10:17 AM, Brian Skinn wrote:
I feel like the semantics of PurePath.suffix (https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix) and PurePath.stem define this pretty unambiguously.
It does indeed -- it declares the last bit to be the suffix. So correspondingly, .with_stem() should be everything before the last bit, or: [corrected examples]
p.with_stem("baz.quux") "baz.quux.bar.txt"
-- ~Ethan~

On Tue, 29 Oct 2019 at 17:08, Andrew Barnert <abarnert@yahoo.com> wrote:
If you wanted my safe_create function to do Mac-like “copy 3 of spam.eggs.txt” instead of Windows-style “spam.eggs (3).txt”, then you would end up calling path.with_stem("copy 3 of spam.eggs"), and you would definitely want the ".eggs" to be added to rather than replaced. So, the OP’s first implementation would be the right one, not his second.
On Tue, 29 Oct 2019 at 17:19, Brian Skinn <brian.skinn@gmail.com> wrote:
I feel like the semantics of PurePath.suffix (https://docs.python.org/3/library/pathlib.html#pathlib.PurePath.suffix) and PurePath.stem define this pretty unambiguously.
[...]
p.with_stem("baz.quux") "baz.quux.txt"
Cool. Sounds like def with_stem(self, new_stem): self.with_name(new_stem + self.suffix) is the preferred solution. I agree it seems like the right option. As I said, I don't think it's a big deal either way. A bugs.python.org issue with an attached PR implementing (and documenting) this seems like the way to go - no big need to debate it. Although it's good that we caught the difference in behaviour between the two options - any patch adding this should include tests for this particular corner case (and any others that you can think of ;-)) Paul

*(Apologies to Paul who is receiving this a second time)* Seems like the right implementation to me. Does anybody disagree that a bug report with a PR is the way to move forward on this? Also: I've filed bug reports before, but I don't think I've ever filed a bug report with a PR and with tests against core python. Is there a link that might help with learning the process, with an eye to how to setup and run the test correctly for the project...? I believe I've already signed the usage agreement and that sort of thing.

On Tue, Oct 29, 2019 at 12:29 PM Ricky Teachey <ricky@teachey.org> wrote:
Depends if you're okay with the PR potentially being rejected because the idea gets rejected. If you're fine with that then do both, if you're not then I would open the issue to make sure other core devs don't object.
https://devguide.python.org/ should have everything you need (and if it doesn't then please submit a PR to fix it 😉). -Brett

I hadn't considered that case. Worth discussing. Not just "for completeness", I had a real-life use case this morning; reached for it, wasn't there, frustrated. The relevant chunk of the code below is here, in which you can see I would have like to use this feature twice: for ym in moduli: update_soup(soup, moduli) new_name = filename_formatter.format(ym=ym) new_path = source_file_path.with_name(new_name).with_suffix(source_file_path.suffix) new_path.write_text(soup.prettify()) run_model(new_path) deflection = get_deflection() discard_data() # completed files are very large; close program and don't save finished file deflection_stem = deflection_formatter.format(deflection=deflection) new_path.rename(new_path.with_name(deflection_stem).with_suffix(source_file_path.suffix) Would be nice to say: for ym in moduli: update_soup(soup, moduli) new_name = filename_formatter.format(ym=ym) new_path = source_file_path.with_stem(new_name) # yay! new_path.write_text(soup.prettify()) run_model(new_path) deflection = get_deflection() discard_data() # completed files are very large; close program and don't save finished file deflection_stem = deflection_formatter.format(deflection=deflection) new_path.rename(new_path.with_stem(deflection_stem) # yay! *Details* I have a file named like so: *5340 ksi 69% plastic E 90% plastic TIG - 25.02 mm.liml* It is an XML format input file (for a finite element analysis program). The file name is my own convention; it has the YM as part of the file name (with some other info I'm not changing), and the deflection of a model of a wall in mm at the end. I'm doing a sensitivity analysis so the goal is to make a copy of the input file changing a single input parameter for one of the materials (YM) and name the new input file appropriately (with XX as an intial placeholder for the final deflection), run the model, obtain the model deflection, and finally rename the file with the final deflection. I'm parsing the file name (using pent) and content (using beautifulsoup to get the XML element), replacing the YM in the relevant XML attribute, and copying the file to a new filename. In the end I'll have a directory of files like this: 5340 ksi 69% plastic E 90% plastic TIG - 25.02 mm.liml 3041 ksi 69% plastic E 90% plastic TIG - XX mm.liml 2087 ksi 69% plastic E 90% plastic TIG - XX mm.liml 1693 ksi 69% plastic E 90% plastic TIG - XX mm.liml (the XX of each will be replaced after the files have been run) The code I am using to do much of this is, roughly: # define starting file and some list of moduli i am interested in (strings) moduli = [ "3041 ksi", "2087 ksi", "1693 ksi", ] source_file_path = Path("5340 ksi 69% plastic E 90% plastic TIG - 25.02 mm.liml") # parse the filename captured = pent.Parser(body="#.+i &. ~! #.+d @.mm").parse_body(source_file_path.stem)[0][0] captured_string = " ".join(captured) filename_formatter = f"{{ym}} {captured_string} XX mm" deflection_formatter = f"{{ym}} {captured_string} {{deflection:.2f} mm}" # load the XML soup with source_file_path.open() as doc: soup = bs4.bs.BeautifulSoup(doc, 'html.parser') # create a file for each young's modulus, run it, and rename it for ym in moduli: update_soup(soup, moduli) new_name = filename_formatter.format(ym=ym) new_path = source_file_path.with_stem(new_name) # shorter code: yay! new_path.write_text(soup.prettify()) run_model(new_path) deflection = get_deflection() discard_data() # completed files are very large; close program and don't save finished file deflection_stem = deflection_formatter.format(deflection=deflection, ym=ym) new_path.rename(new_path.with_stem(deflection_stem) # shorter code: yay!
participants (6)
-
Andrew Barnert
-
Brett Cannon
-
Brian Skinn
-
Ethan Furman
-
Paul Moore
-
Ricky Teachey