1
Fork 0
mirror of https://github.com/RGBCube/CSAssignments synced 2025-06-20 04:52:10 +00:00
This commit is contained in:
RGBCube 2022-11-05 16:19:31 +03:00
parent df3ebcd560
commit 57f4be66d2
20 changed files with 322 additions and 1681 deletions

File diff suppressed because it is too large Load diff

9
.fleet/run.json Normal file
View file

@ -0,0 +1,9 @@
{
"configurations": [
{
"type": "python",
"name": "Run Interactive Runner",
"arguments": ["-m", "interactive_runner"],
}
]
}

2
.gitignore vendored
View file

@ -39,3 +39,5 @@ run/
*.ipr
*.iws
compiled/
__pycache__/
*.pyc

View file

@ -11,15 +11,15 @@ git clone https://github.com/RGBCube/CSAssignments
cd CSAssignments
```
OK, now you have the code on your machine. You can use the interactive code runner script to run the
code
Now you have the code on your machine. You can use the interactive code runner script to run the
code.
## Running
## Linux
```bash
./run
./run.sh
```
## Windows

View file

@ -0,0 +1,3 @@
from .assignment import *
from .language import *
from .sources import *

View file

@ -0,0 +1,24 @@
import interactive_runner as lib
s = lib.Sources()
for l in s.languages.values():
print("lname",l.name)
print("lsname",l.styled_name)
print("ldesc",l.description)
print("lbc",l._build_command)
print("ldni",l.check_dependencies_installed())
print("lisc",l.is_compiled)
print("lrcmd",l._run_command)
print()
for a in l.assignments.values():
print("alang",a.language)
print("aname",a.name)
print("agivend",a.given_date)
print("adesc",a.description)
print("adirs",a.directions)
try:
a.run()
except Exception as e:
print("errrun",e)
print()

View file

@ -0,0 +1,85 @@
from __future__ import annotations
__all__ = ("Assignment",)
from functools import cached_property
from typing import TypedDict, TYPE_CHECKING
from tomllib import loads as decode_toml
from os import system as cmd
if TYPE_CHECKING:
from .language import Language
AssignmentConfig = TypedDict(
"AssignmentConfig",
{
"name": str,
"given-date": str,
"main-file": str,
"description": str,
"directions": str,
}
)
class Assignment:
__config: AssignmentConfig
language: Language
name: str
given_date: str
__main_file: str
description: str
directions: str
def __init__(self, directory: Path, language: Language) -> None:
self.__directory = directory
self.language = language
@cached_property
def __config(self) -> AssignmentConfig:
return decode_toml((self.__directory / "assignment.toml").read_text())
def refresh(self) -> None:
del self.__config
@property
def name(self) -> str:
return self.__config["name"]
@property
def given_date(self) -> str:
return self.__config["given-date"]
@property
def __main_file(self) -> Path:
return self.__directory / self.__config["main-file"]
@property
def description(self) -> str:
return self.__config["description"]
@property
def directions(self) -> str:
return self.__config["directions"]
def compile(self, *, quiet: bool) -> int | None:
if missing := self.language.check_dependencies_installed():
raise ValueError(f"Needed depencencies are not installed: {', '.join(missing)}")
if not self.language.is_compiled:
return None
return cmd(self.language._build_command.format(
**{
"out-file": self.__directory / "compiled.out",
"main-file": self.__main_file.absolute(),
}
) +
(QUIET_SUFFIX if quiet else "")
)
def run(self) -> int:
if self.language.is_compiled and not (self.__directory / "compiled.out").exists():
self.compile(quiet=True)
return cmd(self.language._run_command.format((self.__directory / "compiled.out").absolute() if self.language.is_compiled else self.__main_file.absolute()))

View file

@ -0,0 +1,10 @@
__all__ = ("ROOT", "IS_WINDOWS", "QUIET_SUFFIX", "CHECK_COMMAND_EXISTS", "OS_KEY")
from os import name as os_name
from pathlib import Path
ROOT = Path(__file__).parent.parent
IS_WINDOWS = os_name == "nt"
QUIET_SUFFIX = " | Out-Null" if IS_WINDOWS else " > /dev/null"
CHECK_COMMAND_EXISTS = ("Get-Command {}" if IS_WINDOWS else "command -v {}") + QUIET_SUFFIX
OS_KEY = "windows" if IS_WINDOWS else "unix"

View file

@ -0,0 +1,135 @@
from __future__ import annotations
__all__ = ("Language",)
from .assignment import Assignment
from .consts import OS_KEY, CHECK_COMMAND_EXISTS
from typing import TYPE_CHECKING, TypedDict
from tomllib import loads as decode_toml
from chalky import hex
from functools import cached_property
from os import system as cmd
from chalky.shortcuts.sty import bold
if TYPE_CHECKING:
from pathlib import Path
LanguageConfig = TypedDict(
"LanguageConfig",
{
"name": str,
"descripton": str,
"colors": TypedDict(
"ColorConfig",
{
"foregroud": int,
"background": int,
}
),
"build": None | TypedDict(
"BuildConfig",
{
"common": str | None,
"unix": str | None,
"windows": str | None,
}
),
"run": TypedDict(
"RunConfig",
{
"common": str | None,
"unix": str | None,
"windows": str | None,
}
),
"dependencies": None | TypedDict(
"DependencyConfig",
{
"common": list[str] | None,
"unix": list[str] | None,
"windows": list[str] | None,
}
),
}
)
class Language:
__directory: Path
__config: LanguageConfig
name: str
styled_name: str
description: str
__dependencies: list[str] | None
_build_command: str | None
is_compiled: bool
_run_command: str
assignments: dict[str, Assignment]
def __init__(self, directory: Path) -> None:
self.__directory = directory
@cached_property
def __config(self) -> LanguageConfig:
return decode_toml((self.__directory / "language.toml").read_text())
def refresh(self) -> None:
del self.__config
del self.assignments
@property
def name(self) -> str:
return self.__config["name"]
@property
def styled_name(self) -> str:
colors = self.__config["colors"]
# hack
return hex(colors["foreground"]) & hex(colors["background"], background=True) | self.name
@property
def description(self) -> str:
return self.__config["description"]
@property
def __dependencies(self) -> list[str] | None:
dependencies = self.__config.get("dependencies", {})
return dependencies.get("common", dependencies.get(OS_KEY))
def check_dependencies_installed(self) -> list[str]:
if self.__dependencies is None:
return []
not_installed = []
for dependency in self.__dependencies:
exit = cmd(CHECK_COMMAND_EXISTS.format(dependency))
if exit:
not_installed.append(dependency)
return not_installed
@property
def _build_command(self) -> str | None:
build = self.__config.get("build", {})
return build.get("common", build.get(OS_KEY))
@property
def is_compiled(self) -> bool:
return bool(self._build_command)
@property
def _run_command(self) -> str:
run = self.__config["run"]
return run.get("common", run.get(OS_KEY))
@cached_property
def assignments(self) -> dict[str, Assignment]:
assignments = {}
for assignment_directory in self.__directory.iterdir():
if not assignment_directory.is_dir():
continue
assignments[assignment_directory.name] = Assignment(assignment_directory, self)
return assignments

View file

@ -0,0 +1,33 @@
from __future__ import annotations
__all__ = ("Sources",)
from .language import Language
from .consts import ROOT
from functools import cached_property
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from pathlib import Path
class Sources:
__directory: Path
languages: dict[str, Language]
def __init__(self) -> None:
self.__directory = ROOT / "sources"
@cached_property
def languages(self) -> dict[str, Language]:
languages = {}
for language_directory in self.__directory.iterdir():
if not language_directory.is_dir():
continue
languages[language_directory.name] = Language(language_directory)
return languages
def refresh(self) -> None:
del self.languages

280
run
View file

@ -1,280 +0,0 @@
#!/usr/bin/python3
from __future__ import annotations
from functools import cached_property
from os import name as os_name, system as cmd
from pathlib import Path
from typing import TypeVar
from chalky import hex, sty
T = TypeVar("T")
ROOT = Path(__file__).parent
is_windows = os_name == "nt"
class Sources:
def __init__(self) -> None:
self.__sources_dir = (ROOT / "src")
@cached_property
def languages(self) -> dict[str, Language]:
languages = {}
i = 0
for ldr in self.__sources_dir.iterdir():
if ldr.is_dir():
i += 1
language = Language(ldr)
languages[language.name] = language
languages[str(i)] = language
return languages
class Language:
def __init__(self, path: Path) -> None:
self.__path = path
@cached_property
def name(self) -> str:
return self.__path.name
@cached_property
def colored_name(self) -> str:
return hex((self.__path / "FGCOLOR").read_text()) & \
hex((self.__path / "BGCOLOR").read_text(), background=True) \
| self.__path.name
@cached_property
def description(self) -> str:
return (self.__path / "DESCRIPTION").read_text()
@cached_property
def is_compiled(self) -> bool:
return (self.__path / "COMPILECMD").exists()
@cached_property
def compile_command(self) -> str:
"""For compiled languages. Format with `main` and `out`"""
return (self.__path / "COMPILECMD").read_text().strip() if self.is_compiled else None
@cached_property
def run_command(self) -> str:
"""For interpreted languages. Format with `main`"""
return (self.__path / "RUNCMD").read_text().strip()
@cached_property
def assignments(self) -> dict[str, Assignment]:
assignments = {}
i = 0
for adr in self.__path.iterdir():
if adr.is_dir():
i += 1
assignment = Assignment(adr, self)
assignments[assignment.name] = assignment
assignments[str(i)] = assignment
return assignments
class Assignment:
def __init__(self, path: Path, language: Language) -> None:
self.__path = path
self.language = language
@cached_property
def name(self) -> str:
return self.__path.name.split(":", 1)[0]
@cached_property
def date(self) -> str:
return self.__path.name.split(":", 1)[1]
@cached_property
def description(self) -> str:
return (self.__path / "DESCRIPTION").read_text()
@cached_property
def directions(self) -> str:
return (self.__path / "ASSIGNMENT").read_text()
@cached_property
def main_file(self) -> Path:
return self.__path / (self.__path / "MAIN").read_text()
@cached_property
def compiled_file(self) -> Path:
return ROOT / "compiled" / (self.name + (".exe" if is_windows else ""))
def compile(self, *, show: bool) -> bool:
"""Returns True if compilation was successful, False otherwise."""
if self.compiled_file.exists():
self.compiled_file.unlink()
return cmd(
self.language.compile_command.format(main=self.main_file, out=self.compiled_file)
+ "" if show else (" > NUL" if is_windows else " > /dev/null")
) == 0
def run(self) -> None:
if self.language.is_compiled:
if not self.compiled_file.exists():
self.compile(show=False)
cmd(self.compiled_file.absolute())
else:
cmd(self.language.run_command.format(main=self.main_file.absolute()))
src = Sources()
red = hex("ff0000")
green = hex("39ff14")
cyan = hex("4bf0fc")
purple = hex("b026ff")
orange = hex("ff760d")
invalid_choice = red | "Invalid choice! Try again."
exiting = red | "Exiting..."
def nl() -> None:
print()
_nl = nl
def do_exit(s: str, /) -> str:
if s == "exit":
print(exiting)
exit()
return s
def nprnt(*s, nl: bool = False) -> None:
print(*s)
if nl:
_nl()
def scan(prompt: str, /, *, nl: bool = False) -> str:
s = input(prompt).lower().strip()
if nl:
_nl()
return do_exit(s)
def yesno(prompt: str, /, *, nl: bool = False) -> bool:
s = input(prompt).lower().strip()
if s in {"y", "yes"}:
return True
elif s in {"n", "no"}:
return False
else:
print(invalid_choice)
return yesno(prompt, nl=nl)
def iter_int_keys(d: dict[str, T]) -> list[tuple[int, T]]:
for k, v in d.items():
if k.isdigit():
yield int(k), v
def get_ignorecase(d: dict[str, T], k: str) -> T:
for key, value in d.items():
# k is always lowercase
if key.lower() == k:
return value
raise KeyError(k)
while True:
# TODO align columns
print("----------", green & sty.bold | "LANGUAGES", "----------")
for i, language in iter_int_keys(src.languages):
print((green | i) + f": {language.colored_name} - {language.description}")
nl()
choice = scan("Choose a language by its number or name (exit to exit): ", nl=True)
language = get_ignorecase(src.languages, choice)
if language is None:
nprnt(invalid_choice, nl=True)
continue
while True:
# TODO align columns
print("----------", cyan & sty.bold | language.name.upper(), "----------")
for i, assignment in iter_int_keys(language.assignments):
print(
(cyan | i) + f": {assignment.name} ({assignment.date}) - {assignment.description}"
)
nl()
choice = scan(
"Choose an assignment by its number or name (back to go back, exit to exit): ",
nl=True
)
if choice == "back":
break
assignment = get_ignorecase(language.assignments, choice)
if assignment is None:
nprnt(invalid_choice, nl=True)
continue
while True:
print("----------", purple & sty.bold | assignment.name.upper(), "----------")
print((purple | 1) + ": Show directions")
if language.is_compiled:
print((purple | 2) + ": Compile (uses cache if exists) & run")
print((purple | 3) + ": Compile manually")
else:
print((purple | 2) + ": Run")
nl()
choice = scan(
"Choose an option by its number (back to go back, exit to exit): ",
nl=True
)
match choice:
case "back":
break
case "1":
print(
"----------",
orange & sty.bold | f"DIRECTIONS FOR {assignment.name.upper()}",
"----------"
)
nprnt(assignment.directions, nl=True)
case "2":
nprnt("----------", orange | "RUN OUTPUT", "----------", nl=True)
assignment.run()
nl()
case "3":
if not language.is_compiled:
nprnt(invalid_choice, nl=True)
continue
ask = yesno("Show compile output if exists? (y/n): ", nl=True)
print(orange | "Compiling...")
success = assignment.compile(show=ask)
if success:
s = green | "Compiled successfully!"
else:
s = red | "Compilation failed!"
nprnt(s, nl=True)
case _:
nprnt(invalid_choice, nl=True)

View file

@ -1 +1,2 @@
py run
py -m pip install -U chalky > NUL
py -m interactive_runner

137
run.py
View file

@ -1,137 +0,0 @@
#!/usr/bin/python3
from __future__ import annotations
from tomllib import loads as decode_toml
from dataclasses import dataclass
from functools import cached_property
from os import name as os_name, system as cmd
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from chalky import Chalk
ROOT = Path(__file__).parent
IS_WIN = os_name == "nt"
QUIET_SUFFIX = " > NUL" if IS_WIN else " > /dev/null"
OS_KEY = "windows" if IS_WIN else "linux"
class Sources:
def __init__(self) -> None:
self.__src_dir = ROOT / "src"
@cached_property
def languages(self) -> dict[str, Language]:
languages = {}
for language_dir in self.__src_dir.iterdir():
if not language_dir.is_dir():
continue
language_metadata = decode_toml((language_dir / "language.toml").read_text())
language_metadata["directory"] = language_dir
language = Language.from_raw(language_metadata)
languages[language.name] = language
return languages
@dataclass
class Colors:
fg: Chalk
bg: Chalk
class Language:
def __init__(
self,
*,
directory: Path,
name: str,
description: str,
colors: Colors,
build_command: str | None,
run_command: str,
) -> None:
self.__directory = directory
self.name = name
self.description = description
self.colors = colors
self.build_command = build_command
self.run_command = run_command
@classmethod
def from_raw(cls, raw: dict) -> Language:
colors = raw["colors"]
build = raw.get("build", {})
run = raw["run"]
return cls(
directory=raw["directory"],
name=raw["name"],
description=raw["description"],
colors=Colors(
fg=colors["foreground"],
bg=colors["background"],
),
build_command=build.get("common", build.get(OS_KEY)),
run_command=run.get("common", run.get(OS_KEY)),
)
@property
def assignments(self) -> dict[str, Assignment]:
for assignment_dir in self.__directory.iterdir():
if not assignment_dir.is_dir():
continue
assignment_metadata = decode_toml((assignment_dir / "assignment.toml").read_text())
assignment_metadata["directory"] = assignment_dir
assignment_metadata["language"] = self
yield Assignment.from_raw(assignment_metadata)
class Assignment:
def __init__(
self,
*,
language: Language,
directory: Path,
name: str,
date: str,
description: str,
directions: str,
) -> None:
self.language = language
self.__directory = directory
self.name = name
self.date = date
self.description = description
self.directions = directions
@classmethod
def from_raw(cls, raw: dict) -> Assignment:
return cls(
language=raw["language"],
directory=raw["directory"],
name=raw["name"],
date=raw["date"],
description=raw["description"],
directions=raw["directions"],
)
def compile(self, *, quiet: bool = False) -> bool | None:
"""None = not a compiled language"""
if self.language.build_command is None:
return None
return cmd(self.language.build_command + (QUIET_SUFFIX if quiet else "")) == 0

3
run.sh Normal file
View file

@ -0,0 +1,3 @@
#!/usr/bin/bash
python3 -m pip install -U chalky > /dev/null
python3 -m interactive_runner

View file

@ -1,9 +1,8 @@
name = "example"
name = "Example"
given-date = "3/11/2022"
main-file = "example.c"
description = "This is an example entry for C"
directions = """
Print "Example" 10 times.
"""
[compile]
main-file = "example.c"

View file

@ -2,15 +2,14 @@ name = "C"
description = "C (pronounced like the letter c) is a general-purpose computer programming language"
[colors]
foreground = 0x000000
background = 0x00599d
foreground = "ffffff"
background = "00599d"
[build]
common = "gcc -o {out-file} {main-file}"
[run]
linux = "./{out-file}"
windows = "{out-file}"
common = "{}"
[dependencies]
common = ["gcc"]

View file

@ -1,5 +1,6 @@
name = "star-rating"
name = "Star Rating"
given-date = "30/10/2022"
main-file = "star_rating.c"
description = "A program that takes 21 reviews and calculates the average rating and prints some stars"
directions = """
1. Read in 21 user ratings for a movie.
@ -26,6 +27,3 @@ star, e.g. 14.3% one star, 23.8% two star, etc.
Display this to the screen in a similar manner
to task 4.
"""
[compile]
main-file = "star_rating.c"

View file

@ -1,9 +1,7 @@
name = "example"
given-date = "3/11/2022"
main-file = "example.py"
description = "This is an example entry for CPython"
directions = """
Print "Example" 10 times.
"""
[compile]
main-file = "example.py"

View file

@ -2,13 +2,13 @@ name = "CPython"
description = "Python is a high-level, general-purpose programming language"
[colors]
foreground = 0xfed140
background = 0x3670a0
foreground = "fed140"
background = "3670a0"
[run]
linux = "python3 {main-file}"
windows = "py {main-file}"
unix = "python3 {}"
windows = "py {}"
[dependencies]
linux = ["python3"]
unix = ["python3"]
windows = ["py"]