ghps 0.5__tar.gz → 0.7__tar.gz

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.
@@ -5,6 +5,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
5
5
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6
6
 
7
7
  ## [Unreleased]
8
+ ## [0.7] - 2026-09-22
9
+ ### Added
10
+ - `host` parameter
11
+ - `--host` CLI argument
12
+ ### Changed
13
+ - Test system modified
14
+ - `README.md` updated
15
+ ## [0.6] - 2026-08-31
16
+ ### Added
17
+ - Custom default error page
18
+ ### Changed
19
+ - Server header modified
8
20
  ## [0.5] - 2026-08-02
9
21
  ### Added
10
22
  - `directory_listing` parameter
@@ -47,7 +59,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
47
59
  - Command line interface
48
60
  - Test system
49
61
 
50
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.5...dev
62
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.7...dev
63
+ [0.7]: https://github.com/sepandhaghighi/ghps/compare/v0.6...v0.7
64
+ [0.6]: https://github.com/sepandhaghighi/ghps/compare/v0.5...v0.6
51
65
  [0.5]: https://github.com/sepandhaghighi/ghps/compare/v0.4...v0.5
52
66
  [0.4]: https://github.com/sepandhaghighi/ghps/compare/v0.3...v0.4
53
67
  [0.3]: https://github.com/sepandhaghighi/ghps/compare/v0.2...v0.3
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghps
3
- Version: 0.5
3
+ Version: 0.7
4
4
  Summary: Ghps: A Minimal GitHub Pages Simulator for Local Development
5
5
  Home-page: https://github.com/sepandhaghighi/ghps
6
- Download-URL: https://github.com/sepandhaghighi/ghps/tarball/v0.5
6
+ Download-URL: https://github.com/sepandhaghighi/ghps/tarball/v0.7
7
7
  Author: Sepand Haghighi
8
8
  Author-email: me@sepand.tech
9
9
  License: MIT
10
10
  Project-URL: Source, https://github.com/sepandhaghighi/ghps
11
11
  Keywords: github gh-pages static-server static-hosting development server local http simulator testing cli python
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Natural Language :: English
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Operating System :: OS Independent
@@ -101,13 +101,13 @@ Dynamic: summary
101
101
  ## Installation
102
102
 
103
103
  ### Source Code
104
- - Download [Version 0.5](https://github.com/sepandhaghighi/ghps/archive/v0.5.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
104
+ - Download [Version 0.7](https://github.com/sepandhaghighi/ghps/archive/v0.7.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
105
105
  - `pip install .`
106
106
 
107
107
  ### PyPI
108
108
 
109
109
  - Check [Python Packaging User Guide](https://packaging.python.org/installing/)
110
- - `pip install ghps==0.5`
110
+ - `pip install ghps==0.7`
111
111
 
112
112
 
113
113
  ## Usage
@@ -117,13 +117,14 @@ Ghps can be used both as a CLI tool and as a Python library.
117
117
  ### CLI
118
118
 
119
119
  ```bash
120
- ghps --port=8080 --no-cache --no-threaded
120
+ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
121
121
  ```
122
122
 
123
123
  #### Options
124
124
 
125
125
  | Option | Description | Default |
126
126
  |--------|-------------|----------|
127
+ | `--host` | Host address to bind to | `localhost` |
127
128
  | `-p, --port` | Port to serve on | `8000` |
128
129
  | `-d, --directory` | Directory to serve | Current directory (`.`) |
129
130
  | `-b, --base-path` | Base path for project pages (e.g. `/repo-name`) | `""` |
@@ -133,7 +134,11 @@ ghps --port=8080 --no-cache --no-threaded
133
134
  | `--auto-open` | Automatically open the server URL | Disable |
134
135
  | `--directory-listing` | Enable directory listing | Disable |
135
136
 
136
- ℹ️ You can set `--port=0` to let the OS automatically select an available port
137
+ > [!NOTE]
138
+ > You can set `--port=0` to let the OS automatically select an available port.
139
+
140
+ > [!NOTE]
141
+ > You can use `--host=0.0.0.0` to bind Ghps to all available network interfaces, or specify a particular address such as `127.0.0.1`.
137
142
 
138
143
 
139
144
  ### Library
@@ -143,6 +148,7 @@ from ghps import GHPageServer
143
148
 
144
149
  server = GHPageServer(
145
150
  directory="dist",
151
+ host="localhost",
146
152
  port=5000,
147
153
  base_path="/repo-name",
148
154
  strict=True,
@@ -159,6 +165,7 @@ server.start()
159
165
  | Parameter | Type | Description | Default |
160
166
  |------------|------|-------------|----------|
161
167
  | `directory` | `str` | Directory to serve | `"."` |
168
+ | `host` | `str` | Host address to bind the server to | `"localhost"` |
162
169
  | `port` | `int` | Port number | `8000` |
163
170
  | `base_path` | `str` | Base path for project page simulation | `""` |
164
171
  | `strict` | `bool` | Enforce GitHub-like routing behavior | `True` |
@@ -167,7 +174,11 @@ server.start()
167
174
  | `auto_open` | `bool` | Automatically open the server URL | `False` |
168
175
  | `directory_listing` | `bool` | Enable directory listing | `False` |
169
176
 
170
- ℹ️ You can set `port=0` to let the OS automatically select an available port
177
+ > [!NOTE]
178
+ > You can set `port=0` to let the OS automatically select an available port.
179
+
180
+ > [!NOTE]
181
+ > The `host` parameter controls the network interface used for server binding. The default is `"localhost"`.
171
182
 
172
183
  ## Issues & Bug Reports
173
184
 
@@ -218,6 +229,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
218
229
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
219
230
 
220
231
  ## [Unreleased]
232
+ ## [0.7] - 2026-09-22
233
+ ### Added
234
+ - `host` parameter
235
+ - `--host` CLI argument
236
+ ### Changed
237
+ - Test system modified
238
+ - `README.md` updated
239
+ ## [0.6] - 2026-08-31
240
+ ### Added
241
+ - Custom default error page
242
+ ### Changed
243
+ - Server header modified
221
244
  ## [0.5] - 2026-08-02
222
245
  ### Added
223
246
  - `directory_listing` parameter
@@ -260,7 +283,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
260
283
  - Command line interface
261
284
  - Test system
262
285
 
263
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.5...dev
286
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.7...dev
287
+ [0.7]: https://github.com/sepandhaghighi/ghps/compare/v0.6...v0.7
288
+ [0.6]: https://github.com/sepandhaghighi/ghps/compare/v0.5...v0.6
264
289
  [0.5]: https://github.com/sepandhaghighi/ghps/compare/v0.4...v0.5
265
290
  [0.4]: https://github.com/sepandhaghighi/ghps/compare/v0.3...v0.4
266
291
  [0.3]: https://github.com/sepandhaghighi/ghps/compare/v0.2...v0.3
@@ -50,13 +50,13 @@
50
50
  ## Installation
51
51
 
52
52
  ### Source Code
53
- - Download [Version 0.5](https://github.com/sepandhaghighi/ghps/archive/v0.5.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
53
+ - Download [Version 0.7](https://github.com/sepandhaghighi/ghps/archive/v0.7.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
54
54
  - `pip install .`
55
55
 
56
56
  ### PyPI
57
57
 
58
58
  - Check [Python Packaging User Guide](https://packaging.python.org/installing/)
59
- - `pip install ghps==0.5`
59
+ - `pip install ghps==0.7`
60
60
 
61
61
 
62
62
  ## Usage
@@ -66,13 +66,14 @@ Ghps can be used both as a CLI tool and as a Python library.
66
66
  ### CLI
67
67
 
68
68
  ```bash
69
- ghps --port=8080 --no-cache --no-threaded
69
+ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
70
70
  ```
71
71
 
72
72
  #### Options
73
73
 
74
74
  | Option | Description | Default |
75
75
  |--------|-------------|----------|
76
+ | `--host` | Host address to bind to | `localhost` |
76
77
  | `-p, --port` | Port to serve on | `8000` |
77
78
  | `-d, --directory` | Directory to serve | Current directory (`.`) |
78
79
  | `-b, --base-path` | Base path for project pages (e.g. `/repo-name`) | `""` |
@@ -82,7 +83,11 @@ ghps --port=8080 --no-cache --no-threaded
82
83
  | `--auto-open` | Automatically open the server URL | Disable |
83
84
  | `--directory-listing` | Enable directory listing | Disable |
84
85
 
85
- ℹ️ You can set `--port=0` to let the OS automatically select an available port
86
+ > [!NOTE]
87
+ > You can set `--port=0` to let the OS automatically select an available port.
88
+
89
+ > [!NOTE]
90
+ > You can use `--host=0.0.0.0` to bind Ghps to all available network interfaces, or specify a particular address such as `127.0.0.1`.
86
91
 
87
92
 
88
93
  ### Library
@@ -92,6 +97,7 @@ from ghps import GHPageServer
92
97
 
93
98
  server = GHPageServer(
94
99
  directory="dist",
100
+ host="localhost",
95
101
  port=5000,
96
102
  base_path="/repo-name",
97
103
  strict=True,
@@ -108,6 +114,7 @@ server.start()
108
114
  | Parameter | Type | Description | Default |
109
115
  |------------|------|-------------|----------|
110
116
  | `directory` | `str` | Directory to serve | `"."` |
117
+ | `host` | `str` | Host address to bind the server to | `"localhost"` |
111
118
  | `port` | `int` | Port number | `8000` |
112
119
  | `base_path` | `str` | Base path for project page simulation | `""` |
113
120
  | `strict` | `bool` | Enforce GitHub-like routing behavior | `True` |
@@ -116,7 +123,11 @@ server.start()
116
123
  | `auto_open` | `bool` | Automatically open the server URL | `False` |
117
124
  | `directory_listing` | `bool` | Enable directory listing | `False` |
118
125
 
119
- ℹ️ You can set `port=0` to let the OS automatically select an available port
126
+ > [!NOTE]
127
+ > You can set `port=0` to let the OS automatically select an available port.
128
+
129
+ > [!NOTE]
130
+ > The `host` parameter controls the network interface used for server binding. The default is `"localhost"`.
120
131
 
121
132
  ## Issues & Bug Reports
122
133
 
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------------- | ------------------ |
7
- | 0.5 | :white_check_mark: |
8
- | < 0.5 | :x: |
7
+ | 0.7 | :white_check_mark: |
8
+ | < 0.7 | :x: |
9
9
 
10
10
  ## Reporting a Vulnerability
11
11
 
@@ -32,6 +32,13 @@ def _parse_args() -> argparse.Namespace:
32
32
  help="Version"
33
33
  )
34
34
 
35
+ parser.add_argument(
36
+ "--host",
37
+ type=str,
38
+ default="localhost",
39
+ help="Host address to bind to (default: localhost)"
40
+ )
41
+
35
42
  parser.add_argument(
36
43
  "-p", "--port",
37
44
  type=int,
@@ -94,6 +101,7 @@ def main() -> None:
94
101
  else:
95
102
  server = GHPageServer(
96
103
  directory=args.directory,
104
+ host=args.host,
97
105
  port=args.port,
98
106
  base_path=args.base_path,
99
107
  strict=not args.no_strict,
@@ -0,0 +1,92 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ghps params."""
3
+
4
+ GHPS_VERSION = "0.7"
5
+ GHPS_REPO = "https://github.com/sepandhaghighi/ghps"
6
+
7
+ INVALID_DIRECTORY_TYPE_ERROR = "`directory` must be str or pathlib.Path."
8
+ DIRECTORY_NOT_FOUND_ERROR = "`directory` does not exist."
9
+ DIRECTORY_NOT_DIR_ERROR = "`directory` must be a valid directory."
10
+
11
+ INVALID_HOST_TYPE_ERROR = "`host` must be str."
12
+ INVALID_PORT_TYPE_ERROR = "`port` must be int."
13
+ INVALID_PORT_RANGE_ERROR = "`port` must be between 0 and 65535."
14
+
15
+ INVALID_BASE_PATH_TYPE_ERROR = "`base_path` must be str."
16
+ INVALID_BASE_PATH_FORMAT_ERROR = "`base_path` must start with '/' or be empty."
17
+
18
+ INVALID_STRICT_TYPE_ERROR = "`strict` must be bool."
19
+ INVALID_NO_CACHE_TYPE_ERROR = "`no_cache` must be bool."
20
+ INVALID_THREADED_TYPE_ERROR = "`threaded` must be bool."
21
+ INVALID_AUTO_OPEN_TYPE_ERROR = "`auto_open` must be bool."
22
+ INVALID_DIRECTORY_LISTING_TYPE_ERROR = "`directory_listing` must be bool."
23
+
24
+ PORT_IN_USE_ERROR = "`port` is already in use. Try another port or use `0` for automatic allocation."
25
+ PORT_ACCESS_DENIED_ERROR = "`port` access denied. Try a different port or check permissions."
26
+ PORT_ADDRESS_NOT_AVAILABLE_ERROR = "`port` address is not available. Check network configuration."
27
+ PORT_BIND_GENERIC_ERROR = "Failed to start server on port `{port}`: {error}"
28
+
29
+ DEFAULT_ERROR_PAGE = """<!DOCTYPE html>
30
+ <html lang="en">
31
+ <head>
32
+ <meta charset="utf-8">
33
+ <meta name="viewport" content="width=device-width, initial-scale=1">
34
+ <title>{code} - {message}</title>
35
+ <style>
36
+ body {{
37
+ margin: 0;
38
+ padding: 40px 20px;
39
+ font-family: system-ui, -apple-system, BlinkMacSystemFont,
40
+ "Segoe UI", sans-serif;
41
+ background: #f6f8fa;
42
+ color: #24292f;
43
+ }}
44
+
45
+ main {{
46
+ max-width: 720px;
47
+ margin: 80px auto;
48
+ padding: 40px;
49
+ background: #ffffff;
50
+ border: 1px solid #d0d7de;
51
+ border-radius: 8px;
52
+ }}
53
+
54
+ h1 {{
55
+ margin: 0 0 12px;
56
+ font-size: 32px;
57
+ }}
58
+
59
+ p {{
60
+ margin: 8px 0;
61
+ color: #57606a;
62
+ }}
63
+
64
+ .status {{
65
+ font-size: 18px;
66
+ font-weight: 600;
67
+ color: #24292f;
68
+ }}
69
+
70
+ footer {{
71
+ margin-top: 32px;
72
+ padding-top: 16px;
73
+ border-top: 1px solid #d0d7de;
74
+ font-size: 13px;
75
+ color: #57606a;
76
+ }}
77
+ </style>
78
+ </head>
79
+ <body>
80
+ <main>
81
+ <h1>{code}</h1>
82
+ <p class="status">{message}</p>
83
+ <p>{explain}</p>
84
+ <p>The requested resource could not be served by Ghps.</p>
85
+
86
+ <footer>
87
+ <a href="{repo}">Ghps/{version} &mdash; A Minimal GitHub Pages Simulator for Local Development</a>
88
+ </footer>
89
+ </main>
90
+ </body>
91
+ </html>
92
+ """
@@ -2,6 +2,7 @@
2
2
  """ghps server."""
3
3
 
4
4
  import http.server
5
+ from http import HTTPStatus
5
6
  import socketserver
6
7
  import webbrowser
7
8
  import errno
@@ -10,9 +11,13 @@ from typing import Optional, Any
10
11
  from pathlib import Path
11
12
  from urllib.parse import unquote
12
13
  from .params import (
14
+ GHPS_VERSION,
15
+ GHPS_REPO,
16
+ DEFAULT_ERROR_PAGE,
13
17
  INVALID_DIRECTORY_TYPE_ERROR,
14
18
  DIRECTORY_NOT_FOUND_ERROR,
15
19
  DIRECTORY_NOT_DIR_ERROR,
20
+ INVALID_HOST_TYPE_ERROR,
16
21
  INVALID_PORT_TYPE_ERROR,
17
22
  INVALID_PORT_RANGE_ERROR,
18
23
  INVALID_BASE_PATH_TYPE_ERROR,
@@ -34,6 +39,7 @@ from .errors import GHPSValidationError, GHPSRuntimeError
34
39
 
35
40
  def _validate_inputs(
36
41
  directory: Any,
42
+ host: Any,
37
43
  port: Any,
38
44
  base_path: Any,
39
45
  strict: Any,
@@ -46,6 +52,7 @@ def _validate_inputs(
46
52
  Validate GHPageServer inputs.
47
53
 
48
54
  :param directory: Root directory to serve files from.
55
+ :param host: Host address to bind the server to.
49
56
  :param port: Port number to bind the server to.
50
57
  :param base_path: URL base path prefix for serving content.
51
58
  :param strict: If False, enables automatic ".html" resolution.
@@ -64,6 +71,9 @@ def _validate_inputs(
64
71
  if not directory.is_dir():
65
72
  raise GHPSValidationError(DIRECTORY_NOT_DIR_ERROR)
66
73
 
74
+ if not isinstance(host, str):
75
+ raise GHPSValidationError(INVALID_HOST_TYPE_ERROR)
76
+
67
77
  if not isinstance(port, int) or isinstance(port, bool):
68
78
  raise GHPSValidationError(INVALID_PORT_TYPE_ERROR)
69
79
 
@@ -119,6 +129,9 @@ def _handle_bind_error(port: int, e: OSError) -> None:
119
129
  class _GHRequestHandler(http.server.SimpleHTTPRequestHandler):
120
130
  """Custom HTTP request handler for serving static files with optional base path stripping, strict routing, custom 404 page support, and cache control headers."""
121
131
 
132
+ server_version = f"Ghps/{GHPS_VERSION}"
133
+ sys_version = ""
134
+
122
135
  def __init__(
123
136
  self,
124
137
  *args: list,
@@ -196,7 +209,26 @@ class _GHRequestHandler(http.server.SimpleHTTPRequestHandler):
196
209
  with open(not_found, "rb") as f:
197
210
  self.wfile.write(f.read())
198
211
  return
199
- super().send_error(code, message, explain)
212
+ if message is None:
213
+ message = HTTPStatus(code).phrase
214
+ if explain is None:
215
+ explain = HTTPStatus(code).description
216
+
217
+ html = DEFAULT_ERROR_PAGE.format(
218
+ code=code,
219
+ message=message,
220
+ explain=explain,
221
+ version=GHPS_VERSION,
222
+ repo=GHPS_REPO,
223
+ )
224
+
225
+ encoded = html.encode("utf-8")
226
+
227
+ self.send_response(code)
228
+ self.send_header("Content-Type", "text/html; charset=utf-8")
229
+ self.send_header("Content-Length", str(len(encoded)))
230
+ self.end_headers()
231
+ self.wfile.write(encoded)
200
232
 
201
233
  def end_headers(self) -> None:
202
234
  """
@@ -223,6 +255,7 @@ class GHPageServer:
223
255
  def __init__(
224
256
  self,
225
257
  directory: str = ".",
258
+ host: str = "localhost",
226
259
  port: int = 8000,
227
260
  base_path: str = "",
228
261
  strict: bool = True,
@@ -235,6 +268,7 @@ class GHPageServer:
235
268
  Initialize the server.
236
269
 
237
270
  :param directory: Root directory to serve files from.
271
+ :param host: Host address to bind the server to.
238
272
  :param port: Port number to bind the server to.
239
273
  :param base_path: URL base path prefix for serving content.
240
274
  :param strict: If False, enables automatic ".html" resolution.
@@ -245,6 +279,7 @@ class GHPageServer:
245
279
  """
246
280
  _validate_inputs(
247
281
  directory=directory,
282
+ host=host,
248
283
  port=port,
249
284
  base_path=base_path,
250
285
  strict=strict,
@@ -254,6 +289,7 @@ class GHPageServer:
254
289
  directory_listing=directory_listing
255
290
  )
256
291
  self._directory = str(Path(directory).resolve())
292
+ self._host = host
257
293
  self._port = port
258
294
  self._url = None
259
295
  self._base_path = base_path
@@ -289,11 +325,11 @@ class GHPageServer:
289
325
  server_cls = _ThreadedTCPServer if self._threaded else socketserver.TCPServer
290
326
 
291
327
  try:
292
- self._httpd = server_cls(("", self._port), handler)
328
+ self._httpd = server_cls((self._host, self._port), handler)
293
329
  except OSError as e:
294
330
  _handle_bind_error(self._port, e)
295
331
  self._port = self._httpd.server_address[1]
296
- self._url = f"http://localhost:{self._port}{self._base_path}"
332
+ self._url = f"http://{self._host}:{self._port}{self._base_path}"
297
333
  self._print_server_info()
298
334
 
299
335
  if self._auto_open:
@@ -1,15 +1,15 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghps
3
- Version: 0.5
3
+ Version: 0.7
4
4
  Summary: Ghps: A Minimal GitHub Pages Simulator for Local Development
5
5
  Home-page: https://github.com/sepandhaghighi/ghps
6
- Download-URL: https://github.com/sepandhaghighi/ghps/tarball/v0.5
6
+ Download-URL: https://github.com/sepandhaghighi/ghps/tarball/v0.7
7
7
  Author: Sepand Haghighi
8
8
  Author-email: me@sepand.tech
9
9
  License: MIT
10
10
  Project-URL: Source, https://github.com/sepandhaghighi/ghps
11
11
  Keywords: github gh-pages static-server static-hosting development server local http simulator testing cli python
12
- Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Natural Language :: English
14
14
  Classifier: License :: OSI Approved :: MIT License
15
15
  Classifier: Operating System :: OS Independent
@@ -101,13 +101,13 @@ Dynamic: summary
101
101
  ## Installation
102
102
 
103
103
  ### Source Code
104
- - Download [Version 0.5](https://github.com/sepandhaghighi/ghps/archive/v0.5.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
104
+ - Download [Version 0.7](https://github.com/sepandhaghighi/ghps/archive/v0.7.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
105
105
  - `pip install .`
106
106
 
107
107
  ### PyPI
108
108
 
109
109
  - Check [Python Packaging User Guide](https://packaging.python.org/installing/)
110
- - `pip install ghps==0.5`
110
+ - `pip install ghps==0.7`
111
111
 
112
112
 
113
113
  ## Usage
@@ -117,13 +117,14 @@ Ghps can be used both as a CLI tool and as a Python library.
117
117
  ### CLI
118
118
 
119
119
  ```bash
120
- ghps --port=8080 --no-cache --no-threaded
120
+ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
121
121
  ```
122
122
 
123
123
  #### Options
124
124
 
125
125
  | Option | Description | Default |
126
126
  |--------|-------------|----------|
127
+ | `--host` | Host address to bind to | `localhost` |
127
128
  | `-p, --port` | Port to serve on | `8000` |
128
129
  | `-d, --directory` | Directory to serve | Current directory (`.`) |
129
130
  | `-b, --base-path` | Base path for project pages (e.g. `/repo-name`) | `""` |
@@ -133,7 +134,11 @@ ghps --port=8080 --no-cache --no-threaded
133
134
  | `--auto-open` | Automatically open the server URL | Disable |
134
135
  | `--directory-listing` | Enable directory listing | Disable |
135
136
 
136
- ℹ️ You can set `--port=0` to let the OS automatically select an available port
137
+ > [!NOTE]
138
+ > You can set `--port=0` to let the OS automatically select an available port.
139
+
140
+ > [!NOTE]
141
+ > You can use `--host=0.0.0.0` to bind Ghps to all available network interfaces, or specify a particular address such as `127.0.0.1`.
137
142
 
138
143
 
139
144
  ### Library
@@ -143,6 +148,7 @@ from ghps import GHPageServer
143
148
 
144
149
  server = GHPageServer(
145
150
  directory="dist",
151
+ host="localhost",
146
152
  port=5000,
147
153
  base_path="/repo-name",
148
154
  strict=True,
@@ -159,6 +165,7 @@ server.start()
159
165
  | Parameter | Type | Description | Default |
160
166
  |------------|------|-------------|----------|
161
167
  | `directory` | `str` | Directory to serve | `"."` |
168
+ | `host` | `str` | Host address to bind the server to | `"localhost"` |
162
169
  | `port` | `int` | Port number | `8000` |
163
170
  | `base_path` | `str` | Base path for project page simulation | `""` |
164
171
  | `strict` | `bool` | Enforce GitHub-like routing behavior | `True` |
@@ -167,7 +174,11 @@ server.start()
167
174
  | `auto_open` | `bool` | Automatically open the server URL | `False` |
168
175
  | `directory_listing` | `bool` | Enable directory listing | `False` |
169
176
 
170
- ℹ️ You can set `port=0` to let the OS automatically select an available port
177
+ > [!NOTE]
178
+ > You can set `port=0` to let the OS automatically select an available port.
179
+
180
+ > [!NOTE]
181
+ > The `host` parameter controls the network interface used for server binding. The default is `"localhost"`.
171
182
 
172
183
  ## Issues & Bug Reports
173
184
 
@@ -218,6 +229,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
218
229
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
219
230
 
220
231
  ## [Unreleased]
232
+ ## [0.7] - 2026-09-22
233
+ ### Added
234
+ - `host` parameter
235
+ - `--host` CLI argument
236
+ ### Changed
237
+ - Test system modified
238
+ - `README.md` updated
239
+ ## [0.6] - 2026-08-31
240
+ ### Added
241
+ - Custom default error page
242
+ ### Changed
243
+ - Server header modified
221
244
  ## [0.5] - 2026-08-02
222
245
  ### Added
223
246
  - `directory_listing` parameter
@@ -260,7 +283,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
260
283
  - Command line interface
261
284
  - Test system
262
285
 
263
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.5...dev
286
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.7...dev
287
+ [0.7]: https://github.com/sepandhaghighi/ghps/compare/v0.6...v0.7
288
+ [0.6]: https://github.com/sepandhaghighi/ghps/compare/v0.5...v0.6
264
289
  [0.5]: https://github.com/sepandhaghighi/ghps/compare/v0.4...v0.5
265
290
  [0.4]: https://github.com/sepandhaghighi/ghps/compare/v0.3...v0.4
266
291
  [0.3]: https://github.com/sepandhaghighi/ghps/compare/v0.2...v0.3
@@ -22,7 +22,7 @@ def read_description() -> str:
22
22
  setup(
23
23
  name='ghps',
24
24
  packages=['ghps'],
25
- version='0.5',
25
+ version='0.7',
26
26
  description='Ghps: A Minimal GitHub Pages Simulator for Local Development',
27
27
  long_description=read_description(),
28
28
  long_description_content_type='text/markdown',
@@ -30,7 +30,7 @@ setup(
30
30
  author='Sepand Haghighi',
31
31
  author_email='me@sepand.tech',
32
32
  url='https://github.com/sepandhaghighi/ghps',
33
- download_url='https://github.com/sepandhaghighi/ghps/tarball/v0.5',
33
+ download_url='https://github.com/sepandhaghighi/ghps/tarball/v0.7',
34
34
  keywords='github gh-pages static-server static-hosting development server local http simulator testing cli python',
35
35
  project_urls={
36
36
  'Source': 'https://github.com/sepandhaghighi/ghps'
@@ -38,7 +38,7 @@ setup(
38
38
  install_requires=['art>=5.3'],
39
39
  python_requires='>=3.7',
40
40
  classifiers=[
41
- 'Development Status :: 3 - Alpha',
41
+ 'Development Status :: 4 - Beta',
42
42
  'Natural Language :: English',
43
43
  'License :: OSI Approved :: MIT License',
44
44
  'Operating System :: OS Independent',
@@ -36,6 +36,7 @@ def test_cli_default_arguments(monkeypatch):
36
36
 
37
37
  run_cli(monkeypatch, [])
38
38
 
39
+ assert captured["host"] == "localhost"
39
40
  assert captured["port"] == 8000
40
41
  assert captured["strict"] is True
41
42
  assert captured["no_cache"] is False
@@ -64,6 +65,26 @@ def test_cli_custom_port_and_directory(monkeypatch, tmp_path):
64
65
  assert captured["directory"] == str(tmp_path.resolve())
65
66
 
66
67
 
68
+ def test_cli_host(monkeypatch):
69
+ captured = {}
70
+
71
+ def fake_init(self, **kwargs):
72
+ captured.update(kwargs)
73
+
74
+ monkeypatch.setattr(ghps.cli, "GHPageServer", type(
75
+ "MockServer",
76
+ (),
77
+ {
78
+ "__init__": fake_init,
79
+ "start": lambda self: None,
80
+ },
81
+ ))
82
+
83
+ run_cli(monkeypatch, ["--host", "127.0.0.1"])
84
+
85
+ assert captured["host"] == "127.0.0.1"
86
+
87
+
67
88
  def test_cli_base_path(monkeypatch):
68
89
  captured = {}
69
90
 
@@ -47,6 +47,20 @@ def test_directory_not_directory(tmp_path):
47
47
  )
48
48
 
49
49
 
50
+ def test_invalid_host_type(tmp_path):
51
+ with pytest.raises(GHPSValidationError, match="`host` must be str."):
52
+ GHPageServer(
53
+ directory=tmp_path,
54
+ port=8000,
55
+ host=123,
56
+ base_path="",
57
+ strict=True,
58
+ no_cache=False,
59
+ threaded=True,
60
+ directory_listing=False,
61
+ )
62
+
63
+
50
64
  def test_invalid_port_type1(tmp_path):
51
65
  with pytest.raises(GHPSValidationError, match="`port` must be int."):
52
66
  GHPageServer(
@@ -6,6 +6,8 @@ from pathlib import Path
6
6
  import requests
7
7
  from unittest.mock import patch, MagicMock
8
8
  from ghps import GHPageServer
9
+ from ghps.server import _GHRequestHandler
10
+ from http import HTTPStatus
9
11
 
10
12
 
11
13
  def run_server(server):
@@ -36,6 +38,25 @@ def test_404_without_custom_page():
36
38
 
37
39
  response = requests.get("http://localhost:9002/missing", timeout=5)
38
40
  assert response.status_code == 404
41
+ assert "404" in response.text
42
+ assert "The requested resource could not be served by Ghps." in response.text
43
+ assert "Ghps/" in response.text
44
+
45
+ server.stop()
46
+
47
+
48
+ def test_default_error_page_content():
49
+ with tempfile.TemporaryDirectory() as tmpdir:
50
+ server = GHPageServer(directory=tmpdir, port=9010)
51
+ run_server(server)
52
+
53
+ response = requests.get("http://localhost:9010/missing", timeout=5)
54
+
55
+ assert response.status_code == 404
56
+ assert response.headers["Content-Type"].startswith("text/html")
57
+ assert "404" in response.text
58
+ assert "The requested resource could not be served by Ghps." in response.text
59
+ assert "A Minimal GitHub Pages Simulator for Local Development" in response.text
39
60
 
40
61
  server.stop()
41
62
 
@@ -54,6 +75,78 @@ def test_custom_404_page():
54
75
  server.stop()
55
76
 
56
77
 
78
+ def test_send_error_uses_default_message(tmp_path):
79
+ handler = _GHRequestHandler.__new__(_GHRequestHandler)
80
+
81
+ handler.directory = str(tmp_path)
82
+ handler.wfile = MagicMock()
83
+ handler._no_cache = False
84
+
85
+ handler.send_response = MagicMock()
86
+ handler.send_header = MagicMock()
87
+ handler.end_headers = MagicMock()
88
+
89
+ _GHRequestHandler.send_error(
90
+ handler,
91
+ 404,
92
+ message=None,
93
+ explain="custom explanation",
94
+ )
95
+
96
+ html = handler.wfile.write.call_args[0][0].decode("utf-8")
97
+
98
+ assert HTTPStatus(404).phrase in html
99
+ assert "custom explanation" in html
100
+
101
+
102
+ def test_send_error_uses_default_explain(tmp_path):
103
+ handler = _GHRequestHandler.__new__(_GHRequestHandler)
104
+
105
+ handler.directory = str(tmp_path)
106
+ handler.wfile = MagicMock()
107
+ handler._no_cache = False
108
+
109
+ handler.send_response = MagicMock()
110
+ handler.send_header = MagicMock()
111
+ handler.end_headers = MagicMock()
112
+
113
+ _GHRequestHandler.send_error(
114
+ handler,
115
+ 404,
116
+ message="Custom message",
117
+ explain=None,
118
+ )
119
+
120
+ html = handler.wfile.write.call_args[0][0].decode("utf-8")
121
+
122
+ assert "Custom message" in html
123
+ assert HTTPStatus(404).description in html
124
+
125
+
126
+ def test_send_error_uses_default_message_and_explain(tmp_path):
127
+ handler = _GHRequestHandler.__new__(_GHRequestHandler)
128
+
129
+ handler.directory = str(tmp_path)
130
+ handler.wfile = MagicMock()
131
+ handler._no_cache = False
132
+
133
+ handler.send_response = MagicMock()
134
+ handler.send_header = MagicMock()
135
+ handler.end_headers = MagicMock()
136
+
137
+ _GHRequestHandler.send_error(
138
+ handler,
139
+ 404,
140
+ message=None,
141
+ explain=None,
142
+ )
143
+
144
+ html = handler.wfile.write.call_args[0][0].decode("utf-8")
145
+
146
+ assert HTTPStatus(404).phrase in html
147
+ assert HTTPStatus(404).description in html
148
+
149
+
57
150
  def test_non_strict_html_fallback():
58
151
  with tempfile.TemporaryDirectory() as tmpdir:
59
152
  Path(tmpdir, "about.html").write_text("About Page")
@@ -166,6 +259,25 @@ def test_auto_open_disabled(mock_server_cls, mock_web_open, tmp_path):
166
259
  mock_web_open.assert_not_called()
167
260
 
168
261
 
262
+ @patch("ghps.server.webbrowser.open")
263
+ @patch("ghps.server._ThreadedTCPServer")
264
+ def test_auto_open_custom_host(mock_server_cls, mock_web_open, tmp_path):
265
+ mock_server = mock_server_cls.return_value
266
+ mock_server.server_address = ("127.0.0.1", 8000)
267
+ mock_server.serve_forever.side_effect = KeyboardInterrupt
268
+
269
+ server = GHPageServer(
270
+ directory=tmp_path,
271
+ port=8000,
272
+ host="127.0.0.1",
273
+ auto_open=True,
274
+ )
275
+
276
+ server.start()
277
+
278
+ mock_web_open.assert_called_once_with("http://127.0.0.1:8000")
279
+
280
+
169
281
  @patch("ghps.server._ThreadedTCPServer")
170
282
  def test_random_port_resolution(mock_server_cls, tmp_path):
171
283
  mock_server = mock_server_cls.return_value
@@ -179,6 +291,26 @@ def test_random_port_resolution(mock_server_cls, tmp_path):
179
291
  assert server._port == 54321
180
292
 
181
293
 
294
+ @patch("ghps.server._ThreadedTCPServer")
295
+ def test_custom_host_binding(mock_server_cls, tmp_path):
296
+ mock_server = mock_server_cls.return_value
297
+ mock_server.server_address = ("127.0.0.1", 8000)
298
+ mock_server.serve_forever.side_effect = KeyboardInterrupt
299
+
300
+ server = GHPageServer(
301
+ directory=tmp_path,
302
+ port=8000,
303
+ host="127.0.0.1",
304
+ )
305
+
306
+ server.start()
307
+
308
+ mock_server_cls.assert_called_once()
309
+ address = mock_server_cls.call_args[0][0]
310
+
311
+ assert address == ("127.0.0.1", 8000)
312
+
313
+
182
314
  def test_directory_listing_enabled():
183
315
  with tempfile.TemporaryDirectory() as tmpdir:
184
316
  Path(tmpdir, "file.txt").write_text("Hello")
ghps-0.5/ghps/params.py DELETED
@@ -1,25 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """ghps params."""
3
-
4
- GHPS_VERSION = "0.5"
5
-
6
- INVALID_DIRECTORY_TYPE_ERROR = "`directory` must be str or pathlib.Path."
7
- DIRECTORY_NOT_FOUND_ERROR = "`directory` does not exist."
8
- DIRECTORY_NOT_DIR_ERROR = "`directory` must be a valid directory."
9
-
10
- INVALID_PORT_TYPE_ERROR = "`port` must be int."
11
- INVALID_PORT_RANGE_ERROR = "`port` must be between 0 and 65535."
12
-
13
- INVALID_BASE_PATH_TYPE_ERROR = "`base_path` must be str."
14
- INVALID_BASE_PATH_FORMAT_ERROR = "`base_path` must start with '/' or be empty."
15
-
16
- INVALID_STRICT_TYPE_ERROR = "`strict` must be bool."
17
- INVALID_NO_CACHE_TYPE_ERROR = "`no_cache` must be bool."
18
- INVALID_THREADED_TYPE_ERROR = "`threaded` must be bool."
19
- INVALID_AUTO_OPEN_TYPE_ERROR = "`auto_open` must be bool."
20
- INVALID_DIRECTORY_LISTING_TYPE_ERROR = "`directory_listing` must be bool."
21
-
22
- PORT_IN_USE_ERROR = "`port` is already in use. Try another port or use `0` for automatic allocation."
23
- PORT_ACCESS_DENIED_ERROR = "`port` access denied. Try a different port or check permissions."
24
- PORT_ADDRESS_NOT_AVAILABLE_ERROR = "`port` address is not available. Check network configuration."
25
- PORT_BIND_GENERIC_ERROR = "Failed to start server on port `{port}`: {error}"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes