crystalwindow 2.4__py3-none-any.whl → 2.9.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crystalwindow/FileHelper.py +142 -0
- crystalwindow/Icons/default_icon.png +0 -0
- crystalwindow/__init__.py +57 -0
- crystalwindow/animation.py +15 -0
- crystalwindow/assets.py +30 -0
- crystalwindow/collision.py +15 -0
- crystalwindow/draw_helpers.py +23 -0
- crystalwindow/draw_rects.py +36 -0
- crystalwindow/draw_text_helper.py +46 -0
- crystalwindow/draw_tool.py +49 -0
- crystalwindow/fun_helpers.py +28 -0
- crystalwindow/gametests/__init__.py +14 -0
- crystalwindow/gametests/__main__.py +23 -0
- crystalwindow/gametests/gravitytest.py +47 -0
- crystalwindow/gametests/guitesting.py +56 -0
- crystalwindow/gametests/sandbox.py +49 -0
- crystalwindow/gametests/windowtesting.py +5 -0
- crystalwindow/gravity.py +75 -0
- crystalwindow/gui.py +69 -0
- crystalwindow/gui_ext.py +66 -0
- crystalwindow/math.py +40 -0
- crystalwindow/player.py +106 -0
- crystalwindow/sprites.py +37 -0
- crystalwindow/tilemap.py +13 -0
- crystalwindow/ver_warner.py +54 -0
- crystalwindow/window.py +218 -0
- {crystalwindow-2.4.dist-info → crystalwindow-2.9.8.dist-info}/METADATA +3 -1
- crystalwindow-2.9.8.dist-info/RECORD +31 -0
- crystalwindow-2.9.8.dist-info/licenses/LICENSE +21 -0
- crystalwindow-2.9.8.dist-info/top_level.txt +1 -0
- crystalwindow-2.4.dist-info/RECORD +0 -4
- crystalwindow-2.4.dist-info/top_level.txt +0 -1
- {crystalwindow-2.4.dist-info → crystalwindow-2.9.8.dist-info}/WHEEL +0 -0
crystalwindow/gravity.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
class Gravity:
|
|
2
|
+
def __init__(self, obj, force=1, terminal_velocity=15, bouncy=False, bounce_strength=0.7):
|
|
3
|
+
self.obj = obj
|
|
4
|
+
if not hasattr(self.obj, "vel_y"):
|
|
5
|
+
self.obj.vel_y = 0
|
|
6
|
+
self.force = force
|
|
7
|
+
self.terminal_velocity = terminal_velocity
|
|
8
|
+
self.bouncy = bouncy
|
|
9
|
+
self.bounce_strength = bounce_strength
|
|
10
|
+
self.on_ground = False
|
|
11
|
+
self.stretch_factor = 0 # for squishy effect
|
|
12
|
+
|
|
13
|
+
# --- NEW: choose mode ---
|
|
14
|
+
# If object has .sprite use that, else fallback to rect mode
|
|
15
|
+
if hasattr(obj, "sprite"):
|
|
16
|
+
self.mode = "sprite"
|
|
17
|
+
else:
|
|
18
|
+
self.mode = "rect"
|
|
19
|
+
|
|
20
|
+
def get_obj_rect(self):
|
|
21
|
+
# helper to get x,y,w,h depending on mode
|
|
22
|
+
if self.mode == "sprite":
|
|
23
|
+
s = self.obj.sprite
|
|
24
|
+
x = getattr(s, "x", 0)
|
|
25
|
+
y = getattr(s, "y", 0)
|
|
26
|
+
w = getattr(s, "width", getattr(s, "w", 32))
|
|
27
|
+
h = getattr(s, "height", getattr(s, "h", 32))
|
|
28
|
+
else:
|
|
29
|
+
x = getattr(self.obj, "x", 0)
|
|
30
|
+
y = getattr(self.obj, "y", 0)
|
|
31
|
+
w = getattr(self.obj, "width", getattr(self.obj, "w", 32))
|
|
32
|
+
h = getattr(self.obj, "height", getattr(self.obj, "h", 32))
|
|
33
|
+
return x, y, w, h
|
|
34
|
+
|
|
35
|
+
def update(self, dt, platforms=[]):
|
|
36
|
+
# apply gravity
|
|
37
|
+
self.obj.vel_y += self.force * dt * 60
|
|
38
|
+
if self.obj.vel_y > self.terminal_velocity:
|
|
39
|
+
self.obj.vel_y = self.terminal_velocity
|
|
40
|
+
|
|
41
|
+
# move object
|
|
42
|
+
x, y, w, h = self.get_obj_rect()
|
|
43
|
+
y += self.obj.vel_y
|
|
44
|
+
self.on_ground = False
|
|
45
|
+
|
|
46
|
+
# check collisions
|
|
47
|
+
for plat in platforms:
|
|
48
|
+
plat_w = getattr(plat, "width", getattr(plat, "w", 0))
|
|
49
|
+
plat_h = getattr(plat, "height", getattr(plat, "h", 0))
|
|
50
|
+
if (x + w > plat.x and x < plat.x + plat_w and
|
|
51
|
+
y + h > plat.y and y < plat.y + plat_h):
|
|
52
|
+
|
|
53
|
+
y = plat.y - h
|
|
54
|
+
self.on_ground = True
|
|
55
|
+
|
|
56
|
+
if self.bouncy:
|
|
57
|
+
self.obj.vel_y = -self.obj.vel_y * self.bounce_strength
|
|
58
|
+
self.stretch_factor = min(0.5, self.stretch_factor + 0.2)
|
|
59
|
+
else:
|
|
60
|
+
self.obj.vel_y = 0
|
|
61
|
+
self.stretch_factor = 0
|
|
62
|
+
|
|
63
|
+
# slowly reset stretch
|
|
64
|
+
if self.stretch_factor > 0:
|
|
65
|
+
self.stretch_factor -= 0.05
|
|
66
|
+
if self.stretch_factor < 0:
|
|
67
|
+
self.stretch_factor = 0
|
|
68
|
+
|
|
69
|
+
# write back position
|
|
70
|
+
if self.mode == "sprite":
|
|
71
|
+
self.obj.sprite.x = x
|
|
72
|
+
self.obj.sprite.y = y
|
|
73
|
+
else:
|
|
74
|
+
self.obj.x = x
|
|
75
|
+
self.obj.y = y
|
crystalwindow/gui.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#gui.py
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
# ----------------- Color Helpers -----------------
|
|
5
|
+
def hex_to_rgb(hex_str):
|
|
6
|
+
"""Convert hex color string to RGB tuple"""
|
|
7
|
+
hex_str = hex_str.lstrip("#")
|
|
8
|
+
return tuple(int(hex_str[i:i+2], 16) for i in (0, 2, 4))
|
|
9
|
+
|
|
10
|
+
def random_color():
|
|
11
|
+
"""Return a random RGB color"""
|
|
12
|
+
return (random.randint(0,255), random.randint(0,255), random.randint(0,255))
|
|
13
|
+
|
|
14
|
+
def lerp_color(c1, c2, t):
|
|
15
|
+
"""Linearly interpolate between two colors"""
|
|
16
|
+
return tuple(int(a + (b-a)*t) for a,b in zip(c1,c2))
|
|
17
|
+
|
|
18
|
+
# ----------------- GUI Elements -----------------
|
|
19
|
+
class Button:
|
|
20
|
+
def __init__(self, rect, text, color=(200,200,200), hover_color=(255,255,255), callback=None):
|
|
21
|
+
self.rect = rect # (x, y, w, h)
|
|
22
|
+
self.text = text
|
|
23
|
+
self.color = color
|
|
24
|
+
self.hover_color = hover_color
|
|
25
|
+
self.callback = callback
|
|
26
|
+
self.hovered = False
|
|
27
|
+
|
|
28
|
+
def draw(self, win):
|
|
29
|
+
x, y, w, h = self.rect
|
|
30
|
+
mx, my = win.mouse_pos
|
|
31
|
+
self.hovered = x <= mx <= x+w and y <= my <= y+h
|
|
32
|
+
cur_color = self.hover_color if self.hovered else self.color
|
|
33
|
+
win.draw_rect(cur_color, self.rect)
|
|
34
|
+
win.draw_text(self.text, pos=(x+5, y+5))
|
|
35
|
+
|
|
36
|
+
def check_click(self, win):
|
|
37
|
+
mx, my = win.mouse_pos
|
|
38
|
+
x, y, w, h = self.rect
|
|
39
|
+
if x <= mx <= x+w and y <= my <= y+h and win.mouse_pressed(1):
|
|
40
|
+
if self.callback:
|
|
41
|
+
self.callback()
|
|
42
|
+
|
|
43
|
+
class Label:
|
|
44
|
+
def __init__(self, pos, text, color=(255,255,255), font="Arial", size=16):
|
|
45
|
+
self.pos = pos
|
|
46
|
+
self.text = text
|
|
47
|
+
self.color = color
|
|
48
|
+
self.font = font
|
|
49
|
+
self.size = size
|
|
50
|
+
|
|
51
|
+
def draw(self, win):
|
|
52
|
+
win.draw_text(self.text, font=self.font, size=self.size, color=self.color, pos=self.pos)
|
|
53
|
+
|
|
54
|
+
# ----------------- Optional GUI Manager -----------------
|
|
55
|
+
class GUIManager:
|
|
56
|
+
def __init__(self):
|
|
57
|
+
self.elements = []
|
|
58
|
+
|
|
59
|
+
def add(self, element):
|
|
60
|
+
self.elements.append(element)
|
|
61
|
+
|
|
62
|
+
def draw(self, win):
|
|
63
|
+
for e in self.elements:
|
|
64
|
+
e.draw(win)
|
|
65
|
+
|
|
66
|
+
def update(self, win):
|
|
67
|
+
for e in self.elements:
|
|
68
|
+
if isinstance(e, Button):
|
|
69
|
+
e.check_click(win)
|
crystalwindow/gui_ext.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from .window import Window
|
|
2
|
+
from .assets import load_image
|
|
3
|
+
from .sprites import Sprite
|
|
4
|
+
from .gui import Button
|
|
5
|
+
|
|
6
|
+
class Toggle:
|
|
7
|
+
def __init__(self, rect, value=False, color=(200,200,200), hover_color=(255,255,255)):
|
|
8
|
+
# rect = (x, y, w, h)
|
|
9
|
+
self.rect = rect
|
|
10
|
+
self.value = value
|
|
11
|
+
self.color = color
|
|
12
|
+
self.hover_color = hover_color
|
|
13
|
+
self.hovered = False
|
|
14
|
+
|
|
15
|
+
def update(self, win: Window):
|
|
16
|
+
mx, my = win.mouse_pos
|
|
17
|
+
x, y, w, h = self.rect
|
|
18
|
+
self.hovered = x <= mx <= x+w and y <= my <= y+h
|
|
19
|
+
if self.hovered and win.mouse_pressed(1):
|
|
20
|
+
self.value = not self.value
|
|
21
|
+
|
|
22
|
+
def draw(self, win: Window):
|
|
23
|
+
draw_color = self.hover_color if self.hovered else self.color
|
|
24
|
+
win.draw_rect(draw_color, self.rect)
|
|
25
|
+
# optional: small “on” indicator
|
|
26
|
+
if self.value:
|
|
27
|
+
inner = (self.rect[0]+4, self.rect[1]+4, self.rect[2]-8, self.rect[3]-8)
|
|
28
|
+
win.draw_rect((0,255,0), inner)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Slider:
|
|
32
|
+
def __init__(self, rect, min_val=0, max_val=100, value=50, color=(150,150,150), handle_radius=10):
|
|
33
|
+
# rect = (x, y, w, h)
|
|
34
|
+
self.rect = rect
|
|
35
|
+
self.min_val = min_val
|
|
36
|
+
self.max_val = max_val
|
|
37
|
+
self.value = value
|
|
38
|
+
self.color = color
|
|
39
|
+
self.handle_radius = handle_radius
|
|
40
|
+
self.dragging = False
|
|
41
|
+
|
|
42
|
+
def update(self, win: Window):
|
|
43
|
+
mx, my = win.mouse_pos
|
|
44
|
+
x, y, w, h = self.rect
|
|
45
|
+
|
|
46
|
+
handle_x = x + ((self.value - self.min_val) / (self.max_val - self.min_val)) * w
|
|
47
|
+
handle_y = y + h // 2
|
|
48
|
+
|
|
49
|
+
# start drag if mouse clicks inside slider bar area
|
|
50
|
+
if win.mouse_pressed(1):
|
|
51
|
+
if not self.dragging and (x <= mx <= x+w and y <= my <= y+h):
|
|
52
|
+
self.dragging = True
|
|
53
|
+
else:
|
|
54
|
+
self.dragging = False # stop drag when released
|
|
55
|
+
|
|
56
|
+
# update value while dragging
|
|
57
|
+
if self.dragging:
|
|
58
|
+
rel_x = max(0, min(mx - x, w)) # clamp
|
|
59
|
+
self.value = self.min_val + (rel_x / w) * (self.max_val - self.min_val)
|
|
60
|
+
|
|
61
|
+
def draw(self, win: Window):
|
|
62
|
+
x, y, w, h = self.rect
|
|
63
|
+
win.draw_rect(self.color, (x, y + h//2 - 2, w, 4))
|
|
64
|
+
handle_x = x + ((self.value - self.min_val) / (self.max_val - self.min_val)) * w
|
|
65
|
+
handle_y = y + h // 2
|
|
66
|
+
win.draw_circle((255, 0, 0), (int(handle_x), int(handle_y)), self.handle_radius)
|
crystalwindow/math.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
class Mathematics:
|
|
2
|
+
"""
|
|
3
|
+
A class used to perform basic mathematical operations.
|
|
4
|
+
|
|
5
|
+
Methods:
|
|
6
|
+
--------
|
|
7
|
+
add(num1, num2) : Adds two numbers.
|
|
8
|
+
subtract(num1, num2) : Subtracts two numbers.
|
|
9
|
+
multiply(num1, num2) : Multiplies two numbers.
|
|
10
|
+
divide(num1, num2) : Divides two numbers.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def add(x, y):
|
|
15
|
+
"""Adds two numbers together"""
|
|
16
|
+
return x + y
|
|
17
|
+
|
|
18
|
+
@staticmethod
|
|
19
|
+
def subtract(num1, num2):
|
|
20
|
+
"""Subtracts two numbers."""
|
|
21
|
+
return num1 - num2
|
|
22
|
+
|
|
23
|
+
@staticmethod
|
|
24
|
+
def multiply(num1, num2):
|
|
25
|
+
"""Multiplies two numbers."""
|
|
26
|
+
return num1 * num2
|
|
27
|
+
|
|
28
|
+
@staticmethod
|
|
29
|
+
def divide(num1, num2):
|
|
30
|
+
"""
|
|
31
|
+
Divides two numbers.
|
|
32
|
+
|
|
33
|
+
Raises:
|
|
34
|
+
------
|
|
35
|
+
ZeroDivisionError : If num2 is zero.
|
|
36
|
+
"""
|
|
37
|
+
if num2 == 0:
|
|
38
|
+
raise ZeroDivisionError("Cannot divide by zero!")
|
|
39
|
+
return num1 / num2
|
|
40
|
+
|
crystalwindow/player.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import math, os, re
|
|
2
|
+
from .assets import load_folder_images
|
|
3
|
+
from .animation import Animation
|
|
4
|
+
|
|
5
|
+
class Player:
|
|
6
|
+
def __init__(self, pos=(0, 0), speed=5, size=(32, 32)):
|
|
7
|
+
self.x, self.y = pos
|
|
8
|
+
self.speed = speed
|
|
9
|
+
self.width, self.height = size
|
|
10
|
+
self.animations = {}
|
|
11
|
+
self.current_anim = None
|
|
12
|
+
self.image = None
|
|
13
|
+
self.rect = self._make_rect()
|
|
14
|
+
|
|
15
|
+
# make a lil rect obj for collisions
|
|
16
|
+
def _make_rect(self):
|
|
17
|
+
return type("Rect", (), {"x": self.x, "y": self.y, "w": self.width, "h": self.height,
|
|
18
|
+
"topleft": (self.x, self.y)})()
|
|
19
|
+
|
|
20
|
+
# === animation setup ===
|
|
21
|
+
def idle(self, folder):
|
|
22
|
+
imgs = self._load_sorted(folder)
|
|
23
|
+
anim = Animation(imgs)
|
|
24
|
+
self.animations["idle"] = anim
|
|
25
|
+
self.current_anim = anim
|
|
26
|
+
return anim
|
|
27
|
+
|
|
28
|
+
class walk:
|
|
29
|
+
@staticmethod
|
|
30
|
+
def cycle(folder):
|
|
31
|
+
imgs = Player._load_sorted_static(folder)
|
|
32
|
+
anim = Animation(imgs)
|
|
33
|
+
anim.loop = True
|
|
34
|
+
return anim
|
|
35
|
+
|
|
36
|
+
@staticmethod
|
|
37
|
+
def once(folder):
|
|
38
|
+
imgs = Player._load_sorted_static(folder)
|
|
39
|
+
anim = Animation(imgs)
|
|
40
|
+
anim.loop = False
|
|
41
|
+
return anim
|
|
42
|
+
|
|
43
|
+
class jump:
|
|
44
|
+
@staticmethod
|
|
45
|
+
def cycle(folder):
|
|
46
|
+
imgs = Player._load_sorted_static(folder)
|
|
47
|
+
anim = Animation(imgs)
|
|
48
|
+
anim.loop = True
|
|
49
|
+
return anim
|
|
50
|
+
|
|
51
|
+
@staticmethod
|
|
52
|
+
def once(folder):
|
|
53
|
+
imgs = Player._load_sorted_static(folder)
|
|
54
|
+
anim = Animation(imgs)
|
|
55
|
+
anim.loop = False
|
|
56
|
+
return anim
|
|
57
|
+
|
|
58
|
+
# === main update ===
|
|
59
|
+
def update(self, dt, win):
|
|
60
|
+
moving = False
|
|
61
|
+
if win.is_key_pressed("left"):
|
|
62
|
+
self.x -= self.speed * dt * 60
|
|
63
|
+
moving = True
|
|
64
|
+
if win.is_key_pressed("right"):
|
|
65
|
+
self.x += self.speed * dt * 60
|
|
66
|
+
moving = True
|
|
67
|
+
if win.is_key_pressed("up"):
|
|
68
|
+
self.y -= self.speed * dt * 60
|
|
69
|
+
moving = True
|
|
70
|
+
if win.is_key_pressed("down"):
|
|
71
|
+
self.y += self.speed * dt * 60
|
|
72
|
+
moving = True
|
|
73
|
+
|
|
74
|
+
# update animation
|
|
75
|
+
if moving and "run" in self.animations:
|
|
76
|
+
self.current_anim = self.animations["run"]
|
|
77
|
+
elif not moving and "idle" in self.animations:
|
|
78
|
+
self.current_anim = self.animations["idle"]
|
|
79
|
+
|
|
80
|
+
if self.current_anim:
|
|
81
|
+
self.current_anim.update(dt)
|
|
82
|
+
self.image = self.current_anim.get_frame()
|
|
83
|
+
|
|
84
|
+
self.rect.x, self.rect.y = self.x, self.y
|
|
85
|
+
self.rect.topleft = (self.x, self.y)
|
|
86
|
+
|
|
87
|
+
def draw(self, win):
|
|
88
|
+
if self.image:
|
|
89
|
+
win.screen.blit(self.image, (self.x, self.y))
|
|
90
|
+
|
|
91
|
+
# ---------- helpers ----------
|
|
92
|
+
def _load_sorted(self, folder):
|
|
93
|
+
imgs_dict = load_folder_images(folder)
|
|
94
|
+
return self._sort_images(imgs_dict)
|
|
95
|
+
|
|
96
|
+
@staticmethod
|
|
97
|
+
def _load_sorted_static(folder):
|
|
98
|
+
imgs_dict = load_folder_images(folder)
|
|
99
|
+
return Player._sort_images(imgs_dict)
|
|
100
|
+
|
|
101
|
+
@staticmethod
|
|
102
|
+
def _sort_images(imgs_dict):
|
|
103
|
+
def extract_num(filename):
|
|
104
|
+
match = re.search(r'(\d+)', filename)
|
|
105
|
+
return int(match.group(1)) if match else 0
|
|
106
|
+
return [img for name, img in sorted(imgs_dict.items(), key=lambda x: extract_num(x[0]))]
|
crystalwindow/sprites.py
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
#Sprites.py
|
|
2
|
+
from tkinter import PhotoImage
|
|
3
|
+
|
|
4
|
+
class Sprite:
|
|
5
|
+
def __init__(self, pos, size=None, image=None, color=(255, 0, 0)):
|
|
6
|
+
"""
|
|
7
|
+
pos: (x, y) top-left
|
|
8
|
+
size: (w, h) if you want a plain colored rect
|
|
9
|
+
image: PhotoImage (Tkinter)
|
|
10
|
+
color: fill color if size is used
|
|
11
|
+
"""
|
|
12
|
+
self.pos = pos
|
|
13
|
+
self.x, self.y = pos
|
|
14
|
+
|
|
15
|
+
if image is not None:
|
|
16
|
+
self.image = image
|
|
17
|
+
self.width = image.width()
|
|
18
|
+
self.height = image.height()
|
|
19
|
+
elif size is not None:
|
|
20
|
+
self.width, self.height = size
|
|
21
|
+
self.image = None
|
|
22
|
+
self.color = color
|
|
23
|
+
else:
|
|
24
|
+
raise ValueError("Sprite needs either size or image")
|
|
25
|
+
|
|
26
|
+
def draw(self, win):
|
|
27
|
+
"""Draw sprite on given Window"""
|
|
28
|
+
if self.image:
|
|
29
|
+
win.canvas.create_image(self.x, self.y, anchor="nw", image=self.image)
|
|
30
|
+
else:
|
|
31
|
+
win.draw_rect(self.color, (self.x, self.y, self.width, self.height))
|
|
32
|
+
|
|
33
|
+
def move(self, dx, dy):
|
|
34
|
+
"""Move sprite by dx/dy"""
|
|
35
|
+
self.x += dx
|
|
36
|
+
self.y += dy
|
|
37
|
+
self.pos = (self.x, self.y)
|
crystalwindow/tilemap.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .sprites import Sprite
|
|
2
|
+
|
|
3
|
+
class TileMap:
|
|
4
|
+
def __init__(self, tile_size):
|
|
5
|
+
self.tile_size = tile_size
|
|
6
|
+
self.tiles = []
|
|
7
|
+
|
|
8
|
+
def add_tile(self, image, x, y):
|
|
9
|
+
self.tiles.append(Sprite(image, x, y))
|
|
10
|
+
|
|
11
|
+
def draw(self, win):
|
|
12
|
+
for t in self.tiles:
|
|
13
|
+
win.draw_sprite(t)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from importlib.metadata import version as get_version, PackageNotFoundError
|
|
2
|
+
import requests
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
def check_for_update(package_name="crystalwindow"):
|
|
7
|
+
"""
|
|
8
|
+
Checks PyPI for updates.
|
|
9
|
+
Warns every run if outdated.
|
|
10
|
+
Only skips check when version is already latest.
|
|
11
|
+
"""
|
|
12
|
+
try:
|
|
13
|
+
# get current version
|
|
14
|
+
try:
|
|
15
|
+
current_version = get_version(package_name)
|
|
16
|
+
except PackageNotFoundError:
|
|
17
|
+
print(f"(⚠️ Package '{package_name}' not found)")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
cache_file = os.path.join(os.path.expanduser("~"), f".{package_name}_version_cache.json")
|
|
21
|
+
|
|
22
|
+
# load cache
|
|
23
|
+
if os.path.exists(cache_file):
|
|
24
|
+
with open(cache_file, "r") as f:
|
|
25
|
+
cache = json.load(f)
|
|
26
|
+
else:
|
|
27
|
+
cache = {}
|
|
28
|
+
|
|
29
|
+
# get newest version from PyPI
|
|
30
|
+
url = f"https://pypi.org/pypi/{package_name}/json"
|
|
31
|
+
response = requests.get(url, timeout=3)
|
|
32
|
+
|
|
33
|
+
if response.status_code == 200:
|
|
34
|
+
latest_version = response.json()["info"]["version"]
|
|
35
|
+
|
|
36
|
+
if latest_version == current_version:
|
|
37
|
+
# only print once per version
|
|
38
|
+
if cache.get("last_checked") != current_version:
|
|
39
|
+
print(f"✅ Up to date! ver = {current_version}.")
|
|
40
|
+
cache["last_checked"] = current_version
|
|
41
|
+
with open(cache_file, "w") as f:
|
|
42
|
+
json.dump(cache, f)
|
|
43
|
+
else:
|
|
44
|
+
# always warn until updated
|
|
45
|
+
print(f"\n⚠️ Yo dev! '{package_name}' is outdated ({current_version})")
|
|
46
|
+
print(f"👉 Newest is {latest_version}! Run:")
|
|
47
|
+
print(f" pip install --upgrade {package_name}")
|
|
48
|
+
print(f"Or peep: https://pypi.org/project/{package_name}/{latest_version}/\n")
|
|
49
|
+
|
|
50
|
+
else:
|
|
51
|
+
print("(⚠️ PyPI request failed, skipping version check)")
|
|
52
|
+
|
|
53
|
+
except Exception as e:
|
|
54
|
+
print(f"(⚠️ Version check failed: {e})")
|