Skip to content

Instantly share code, notes, and snippets.

@spikespaz
Last active August 25, 2024 14:28
Show Gist options
  • Save spikespaz/ed35f6e4d774722c156d8038106f4926 to your computer and use it in GitHub Desktop.
Save spikespaz/ed35f6e4d774722c156d8038106f4926 to your computer and use it in GitHub Desktop.
Python Snippets

Python Snippets

A collection of useful code snippets that I have collected over time. They are mostly all written by me.

class Color:
def __init__(self, string=None, rgb=None, hsl=None, alpha=None):
arguments = iter((string, rgb, hsl))
if string is not None:
self._from_hex(string)
return
if rgb is not None:
self._from_rgb(*rgb, alpha)
return
if hsl is not None:
self._from_hsl(*hsl, alpha)
return
def _from_hex(self, rgba):
rgba = rgba.lstrip("#")
if len(rgba) > 8:
raise Exception("Expected up to 8 hexadecimal characters")
self._red = int(rgba[0:2], 16)
self._green = int(rgba[2:4], 16)
self._blue = int(rgba[4:6], 16)
if len(rgba) == 8:
self.alpha = int(rgba[6:8], 16)
else:
self.alpha = None
self._rgb_changed = True
self._hsl_changed = False
def _from_rgb(self, red, green, blue, alpha=None):
self._red = red
self._green = green
self._blue = blue
self.alpha = alpha
self._rgb_changed = True
self._hsl_changed = False
def _from_hsl(self, hue, saturation, lightness, alpha=None):
self._hue = hue
self._saturation = saturation
self._lightness = lightness
self.alpha = alpha
self._rgb_changed = False
self._hsl_changed = True
def __repr__(self):
return self.hex()
def __hash__(self):
return hash(self.hex())
def __eq__(self, other):
return self.__class__ is other.__class__ and self.__hash__() == other.__hash__()
def hex(self):
return "#{:02x}{:02x}{:02x}".format(self.red, self.green, self.blue) + \
("" if self.alpha is None else "{:02x}".format(self.alpha))
def hsl(self):
if self._hsl_changed:
raise Exception(
"Cannot convert from RGB when the last modified value was of HSL")
rp = self._red / 255
gp = self._green / 255
bp = self._blue / 255
cmax = max(rp, gp, bp)
cmin = min(rp, gp, bp)
delta = cmax - cmin
if delta == 0:
self._hue = 0
elif cmax == rp:
self._hue = 60 * ((gp - bp) / delta % 6)
elif cmax == gp:
self._hue = 60 * ((bp - rp) / delta + 2)
elif cmax == bp:
self._hue = 60 * ((rp - gp) / delta + 4)
self._lightness = (cmax + cmin) / 2
if delta == 0:
self._saturation = 0
else:
self._saturation = delta / (1 - abs(2 * self._lightness - 1))
self._rgb_changed = False
return (self._hue, self._saturation, self._lightness)
def rgb(self):
if self._rgb_changed:
raise Exception(
"Cannot convert from HSL when the last modified value was of RGB")
c = (1 - abs(2 * self._lightness - 1)) * self._saturation
x = c * (1 - abs((self._hue / 60) % 2 - 1))
m = self._lightness - c / 2
if self._hue >= 0 and self._hue < 60:
(rp, gp, bp) = (c, x, 0)
elif self._hue >= 60 and self._hue < 120:
(rp, gp, bp) = (x, c, 0)
elif self._hue >= 120 and self._hue < 180:
(rp, gp, bp) = (0, c, x)
elif self._hue >= 180 and self._hue < 240:
(rp, gp, bp) = (0, x, c)
elif self._hue >= 240 and self._hue < 300:
(rp, gp, bp) = (x, 0, c)
elif self._hue >= 300 and self._hue < 360:
(rp, gp, bp) = (c, 0, x)
self._red = round((rp + m) * 255)
self._green = round((gp + m) * 255)
self._blue = round((bp + m) * 255)
self._hsl_changed = False
return (self._red, self._green, self._blue)
@property
def red(self):
if self._hsl_changed:
self.rgb()
return self._red
@property
def green(self):
if self._hsl_changed:
self.rgb()
return self._green
@property
def blue(self):
if self._hsl_changed:
self.rgb()
return self._blue
@red.setter
def red(self, red):
self._red = red
self._rgb_changed = True
@green.setter
def green(self, green):
self._green = green
self._rgb_changed = True
@blue.setter
def blue(self, blue):
self._blue = blue
self._rgb_changed = True
@property
def hue(self):
if self._rgb_changed:
self.hsl()
return self._hue
@property
def saturation(self):
if self._rgb_changed:
self.hsl()
return self._saturation
@property
def lightness(self):
if self._rgb_changed:
self.hsl()
return self._lightness
@hue.setter
def hue(self, hue):
self._hue = hue
self._hsl_changed = True
@saturation.setter
def saturation(self, saturation):
self._saturation = saturation
self._hsl_changed = True
@lightness.setter
def lightness(self, lightness):
self._lightness = lightness
self._hsl_changed = True
def __test(colors):
from copy import deepcopy
colors = [Color(color) for color in colors]
colors_mutated = deepcopy(colors)
for color in colors_mutated:
color.hsl()
color.rgb()
for (color_a, color_b) in zip(colors, colors_mutated):
if color_a != color_b:
raise AssertionError(
"Colors do not match! ({}, {})".format(color_a, color_b))
print("{} == {}".format(color_a, color_b))
if __name__ == "__main__":
colors = set("#353b48, #666666, #444852, #fcfcfc, #434343, #90939b, #353537, #2b303b, #b6b8c0, #241f31, #303440, #000000, #9398a2, #dfdfdf, #f0f1f2, #cfcfcf, #d3d8e2, #505666, #808080, #8a939f, #282b36, #afb8c6, #383838, #4dadd4, #353a48, #838383, #202229, #7a7f8a, #7a7f8b, #2e3340, #70788d, #66a1dc, #17191f, #d7d7d7, #545860, #39404d, #161a26, #be3841, #3c4049, #2f3a42, #f0f2f5, #4e4eff, #262934, #1d1f26, #404552, #353945, #383c45, #8f939d, #f7ef45, #a4aab7, #b2cdf1, #444a58, #bac3cf, #ff00ff, #f46067, #5c6070, #c7cacf, #525762, #ff0b00, #323644, #f75a61, #464646, #ecedf0, #171717, #e01b24, #1b1b1b, #797d87, #15171c, #8c919d, #4d4f52, #5b627b, #728495, #454c5c, #4080fb, #e2e2e2, #d1d3da, #c0e3ff, #3580e4, #b7c0d3, #232428, #2d323f, #6e6e6e, #dcdcdc, #b9bcc2, #cc575d, #a1a1a1, #52555e, #353a47, #7c818c, #979dac, #2f343f, #dde3e9, #828282, #c5dcf7, #001aff, #722563, #afb8c5, #222529, #8abfdd, #666a74, #f68086, #edf5fb, #4b5162, #a9acb2, #786613, #c7c7c7, #eeeff1, #2b2e37, #f70505, #292c36, #3e434f, #5c616c, #f57900, #2d303b, #f5f6f7, #5f697f, #2e3436, #808791, #f08437, #cbd2e3, #e5a50a, #eeeeee, #252932, #e7e8eb, #3e4350, #ff1111, #ef2929, #fc4138, #fcfdfd, #7a7a7a, #21242b, #bebebe, #ffffff, #252a35, #5252ff, #767b87, #535353, #3e3e3e, #aa5555, #5f6578, #c4c7cc, #383c4a, #102b68, #21252b, #f3af0b, #cfd6e6, #d7787d, #ff7a80, #fdfdfd, #398dd3, #a51d2d, #73d216, #f8f8f9, #262932, #2f343b, #2b2e39, #2d3036, #f04a50, #006098, #3f4453, #ad4242, #1b1c21, #b9bfce, #ff1616, #e5e5e5, #ed686f, #eaebed, #fbfcfc, #398cd3, #262933, #5294e2, #0000ff, #d7d8dd, #2b2f3b, #f13039, #999999, #1f1f1f, #50dbb5, #525252, #ff2121, #f27835, #91949c, #adafb5, #3b3c3e, #d3d4d8, #525d76, #434652, #cacaca, #2d323d, #f9fafb, #617c95, #ededed, #1a1a1a, #d8354a, #90949e, #313541, #a8a8a8, #dbdfe3, #cecece, #0f0f0f, #1d242a, #b8babf, #0f1116, #eef4fc, #e2e7ef, #d3dae3".split(", "))
__test(colors)
class ConcurrentLineWriter:
class LineFileIO(StringIO):
def __init__(self, parent, row):
super().__init__()
self.parent = parent
self.row = row
def write(self, string):
super().write(string)
self.parent._write(self.row)
def flush(self):
super().flush()
self.parent.file.flush()
def __init__(self, file=sys.stdout):
self.file = file
self.lines = []
self.__pos = 0
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
def __move_cursor(self, row):
offset = abs(self.__pos - row)
if row < self.__pos:
self.file.write(f"\033[{offset}A\r")
elif row > self.__pos:
self.file.write(f"\033[{offset}B\r")
self.__pos = row
def _write(self, row):
self.__move_cursor(row)
self.file.write(self.lines[row].getvalue())
def add_line(self):
self.lines.append(ConcurrentLineWriter.LineFileIO(self, len(self.lines)))
return self.lines[-1]
def close(self):
for line in self.lines:
line.close()
self.__move_cursor(len(self.lines))
def get_width():
"""Get the console width (number of chars or columns)."""
if platform == "win32":
result = Popen("mode con", stdout=PIPE, shell=True).communicate()[0].decode()
return int(search(r"Columns:\s*(\d+)", result)[1])
else:
return int(Popen("stty size", stdout=PIPE, shell=True).communicate()[0].decode().split()[1])
def get_size():
"""Get the console width and height."""
if platform == "win32":
raise Exception("Platform not supported.")
else:
size = Popen("stty size", stdout=PIPE, shell=True).communicate()[0].decode().split()[1]
return int(size[1]), int(size[0])
#! /usr/bin/env python3
# -*- coding: utf8 -*-
from PIL import Image
import numpy as np
# Standard cube map
# | . | T | . | . |
# | L | F | R | B |
# | . | B | . | . |
def make_map(im_paths, bg=0):
face_im = Image.open(im_paths)
w, h = face_im.size
if not w == h:
raise ValueError("Cube map size improper. Must be square.")
map_im = Image.new("RGBA", (w * 4, h * 3), bg)
map_im.paste(face_im, (w, 0))
map_im.paste(face_im, (w, 2 * h))
map_im.paste(face_im, (0, h))
map_im.paste(face_im, (2 * w, h))
map_im.paste(face_im, (w, h))
map_im.paste(face_im, (3 * w, h))
return map_im
def generate(map_path, bg=1):
map_im = Image.open(map_path)
if not (map_im.height == 3 / 4 * map_im.width and map_im.width % 4 == 0 and map_im.height % 3 == 0):
raise ValueError("Cube map size improper. Must be 4:3 scale image, width divisible by 4, and height divisible by 3.")
w, h = map_im.size
face_bounds = {
"top": (1/4 * w, 0, 2/4 * w, 1/3 * h),
"bottom": (1/4 * w, 2/3 * h, 2/4 * w, 3/3 * h),
"left": (0, 1/3 * h, 1/4 * w, 2/3 * h),
"right": (2/4 * w, 1/3 * h, 3/4 * w, 2/3 * h),
"front": (1/4 * w, 1/3 * h, 2/4 * w, 2/3 * h),
"back": (3/4 * w, 1/3 * h, 4/4 * w, 2/3 * h)
}
face_ims = {}
for face, box in face_bounds.items():
face_ims[face] = map_im.crop(face_bounds[face])
#
# for face in face_ims:
# face_ims[face].show()
generate("test.png")
make_map("texture.png").show()
#! /usr/bin/env python3
from time import strftime
from pyfiglet import figlet_format
from os import system, name as os_name, popen
# Function to pretty print to the perfect center of a console window
def fullscreen_print(text, font="dotmatrix", top_pad=0):
size = popen("stty size", "r").read().split() # Make a tuple for the size of the console
size = int(size[1]), int(size[0]) # Switch them, make it W, H with ints
fig = figlet_format(str(text), font=font, justify="center", width=size[0]) # Get the fig formatted string
fig_height = len(fig.split("\n")) # Get the line length of the figlet formatted string
top_pad += round((size[1] - fig_height) / 2) # Calculate top padding (newlines) to put before the fig
clear() # Clear the screen
print("\n" * top_pad, fig) # Print the figlet and padding
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
# -*- author: spikespaz -*-
from PIL import Image
from os import path, listdir, mkdir
# Define constants / settings to be used for image conversion.
# Shape size is thesize of each image to be generated. Pick a size that seems to be close to the average.
shape_size = (100, 120)
# Resample is the algorithm to use when downscaling. Image.LANCZOS is best for thumbnails.
# NEAREST, BOX, BILINEAR, HAMMING, BICUBIC, LANCZOS, NEAREST
resample = Image.LANCZOS
# raw_dir is the RELATIVE PATH to the original face images.
raw_dir = path.abspath("./faces_raw")
# snape_dir and grey_dir are relative paths to the shaped and greyscale outputs.
shape_dir = path.abspath("./faces_shape")
grey_dir = path.abspath("./faces_grey")
# If the shape_dir and grey_dir paths don't exist, create them.
try:
mkdir(shape_dir)
except FileExistsError:
pass
try:
mkdir(grey_dir)
except FileExistsError:
pass
# Helper function to join arbitrary paths and get the absolute full path.
def absjoin(*p):
return path.abspath(path.join(*p))
# Helper function to rename the file to a .png file.
def makepng(p):
return path.splitext(p)[0] + ".png"
# Looping for each file in raw_dir.
for fp in listdir(raw_dir):
# If the file is an accepted file type (images).
if fp.split(".")[-1] in ("png", "jpg", "jpeg"):
# Open and initiate the image.
raw_fp = absjoin(raw_dir, fp)
print("Opening:", raw_fp)
im = Image.open(raw_fp)
# Create and resize the image to the shape defined above.
shape_fp = makepng(absjoin(shape_dir, fp))
print("Saving:", shape_fp)
im = im.resize(shape_size, resample)
im.save(shape_fp, "JPEG")
# Convert open image to greyscale (LA) and save as a PNG format.
grey_fp = makepng(absjoin(grey_dir, fp))
print("Saving:", grey_fp)
im = im.convert("LA")
im.save(grey_fp, "PNG")
# File is not an acceptable image file, skip it.
else:
print("Skipping:", absjoin(raw_dir, fp))
#! /usr/bin/env python3
from PIL import Image, ImageDraw, ImageFont
from os import path
from parser import read_config
def read_sentences(fp):
with open(fp) as f:
return tuple(l.strip() for l in f.readlines() if l.strip())
training_settings_path = path.abspath("./store/training_settings.txt")
print("Training settings path:", training_settings_path)
def get_settings_rel(*p):
return path.abspath(path.join(path.dirname(training_settings_path), *p))
training_settings = read_config(training_settings_path)
try:
training_settings["rotation_filter"] = eval(training_settings["rotation_filter"])
except NameError:
print("Rotation filter invalid. Defaulting to BICUBIC.")
training_settings["rotation_filter"] = Image.BICUBIC
try:
training_settings["training_filter"] = eval(training_settings["training_filter"])
except NameError:
print("Rotation filter invalid. Defaulting to BICUBIC.")
training_settings["training_filter"] = Image.BICUBIC
training_sentences_path = get_settings_rel(training_settings["training_sentences"])
print("Training sentences path:", training_sentences_path)
training_sentences = read_sentences(training_sentences_path)
font_default_path = get_settings_rel(training_settings["font_default"])
print("Font default path:", font_default_path)
def create_text_image(text, size=10, rot=None, pad=(4,) * 4, fg=(0,) * 3, bg=(255,) * 3,
fpath=font_default_path,
ttf=training_settings["font_default_ttf"]):
if ttf:
font = ImageFont.truetype(fpath, size)
else:
font = ImageFont.load(fpath)
im_size = font.getsize(text)
im_size = (im_size[0] + pad[2] + pad[3], im_size[1] + pad[0] + pad[1])
im = Image.new("RGBA", im_size, bg)
im_draw = ImageDraw.Draw(im)
im_draw.text((pad[2], pad[0]), text, font=font, fill=fg)
if rot:
return im.rotate(rot, resample=training_settings["rotation_filter"], expand=True)
else:
return im
def get_save_path(scale, name):
return path.join(get_settings_rel(training_settings["training_path"], str(scale), name))
counter = -1
for sentence in training_sentences:
for size in training_settings["font_sizes"]:
for rot in training_settings["rotation_degrees"]:
counter += 1
print(str(counter) + ": " + str((sentence, size, rot)))
text_im = create_text_image(sentence, size, rot, training_settings["font_padding"],
training_settings["font_foreground"],
training_settings["font_background"])
text_im.save(get_save_path(1.0, str(counter) + ".png"))
im_size = (int(text_im.width * training_settings["training_scale"]),
int(text_im.height * training_settings["training_scale"]))
text_im = text_im.resize(im_size, training_settings["training_filter"])
text_im.save(get_save_path(training_settings["training_scale"], str(counter) + ".png"))
import re
import string
import random
def generate_id(size=6, chars=string.ascii_lowercase + string.digits):
return "".join(random.choice(chars) for _ in range(size))
def make_slug(title, base_id=None, size=20, allowed_chars=string.ascii_lowercase + string.digits):
slug = re.sub(r"[^" + allowed_chars + "]", "-", title)
slug = re.sub(r"-+", "", slug)
return (base_id if base_id else "") + slug.strip()[size:] if size else slug.strip()
def title_filter(title):
filtered = sub(r"[^\w\d~!@#$%&*()+=/,.:;\"' ]", "", title)
return filtered.strip()[50:]
#! /usr/bin/env python3
from sys import argv
from pprint import PrettyPrinter
from functools import wraps
from json import dumps
pp = PrettyPrinter(indent=4)
class Testable:
"""A decorator that allows functions to be tested from the terminal. Pass the function name and simple arguments
(`int`s, `float`s, and `str`s) into the terminal to read the direct output. Some advanced printing is available.
Example:
These are the contents of `testable.py`.
>>> @Testable(5, 6, 7)
>>> def test_function(first, second, special=1):
... return sum(first, second) * special
...
>>>
Command line examples are as follows.
$ python testable.py test_function
77
$ python testable.py test_function 80 10
90
$ python testable.py test_function 80 10 2
180
Additionally, `_pretty` and `_json` can be appended before the function name in the command to get more visually
appealing outputs.
>>> @Testable()
>>> def test_function():
... return {"one": 1, "two": 2, "three": 3, "four": 4, "five": 5}
...
>>>
Followed by the commands and their outputs.
$ python utils.py _json test_function
{
"five": 5,
"one": 1,
"four": 4,
"three": 3,
"two": 2
}
Replacing `_json` with `_pretty` works better for lists with really long values.
Attributes:
args (tuple): A variable length list of arguments to be tested with if none are supplied by the command line.
kwargs (tuple): A dictionary of keyword arguments to be tested with.
"""
def __init__(self, *args, **kwargs):
self.args = args
self.kwargs = kwargs
def __call__(self, func):
if len(argv) > 1 and argv[1] == func.__name__:
if len(argv) > 2:
parsed_args = argparse(*argv[2:])
print(func(*parsed_args[0], **parsed_args[1]))
else:
print(func(*self.args, **self.kwargs))
elif len(argv) > 2 and argv[2] == func.__name__:
if argv[1] == "_pretty":
def printer(output):
pp.pprint(output)
elif argv[1] == "_json":
def printer(output):
print(dumps(output, indent=4))
if len(argv) > 3:
parsed_args = argparse(*argv[3:])
printer(func(*parsed_args[0], **parsed_args[1]))
else:
printer(func(*self.args, **self.kwargs))
return func
@Testable("90.8")
def parsestr(string):
"""Attempts to parse a string into a number. This will return the original string if failed.
Args:
string (str): The string input to parse.
Returns:
int: The integer value from the string.
float: The float value from the string.
str: The original string passed in. Returned when `int` and `float` failed.
"""
try:
return int(string)
except ValueError:
try:
return float(string)
except ValueError:
return string
@Testable("90.8", "-1", "10", "string", "--kwarg0", "test", "--kwarg1", "90")
def argparse(*args):
"""Parse a list of arguments (in ``sys.argv`` format) into a variable length list and a keyword arguments dictionary.
This is just a simpler version of the argument parser included with the Python Standard Library.
Arguments preceded by two dashes will be considered keys in the keyword arguments dictionary.
Example:
>>> args = ("90.8", "-1", "10", "string", "--kwarg0", "test", "--kwarg1", "90")
>>> argparse(args)
([90.8, -1, 10, 'string'], {'kwarg1': 90, 'kwarg0': 'test'})
>>>
Args:
*args: Variable length argument list of strings to be parsed.
Returns:
tuple: A two-item tuple with a list of arguments and then the keyword arguments dictionary.
"""
parsed_args = []
parsed_kwargs = {}
iterable = iter(enumerate(args))
for arg in iterable:
value = arg[1].strip()
if arg[1].startswith("--"):
parsed_kwargs[value[2:]] = parsestr(args[arg[0] + 1])
next(iterable)
else:
parsed_args.append(parsestr(value))
return parsed_args, parsed_kwargs
def return_yield(func):
"""Decorator that wraps a generator and returns its value in a list as if it were a normal function.
Example:
Test function definition **without** the ``@return_yield`` applied, returns a generator.
>>> def test_generator(loops):
... for i in range(loops):
... yield str(i)
...
>>> test_generator(10)
<generator object test_generator at 0x00000215B2FC00A0>
>>>
Now, **with** ``@return_yield``.
>>> @return_yield
>>> def test_generator(loops):
... for i in range(loops):
... yield str(i)
...
>>> test_generator(10)
["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
>>>
Args:
func: Function to wrap, applied as a decorator with no parenthesis.
Returns:
func: Returns the original function as a non-generator, and returns values immediately.
"""
@wraps(func)
def wrapper(*args, **kwargs):
return list(func(*args, **kwargs))
return wrapper
@Testable()
def clamp(value, imin=0, imax=1):
"""Return a clamped number between two numbers.
Args:
value (int, float, double): The value to clamp.
imin (int, float, double): The lowest allowed value.
imax (int, float, double): The highest allowed value.
Returns:
value: The clamped value between `imin` and `imax`.
"""
if value < imin:
value = imin
elif value > imax:
value = imax
return value
class WebDriverThreadPoolExecutor(ThreadPoolExecutor):
def __init__(self, *args, driver_class, driver_options=None, logger=LOGGER_NAME, **kwargs):
self._logger = logging.getLogger(logger)
self._drivers = SimpleQueue()
for number in range(kwargs["max_workers"]):
self._logger.debug(
f"Spawning driver '{driver_class.__name__}' number {number}/{kwargs['max_workers']}")
self._drivers.put(driver_class(options=driver_options))
super().__init__(*args, **kwargs)
def submit(self, fn, /, *args, **kwargs):
self._logger.debug(
f"Submitting task '{fn.__qualname__}' with args {args} and kwargs {kwargs}")
def _fn(*args, **kwargs):
driver = self._drivers.get(block=True)
result = fn(driver, *args, **kwargs)
if self._shutdown:
driver.close()
self._drivers.put(driver)
return result
return super().submit(_fn, *args, **kwargs)
def shutdown(self, *args, **kwargs):
self._logger.debug("Shutting down thread pool")
super().shutdown(*args, **kwargs)
while not self._drivers.empty():
self._drivers.get().close()
#! /usr/bin/env python3
import math
def pprint(*l):
"""Pretty print iterables.
A simpler alternative for the python standard pprint.PrettyPrint() object.
> # Pretty Print iterables.
:param l: Objects implementing `__iter__()`.
:type l: `list_iterator`
"""
for i in l:
if type(i) is str:
print(i)
else:
try:
print("[" + "\n ".join([str(j) for j in i]) + "]")
except TypeError:
print(i)
def flatten(*l):
"""Return a flattened list.
*Support for 2 dimensions ONLY.*
> # Flatten a 2D list.
:param l: Objects implementing `__iter__()`.
:type l: `list_iterator`
:return: `list`
"""
l = l[0] if len(l) > 1 else l
return [i for s in l for i in s]
def row_major(l, *xy):
"""Get the position of an orered pair as if the grid was flattened.
> # Return the flattened position of an item by ordered pair.
:param l: A 2D grid of objects implementing `__iter__()`
:param xy: An ordered pair.
:type l: `list_iterator`
:type xy: `list`, `tuple`
:return: `int`
:raises: `ValueError`
"""
x, y = xy[0] if len(xy) == 1 else xy
if validate(l):
return len(l) * x + y
else:
ValueError("Grid width is not constant. All second level iterables must be equal length.")
def flat_len(*l):
"""Get the flattened length of a list.
> # Return how many cells are in a 2D list.
:param l: A list or tuple-like object to be flattened.
:type l: `tuple`, `list`
:return: `int`
:raises: `ValueError`
"""
l = l[0] if len(l) > 1 else l
if validate(l):
return len(l[0]) * len(l)
else:
ValueError("Grid width is not constant. All second level iterables must be equal length.")
def grid_print(g, h=0.5):
"""Pretty print a 2D iterable, and justify the space between.
> # Pretty Print a grid.
:param g: Grid. Must have sublists. (2D)
:param h: The width of the console text. Usually half of the height.
:type g: `list_iterator`
:type h: `float`
"""
widest = len(str(max(flatten(g), key=lambda x: len(str(x)))))
for i in g:
for j in i:
print(" " + str(j) + " " * (widest - len(str(j))), end="")
if i == g:
print()
else:
print("\n" * math.floor(widest * h))
def gen_grid(*wh):
"""Generates a simple grid with items in XX:YY format.
> # Generate a grid by W:H.
:param wh: Width, height.
:type wh: `list`, `tuple`
:return: `list`
"""
w, h = wh[0] if len(wh) == 1 else wh
return [[str(y) + ":" + str(x) for y in range(h)] for x in range(w)]
def validate(*l):
"""Returns True if all of the sublists in a grid are the same length.
> # Return true if all sublists are the same length.
:param l: A grid to validate.
:type l: `list`, `tuple`
:return: `True`, `False`
"""
l = l[0] if len(l) > 1 else l
return all(len(i) == len(l[0]) for i in l)
def repair(l, fill=0):
"""Repairs a grid (for example, it does not pass `concepts.validate()`) if it isn't a perfect shape.
> # If not all sublists are equal length, fill the others with 0's until it matches a constant width.
:param l: Grid to repair.
:param fill: Value to fill missing spaces with.
:type l: `list`, `tuple`
:type fill: `object`
:return: `list`, `tuple`
"""
width = len(max(l, key=len))
for i in l:
i.extend([fill] * (width - len(i)))
return l
def clamp(v, min_v, max_v):
"""Clamp a value between a minimum and maximum value.
> # Clamp a value between a min and a max.
:param v: Value to clamp.
:param min_v: Minimum allowed value.
:param max_v: Maximum allowed value.
:type v: `int`, `float`, `double`
:type min_v: `int`, `float`, `double`
:type max_v: `int`, `float`, `double`
:return: `type(v)`
"""
return max(min(v, max_v), min_v)
# Class for easy coordinate access. Behaves like a tuple.
class XY:
__hash__ = tuple.__hash__
def __init__(self, *xy):
x, y = xy[0] if len(xy) == 1 else xy
self.x = x
self.y = y
def __iter__(self):
return iter((self.x, self.y))
# Class to make a window of specified size, that can slide to give smaller sections of a large grid.
class Window:
def __init__(self, grid, size=(3, 3), pos=(0, 0)):
if validate(grid):
self.width, self.height = size
self.pos = list(pos)
self.grid = grid
self.margin = (math.floor(size[0] / 2),
math.floor(size[1] / 2))
else:
raise ValueError("Grid width is not constant. All second level iterables must be equal length.")
# Return maximum and minimum array bounds, plus half the width of the window.
def bounds(self):
return (-self.margin[0], -self.margin[1],
self.grid_size()[0] - self.margin[0] - 1,
self.grid_size()[1] - self.margin[1] - 1)
# Getter for window W, H
def size(self):
return self.width, self.height
# Getter for grid W, H
def grid_size(self):
return len(self.grid[0]), len(self.grid)
# Slide the window along the X and Y axis.
def slide(self, *xy):
x, y = xy[0] if len(xy) == 1 else xy
bounds = self.bounds()
self.pos[0] = clamp(self.pos[0] + x, bounds[0], bounds[2])
self.pos[1] = clamp(self.pos[1] + y, bounds[1], bounds[3])
# Get item by index relative to window.
def get_rel(self, *xy):
x, y = xy[0] if len(xy) == 1 else xy
return self.grid[self.pos[1] + y][self.pos[0] + x]
# Set item by index relative to window.
def set_rel(self, v, *xy):
x, y = xy[0] if len(xy) == 1 else xy
self.grid[self.pos[1] + y][self.pos[0] + x] = v
# Set all the data in a complete flattened window form.
def set(self, *data):
size = self.width * self.height
data = data[0] if len(data) == 1 else data
if len(data) == size:
pass
elif len(data) == self.height:
data = flatten(data)
else:
raise ValueError("Data does not appear to be proper size. Must match flattened window length.")
for x in range(self.height):
for y in range(self.width):
self.set_rel(data[self.height * x + y], x, y)
# Getter by index.
def __getitem__(self, k):
if type(k) == tuple:
return self.get_rel(k)
else:
return self.grid[self.pos[1] + k]
# Setter by index.
def __setitem__(self, k, val):
if type(k) == tuple:
self.set_rel(val, k)
else:
self.grid[self.pos[1] + k]
# Define behavior for being iterated. Returns a 2D list (subgrid).
def __iter__(self):
window = [[] for _ in range(self.height)]
for x in range(self.width):
for y in range(self.height):
pos = self.pos[0] + x, self.pos[1] + y
# Conditional to check if the index is out of negative bounds.
if pos[0] < 0 or pos[1] < 0:
window[y].append(None)
else:
try:
window[y].append(self.grid[pos[1]][pos[0]])
except IndexError:
window[y].append(None)
return iter(window)
from concepts import *
from msvcrt import getch
from os import system
from time import sleep
# grid_size = input("Grid Size (WW, HH): ").split(",")
# grid_size = int(grid_size[0]), int(grid_size[1])
grid_size = 100, 100
# window_size = input("Window Size (WW, HH): ").split(",")
# window_size = int(window_size[0]), int(window_size[1])
window_size = 5, 5
# window_pos = input("Window Position (XX, YY): ").split(",")
# window_pos = int(window_pos[0]), int(window_pos[1])
window_pos = 0, 0
grid = gen_grid(*grid_size)
window = Window(grid, window_size, window_pos)
while True:
system("cls")
print("\n", window.pos, "\n")
grid_print(window)
# window_slide = input("\nWindow Slide (XX, YY): ").split(",")
# window_slide = int(window_slide[0]), int(window_slide[1])
cmd = getch(), getch()
if cmd[0] == b"t":
window_slide = input("\nWindow Slide (XX, YY): ").split(",")
window_slide = int(window_slide[0]), int(window_slide[1])
window.slide(*window_slide)
elif not cmd[0] == b"\xe0":
pass
elif cmd[1] == b"H":
window.slide(0, -1)
elif cmd[1] == b"P":
window.slide(0, 1)
elif cmd[1] == b"K":
window.slide(-1, 0)
elif cmd[1] == b"M":
window.slide(1, 0)
sleep(0.02)
# window.slide(*window_slide)
@contextlib.contextmanager
def work_directory(path):
try:
cwd = os.getcwd()
os.chdir(path)
yield Path(path)
finally:
os.chdir(cwd)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment