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:
old_path = Path(r"C:\x.txt") old_path.with_name("y"+old_path.suffix) WindowsPath('C:/y.txt')
Or:
old_path = Path(r"C:\x.txt") old_path.with_name("y"+old_path.suffix) WindowsPath('C:/y.txt')
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
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:
old_path = Path(r"C:\x.txt") old_path.with_name("y.xxx" + old_path.suffix) WindowsPath('C:/y.xxx.txt') old_path.with_name("y.xxx").with_suffix(old_path.suffix) WindowsPath('C:/y.txt')
On Oct 29, 2019, at 09:05, Paul Moore
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)?
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
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 = Path("foo.bar.txt") p.with_stem("quux") "quux.bar.txt"
p.with_stem("baz.quux") "baz.quux.bar.txt"
-- ~Ethan~
On Tue, 29 Oct 2019 at 17:38, Ethan Furman
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 = Path("foo.bar.txt") p.with_stem("quux") "quux.bar.txt"
Um,
Path("foo.bar.txt").suffix '.txt'
So if ".txt" is the suffix, then with_stem("quux") should be "quux.txt", surely? Paul
On 10/29/2019 10:41 AM, Paul Moore wrote:
On Tue, 29 Oct 2019 at 17:38, Ethan Furman
wrote: 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 = Path("foo.bar.txt") p.with_stem("quux") "quux.bar.txt"
Um,
Path("foo.bar.txt").suffix '.txt'
So if ".txt" is the suffix, then with_stem("quux") should be "quux.txt", surely?
Argh. Can I plead lack of coffee? And since I don't drink coffee, that explains a lot. ;-) -- ~Ethan~
On Tue, 29 Oct 2019 at 17:08, Andrew Barnert
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
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
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
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?
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.
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.
https://devguide.python.org/ should have everything you need (and if it doesn't then please submit a PR to fix it 😉). -Brett
_______________________________________________ Python-ideas mailing list -- python-ideas@python.org To unsubscribe send an email to python-ideas-leave@python.org https://mail.python.org/mailman3/lists/python-ideas.python.org/ Message archived at https://mail.python.org/archives/list/python-ideas@python.org/message/MEARZK... Code of Conduct: http://python.org/psf/codeofconduct/
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?
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.
Sounds like opening an issue first is the way to go, I'll do that.
https://devguide.python.org/ should have everything you need (and if it doesn't then please submit a PR to fix it 😉).
-Brett
Thanks!
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:
old_path = Path(r"C:\x.txt") old_path.with_name("y.xxx" + old_path.suffix) WindowsPath('C:/y.xxx.txt') old_path.with_name("y.xxx").with_suffix(old_path.suffix) WindowsPath('C:/y.txt')
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