gh-159799: Require pathlib.[Pure]Path for file paths in the PEG generator (#159800)

The pegen entry points now take `pathlib.Path` objects instead of
strings, and the command-line tools (`python -m pegen`, `keywordgen`,
`first_sets`, `grammar_visualizer`) parse their path arguments into
`Path` objects.

The generators' `generate` and `prepare` take a `pathlib.PurePath`
since they only use the file's name (for the "generated by" comment
in the output) and callers pass names that are not files, such as
`"<string>"`.
diff --git a/Lib/test/test_peg_generator/test_c_generator.py b/Lib/test/test_peg_generator/test_c_generator.py
index 5b48738..3f66db2 100644
--- a/Lib/test/test_peg_generator/test_c_generator.py
+++ b/Lib/test/test_peg_generator/test_c_generator.py
@@ -1,5 +1,6 @@
 import io
 import unittest
+from pathlib import PurePath
 from unittest import mock
 
 from test import test_tools
@@ -33,7 +34,7 @@ def test_rule_types_distinguish_implicit_and_explicit_void_pointer(self):
             explicit[void*]: NAME
         """)
         generator.rules["explicit"].type = "void *"
-        start, implicit, explicit = generator.prepare("example.gram").rules
+        start, implicit, explicit = generator.prepare(PurePath("example.gram")).rules
 
         self.assertIsNone(implicit.signature.return_type)
         self.assertEqual(explicit.signature.return_type, "void *")
@@ -49,7 +50,7 @@ def test_rule_types_distinguish_implicit_and_explicit_void_pointer(self):
         self.assertEqual(call.function, "_PyPegen_lookahead")
         generator = self.make_generator("start: &explicit\nexplicit[void*]: NAME\n")
         with self.assertRaisesRegex(RuntimeError, "return type is incompatible"):
-            generator.prepare("example.gram")
+            generator.prepare(PurePath("example.gram"))
 
     def test_parser_plan_does_not_depend_on_compilation_state(self):
         generator = self.make_generator("""
@@ -63,7 +64,7 @@ def test_parser_plan_does_not_depend_on_compilation_state(self):
             expr[expr_ty]: expr '+' NAME | NAME
         """)
         generator.debug = True
-        parser = generator.prepare("some/path/example.gram")
+        parser = generator.prepare(PurePath("some/path/example.gram"))
         expected = self.emit_parser(parser)
 
         self.assertEqual(generator.file.getvalue(), "")
@@ -101,12 +102,12 @@ def test_repeated_preparation_keeps_variable_names_local(self):
             other: name_var=NUMBER name_var=NAME { name_var_1 }
         """
         generator = self.make_generator(source)
-        parser = generator.prepare("example.gram")
+        parser = generator.prepare(PurePath("example.gram"))
         expected = self.emit_parser(parser)
 
         self.assertEqual(self.emit_parser(parser), expected)
-        self.assertEqual(generator.prepare("example.gram"), parser)
-        other = self.make_generator(source).prepare("example.gram")
+        self.assertEqual(generator.prepare(PurePath("example.gram")), parser)
+        other = self.make_generator(source).prepare(PurePath("example.gram"))
         self.assertEqual(other, parser)
         self.assertEqual(self.emit_parser(other), expected)
         self.assertEqual(expected.count("expr_ty name_var_1;"), 2)
@@ -118,7 +119,7 @@ def test_prepared_prefixes_preserve_reuse_and_repeatability(self):
             start: prefix ':' NAME | prefix ':' NUMBER | NAME | prefix '=' NAME
             prefix[expr_ty] (memo): NAME
         """)
-        parser = generator.prepare("example.gram")
+        parser = generator.prepare(PurePath("example.gram"))
         start = parser.rules[0]
         prefix, = start.prefixes
         self.assertEqual(prefix.type, "expr_ty")
@@ -126,7 +127,7 @@ def test_prepared_prefixes_preserve_reuse_and_repeatability(self):
             self.assertIn("!p->call_invalid_rules", alt.calls[0].expression())
             self.assertIn(prefix.result, alt.calls[0].expression())
         self.assertEqual(start.alternatives[3].calls[0].expression(), "prefix_rule(p)")
-        self.assertEqual(generator.prepare("example.gram"), parser)
+        self.assertEqual(generator.prepare(PurePath("example.gram")), parser)
         expected = self.emit_parser(parser)
         generator.rules.clear()
         generator.all_rules.clear()
@@ -137,7 +138,7 @@ def test_nullable_prefix_is_not_reused(self):
             start: prefix ':' NAME | prefix ':' NUMBER
             prefix (memo): NAME?
         """)
-        start = generator.prepare("example.gram").rules[0]
+        start = generator.prepare(PurePath("example.gram")).rules[0]
         self.assertEqual(start.prefixes, ())
         for alt in start.alternatives:
             self.assertEqual(alt.calls[0].expression(), "prefix_rule(p)")
@@ -148,11 +149,11 @@ def test_invalid_trailer_fails_before_output(self):
             start: NAME ENDMARKER
         """)
         with self.assertRaisesRegex(KeyError, "missing"):
-            generator.generate("example.gram")
+            generator.generate(PurePath("example.gram"))
         self.assertEqual(generator.file.getvalue(), "")
 
     def test_empty_keyword_tables(self):
-        parser = self.make_generator("start: NAME ENDMARKER\n").prepare("example.gram")
+        parser = self.make_generator("start: NAME ENDMARKER\n").prepare(PurePath("example.gram"))
         source = self.emit_parser(parser)
 
         self.assertEqual(parser.keyword_groups, ())
@@ -195,9 +196,9 @@ def test_helper_resolution_does_not_depend_on_display_settings(self):
                 grammar_module, "SIMPLE_STR", simple
             ):
                 generator = self.make_generator(source)
-                expected = generator.prepare("example.gram").rules
+                expected = generator.prepare(PurePath("example.gram")).rules
                 with mock.patch.object(grammar_module, "SIMPLE_STR", not simple):
-                    actual = generator.prepare("example.gram").rules
+                    actual = generator.prepare(PurePath("example.gram")).rules
                 self.assertEqual(len(actual), len(expected))
                 for old, new in zip(expected, actual):
                     self.assertEqual(old.signature, new.signature)
@@ -232,7 +233,7 @@ def test_invalid_rule_gating_uses_references(self):
                         start: {item} {{ _PyPegen_dummy_name(p) }}
                         invalid_example: NAME
                     """)
-                    start = generator.prepare("example.gram").rules[0]
+                    start = generator.prepare(PurePath("example.gram")).rules[0]
                     self.assertEqual(
                         start.alternatives[0].requires_invalid_rules,
                         requires_invalid_rules,
@@ -243,7 +244,7 @@ def test_lowering_preserves_legacy_named_call_types(self):
             start: Mixed LPAR ENDMARKER
             Mixed[expr_ty]: NAME
         """)
-        start, mixed = generator.prepare("example.gram").rules
+        start, mixed = generator.prepare(PurePath("example.gram")).rules
         self.assertEqual(mixed.signature.return_type, "expr_ty")
         for call, name in zip(start.alternatives[0].calls, ("Mixed", "LPAR")):
             with self.subTest(name=name):
diff --git a/Lib/test/test_peg_generator/test_pegen.py b/Lib/test/test_peg_generator/test_pegen.py
index 64c8753..e95cae0 100644
--- a/Lib/test/test_peg_generator/test_pegen.py
+++ b/Lib/test/test_peg_generator/test_pegen.py
@@ -3,6 +3,7 @@
 import io
 import textwrap
 import unittest
+from pathlib import PurePath
 
 from test import test_tools
 from typing import Dict, Any
@@ -573,7 +574,7 @@ def test_mutually_left_recursive(self) -> None:
         self.assertFalse(rules["start"].left_recursive)
         self.assertTrue(rules["foo"].left_recursive)
         self.assertTrue(rules["bar"].left_recursive)
-        genr.generate("<string>")
+        genr.generate(PurePath("<string>"))
         ns: Dict[str, Any] = {}
         exec(out.getvalue(), ns)
         parser_class: Type[Parser] = ns["GeneratedParser"]
@@ -670,7 +671,7 @@ def test_nasty_mutually_left_recursive(self) -> None:
         grammar: Grammar = parse_string(grammar_source, GrammarParser)
         out = io.StringIO()
         genr = PythonParserGenerator(grammar, out)
-        genr.generate("<string>")
+        genr.generate(PurePath("<string>"))
         ns: Dict[str, Any] = {}
         exec(out.getvalue(), ns)
         parser_class = ns["GeneratedParser"]
@@ -902,7 +903,7 @@ def test_unreachable_explicit(self) -> None:
         genr = PythonParserGenerator(
             grammar, out, unreachable_formatting="This is a test"
         )
-        genr.generate("<string>")
+        genr.generate(PurePath("<string>"))
         self.assertIn("This is a test", out.getvalue())
 
     def test_unreachable_implicit1(self) -> None:
@@ -915,7 +916,7 @@ def test_unreachable_implicit1(self) -> None:
         genr = PythonParserGenerator(
             grammar, out, unreachable_formatting="This is a test"
         )
-        genr.generate("<string>")
+        genr.generate(PurePath("<string>"))
         self.assertIn("This is a test", out.getvalue())
 
     def test_unreachable_implicit2(self) -> None:
@@ -928,7 +929,7 @@ def test_unreachable_implicit2(self) -> None:
         genr = PythonParserGenerator(
             grammar, out, unreachable_formatting="This is a test"
         )
-        genr.generate("<string>")
+        genr.generate(PurePath("<string>"))
         self.assertIn("This is a test", out.getvalue())
 
     def test_unreachable_implicit3(self) -> None:
@@ -941,7 +942,7 @@ def test_unreachable_implicit3(self) -> None:
         genr = PythonParserGenerator(
             grammar, out, unreachable_formatting="This is a test"
         )
-        genr.generate("<string>")
+        genr.generate(PurePath("<string>"))
         self.assertNotIn("This is a test", out.getvalue())
 
     def test_locations_in_alt_action_and_group(self) -> None:
diff --git a/Tools/peg_generator/pegen/__main__.py b/Tools/peg_generator/pegen/__main__.py
index 7a82ad8..e275c17 100755
--- a/Tools/peg_generator/pegen/__main__.py
+++ b/Tools/peg_generator/pegen/__main__.py
@@ -6,6 +6,7 @@
 """
 
 import argparse
+import pathlib
 import sys
 import time
 import token
@@ -87,10 +88,15 @@ def generate_python_code(
 
 c_parser = subparsers.add_parser("c", help="Generate C code for inclusion into CPython")
 c_parser.set_defaults(func=generate_c_code)
-c_parser.add_argument("grammar_filename", help="Grammar description")
-c_parser.add_argument("tokens_filename", help="Tokens description")
+c_parser.add_argument("grammar_filename", type=pathlib.Path, help="Grammar description")
+c_parser.add_argument("tokens_filename", type=pathlib.Path, help="Tokens description")
 c_parser.add_argument(
-    "-o", "--output", metavar="OUT", default="parse.c", help="Where to write the generated parser"
+    "-o",
+    "--output",
+    metavar="OUT",
+    type=pathlib.Path,
+    default=pathlib.Path("parse.c"),
+    help="Where to write the generated parser",
 )
 c_parser.add_argument(
     "--compile-extension",
@@ -111,12 +117,13 @@ def generate_python_code(
     help="Generate Python code, needs grammar definition with Python actions",
 )
 python_parser.set_defaults(func=generate_python_code)
-python_parser.add_argument("grammar_filename", help="Grammar description")
+python_parser.add_argument("grammar_filename", type=pathlib.Path, help="Grammar description")
 python_parser.add_argument(
     "-o",
     "--output",
     metavar="OUT",
-    default="parse.py",
+    type=pathlib.Path,
+    default=pathlib.Path("parse.py"),
     help="Where to write the generated parser",
 )
 python_parser.add_argument(
diff --git a/Tools/peg_generator/pegen/build.py b/Tools/peg_generator/pegen/build.py
index eb556e5..684492c 100644
--- a/Tools/peg_generator/pegen/build.py
+++ b/Tools/peg_generator/pegen/build.py
@@ -70,7 +70,7 @@ def fixup_build_ext(cmd: Incomplete) -> None:
 
 
 def compile_c_extension(
-    generated_source_path: str,
+    generated_source_path: pathlib.Path,
     build_dir: str | None = None,
     verbose: bool = False,
     keep_asserts: bool = True,
@@ -101,8 +101,7 @@ def compile_c_extension(
     if verbose:
         setuptools.logging.set_threshold(logging.DEBUG)
 
-    source_file_path = pathlib.Path(generated_source_path)
-    extension_name = source_file_path.stem
+    extension_name = generated_source_path.stem
     extra_compile_args = get_extra_flags("CFLAGS", "PY_CFLAGS_NODIST")
     extra_compile_args.append("-DPy_BUILD_CORE_MODULE")
     # Define _Py_TEST_PEGEN to not call PyAST_Validate() in Parser/pegen.c
@@ -122,39 +121,39 @@ def compile_c_extension(
                 extra_link_args.append("-fno-lto")
 
     common_sources = [
-        str(MOD_DIR.parent.parent.parent / "Python" / "Python-ast.c"),
-        str(MOD_DIR.parent.parent.parent / "Python" / "asdl.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "lexer.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "layout.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "number.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "state.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "string.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "tokenizer" / "decoder.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "tokenizer" / "api.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "tokenizer" / "reader.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "tokenizer" / "helpers.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "pegen.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "pegen_errors.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "action_helpers.c"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "string_parser.c"),
-        str(MOD_DIR.parent / "peg_extension" / "peg_extension.c"),
+        MOD_DIR.parent.parent.parent / "Python" / "Python-ast.c",
+        MOD_DIR.parent.parent.parent / "Python" / "asdl.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "lexer.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "layout.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "number.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "state.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "lexer" / "string.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "tokenizer" / "decoder.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "tokenizer" / "api.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "tokenizer" / "reader.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "tokenizer" / "helpers.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "pegen.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "pegen_errors.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "action_helpers.c",
+        MOD_DIR.parent.parent.parent / "Parser" / "string_parser.c",
+        MOD_DIR.parent / "peg_extension" / "peg_extension.c",
     ]
     include_dirs = [
-        str(MOD_DIR.parent.parent.parent / "Include" / "internal"),
-        str(MOD_DIR.parent.parent.parent / "Include" / "internal" / "mimalloc"),
-        str(MOD_DIR.parent.parent.parent / "Parser"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "lexer"),
-        str(MOD_DIR.parent.parent.parent / "Parser" / "tokenizer"),
+        MOD_DIR.parent.parent.parent / "Include" / "internal",
+        MOD_DIR.parent.parent.parent / "Include" / "internal" / "mimalloc",
+        MOD_DIR.parent.parent.parent / "Parser",
+        MOD_DIR.parent.parent.parent / "Parser" / "lexer",
+        MOD_DIR.parent.parent.parent / "Parser" / "tokenizer",
     ]
-    library_dirs: list[str] = []
+    library_dirs: list[pathlib.Path] = []
     if sys.platform == "win32":
         # HACK: The location of pyconfig.h has moved within our build, and
         # setuptools hasn't updated for it yet. So add the path manually for now
-        include_dirs.append(str(pathlib.Path(sysconfig.get_config_h_filename()).parent))
+        include_dirs.append(pathlib.Path(sysconfig.get_config_h_filename()).parent)
         if sysconfig.is_python_build():
             # HACK: Our output directory for free-threaded builds has moved, and so
             # tests running in-tree require our sys.executable directory for libs
-            library_dirs.append(str(pathlib.Path(sys.executable).parent))
+            library_dirs.append(pathlib.Path(sys.executable).parent)
     sources = [generated_source_path]
     extension = Extension(
         extension_name,
@@ -166,9 +165,9 @@ def compile_c_extension(
     cmd = dist.get_command_obj("build_ext")
     assert isinstance(cmd, setuptools.command.build_ext.build_ext)
     fixup_build_ext(cmd)
-    cmd.build_lib = str(source_file_path.parent)
-    cmd.include_dirs = include_dirs
-    cmd.library_dirs = library_dirs
+    cmd.build_lib = str(generated_source_path.parent)
+    cmd.include_dirs = [str(d) for d in include_dirs]
+    cmd.library_dirs = [str(d) for d in library_dirs]
     if build_dir:
         cmd.build_temp = build_dir
     cmd.ensure_finalized()
@@ -247,7 +246,7 @@ def compile_c_extension(
 
 
 def build_parser(
-    grammar_file: str, verbose_tokenizer: bool = False, verbose_parser: bool = False
+    grammar_file: pathlib.Path, verbose_tokenizer: bool = False, verbose_parser: bool = False
 ) -> tuple[Grammar, Parser, Tokenizer]:
     with open(grammar_file) as file:
         tokenizer = Tokenizer(tokenize.generate_tokens(file.readline), verbose=verbose_tokenizer)
@@ -255,7 +254,7 @@ def build_parser(
         grammar = parser.start()
 
         if not grammar:
-            raise parser.make_syntax_error(grammar_file)
+            raise parser.make_syntax_error(str(grammar_file))
 
     return grammar, parser, tokenizer
 
@@ -291,9 +290,9 @@ def generate_token_definitions(tokens: IO[str]) -> TokenDefinitions:
 
 def build_c_generator(
     grammar: Grammar,
-    grammar_file: str,
-    tokens_file: str,
-    output_file: str,
+    grammar_file: pathlib.Path,
+    tokens_file: pathlib.Path,
+    output_file: pathlib.Path,
     compile_extension: bool = False,
     verbose_c_extension: bool = False,
     keep_asserts_in_extension: bool = True,
@@ -320,8 +319,8 @@ def build_c_generator(
 
 def build_python_generator(
     grammar: Grammar,
-    grammar_file: str,
-    output_file: str,
+    grammar_file: pathlib.Path,
+    output_file: pathlib.Path,
     skip_actions: bool = False,
 ) -> ParserGenerator:
     with open(output_file, "w") as file:
@@ -331,9 +330,9 @@ def build_python_generator(
 
 
 def build_c_parser_and_generator(
-    grammar_file: str,
-    tokens_file: str,
-    output_file: str,
+    grammar_file: pathlib.Path,
+    tokens_file: pathlib.Path,
+    output_file: pathlib.Path,
     compile_extension: bool = False,
     verbose_tokenizer: bool = False,
     verbose_parser: bool = False,
@@ -375,8 +374,8 @@ def build_c_parser_and_generator(
 
 
 def build_python_parser_and_generator(
-    grammar_file: str,
-    output_file: str,
+    grammar_file: pathlib.Path,
+    output_file: pathlib.Path,
     verbose_tokenizer: bool = False,
     verbose_parser: bool = False,
     skip_actions: bool = False,
diff --git a/Tools/peg_generator/pegen/c_generator.py b/Tools/peg_generator/pegen/c_generator.py
index b2978d3..d1f8781 100644
--- a/Tools/peg_generator/pegen/c_generator.py
+++ b/Tools/peg_generator/pegen/c_generator.py
@@ -1,6 +1,6 @@
 """Prepare a complete C parser description and emit it."""
 
-import os.path
+import pathlib
 from typing import IO
 
 from pegen import grammar
@@ -76,11 +76,11 @@ def __init__(
         self.debug = debug
         self.skip_actions = skip_actions
 
-    def generate(self, filename: str) -> None:
+    def generate(self, filename: pathlib.PurePath) -> None:
         parser = self.prepare(filename)
         CParserEmitter(parser, self.file).emit()
 
-    def prepare(self, filename: str) -> CParser:
+    def prepare(self, filename: pathlib.PurePath) -> CParser:
         self.collect_rules()
         lowerer = self.callmakervisitor.make_lowerer()
         rules = tuple(
@@ -93,7 +93,7 @@ def prepare(self, filename: str) -> CParser:
         if subheader := self.grammar.metas.get("subheader", ""):
             headers.append(subheader)
         return CParser(
-            source_name=os.path.basename(filename),
+            source_name=filename.name,
             headers=tuple(headers),
             keyword_groups=self._prepare_keywords(),
             soft_keywords=tuple(sorted(self.soft_keywords)),
diff --git a/Tools/peg_generator/pegen/first_sets.py b/Tools/peg_generator/pegen/first_sets.py
index f6b83c0..9980f76 100755
--- a/Tools/peg_generator/pegen/first_sets.py
+++ b/Tools/peg_generator/pegen/first_sets.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python3.8
 
 import argparse
+import pathlib
 import pprint
 import sys
 
@@ -28,7 +29,7 @@
     prog="calculate_first_sets",
     description="Calculate the first sets of a grammar",
 )
-argparser.add_argument("grammar_file", help="The grammar file")
+argparser.add_argument("grammar_file", type=pathlib.Path, help="The grammar file")
 
 
 class FirstSetCalculator(GrammarVisitor):
diff --git a/Tools/peg_generator/pegen/grammar_visualizer.py b/Tools/peg_generator/pegen/grammar_visualizer.py
index eadb698..77a5bc7 100644
--- a/Tools/peg_generator/pegen/grammar_visualizer.py
+++ b/Tools/peg_generator/pegen/grammar_visualizer.py
@@ -1,4 +1,5 @@
 import argparse
+import pathlib
 import sys
 from collections.abc import Callable, Iterator
 from typing import Any
@@ -9,7 +10,7 @@
 argparser = argparse.ArgumentParser(
     prog="pegen", description="Pretty print the AST for a given PEG grammar"
 )
-argparser.add_argument("filename", help="Grammar description")
+argparser.add_argument("filename", type=pathlib.Path, help="Grammar description")
 
 
 class ASTGrammarPrinter:
diff --git a/Tools/peg_generator/pegen/keywordgen.py b/Tools/peg_generator/pegen/keywordgen.py
index 52611ea..1ded1d3 100644
--- a/Tools/peg_generator/pegen/keywordgen.py
+++ b/Tools/peg_generator/pegen/keywordgen.py
@@ -1,6 +1,7 @@
 """Generate Lib/keyword.py from the Grammar and Tokens files using pgen"""
 
 import argparse
+import pathlib
 
 from .build import build_parser, generate_token_definitions
 from .c_generator import CParserGenerator
@@ -41,13 +42,14 @@ def main() -> None:
         description="Generate the Lib/keywords.py file from the grammar."
     )
     parser.add_argument(
-        "grammar", help="The file with the grammar definition in PEG format"
+        "grammar", type=pathlib.Path, help="The file with the grammar definition in PEG format"
     )
     parser.add_argument(
-        "tokens_file", help="The file with the token definitions"
+        "tokens_file", type=pathlib.Path, help="The file with the token definitions"
     )
     parser.add_argument(
         "keyword_file",
+        type=pathlib.Path,
         help="The path to write the keyword definitions",
     )
     args = parser.parse_args()
diff --git a/Tools/peg_generator/pegen/parser_generator.py b/Tools/peg_generator/pegen/parser_generator.py
index f7783f1..18736be 100644
--- a/Tools/peg_generator/pegen/parser_generator.py
+++ b/Tools/peg_generator/pegen/parser_generator.py
@@ -1,5 +1,6 @@
 import ast
 import contextlib
+import pathlib
 import re
 import sys
 from abc import abstractmethod
@@ -134,7 +135,7 @@ def local_variable_names(self) -> list[str]:
         return self._local_variable_stack[-1]
 
     @abstractmethod
-    def generate(self, filename: str) -> None:
+    def generate(self, filename: pathlib.PurePath) -> None:
         raise NotImplementedError
 
     @contextlib.contextmanager
diff --git a/Tools/peg_generator/pegen/python_generator.py b/Tools/peg_generator/pegen/python_generator.py
index e69ea4b..cf3eb86 100644
--- a/Tools/peg_generator/pegen/python_generator.py
+++ b/Tools/peg_generator/pegen/python_generator.py
@@ -1,4 +1,4 @@
-import os.path
+import pathlib
 import token
 from collections.abc import Callable, Sequence
 from typing import IO, Any
@@ -232,12 +232,11 @@ def __init__(
             "end_lineno=end_lineno, end_col_offset=end_col_offset"
         )
 
-    def generate(self, filename: str) -> None:
+    def generate(self, filename: pathlib.PurePath) -> None:
         self.collect_rules()
         header = self.grammar.metas.get("header", MODULE_PREFIX)
         if header is not None:
-            basename = os.path.basename(filename)
-            self.print(header.rstrip("\n").format(filename=basename))
+            self.print(header.rstrip("\n").format(filename=filename.name))
         subheader = self.grammar.metas.get("subheader", "")
         if subheader:
             self.print(subheader)
diff --git a/Tools/peg_generator/pegen/testutil.py b/Tools/peg_generator/pegen/testutil.py
index 46f6c7f..a09e0b9 100644
--- a/Tools/peg_generator/pegen/testutil.py
+++ b/Tools/peg_generator/pegen/testutil.py
@@ -1,11 +1,11 @@
 import importlib.util
 import io
 import os
-import pathlib
 import sys
 import textwrap
 import token
 import tokenize
+from pathlib import Path, PurePath
 from typing import IO, Any, Final, cast
 
 from pegen.build import compile_c_extension
@@ -27,7 +27,7 @@ def generate_parser(grammar: Grammar) -> type[Parser]:
     # Generate a parser.
     out = io.StringIO()
     genr = PythonParserGenerator(grammar, out)
-    genr.generate("<string>")
+    genr.generate(PurePath("<string>"))
 
     # Load the generated parser class.
     ns: dict[str, Any] = {}
@@ -78,13 +78,13 @@ def import_file(full_name: str, path: str) -> Any:
 def generate_c_parser_source(grammar: Grammar) -> str:
     out = io.StringIO()
     genr = CParserGenerator(grammar, ALL_TOKENS, EXACT_TOKENS, NON_EXACT_TOKENS, out)
-    genr.generate("<string>")
+    genr.generate(PurePath("<string>"))
     return out.getvalue()
 
 
 def generate_parser_c_extension(
     grammar: Grammar,
-    path: pathlib.PurePath,
+    path: Path,
     debug: bool = False,
     library_dir: str | None = None,
 ) -> Any:
@@ -103,9 +103,9 @@ def generate_parser_c_extension(
         genr = CParserGenerator(
             grammar, ALL_TOKENS, EXACT_TOKENS, NON_EXACT_TOKENS, file, debug=debug
         )
-        genr.generate("parse.c")
+        genr.generate(PurePath("parse.c"))
     compile_c_extension(
-        str(source),
+        source,
         build_dir=str(path),
         # Significant test_peg_generator speedups
         disable_optimization=True,