shell_adventure.api
The shell_adventure.api package contains the classes and methods needed to make puzzle templates and autograders
1""" 2The shell_adventure.api package contains the classes and methods needed to make puzzle templates and autograders 3""" 4 5from __future__ import annotations 6from shell_adventure.shared.puzzle import Puzzle, PuzzleTemplate, AutoGrader 7from .file import File 8from .permissions import (change_user, user_exists, Permissions, LinkedPermissions, 9 PermissionsGroup, LinkedPermissionsGroup) 10from .random_helper import RandomHelper, RandomHelperException 11 12from pathlib import Path as _Path 13 14PKG_PATH = _Path(__path__[0]).resolve() # type: ignore # mypy issue #1422 15 16# Unfortunately we need some package level variables to allow File methods to access 17# the RandomHelper and student home. They will be set when the tutorial is created. 18_home: File = None 19""" Global that is set to the home of the student. """ 20 21_rand: RandomHelper = None 22""" 23Global that is set to the `RandomHelper` of the tutorial, so that `rand()` and `File` methods can access it. 24""" 25 26# After restart and pickling the lambdas we don't restore the RandomHelper state so we don't want you to be able to 27# call it in autograder functions. So we make rand a function so that it doesn't get captured directly, that way we can 28# set tutorial.rand to None and trying to use rand() will fail. 29# TutorialDocker will set _rand when it runs. 30def rand() -> RandomHelper: 31 """ 32 Returns the `RandomHelper` which should be used when creating random files and folders. 33 """ 34 if not _rand: 35 raise RandomHelperException("You can only use randomization in Puzzle templates, not autograders") 36 return _rand 37 38__all__ = [ 39 "Puzzle", 40 "PuzzleTemplate", 41 "AutoGrader", 42 "File", 43 "change_user", 44 "user_exists", 45 "Permissions", 46 "LinkedPermissions", 47 "PermissionsGroup", 48 "LinkedPermissionsGroup", 49 "RandomHelper", 50 "RandomHelperException", 51 "rand", 52]
8class Puzzle: 9 """ Represents a single puzzle in the tutorial. """ 10 11 question: str 12 """ The question string that will be shown to the student. """ 13 14 score: int 15 """ The score that will be given when the puzzle is solved. """ 16 17 checker: AutoGrader 18 """ 19 The function that will be used to autograde the puzzle. 20 """ 21 22 _allowed_checker_args: ClassVar[List[str]] = ["cwd", "flag"] 23 """ A set of the checker args that are recognized. """ 24 25 def __init__(self, question: str, checker: AutoGrader, score: int = 1): 26 """ 27 Construct a `Puzzle` object. 28 29 Parameters: 30 31 question: 32 The question to be asked. 33 checker: 34 The function that will grade whether the puzzle was completed correctly or not. 35 The checker function can take the following parameters. All parameters are optional, and order does not matter, 36 but the parameters must have the same name as listed here: 37 flag: str 38 If the flag parameter is present, an input dialog will be shown to the student when sumbitting a puzzle, 39 and their input will be passed to this parameter. 40 cwd: File 41 The path to the students current directory 42 43 The checker function should return a string or a boolean. If it returns True, the puzzle is solved. If it returns 44 False or a string, the puzzle was not solved. Returning a string will show the string as feedback to the student. 45 score: 46 The score given on success. Defaults to 1. 47 """ 48 if not isinstance(question, str): raise TypeError("Puzzle.question should be a string.") 49 if not callable(checker): raise TypeError("Puzzle.checker should be a Callable.") 50 if not isinstance(score, int): raise TypeError("Puzzle.score should be an int.") 51 52 self.question = question 53 self.score = score 54 self.checker = checker # type: ignore # MyPy fusses about "Cannot assign to a method" 55 56 extra_params = extra_func_params(self.checker, Puzzle._allowed_checker_args) 57 if extra_params: 58 raise UnrecognizedParamsError( 59 f'Unrecognized param(s) {sentence_list(extra_params, quote = True)} in checker function.' + 60 f' Expected {sentence_list(Puzzle._allowed_checker_args, last_sep = " and/or ", quote = True)}.', 61 extra_params = extra_params 62 )
Represents a single puzzle in the tutorial.
25 def __init__(self, question: str, checker: AutoGrader, score: int = 1): 26 """ 27 Construct a `Puzzle` object. 28 29 Parameters: 30 31 question: 32 The question to be asked. 33 checker: 34 The function that will grade whether the puzzle was completed correctly or not. 35 The checker function can take the following parameters. All parameters are optional, and order does not matter, 36 but the parameters must have the same name as listed here: 37 flag: str 38 If the flag parameter is present, an input dialog will be shown to the student when sumbitting a puzzle, 39 and their input will be passed to this parameter. 40 cwd: File 41 The path to the students current directory 42 43 The checker function should return a string or a boolean. If it returns True, the puzzle is solved. If it returns 44 False or a string, the puzzle was not solved. Returning a string will show the string as feedback to the student. 45 score: 46 The score given on success. Defaults to 1. 47 """ 48 if not isinstance(question, str): raise TypeError("Puzzle.question should be a string.") 49 if not callable(checker): raise TypeError("Puzzle.checker should be a Callable.") 50 if not isinstance(score, int): raise TypeError("Puzzle.score should be an int.") 51 52 self.question = question 53 self.score = score 54 self.checker = checker # type: ignore # MyPy fusses about "Cannot assign to a method" 55 56 extra_params = extra_func_params(self.checker, Puzzle._allowed_checker_args) 57 if extra_params: 58 raise UnrecognizedParamsError( 59 f'Unrecognized param(s) {sentence_list(extra_params, quote = True)} in checker function.' + 60 f' Expected {sentence_list(Puzzle._allowed_checker_args, last_sep = " and/or ", quote = True)}.', 61 extra_params = extra_params 62 )
Construct a Puzzle
object.
Parameters:
question: The question to be asked. checker: The function that will grade whether the puzzle was completed correctly or not. The checker function can take the following parameters. All parameters are optional, and order does not matter, but the parameters must have the same name as listed here: flag: str If the flag parameter is present, an input dialog will be shown to the student when sumbitting a puzzle, and their input will be passed to this parameter. cwd: File The path to the students current directory
The checker function should return a string or a boolean. If it returns True, the puzzle is solved. If it returns
False or a string, the puzzle was not solved. Returning a string will show the string as feedback to the student.
score: The score given on success. Defaults to 1.
9class File(PosixPath): 10 """ 11 File is an extention of pathlib.PosixPath, with a few convenience methods added. 12 Refer to pathlib documentation at [python.org](https://docs.python.org/3/library/pathlib.html) 13 """ 14 15 @classmethod 16 def home(cls) -> File: 17 """ Return the home directory of the student. Equivalent to the "home" parameter in puzzle templates. """ 18 if shell_adventure.api._home: # Get home directory from tutorial 19 return shell_adventure.api._home 20 else: # Default to PosixPath 21 return File(PosixPath.home()) 22 23 24 # === Convenience Methods === 25 26 @property 27 def children(self) -> List[File]: 28 """ 29 A property that returns a list of directory's contents. Raises `NotADirectoryError` if not a directory. 30 Basically an alias of `Path.iterdir()` but returns a list instead of a generator. 31 """ 32 return list(self.iterdir()) # type: ignore 33 34 @property 35 def path(self) -> str: 36 """ A property that returns the absolute path to this file as a string. """ 37 return str(self.resolve()) 38 39 def create(self, *, mode: int = None, exist_ok = True, recursive = True, content: str = None) -> File: 40 """ 41 An combined version of `Path.mkdir()`, `Path.touch()`, `Path.chmod()` and `Path.write_text()`. It 42 will `mkdir` missing dirs in the path if recursive is True (the default). New directories will use 43 the default mode regardless of the `mode` parameter to match POSIX `mkdir -p` behavior. You can also 44 specify a content string which will be written to the file. 45 46 Returns self. 47 """ 48 if recursive: 49 self.parent.mkdir(parents = True, exist_ok = True) # mkdir is already recursive 50 self.touch(exist_ok = exist_ok) 51 if mode != None: 52 self.chmod(mode) # touch(mode=) combines with umask first which results in odd behavior 53 if content != None: 54 self.write_text(content) 55 56 return self 57 58 # === Permissions === 59 60 def chown(self, owner: Union[str, int] = None, group: Union[str, int] = None): 61 """ 62 Change owner and/or group of the given path. Automatically runs as root, you do not have to use `change_user()` 63 before using `chown()`. user can be a system user name or a uid; the same applies to group. At least one 64 argument is required. See also `os.chown()`, the underlying function. 65 """ 66 with change_user("root"): # Automatically set privilege to root. 67 shutil.chown(self, owner, group) 68 69 def chmod(self, mode: Union[str, int]): 70 """ 71 Overrides `Path.chmod()`. Automatically runs as root, you do not have to `change_user()` before using 72 `chmod()`. You can pass it a mode as an int, ie. `0o777` like `Path.chmod()`, or you can pass it a string 73 that the unix `chmod` command would recognize such as `"u+x"`. See the [chmod man page](https://linux.die.net/man/1/chmod) 74 """ 75 with change_user("root"): # Automatically set privilege to root. 76 if isinstance(mode, str): # Just call chmod directly instead of trying to parse the mode string ourselves. 77 if not self.exists(): raise FileNotFoundError 78 process = subprocess.run(["chmod", mode, self.path]) 79 if process.returncode != 0: 80 raise ValueError(f'Invalid mode "{mode}"') 81 else: 82 super().chmod(mode) 83 84 @property 85 def permissions(self) -> LinkedPermissions: 86 """ 87 A property that returns a `Permissions` object representing the permissions of this file. 88 Eg. 89 >>> File("A.py").permissions.user.execute 90 True 91 """ 92 return LinkedPermissions(self) 93 94 @permissions.setter 95 def permissions(self, val: Union[int, Permissions]): 96 """ Set this `File`'s permissions. """ 97 if isinstance(val, Permissions): 98 val = int(val) 99 self.chmod(val) 100 101 102 # === Randomization === 103 104 def random_file(self, ext = None) -> File: 105 """ 106 Creates a `File` with a random name under self. The source for random names comes from the `name_dictionary` option 107 in the Tutorial config. The file is not created on disk and is not marked as shared. To create the file on disk 108 call `File.create()` or `mkdir()` on the file returned by `random_file()`. You can pass an extension which will be added 109 to the random name. Will not create a file with a name that already exists. 110 """ 111 return shell_adventure.api.rand()._file(self, ext = ext) 112 113 def random_shared_folder(self, depth: Union[int, Tuple[int, int]] = (1, 3), create_new_chance: float = 0.5) -> File: 114 """ 115 Makes a `File` to a random folder under this file. The folder may be several levels deep, and its parents may or may 116 not exist. Its does not create the file or any parents on disk. 117 118 The returned `File` can include new folders in the path with random names, and it can include existing folders that are 119 "shared". Folders are only "shared" if they were created via `random_shared_folder()` or explicitly marked shared via 120 `mark_shared()`. 121 122 Folders created by `random_shared_folder()` can be "reused" in other calls to `folder()`, so you should not modify 123 the folders in puzzles. This way, folders created by puzzles won't interfere with one another, but multiple puzzles can 124 still be created in the same directory. If you need a folder at a random location that won't have any other puzzles put 125 in it you should explicitly create a folder under the one returned by `random_shared_folder()` with something like 126 >>> home.random_shared_folder().random_file().mkdir() 127 128 depth: Either an int or a (min, max) tuple. Specifies the depth under parent of the returned file. If a tuple is given 129 a random depth in rand [min, max] will be used. 130 create_new_chance: float in [0, 1]. The percentage chance that a new folder will be created even if shared folders are 131 available. 0 means it will only choose existing folders unless there are none, 1 means it will only 132 create new folders. 133 134 >>> home = File("/home/student") 135 >>> home.random_shared_folder() 136 File("/home/student/random/nested/folder") 137 >>> home.random_shared_folder() 138 File("/home/student/apple/banana") 139 >>> folder.mkdir(parents = True) # random_shared_folder() doesn't create the file on disk. Use mkdir() with parents = True to make the folder. 140 >>> # Make a random nested folder, but make the last folder not "shared" so we can safely rm it 141 >>> home.random_shared_folder().random_file().mkdir() 142 File("/home/student/orange/lime/tomato") 143 >>> home.random_shared_folder(create_new_chance = 0) # Will choose an existing "shared" folder 144 File("/home/student/orange/lime") 145 >>> File("/").random_shared_folder(depth = [5, 6]) # Create a folder 5 or 6 levels under root 146 File("/blueberry/lemon/watermellon/kiwi/strawberry") 147 """ 148 return shell_adventure.api.rand()._folder(self, depth, create_new_chance) 149 150 def mark_shared(self): 151 """ Marks the a `File` as shared. `File` should be a directory, though it does not have to exist yet. """ 152 shell_adventure.api.rand()._mark_shared(self)
File is an extention of pathlib.PosixPath, with a few convenience methods added. Refer to pathlib documentation at python.org
15 @classmethod 16 def home(cls) -> File: 17 """ Return the home directory of the student. Equivalent to the "home" parameter in puzzle templates. """ 18 if shell_adventure.api._home: # Get home directory from tutorial 19 return shell_adventure.api._home 20 else: # Default to PosixPath 21 return File(PosixPath.home())
Return the home directory of the student. Equivalent to the "home" parameter in puzzle templates.
A property that returns a list of directory's contents. Raises NotADirectoryError
if not a directory.
Basically an alias of Path.iterdir()
but returns a list instead of a generator.
39 def create(self, *, mode: int = None, exist_ok = True, recursive = True, content: str = None) -> File: 40 """ 41 An combined version of `Path.mkdir()`, `Path.touch()`, `Path.chmod()` and `Path.write_text()`. It 42 will `mkdir` missing dirs in the path if recursive is True (the default). New directories will use 43 the default mode regardless of the `mode` parameter to match POSIX `mkdir -p` behavior. You can also 44 specify a content string which will be written to the file. 45 46 Returns self. 47 """ 48 if recursive: 49 self.parent.mkdir(parents = True, exist_ok = True) # mkdir is already recursive 50 self.touch(exist_ok = exist_ok) 51 if mode != None: 52 self.chmod(mode) # touch(mode=) combines with umask first which results in odd behavior 53 if content != None: 54 self.write_text(content) 55 56 return self
An combined version of Path.mkdir()
, Path.touch()
, Path.chmod()
and Path.write_text()
. It
will mkdir
missing dirs in the path if recursive is True (the default). New directories will use
the default mode regardless of the mode
parameter to match POSIX mkdir -p
behavior. You can also
specify a content string which will be written to the file.
Returns self.
60 def chown(self, owner: Union[str, int] = None, group: Union[str, int] = None): 61 """ 62 Change owner and/or group of the given path. Automatically runs as root, you do not have to use `change_user()` 63 before using `chown()`. user can be a system user name or a uid; the same applies to group. At least one 64 argument is required. See also `os.chown()`, the underlying function. 65 """ 66 with change_user("root"): # Automatically set privilege to root. 67 shutil.chown(self, owner, group)
Change owner and/or group of the given path. Automatically runs as root, you do not have to use change_user()
before using chown()
. user can be a system user name or a uid; the same applies to group. At least one
argument is required. See also os.chown()
, the underlying function.
69 def chmod(self, mode: Union[str, int]): 70 """ 71 Overrides `Path.chmod()`. Automatically runs as root, you do not have to `change_user()` before using 72 `chmod()`. You can pass it a mode as an int, ie. `0o777` like `Path.chmod()`, or you can pass it a string 73 that the unix `chmod` command would recognize such as `"u+x"`. See the [chmod man page](https://linux.die.net/man/1/chmod) 74 """ 75 with change_user("root"): # Automatically set privilege to root. 76 if isinstance(mode, str): # Just call chmod directly instead of trying to parse the mode string ourselves. 77 if not self.exists(): raise FileNotFoundError 78 process = subprocess.run(["chmod", mode, self.path]) 79 if process.returncode != 0: 80 raise ValueError(f'Invalid mode "{mode}"') 81 else: 82 super().chmod(mode)
Overrides Path.chmod()
. Automatically runs as root, you do not have to change_user()
before using
chmod()
. You can pass it a mode as an int, ie. 0o777
like Path.chmod()
, or you can pass it a string
that the unix chmod
command would recognize such as "u+x"
. See the chmod man page
A property that returns a Permissions
object representing the permissions of this file.
Eg.
>>> File("A.py").permissions.user.execute
True
104 def random_file(self, ext = None) -> File: 105 """ 106 Creates a `File` with a random name under self. The source for random names comes from the `name_dictionary` option 107 in the Tutorial config. The file is not created on disk and is not marked as shared. To create the file on disk 108 call `File.create()` or `mkdir()` on the file returned by `random_file()`. You can pass an extension which will be added 109 to the random name. Will not create a file with a name that already exists. 110 """ 111 return shell_adventure.api.rand()._file(self, ext = ext)
Creates a File
with a random name under self. The source for random names comes from the name_dictionary
option
in the Tutorial config. The file is not created on disk and is not marked as shared. To create the file on disk
call File.create()
or mkdir()
on the file returned by random_file()
. You can pass an extension which will be added
to the random name. Will not create a file with a name that already exists.
Inherited Members
- pathlib.Path
- cwd
- samefile
- iterdir
- glob
- rglob
- absolute
- resolve
- stat
- owner
- group
- open
- read_bytes
- read_text
- write_bytes
- write_text
- readlink
- touch
- mkdir
- lchmod
- unlink
- rmdir
- lstat
- rename
- replace
- symlink_to
- hardlink_to
- link_to
- exists
- is_dir
- is_file
- is_mount
- is_symlink
- is_block_device
- is_char_device
- is_fifo
- is_socket
- expanduser
- pathlib.PurePath
- as_posix
- as_uri
- drive
- root
- anchor
- name
- suffix
- suffixes
- stem
- with_name
- with_stem
- with_suffix
- relative_to
- is_relative_to
- parts
- joinpath
- parent
- parents
- is_absolute
- is_reserved
- match
8@contextmanager 9def change_user(user: str, group: str = None): 10 """ 11 `change_user()` is a context manager which changes the effective user of the process to user (by name), and changes 12 it back when the context manager exits. Group will default to the group with the same name as user. 13 14 Note that `os.system()` and the like will run as root regardles of `change_user` since it starts a new process. 15 16 Example: 17 >>> with change_user("root"): 18 ... File("root_file").create() # root will own this file 19 >>> File("student_file").create() # We are back to default user, student will own this file. 20 """ 21 group = group if group else user 22 prev_user = os.geteuid() 23 prev_group = os.getegid() 24 25 uid = pwd.getpwnam(user).pw_uid 26 gid = grp.getgrnam(group).gr_gid 27 os.setegid(gid) 28 os.seteuid(uid) 29 30 try: 31 yield (uid, gid) 32 finally: # change back to original user. 33 os.setegid(prev_user) 34 os.seteuid(prev_group)
change_user()
is a context manager which changes the effective user of the process to user (by name), and changes
it back when the context manager exits. Group will default to the group with the same name as user.
Note that os.system()
and the like will run as root regardles of change_user
since it starts a new process.
Example:
>>> with change_user("root"):
... File("root_file").create() # root will own this file
>>> File("student_file").create() # We are back to default user, student will own this file.
36def user_exists(user: str) -> bool: 37 """ Returns True if the user exists (by their username) """ 38 try: 39 pwd.getpwnam(user) 40 return True 41 except KeyError: 42 return False
Returns True if the user exists (by their username)
44class Permissions: 45 """ 46 Plain old data structure that represents basic Linux permissions, with user, group, and others sections. 47 Currently doesn't include special permission bits such as the sticky bit. 48 """ 49 50 user: PermissionsGroup 51 """ User permission bits """ 52 group: PermissionsGroup 53 """ Group permission bits """ 54 others: PermissionsGroup 55 """ Others permission bits """ 56 57 def __init__(self, mode: int = None, *, user: str = "", group: str = "", others: str = ""): 58 """ 59 Create a `Permissions` object. You can create one from an octal `int`, or by specifying user, group, and others 60 with strings containing some combination of "rwx". 61 Eg. 62 >>> Permissions(0o777) 63 >>> Permissions(user = "rw", group = "r", others = "") 64 """ 65 if mode != None: 66 self.user = PermissionsGroup.from_int(mode >> 6) 67 self.group = PermissionsGroup.from_int(mode >> 3) 68 self.others = PermissionsGroup.from_int(mode >> 0) 69 else: 70 self.user = PermissionsGroup.from_str(user) 71 self.group = PermissionsGroup.from_str(group) 72 self.others = PermissionsGroup.from_str(others) 73 74 def __eq__(self, other) -> bool: 75 """ Compare `Permissions` objects. You can also compare a `Permissions` object with its octal representation. """ 76 if isinstance(other, Permissions) or isinstance(other, int): 77 return int(self) == int(other) 78 else: 79 raise NotImplementedError("You can only compare Permissions with other Permissions or with ints") 80 81 def __int__(self): 82 """ Returns the integer representation of the permissions. Ie. 0o777 """ 83 return (int(self.user) << 6) | (int(self.group) << 3) | (int(self.others)) 84 85 def __str__(self): 86 """ 87 Returns the string representation of the permissions, as ls -l would. 88 >>> str(Permissions(0o764)) 89 "rwx-rw-r--" 90 """ 91 return str(self.user) + str(self.group) + str(self.others) 92 93 def __repr__(self) -> str: 94 return f"Permissions({oct(int(self))})"
Plain old data structure that represents basic Linux permissions, with user, group, and others sections. Currently doesn't include special permission bits such as the sticky bit.
57 def __init__(self, mode: int = None, *, user: str = "", group: str = "", others: str = ""): 58 """ 59 Create a `Permissions` object. You can create one from an octal `int`, or by specifying user, group, and others 60 with strings containing some combination of "rwx". 61 Eg. 62 >>> Permissions(0o777) 63 >>> Permissions(user = "rw", group = "r", others = "") 64 """ 65 if mode != None: 66 self.user = PermissionsGroup.from_int(mode >> 6) 67 self.group = PermissionsGroup.from_int(mode >> 3) 68 self.others = PermissionsGroup.from_int(mode >> 0) 69 else: 70 self.user = PermissionsGroup.from_str(user) 71 self.group = PermissionsGroup.from_str(group) 72 self.others = PermissionsGroup.from_str(others)
Create a Permissions
object. You can create one from an octal int
, or by specifying user, group, and others
with strings containing some combination of "rwx".
Eg.
>>> Permissions(0o777)
>>> Permissions(user = "rw", group = "r", others = "")
96class LinkedPermissions(Permissions): 97 """ 98 `LinkedPermissions` is linked to an actual file. Modifying the `LinkedPermissions` object will modify the permissions of 99 the actual file on disk. 100 101 You can access and modify `File` permissions via the `File.permissions` property, which offers a more convenient API to manipulate 102 UNIX file permissions than `os` and `stat` modules. 103 """ 104 105 def __init__(self, file: file.File): 106 self._file = file 107 self.user = LinkedPermissionsGroup(file, 6) 108 self.group = LinkedPermissionsGroup(file, 3) 109 self.others = LinkedPermissionsGroup(file, 0)
LinkedPermissions
is linked to an actual file. Modifying the LinkedPermissions
object will modify the permissions of
the actual file on disk.
You can access and modify File
permissions via the File.permissions
property, which offers a more convenient API to manipulate
UNIX file permissions than os
and stat
modules.
105 def __init__(self, file: file.File): 106 self._file = file 107 self.user = LinkedPermissionsGroup(file, 6) 108 self.group = LinkedPermissionsGroup(file, 3) 109 self.others = LinkedPermissionsGroup(file, 0)
Create a Permissions
object. You can create one from an octal int
, or by specifying user, group, and others
with strings containing some combination of "rwx".
Eg.
>>> Permissions(0o777)
>>> Permissions(user = "rw", group = "r", others = "")
112class PermissionsGroup: 113 """ 114 Represents the read, write, and execute bits for either "user", "group", or "other" 115 """ 116 117 read: bool 118 """ The read permission bit """ 119 write: bool 120 """ The write permission bit """ 121 execute: bool 122 """ The execute permission bit """ 123 124 def __init__(self, read: bool, write: bool, execute: bool): 125 self.read = read 126 self.write = write 127 self.execute = execute 128 129 @staticmethod 130 def from_str(mode: str) -> PermissionsGroup: 131 """ Takes a string in "rwx" format and returns a `PermissionsGroup` """ 132 mode_set = set(mode) 133 if len(mode_set) == len(mode) and mode_set.issubset({'r', 'w', 'x'}): # only contains rwx, and only one of each 134 read = "r" in mode_set 135 write = "w" in mode_set 136 execute = "x" in mode_set 137 else: 138 raise ValueError(f'Invalid string "{mode}" for permissions, must only contain "r", "w", and/or "x"') 139 140 return PermissionsGroup(read, write, execute) 141 142 @staticmethod 143 def from_int(mode: int) -> PermissionsGroup: 144 """ Takes an int and assigns lowermost 3 bits to read/write/execute. Returns a `PermissionGroup` """ 145 read = bool(mode & 0o4) 146 write = bool(mode & 0o2) 147 execute = bool(mode & 0o1) 148 return PermissionsGroup(read, write, execute) 149 150 def __eq__(self, other) -> bool: 151 """ Compare `PermissionGroup` objects. You can also compare a `PermissionGroup` with its octal representation. """ 152 if isinstance(other, PermissionsGroup) or isinstance(other, int): 153 return int(self) == int(other) 154 else: 155 raise NotImplementedError("You can only compare Permissions with other Permissions or with ints") 156 157 def __int__(self) -> int: 158 """ Convert the `PermissionGroup` to an int by treating read/write/execute as bits. """ 159 return (self.read << 2) | (self.write << 1) | (self.execute) 160 161 def __str__(self) -> str: 162 """ Convert the `PermissionGroup` to an str in "rwx" format, eg. "rw-" or "r--" """ 163 return ("r" if self.read else "-") + ("w" if self.write else "-") + ("x" if self.execute else "-")
Represents the read, write, and execute bits for either "user", "group", or "other"
129 @staticmethod 130 def from_str(mode: str) -> PermissionsGroup: 131 """ Takes a string in "rwx" format and returns a `PermissionsGroup` """ 132 mode_set = set(mode) 133 if len(mode_set) == len(mode) and mode_set.issubset({'r', 'w', 'x'}): # only contains rwx, and only one of each 134 read = "r" in mode_set 135 write = "w" in mode_set 136 execute = "x" in mode_set 137 else: 138 raise ValueError(f'Invalid string "{mode}" for permissions, must only contain "r", "w", and/or "x"') 139 140 return PermissionsGroup(read, write, execute)
Takes a string in "rwx" format and returns a PermissionsGroup
142 @staticmethod 143 def from_int(mode: int) -> PermissionsGroup: 144 """ Takes an int and assigns lowermost 3 bits to read/write/execute. Returns a `PermissionGroup` """ 145 read = bool(mode & 0o4) 146 write = bool(mode & 0o2) 147 execute = bool(mode & 0o1) 148 return PermissionsGroup(read, write, execute)
Takes an int and assigns lowermost 3 bits to read/write/execute. Returns a PermissionGroup
165class LinkedPermissionsGroup(PermissionsGroup): 166 """ 167 Represents the read, write, and execute bits for either "user", "group", or "other". Is linked to an actual file 168 on disk. Modifying the `LinkedPermissionsGroup` will modify the file's permissions. 169 """ 170 171 def _get_bit(self, mask: int) -> bool: 172 """ Gets the read (0o4), write (0o2), or execute (0o1) bit. """ 173 mode = stat.S_IMODE(self._file.stat().st_mode) 174 return bool((mode >> self._bit_shift) & mask) 175 176 def _set_bit(self, mask: int, val: bool): 177 """ Gets the read (0o4), write (0o2), or execute (0o1) bit. """ 178 mode = stat.S_IMODE(self._file.stat().st_mode) 179 if val: # set bit 180 mode = mode | (mask << self._bit_shift) 181 else: # clear bit at mask 182 mode = mode & ~(mask << self._bit_shift) 183 self._file.chmod(mode) 184 185 # MyPy fusses at overriding field with property. See https://github.com/python/mypy/issues/4125 186 read = property(lambda self: self._get_bit(0o4), lambda self, val: self._set_bit(0o4, val)) #type: ignore 187 """ The read permission bit """ 188 write = property(lambda self: self._get_bit(0o2), lambda self, val: self._set_bit(0o2, val)) #type: ignore 189 """ The write permission bit """ 190 execute = property(lambda self: self._get_bit(0o1), lambda self, val: self._set_bit(0o1, val)) #type: ignore 191 """ The execute permission bit """ 192 193 def __init__(self, file: file.File, bit_shift: int): 194 """ 195 bit_shift is the number of bits to shift to the right to get the rwx bits in the lowest position 196 user: 6, group: 3, others: 0 197 """ 198 self._file = file 199 self._bit_shift = bit_shift
Represents the read, write, and execute bits for either "user", "group", or "other". Is linked to an actual file
on disk. Modifying the LinkedPermissionsGroup
will modify the file's permissions.
193 def __init__(self, file: file.File, bit_shift: int): 194 """ 195 bit_shift is the number of bits to shift to the right to get the rwx bits in the lowest position 196 user: 6, group: 3, others: 0 197 """ 198 self._file = file 199 self._bit_shift = bit_shift
bit_shift is the number of bits to shift to the right to get the rwx bits in the lowest position user: 6, group: 3, others: 0
Inherited Members
8class RandomHelper: 9 """ 10 RandomHelper is a class that generates random names, contents, and file paths. 11 You can access an instance of `RandomHelper` in puzzle modules via the `shell_adventure.api.rand()` function. 12 """ 13 14 def __init__(self, name_dictionary: str, content_sources: List[str] = []): 15 """ 16 Creates a `RandomHelper`. 17 name_dictionary is a string containing words, each on its own line. 18 """ 19 names = set(name_dictionary.splitlines()) 20 names.discard("") # remove empty entries 21 22 # A list of strings that will be used to generate random names. 23 self._name_dictionary: List[str] = list(names) # choice() only works on list. 24 25 def clean_paragraph(paragraph: str) -> str: 26 # paragraph = re.sub(r"\s*\n\s*", "", paragraph) # unwrap 27 paragraph = paragraph.rstrip() 28 paragraph = "\n".join([l for l in paragraph.split("\n") if l.strip() != ""]) # remove blank lines. 29 return paragraph 30 31 # The sources that will be used to generate random content. List of files, each file is a list of paragraphs. 32 self._content_sources: List[List[str]] = [] 33 for source in content_sources: 34 paragraphs = re.split(r"\s*\n\s*\n", source) # split into paragraphs 35 paragraphs = [clean_paragraph(para) for para in paragraphs if para.strip() != ""] 36 # # split paragraphs into lists of sentences 37 # para_sentences = [re.findall(r".*?\.\s+", para, flags = re.DOTALL) for para in paragraphs] 38 self._content_sources.append(paragraphs) 39 40 # A set of shared folders. random._folder() can use existing folders if they are shared. 41 self._shared_folders: Set[File] = set() 42 43 44 def name(self) -> str: 45 """ Returns a random word that can be used as a file name. The name is taken from the name_dictionary. """ 46 if len(self._name_dictionary) == 0: 47 raise RandomHelperException("Out of unique names.") 48 choice = random.choice(self._name_dictionary) 49 self._name_dictionary.remove(choice) # We can't choose the same name again. 50 return choice 51 52 def paragraphs(self, count: Union[int, Tuple[int, int]] = (1, 3)) -> str: 53 """ 54 Return a random sequence of paragraphs from the content_sources. 55 If no content sources are provided or there isn't enough content to provide the size it will default to a lorem ipsum generator. 56 57 paramaters: 58 count: Either an int or a (min, max) tuple. If a tuple is given, will return a random number of paragraphs 59 in the range, inclusive. 60 """ 61 if isinstance(count, tuple): count = random.randint(count[0], count[1]) 62 # filter sources too small for chosen size 63 sources = [source for source in self._content_sources if count <= len(source)] 64 65 if sources: # If we have source 66 # Weight the files so all paragraphs are equally likely regardless of source 67 weights = [len(source) - count + 1 for source in sources] 68 [source] = random.choices(sources, k = 1, weights = weights) 69 70 index = random.randint(0, len(source) - count) 71 return "\n\n".join(source[index:index+count]) + "\n" 72 else: 73 return lorem.get_paragraph(count = count, sep = "\n\n") + "\n" 74 75 # === Files === 76 77 def _file(self, parent: PathLike, ext = None) -> File: 78 """ Creates a `File` with a random name. You should use `File.rand_file()` instead of calling this method directly. """ 79 parent = File(parent).resolve() 80 ext = "" if ext == None else f".{ext}" 81 new_file = File("/") # garanteed to exist. 82 83 # check if file already exists. This can happen if a hardcoded name happens to match the random one. 84 while new_file.exists(): 85 new_file = parent / f"{self.name()}{ext}" 86 87 return new_file 88 89 def _folder(self, parent: PathLike, depth: Union[int, Tuple[int, int]] = (1, 3), create_new_chance: float = 0.5) -> File: 90 """ Makes a `File` to a random folder under parent. You should use `File.random_shared_folder()` instead of calling this method directly. """ 91 92 if isinstance(depth, tuple): depth = random.randint(depth[0], depth[1]) 93 folder = File(parent).resolve() 94 95 for i in range(depth): 96 if folder.exists(): 97 choices = [d for d in folder.iterdir() if d.is_dir() and d in self._shared_folders] 98 else: 99 choices = [] 100 # Create new shared folder if no choices or random chance succeeds. 101 # Add check for 1 since uniform() is an inclusive range 102 roll = random.uniform(0, 1) 103 if len(choices) == 0 or create_new_chance == 1 or roll < create_new_chance: 104 folder = self._file(folder) # create random file under folder 105 self._mark_shared(folder) 106 else: 107 folder = File(random.choice(choices)) 108 109 return folder 110 111 def _mark_shared(self, folder: PathLike): 112 """ Marks a folder as shared. You should use `File.mark_shared()` instead of calling this method directly. """ 113 folder = File(folder) 114 if folder.exists() and not folder.is_dir(): 115 raise RandomHelperException(f"Can't mark {folder} as shared, it already exists as a f. Can only mark folders as shared.") 116 self._shared_folders.add(folder.resolve())
RandomHelper is a class that generates random names, contents, and file paths.
You can access an instance of RandomHelper
in puzzle modules via the shell_adventure.api.rand()
function.
14 def __init__(self, name_dictionary: str, content_sources: List[str] = []): 15 """ 16 Creates a `RandomHelper`. 17 name_dictionary is a string containing words, each on its own line. 18 """ 19 names = set(name_dictionary.splitlines()) 20 names.discard("") # remove empty entries 21 22 # A list of strings that will be used to generate random names. 23 self._name_dictionary: List[str] = list(names) # choice() only works on list. 24 25 def clean_paragraph(paragraph: str) -> str: 26 # paragraph = re.sub(r"\s*\n\s*", "", paragraph) # unwrap 27 paragraph = paragraph.rstrip() 28 paragraph = "\n".join([l for l in paragraph.split("\n") if l.strip() != ""]) # remove blank lines. 29 return paragraph 30 31 # The sources that will be used to generate random content. List of files, each file is a list of paragraphs. 32 self._content_sources: List[List[str]] = [] 33 for source in content_sources: 34 paragraphs = re.split(r"\s*\n\s*\n", source) # split into paragraphs 35 paragraphs = [clean_paragraph(para) for para in paragraphs if para.strip() != ""] 36 # # split paragraphs into lists of sentences 37 # para_sentences = [re.findall(r".*?\.\s+", para, flags = re.DOTALL) for para in paragraphs] 38 self._content_sources.append(paragraphs) 39 40 # A set of shared folders. random._folder() can use existing folders if they are shared. 41 self._shared_folders: Set[File] = set()
Creates a RandomHelper
.
name_dictionary is a string containing words, each on its own line.
44 def name(self) -> str: 45 """ Returns a random word that can be used as a file name. The name is taken from the name_dictionary. """ 46 if len(self._name_dictionary) == 0: 47 raise RandomHelperException("Out of unique names.") 48 choice = random.choice(self._name_dictionary) 49 self._name_dictionary.remove(choice) # We can't choose the same name again. 50 return choice
Returns a random word that can be used as a file name. The name is taken from the name_dictionary.
52 def paragraphs(self, count: Union[int, Tuple[int, int]] = (1, 3)) -> str: 53 """ 54 Return a random sequence of paragraphs from the content_sources. 55 If no content sources are provided or there isn't enough content to provide the size it will default to a lorem ipsum generator. 56 57 paramaters: 58 count: Either an int or a (min, max) tuple. If a tuple is given, will return a random number of paragraphs 59 in the range, inclusive. 60 """ 61 if isinstance(count, tuple): count = random.randint(count[0], count[1]) 62 # filter sources too small for chosen size 63 sources = [source for source in self._content_sources if count <= len(source)] 64 65 if sources: # If we have source 66 # Weight the files so all paragraphs are equally likely regardless of source 67 weights = [len(source) - count + 1 for source in sources] 68 [source] = random.choices(sources, k = 1, weights = weights) 69 70 index = random.randint(0, len(source) - count) 71 return "\n\n".join(source[index:index+count]) + "\n" 72 else: 73 return lorem.get_paragraph(count = count, sep = "\n\n") + "\n"
Return a random sequence of paragraphs from the content_sources. If no content sources are provided or there isn't enough content to provide the size it will default to a lorem ipsum generator.
paramaters: count: Either an int or a (min, max) tuple. If a tuple is given, will return a random number of paragraphs in the range, inclusive.
Error for when the RandomHelper
fails.
Inherited Members
- builtins.Exception
- Exception
- builtins.BaseException
- with_traceback
- args
31def rand() -> RandomHelper: 32 """ 33 Returns the `RandomHelper` which should be used when creating random files and folders. 34 """ 35 if not _rand: 36 raise RandomHelperException("You can only use randomization in Puzzle templates, not autograders") 37 return _rand
Returns the RandomHelper
which should be used when creating random files and folders.