Skip to content

Commit

Permalink
pgzrun a directory
Browse files Browse the repository at this point in the history
Fixes #330
  • Loading branch information
lordmauve committed Jan 18, 2025
1 parent bde0fbe commit 29d0361
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 14 deletions.
6 changes: 6 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
61 changes: 53 additions & 8 deletions src/pgzero/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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`
* `<name>.py` where `<name>` 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
Expand Down
2 changes: 2 additions & 0 deletions test/game_tests/blue/run_game.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def draw():
screen.fill('blue')
2 changes: 2 additions & 0 deletions test/game_tests/green/green.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def draw():
screen.fill('green')
2 changes: 2 additions & 0 deletions test/game_tests/pink/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def draw():
screen.fill('pink')
2 changes: 2 additions & 0 deletions test/game_tests/red/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
def draw():
screen.fill('red')
30 changes: 24 additions & 6 deletions test/test_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <basename>.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')

0 comments on commit 29d0361

Please sign in to comment.