diff --git a/pyproject.toml b/pyproject.toml index cb27f399..18bf0526 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,3 +23,9 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "src/pgzero/_version.py" + +[tool.ruff] +builtins = [ + "Actor", "Rect", "ZRect", "animate", "clock", "exit", "images", "keyboard", "keymods", + "keys", "mouse", "music", "screen", "sounds", "storage", "tone" +] diff --git a/src/pgzero/runner.py b/src/pgzero/runner.py index b4166f76..adeaf64e 100644 --- a/src/pgzero/runner.py +++ b/src/pgzero/runner.py @@ -69,30 +69,75 @@ def main(): help="Print periodic FPS measurements on the terminal." ) parser.add_argument( - 'program', - help="The Pygame Zero program to run." + 'game', + help="The Pygame Zero game to run (a Python file or directory)." ) args = parser.parse_args() if __debug__: warnings.simplefilter('default', DeprecationWarning) - load_and_run(args.program, fps=args.fps) + try: + load_and_run(args.game, fps=args.fps) + except NoMainModule as e: + sys.exit(e) + + +class NoMainModule(Exception): + """Indicate that we couldn't find a main module to run.""" def load_and_run(path, *, fps: bool = False): - """Load and run the given Python file as the main PGZero game module. + """Load and run the given Python file or directory. + + If a file, run this as the main PGZero game module. + + If a directory, run the first file inside the directory containing: + + * `__main__.py` + * `main.py` + * `run_game.py` + * `.py` where `` is the basename of the directory Note that the 'import pgzrun' IDE mode doesn't pass through this entry point, as the module is already loaded. """ - with open(path, 'rb') as f: - src = f.read() + path = path.rstrip(os.sep) + try: + with open(path, 'rb') as f: + src = f.read() + except FileNotFoundError: + raise NoMainModule(f"Error: {path} does not exist.") + except IsADirectoryError: + name = os.path.basename(path) + for candidate in ( + '__main__.py', + 'main.py', + 'run_game.py', + f'{name}.py' + ): + try: + with open(os.path.join(path, candidate), 'rb') as f: + src = f.read() + break + except FileNotFoundError: + pass + else: + raise NoMainModule(f"""\ +Error: {path} is a directory. + +To run a directory with pgzrun, it must contain a file named __main__.py, +main.py, run_game.py, or a .py file with the same name as the directory. + +You can also run a specific file with: + + pgzrun path/to/your/file.py +""") + else: + name, _ = os.path.splitext(os.path.basename(path)) code = compile(src, os.path.basename(path), 'exec', dont_inherit=True) - - name, _ = os.path.splitext(os.path.basename(path)) mod = ModuleType(name) mod.__file__ = path mod.__name__ = name diff --git a/test/game_tests/blue/run_game.py b/test/game_tests/blue/run_game.py new file mode 100644 index 00000000..dd552e3a --- /dev/null +++ b/test/game_tests/blue/run_game.py @@ -0,0 +1,2 @@ +def draw(): + screen.fill('blue') diff --git a/test/game_tests/green/green.py b/test/game_tests/green/green.py new file mode 100644 index 00000000..3f99ef4b --- /dev/null +++ b/test/game_tests/green/green.py @@ -0,0 +1,2 @@ +def draw(): + screen.fill('green') diff --git a/test/game_tests/pink/main.py b/test/game_tests/pink/main.py new file mode 100644 index 00000000..20c326e1 --- /dev/null +++ b/test/game_tests/pink/main.py @@ -0,0 +1,2 @@ +def draw(): + screen.fill('pink') diff --git a/test/game_tests/red/__main__.py b/test/game_tests/red/__main__.py new file mode 100644 index 00000000..6b1ccf61 --- /dev/null +++ b/test/game_tests/red/__main__.py @@ -0,0 +1,2 @@ +def draw(): + screen.fill('red') diff --git a/test/test_runner.py b/test/test_runner.py index 4f1b33c7..2976046e 100644 --- a/test/test_runner.py +++ b/test/test_runner.py @@ -16,14 +16,32 @@ class RunnerTest(unittest.TestCase): """Test that we can load and run the current file.""" - def test_run(self): - """We can load and run a game saved as UTF-8.""" + def assert_runnable(self, path: Path): + """Check that we can run the given file.""" clock.schedule_unique(sys.exit, 0.05) with self.assertRaises(SystemExit): - load_and_run(str(game_tests / 'utf8.py')) + load_and_run(str(path)) + + def test_run_utf8(self): + """We can load and run a game saved as UTF-8.""" + self.assert_runnable(game_tests / 'utf8.py') def test_import(self): """Games can import other modules, which can acccess the builtins.""" - clock.schedule_unique(sys.exit, 0.05) - with self.assertRaises(SystemExit): - load_and_run(str(game_tests / 'importing.py')) + self.assert_runnable(game_tests / 'importing.py') + + def test_run_directory_dunder_main(self): + """We can run a directory containing __main__.py""" + self.assert_runnable(game_tests / 'red') + + def test_run_directory_main(self): + """We can run a directory containing main.py""" + self.assert_runnable(game_tests / 'pink') + + def test_run_directory_ane_name(self): + """We can run a directory containing .py""" + self.assert_runnable(game_tests / 'green') + + def test_run_directory_run_game(self): + """We can run a directory containing run_game.py""" + self.assert_runnable(game_tests / 'blue')