Skip to content

Commit 5b6d776

Browse files
committed
(feat) native hot-reloading
1 parent 3dc800c commit 5b6d776

14 files changed

+645
-93
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
.pytest_cache
12
__pycache__/
23
public/
34
venv

content/index.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22

33
I like Tolkien. Read my [first post here](/majesty)
44

5-
and now a second special post by maniac [here](/maniac)
5+
and now a very special post by maniac [here](/maniac)

requirements.txt

+2-4
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
exceptiongroup==1.2.1
1+
exceptiongroup==1.2.2
22
iniconfig==2.0.0
3-
livereload==2.7.0
43
packaging==24.1
54
pluggy==1.5.0
6-
pytest==8.2.2
5+
pytest==8.3.1
76
tomli==2.0.1
8-
tornado==6.4.1

src/core/server.py

+277
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
from http.server import SimpleHTTPRequestHandler
2+
import socketserver
3+
import os
4+
from typing import Tuple, Set, Callable, List, Dict
5+
6+
from src.core.utils import find_files_rec, find_file_timestamps
7+
8+
HOSTNAME: str = "localhost"
9+
PORT: int = 8080
10+
EXCLUDE_DIRS: List[str] = [
11+
".git",
12+
"__pycache__",
13+
"venv",
14+
".pytest_cache",
15+
"tests",
16+
"public",
17+
]
18+
EXCLUDE_FILES: List[str] = ["README.md", "TODO.md"]
19+
FILETYPES_TO_MONITOR: List[str] = ["md", "html", "css", "js"]
20+
21+
22+
def get_updated_tracked_lists(root_path: str) -> Tuple[List[str], Dict[str, float]]:
23+
"""
24+
Retrieves updated lists of tracked files and their timestamps based on
25+
the specified root path.
26+
27+
Args:
28+
- root_path (str): The root directory path to monitor.
29+
30+
Returns:
31+
- Tuple[List[str], Dict[str, float]]: A tuple containing the updated list
32+
of tracked files and their timestamps.
33+
"""
34+
files = find_files_rec(
35+
path=root_path,
36+
exclude_dirs=EXCLUDE_DIRS,
37+
exclude_files=EXCLUDE_FILES,
38+
filetypes_to_monitor=FILETYPES_TO_MONITOR,
39+
)
40+
filestamps = find_file_timestamps(files)
41+
return (files, filestamps)
42+
43+
44+
def compare_files(
45+
tracked_files: List[str], u_tracked_files: List[str]
46+
) -> Tuple[Set[str], Set[str]]:
47+
"""
48+
Compares the current tracked files with the updated tracked files
49+
to identify added and deleted files.
50+
51+
Args:
52+
- tracked_files (List[str]): Current list of tracked files.
53+
- u_tracked_files (List[str]): Updated list of tracked files.
54+
55+
Returns:
56+
- Tuple[Set[str], Set[str]]: A tuple containing sets of added and deleted files.
57+
"""
58+
added_files = set(u_tracked_files) - set(tracked_files)
59+
deleted_files = set(tracked_files) - set(u_tracked_files)
60+
return added_files, deleted_files
61+
62+
63+
def compare_timestamps(
64+
tracked_filestamps: Dict[str, float], u_tracked_filestamps: Dict[str, float]
65+
) -> set[str]:
66+
"""
67+
Compares the current tracked file timestamps with the updated timestamps
68+
to identify modified files.
69+
70+
Args:
71+
- tracked_filestamps (Dict[str, float]): Current timestamps of tracked files.
72+
- u_tracked_filestamps (Dict[str, float]): Updated timestamps of tracked files.
73+
74+
Returns:
75+
- set[str]: A set of filenames that have been modified.
76+
"""
77+
modified_files = {
78+
file
79+
for file in tracked_filestamps
80+
if tracked_filestamps[file] != u_tracked_filestamps.get(file)
81+
}
82+
return modified_files
83+
84+
85+
def is_needed_to_reload(
86+
root_path: str,
87+
tracked_files: List[str],
88+
tracked_filestamps: Dict[str, float],
89+
) -> bool:
90+
"""
91+
Checks if a reload of tracked files is needed based on changes in the file system.
92+
93+
Args:
94+
- root_path (str): The root directory path to monitor.
95+
- tracked_files (List[str]): Current list of tracked files.
96+
- tracked_filestamps (Dict[str, float]): Current timestamps of tracked files.
97+
98+
Returns:
99+
- bool: True if a reload is needed, False otherwise.
100+
"""
101+
u_tracked_files, u_tracked_filestamps = get_updated_tracked_lists(root_path)
102+
103+
# Check for added or deleted files
104+
added_files, deleted_files = compare_files(tracked_files, u_tracked_files)
105+
if added_files or deleted_files:
106+
print("Added files:", added_files)
107+
print("Deleted files:", deleted_files)
108+
return True
109+
110+
# Check for modified files
111+
modified_files = compare_timestamps(tracked_filestamps, u_tracked_filestamps)
112+
if modified_files:
113+
print("Modified files:", modified_files)
114+
return True
115+
116+
return False
117+
118+
119+
class MyHttpRequestHandler(SimpleHTTPRequestHandler):
120+
"""
121+
Custom HTTP request handler to serve files and handle reload checks.
122+
"""
123+
124+
def __init__(self, *args, directory=None, **kwargs):
125+
"""
126+
Initializes the HTTP request handler.
127+
128+
Args:
129+
- *args: Variable length argument list.
130+
- directory (str, optional): The directory to serve files from. Defaults to None.
131+
- **kwargs: Arbitrary keyword arguments.
132+
"""
133+
super().__init__(*args, directory=directory, **kwargs)
134+
135+
136+
def create_handler(
137+
root_path: str,
138+
public_dir: str,
139+
build_handler: Callable[[], None],
140+
tracked_files: List[str],
141+
tracked_filestamps: Dict[str, float],
142+
) -> type[MyHttpRequestHandler]:
143+
"""
144+
Creates a custom HTTP request handler class with reload functionality.
145+
146+
Args:
147+
- root_path (str): The root directory path to monitor.
148+
- public_dir (str): The directory from which to serve public files.
149+
- build_handler (Callable[[], None]): The handler function to execute on build.
150+
- tracked_files (List[str]): The files the server is tracking for changes
151+
- tracked_filestamps (Dict[str, float]): The last modified timestamps for
152+
the files being tracked
153+
154+
Returns:
155+
- type[MyHttpRequestHandler]: Custom HTTP request handler class.
156+
"""
157+
158+
class CustomHandler(MyHttpRequestHandler):
159+
"""
160+
Custom HTTP request handler class with added reload and redirect functionality.
161+
"""
162+
163+
# Class variables to store tracked files and timestamps
164+
tracked_files = tracked_files
165+
tracked_filestamps = tracked_filestamps
166+
167+
def __init__(self, *args, **kwargs):
168+
"""
169+
Initializes the custom HTTP request handler.
170+
"""
171+
self.root_path = root_path
172+
self.public_dir = public_dir
173+
self.build_handler = build_handler
174+
super().__init__(*args, directory=self.public_dir, **kwargs)
175+
176+
def log_message(self, format, *args):
177+
"""
178+
Overrides default log_message to exclude logging for /check_update requests.
179+
"""
180+
if self.path != "/check_update":
181+
super().log_message(format, *args)
182+
183+
def do_GET(self):
184+
"""
185+
Handles GET requests.
186+
187+
If the request path is '/check_update', performs a reload check.
188+
Otherwise, redirects requests for tracked files to their new location
189+
within the public directory.
190+
"""
191+
if self.path == "/check_update":
192+
current_tracked_files = self.__class__.tracked_files
193+
current_tracked_filestamps = self.__class__.tracked_filestamps
194+
195+
if is_needed_to_reload(
196+
self.root_path,
197+
tracked_files=current_tracked_files,
198+
tracked_filestamps=current_tracked_filestamps,
199+
):
200+
print("Reloaded")
201+
self.__class__.tracked_files, self.__class__.tracked_filestamps = (
202+
get_updated_tracked_lists(self.root_path)
203+
)
204+
self.build_handler()
205+
self.send_response(200)
206+
self.end_headers()
207+
self.wfile.write(b"update")
208+
else:
209+
self.send_response(204)
210+
self.end_headers()
211+
else:
212+
# Construct the requested file path within public_dir
213+
requested_file_path = self.translate_path(self.path) + ".html"
214+
215+
# Check if the requested file path exists
216+
if os.path.exists(requested_file_path):
217+
# Construct the redirect URL
218+
redirect_url = f"http://{HOSTNAME}:{PORT}/{os.path.relpath(requested_file_path, self.public_dir)}"
219+
self.send_response(301)
220+
self.send_header("Location", redirect_url)
221+
self.end_headers()
222+
else:
223+
super().do_GET()
224+
225+
def shutdown(self):
226+
"""
227+
Stops the serve_forever loop.
228+
229+
Blocks until the loop has finished. This must be called while
230+
serve_forever() is running in another thread, or it will
231+
deadlock.
232+
"""
233+
self.__shutdown_request = True
234+
self.__is_shut_down.wait()
235+
236+
return CustomHandler
237+
238+
239+
def run(root_path: str, public_dir: str, build_handler: Callable[[], None]):
240+
"""
241+
Runs the HTTP server with the custom request handler.
242+
243+
Args:
244+
- root_path (str): The root directory path to monitor.
245+
- public_dir (str): The directory from which to serve public files.
246+
- build_handler (Callable[[], None]): The handler function to execute for build.
247+
"""
248+
global HOSTNAME, PORT, tracked_files, tracked_filestamps
249+
250+
# Initialize tracked files and timestamps
251+
tracked_files, tracked_filestamps = get_updated_tracked_lists(root_path)
252+
253+
# Execute the build handler function
254+
build_handler()
255+
256+
# Create TCP server with custom handler
257+
TCPHandler = create_handler(
258+
root_path=root_path,
259+
public_dir=public_dir,
260+
build_handler=build_handler,
261+
tracked_files=tracked_files,
262+
tracked_filestamps=tracked_filestamps,
263+
)
264+
265+
# Start TCP server
266+
with socketserver.TCPServer(
267+
(HOSTNAME, PORT), TCPHandler, bind_and_activate=False
268+
) as httpd:
269+
httpd.allow_reuse_address = True
270+
httpd.server_bind()
271+
httpd.server_activate()
272+
print("Serving at http://{}:{}".format(HOSTNAME, PORT))
273+
try:
274+
httpd.serve_forever()
275+
except KeyboardInterrupt:
276+
print("Shutting down the server")
277+
httpd.shutdown()

src/core/text_functions.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import re
2-
from typing import List, Tuple
1+
from typing import List
32
from src.core.htmlnode import LeafNode
43
from src.core.textnode import TextNode
54
import src.core.markdown_functions as mf

0 commit comments

Comments
 (0)