ghps 0.2__tar.gz → 0.4__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,23 @@ 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.4] - 2026-05-08
9
+ ### Added
10
+ - `GHPSError` class
11
+ - `GHPSValidationError` class
12
+ - `GHPSRuntimeError` class
13
+ - Port error messages
14
+ ### Changed
15
+ - Test system modified
16
+ ## [0.3] - 2026-04-05
17
+ ### Added
18
+ - `auto_open` parameter
19
+ - `--auto-open` CLI argument
20
+ - Random port support
21
+ ### Changed
22
+ - Test system modified
23
+ - CLI error messages updated
24
+ - `README.md` updated
8
25
  ## [0.2] - 2026-03-19
9
26
  ### Added
10
27
  - Logo
@@ -18,7 +35,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
18
35
  - Command line interface
19
36
  - Test system
20
37
 
21
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.2...dev
38
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.4...dev
39
+ [0.4]: https://github.com/sepandhaghighi/ghps/compare/v0.3...v0.4
40
+ [0.3]: https://github.com/sepandhaghighi/ghps/compare/v0.2...v0.3
22
41
  [0.2]: https://github.com/sepandhaghighi/ghps/compare/v0.1...v0.2
23
42
  [0.1]: https://github.com/sepandhaghighi/ghps/compare/bc0a425...v0.1
24
43
 
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghps
3
- Version: 0.2
3
+ Version: 0.4
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.2
6
+ Download-URL: https://github.com/sepandhaghighi/ghps/tarball/v0.4
7
7
  Author: Sepand Haghighi
8
8
  Author-email: me@sepand.tech
9
9
  License: MIT
@@ -101,13 +101,13 @@ Dynamic: summary
101
101
  ## Installation
102
102
 
103
103
  ### Source Code
104
- - Download [Version 0.2](https://github.com/sepandhaghighi/ghps/archive/v0.2.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
104
+ - Download [Version 0.4](https://github.com/sepandhaghighi/ghps/archive/v0.4.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.2`
110
+ - `pip install ghps==0.4`
111
111
 
112
112
 
113
113
  ## Usage
@@ -130,6 +130,9 @@ ghps --port=8080 --no-cache --no-threaded
130
130
  | `--no-strict` | Enable `.html` fallback| Strict mode enabled |
131
131
  | `--no-cache` | Disable HTTP caching headers | Cache enabled |
132
132
  | `--no-threaded` | Disable threaded server | Threaded enabled |
133
+ | `--auto-open` | Automatically open the server URL | Disable |
134
+
135
+ ℹ️ You can set `--port=0` to let the OS automatically select an available port
133
136
 
134
137
 
135
138
  ### Library
@@ -144,6 +147,7 @@ server = GHPageServer(
144
147
  strict=True,
145
148
  no_cache=False,
146
149
  threaded=True,
150
+ auto_open=True,
147
151
  )
148
152
 
149
153
  server.start()
@@ -159,6 +163,9 @@ server.start()
159
163
  | `strict` | `bool` | Enforce GitHub-like routing behavior | `True` |
160
164
  | `no_cache` | `bool` | Disable cache headers | `False` |
161
165
  | `threaded` | `bool` | Enable threaded request handling | `True` |
166
+ | `auto_open` | `bool` | Automatically open the server URL | `False` |
167
+
168
+ ℹ️ You can set `port=0` to let the OS automatically select an available port
162
169
 
163
170
  ## Issues & Bug Reports
164
171
 
@@ -209,6 +216,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
209
216
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
210
217
 
211
218
  ## [Unreleased]
219
+ ## [0.4] - 2026-05-08
220
+ ### Added
221
+ - `GHPSError` class
222
+ - `GHPSValidationError` class
223
+ - `GHPSRuntimeError` class
224
+ - Port error messages
225
+ ### Changed
226
+ - Test system modified
227
+ ## [0.3] - 2026-04-05
228
+ ### Added
229
+ - `auto_open` parameter
230
+ - `--auto-open` CLI argument
231
+ - Random port support
232
+ ### Changed
233
+ - Test system modified
234
+ - CLI error messages updated
235
+ - `README.md` updated
212
236
  ## [0.2] - 2026-03-19
213
237
  ### Added
214
238
  - Logo
@@ -222,7 +246,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
222
246
  - Command line interface
223
247
  - Test system
224
248
 
225
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.2...dev
249
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.4...dev
250
+ [0.4]: https://github.com/sepandhaghighi/ghps/compare/v0.3...v0.4
251
+ [0.3]: https://github.com/sepandhaghighi/ghps/compare/v0.2...v0.3
226
252
  [0.2]: https://github.com/sepandhaghighi/ghps/compare/v0.1...v0.2
227
253
  [0.1]: https://github.com/sepandhaghighi/ghps/compare/bc0a425...v0.1
228
254
 
@@ -50,13 +50,13 @@
50
50
  ## Installation
51
51
 
52
52
  ### Source Code
53
- - Download [Version 0.2](https://github.com/sepandhaghighi/ghps/archive/v0.2.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
53
+ - Download [Version 0.4](https://github.com/sepandhaghighi/ghps/archive/v0.4.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.2`
59
+ - `pip install ghps==0.4`
60
60
 
61
61
 
62
62
  ## Usage
@@ -79,6 +79,9 @@ ghps --port=8080 --no-cache --no-threaded
79
79
  | `--no-strict` | Enable `.html` fallback| Strict mode enabled |
80
80
  | `--no-cache` | Disable HTTP caching headers | Cache enabled |
81
81
  | `--no-threaded` | Disable threaded server | Threaded enabled |
82
+ | `--auto-open` | Automatically open the server URL | Disable |
83
+
84
+ ℹ️ You can set `--port=0` to let the OS automatically select an available port
82
85
 
83
86
 
84
87
  ### Library
@@ -93,6 +96,7 @@ server = GHPageServer(
93
96
  strict=True,
94
97
  no_cache=False,
95
98
  threaded=True,
99
+ auto_open=True,
96
100
  )
97
101
 
98
102
  server.start()
@@ -108,6 +112,9 @@ server.start()
108
112
  | `strict` | `bool` | Enforce GitHub-like routing behavior | `True` |
109
113
  | `no_cache` | `bool` | Disable cache headers | `False` |
110
114
  | `threaded` | `bool` | Enable threaded request handling | `True` |
115
+ | `auto_open` | `bool` | Automatically open the server URL | `False` |
116
+
117
+ ℹ️ You can set `port=0` to let the OS automatically select an available port
111
118
 
112
119
  ## Issues & Bug Reports
113
120
 
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------------- | ------------------ |
7
- | 0.2 | :white_check_mark: |
8
- | < 0.2 | :x: |
7
+ | 0.4 | :white_check_mark: |
8
+ | < 0.4 | :x: |
9
9
 
10
10
  ## Reporting a Vulnerability
11
11
 
@@ -5,4 +5,4 @@ pytest-cov>=2.6.1
5
5
  setuptools>=40.8.0
6
6
  vulture>=1.0
7
7
  bandit>=1.5.1
8
- pydocstyle>=3.0.0
8
+ pydocstyle>=6.3.0
@@ -0,0 +1,8 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ghps modules."""
3
+ from .params import GHPS_VERSION
4
+ from .errors import GHPSError, GHPSRuntimeError, GHPSValidationError
5
+ from .server import GHPageServer
6
+ __version__ = GHPS_VERSION
7
+
8
+ __all__ = ["GHPageServer", "GHPSError", "GHPSRuntimeError", "GHPSValidationError"]
@@ -5,9 +5,21 @@ import sys
5
5
  import argparse
6
6
  from pathlib import Path
7
7
  from .params import GHPS_VERSION
8
+ from .errors import GHPSError
8
9
  from .server import GHPageServer
9
10
 
10
11
 
12
+ def _print_cli_error(message: str, exit_code: int = 1) -> None:
13
+ """
14
+ Print a formatted CLI error message and exit.
15
+
16
+ :param message: Error message to display.
17
+ :param exit_code: Exit status code (default: 1).
18
+ """
19
+ print(f"[GHPS ERROR] {message}", file=sys.stderr)
20
+ sys.exit(exit_code)
21
+
22
+
11
23
  def _parse_args() -> argparse.Namespace:
12
24
  """Parse arguments."""
13
25
  parser = argparse.ArgumentParser(
@@ -57,29 +69,34 @@ def _parse_args() -> argparse.Namespace:
57
69
  help="Disable threaded server"
58
70
  )
59
71
 
72
+ parser.add_argument(
73
+ "--auto-open",
74
+ action="store_true",
75
+ help="Automatically open the server URL"
76
+ )
77
+
60
78
  args = parser.parse_args()
61
79
  return args
62
80
 
63
81
 
64
82
  def main() -> None:
65
83
  """CLI main function."""
66
- args = _parse_args()
67
-
68
- if args.version:
69
- print(GHPS_VERSION)
70
- else:
71
- directory = Path(args.directory).resolve()
72
-
73
- if not directory.exists():
74
- print("Error: Directory does not exist.")
75
- sys.exit(1)
76
-
77
- server = GHPageServer(
78
- directory=str(directory),
79
- port=args.port,
80
- base_path=args.base_path,
81
- strict=not args.no_strict,
82
- no_cache=args.no_cache,
83
- threaded=not args.no_threaded,
84
- )
85
- server.start()
84
+ try:
85
+ args = _parse_args()
86
+ if args.version:
87
+ print(GHPS_VERSION)
88
+ else:
89
+ server = GHPageServer(
90
+ directory=args.directory,
91
+ port=args.port,
92
+ base_path=args.base_path,
93
+ strict=not args.no_strict,
94
+ no_cache=args.no_cache,
95
+ threaded=not args.no_threaded,
96
+ auto_open=args.auto_open,
97
+ )
98
+ server.start()
99
+ except GHPSError as e:
100
+ _print_cli_error(str(e))
101
+ except Exception as e:
102
+ _print_cli_error(f"Unexpected error: {e}")
@@ -0,0 +1,14 @@
1
+ # -*- coding: utf-8 -*-
2
+ """ghps errors."""
3
+
4
+
5
+ class GHPSError(Exception):
6
+ """Base exception for GHPS."""
7
+
8
+
9
+ class GHPSValidationError(GHPSError, ValueError):
10
+ """Raised for invalid input parameters."""
11
+
12
+
13
+ class GHPSRuntimeError(GHPSError):
14
+ """Raised for runtime errors during server execution."""
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """ghps params."""
3
3
 
4
- GHPS_VERSION = "0.2"
4
+ GHPS_VERSION = "0.4"
5
5
 
6
6
  INVALID_DIRECTORY_TYPE_ERROR = "`directory` must be str or pathlib.Path."
7
7
  DIRECTORY_NOT_FOUND_ERROR = "`directory` does not exist."
@@ -16,3 +16,9 @@ INVALID_BASE_PATH_FORMAT_ERROR = "`base_path` must start with '/' or be empty."
16
16
  INVALID_STRICT_TYPE_ERROR = "`strict` must be bool."
17
17
  INVALID_NO_CACHE_TYPE_ERROR = "`no_cache` must be bool."
18
18
  INVALID_THREADED_TYPE_ERROR = "`threaded` must be bool."
19
+ INVALID_AUTO_OPEN_TYPE_ERROR = "`auto_open` must be bool."
20
+
21
+ PORT_IN_USE_ERROR = "`port` is already in use. Try another port or use 0."
22
+ PORT_ACCESS_DENIED_ERROR = "`port` access denied. Try a different port or check permissions."
23
+ PORT_ADDRESS_NOT_AVAILABLE_ERROR = "`port` address is not available. Check network configuration."
24
+ PORT_BIND_GENERIC_ERROR = "Failed to start server on port `{port}`: {error}"
@@ -3,6 +3,8 @@
3
3
 
4
4
  import http.server
5
5
  import socketserver
6
+ import webbrowser
7
+ import errno
6
8
  from typing import Optional, Any
7
9
  from pathlib import Path
8
10
  from urllib.parse import unquote
@@ -17,7 +19,15 @@ from .params import (
17
19
  INVALID_STRICT_TYPE_ERROR,
18
20
  INVALID_NO_CACHE_TYPE_ERROR,
19
21
  INVALID_THREADED_TYPE_ERROR,
22
+ INVALID_AUTO_OPEN_TYPE_ERROR
20
23
  )
24
+ from .params import (
25
+ PORT_IN_USE_ERROR,
26
+ PORT_ACCESS_DENIED_ERROR,
27
+ PORT_ADDRESS_NOT_AVAILABLE_ERROR,
28
+ PORT_BIND_GENERIC_ERROR,
29
+ )
30
+ from .errors import GHPSValidationError, GHPSRuntimeError
21
31
 
22
32
 
23
33
  def _validate_inputs(
@@ -27,6 +37,7 @@ def _validate_inputs(
27
37
  strict: Any,
28
38
  no_cache: Any,
29
39
  threaded: Any,
40
+ auto_open: Any,
30
41
  ):
31
42
  """
32
43
  Validate GHPageServer inputs.
@@ -37,37 +48,65 @@ def _validate_inputs(
37
48
  :param strict: If False, enables automatic ".html" resolution.
38
49
  :param no_cache: If True, disables client-side caching.
39
50
  :param threaded: If True, handles requests using threads.
51
+ :param auto_open: If True, automatically opens the server URL in the default web browser.
40
52
  """
41
53
  if not isinstance(directory, (str, Path)):
42
- raise ValueError(INVALID_DIRECTORY_TYPE_ERROR)
54
+ raise GHPSValidationError(INVALID_DIRECTORY_TYPE_ERROR)
43
55
 
44
56
  directory = Path(directory)
45
57
  if not directory.exists():
46
- raise ValueError(DIRECTORY_NOT_FOUND_ERROR)
58
+ raise GHPSValidationError(DIRECTORY_NOT_FOUND_ERROR)
47
59
 
48
60
  if not directory.is_dir():
49
- raise ValueError(DIRECTORY_NOT_DIR_ERROR)
61
+ raise GHPSValidationError(DIRECTORY_NOT_DIR_ERROR)
50
62
 
51
63
  if not isinstance(port, int):
52
- raise ValueError(INVALID_PORT_TYPE_ERROR)
64
+ raise GHPSValidationError(INVALID_PORT_TYPE_ERROR)
53
65
 
54
- if not (1 <= port <= 65535):
55
- raise ValueError(INVALID_PORT_RANGE_ERROR)
66
+ if not (0 <= port <= 65535):
67
+ raise GHPSValidationError(INVALID_PORT_RANGE_ERROR)
56
68
 
57
69
  if not isinstance(base_path, str):
58
- raise ValueError(INVALID_BASE_PATH_TYPE_ERROR)
70
+ raise GHPSValidationError(INVALID_BASE_PATH_TYPE_ERROR)
59
71
 
60
72
  if base_path and not base_path.startswith("/"):
61
- raise ValueError(INVALID_BASE_PATH_FORMAT_ERROR)
73
+ raise GHPSValidationError(INVALID_BASE_PATH_FORMAT_ERROR)
62
74
 
63
75
  if not isinstance(strict, bool):
64
- raise ValueError(INVALID_STRICT_TYPE_ERROR)
76
+ raise GHPSValidationError(INVALID_STRICT_TYPE_ERROR)
65
77
 
66
78
  if not isinstance(no_cache, bool):
67
- raise ValueError(INVALID_NO_CACHE_TYPE_ERROR)
79
+ raise GHPSValidationError(INVALID_NO_CACHE_TYPE_ERROR)
68
80
 
69
81
  if not isinstance(threaded, bool):
70
- raise ValueError(INVALID_THREADED_TYPE_ERROR)
82
+ raise GHPSValidationError(INVALID_THREADED_TYPE_ERROR)
83
+
84
+ if not isinstance(auto_open, bool):
85
+ raise GHPSValidationError(INVALID_AUTO_OPEN_TYPE_ERROR)
86
+
87
+
88
+ def _handle_bind_error(port: int, e: OSError) -> None:
89
+ """
90
+ Handle socket binding errors and raise a user-friendly runtime exception.
91
+
92
+ :param port: Port number that the server attempted to bind to.
93
+ :param e: Original OSError raised during binding.
94
+ """
95
+ err = getattr(e, "errno", None)
96
+
97
+ if err == errno.EADDRINUSE:
98
+ raise GHPSRuntimeError(PORT_IN_USE_ERROR) from e
99
+
100
+ elif err in (errno.EACCES, errno.EPERM):
101
+ raise GHPSRuntimeError(PORT_ACCESS_DENIED_ERROR) from e
102
+
103
+ elif err == errno.EADDRNOTAVAIL:
104
+ raise GHPSRuntimeError(PORT_ADDRESS_NOT_AVAILABLE_ERROR) from e
105
+
106
+ else:
107
+ raise GHPSRuntimeError(
108
+ PORT_BIND_GENERIC_ERROR.format(port=port, error=e)
109
+ ) from e
71
110
 
72
111
 
73
112
  class _GHRequestHandler(http.server.SimpleHTTPRequestHandler):
@@ -178,6 +217,7 @@ class GHPageServer:
178
217
  strict: bool = True,
179
218
  no_cache: bool = False,
180
219
  threaded: bool = True,
220
+ auto_open: bool = False,
181
221
  ):
182
222
  """
183
223
  Initialize the server.
@@ -188,6 +228,7 @@ class GHPageServer:
188
228
  :param strict: If False, enables automatic ".html" resolution.
189
229
  :param no_cache: If True, disables client-side caching.
190
230
  :param threaded: If True, handles requests using threads.
231
+ :param auto_open: If True, automatically opens the server URL in the default web browser.
191
232
  """
192
233
  _validate_inputs(
193
234
  directory=directory,
@@ -196,6 +237,7 @@ class GHPageServer:
196
237
  strict=strict,
197
238
  no_cache=no_cache,
198
239
  threaded=threaded,
240
+ auto_open=auto_open,
199
241
  )
200
242
  self._directory = str(Path(directory).resolve())
201
243
  self._port = port
@@ -203,6 +245,7 @@ class GHPageServer:
203
245
  self._strict = strict
204
246
  self._no_cache = no_cache
205
247
  self._threaded = threaded
248
+ self._auto_open = auto_open
206
249
  self._httpd = None
207
250
 
208
251
  def start(self) -> None:
@@ -222,13 +265,22 @@ class GHPageServer:
222
265
 
223
266
  server_cls = _ThreadedTCPServer if self._threaded else socketserver.TCPServer
224
267
 
225
- self._httpd = server_cls(("", self._port), handler)
226
-
227
- print(f"Serving at http://localhost:{self._port}{self._base_path}")
268
+ try:
269
+ self._httpd = server_cls(("", self._port), handler)
270
+ except OSError as e:
271
+ _handle_bind_error(self._port, e)
272
+ self._port = self._httpd.server_address[1]
273
+ url = f"http://localhost:{self._port}{self._base_path}"
274
+ print(f"Serving at {url}")
228
275
  print(f"Directory: {self._directory}")
229
276
  print(f"Strict mode: {'ON' if self._strict else 'OFF'}")
230
277
  print(f"Cache disabled: {'YES' if self._no_cache else 'NO'}")
231
278
 
279
+ if self._auto_open:
280
+ try:
281
+ webbrowser.open(url)
282
+ except Exception:
283
+ print("[GHPS ERROR]: Failed to open browser automatically")
232
284
  try:
233
285
  self._httpd.serve_forever()
234
286
  except KeyboardInterrupt:
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghps
3
- Version: 0.2
3
+ Version: 0.4
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.2
6
+ Download-URL: https://github.com/sepandhaghighi/ghps/tarball/v0.4
7
7
  Author: Sepand Haghighi
8
8
  Author-email: me@sepand.tech
9
9
  License: MIT
@@ -101,13 +101,13 @@ Dynamic: summary
101
101
  ## Installation
102
102
 
103
103
  ### Source Code
104
- - Download [Version 0.2](https://github.com/sepandhaghighi/ghps/archive/v0.2.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
104
+ - Download [Version 0.4](https://github.com/sepandhaghighi/ghps/archive/v0.4.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.2`
110
+ - `pip install ghps==0.4`
111
111
 
112
112
 
113
113
  ## Usage
@@ -130,6 +130,9 @@ ghps --port=8080 --no-cache --no-threaded
130
130
  | `--no-strict` | Enable `.html` fallback| Strict mode enabled |
131
131
  | `--no-cache` | Disable HTTP caching headers | Cache enabled |
132
132
  | `--no-threaded` | Disable threaded server | Threaded enabled |
133
+ | `--auto-open` | Automatically open the server URL | Disable |
134
+
135
+ ℹ️ You can set `--port=0` to let the OS automatically select an available port
133
136
 
134
137
 
135
138
  ### Library
@@ -144,6 +147,7 @@ server = GHPageServer(
144
147
  strict=True,
145
148
  no_cache=False,
146
149
  threaded=True,
150
+ auto_open=True,
147
151
  )
148
152
 
149
153
  server.start()
@@ -159,6 +163,9 @@ server.start()
159
163
  | `strict` | `bool` | Enforce GitHub-like routing behavior | `True` |
160
164
  | `no_cache` | `bool` | Disable cache headers | `False` |
161
165
  | `threaded` | `bool` | Enable threaded request handling | `True` |
166
+ | `auto_open` | `bool` | Automatically open the server URL | `False` |
167
+
168
+ ℹ️ You can set `port=0` to let the OS automatically select an available port
162
169
 
163
170
  ## Issues & Bug Reports
164
171
 
@@ -209,6 +216,23 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
209
216
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
210
217
 
211
218
  ## [Unreleased]
219
+ ## [0.4] - 2026-05-08
220
+ ### Added
221
+ - `GHPSError` class
222
+ - `GHPSValidationError` class
223
+ - `GHPSRuntimeError` class
224
+ - Port error messages
225
+ ### Changed
226
+ - Test system modified
227
+ ## [0.3] - 2026-04-05
228
+ ### Added
229
+ - `auto_open` parameter
230
+ - `--auto-open` CLI argument
231
+ - Random port support
232
+ ### Changed
233
+ - Test system modified
234
+ - CLI error messages updated
235
+ - `README.md` updated
212
236
  ## [0.2] - 2026-03-19
213
237
  ### Added
214
238
  - Logo
@@ -222,7 +246,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
222
246
  - Command line interface
223
247
  - Test system
224
248
 
225
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.2...dev
249
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.4...dev
250
+ [0.4]: https://github.com/sepandhaghighi/ghps/compare/v0.3...v0.4
251
+ [0.3]: https://github.com/sepandhaghighi/ghps/compare/v0.2...v0.3
226
252
  [0.2]: https://github.com/sepandhaghighi/ghps/compare/v0.1...v0.2
227
253
  [0.1]: https://github.com/sepandhaghighi/ghps/compare/bc0a425...v0.1
228
254
 
@@ -10,6 +10,7 @@ setup.py
10
10
  ghps/__init__.py
11
11
  ghps/__main__.py
12
12
  ghps/cli.py
13
+ ghps/errors.py
13
14
  ghps/params.py
14
15
  ghps/server.py
15
16
  ghps.egg-info/PKG-INFO
@@ -32,7 +32,7 @@ def read_description() -> str:
32
32
  setup(
33
33
  name='ghps',
34
34
  packages=['ghps'],
35
- version='0.2',
35
+ version='0.4',
36
36
  description='Ghps: A Minimal GitHub Pages Simulator for Local Development',
37
37
  long_description=read_description(),
38
38
  long_description_content_type='text/markdown',
@@ -40,7 +40,7 @@ setup(
40
40
  author='Sepand Haghighi',
41
41
  author_email='me@sepand.tech',
42
42
  url='https://github.com/sepandhaghighi/ghps',
43
- download_url='https://github.com/sepandhaghighi/ghps/tarball/v0.2',
43
+ download_url='https://github.com/sepandhaghighi/ghps/tarball/v0.4',
44
44
  keywords="github gh-pages static-server static-hosting development server local http simulator testing cli python",
45
45
  project_urls={
46
46
  'Source': 'https://github.com/sepandhaghighi/ghps'
@@ -110,3 +110,17 @@ def test_cli_invalid_directory(monkeypatch):
110
110
 
111
111
  with pytest.raises(SystemExit):
112
112
  ghps.cli.main()
113
+
114
+
115
+ @patch("ghps.cli.GHPageServer")
116
+ @patch("sys.stderr")
117
+ def test_cli_unexpected_error(mock_stderr, mock_server_cls):
118
+ mock_server = mock_server_cls.return_value
119
+ mock_server.start.side_effect = Exception("boom")
120
+ test_args = ["ghps"]
121
+ with patch("sys.argv", test_args):
122
+ with pytest.raises(SystemExit) as exc:
123
+ ghps.cli.main()
124
+ assert exc.value.code == 1
125
+ written = "".join(call.args[0] for call in mock_stderr.write.call_args_list)
126
+ assert "[GHPS ERROR] Unexpected error: boom" in written
@@ -0,0 +1,209 @@
1
+ # -*- coding: utf-8 -*-
2
+ import errno
3
+ import pytest
4
+ from unittest.mock import patch
5
+ from ghps import GHPageServer, GHPSValidationError, GHPSRuntimeError
6
+
7
+
8
+ def test_invalid_directory_type():
9
+ with pytest.raises(GHPSValidationError, match="`directory` must be str or pathlib.Path."):
10
+ GHPageServer(
11
+ directory=123,
12
+ port=8000,
13
+ base_path="",
14
+ strict=True,
15
+ no_cache=False,
16
+ threaded=True,
17
+ )
18
+
19
+
20
+ def test_directory_not_found():
21
+ with pytest.raises(GHPSValidationError, match="`directory` does not exist."):
22
+ GHPageServer(
23
+ directory="__not_existing_dir__",
24
+ port=8000,
25
+ base_path="",
26
+ strict=True,
27
+ no_cache=False,
28
+ threaded=True,
29
+ )
30
+
31
+
32
+ def test_directory_not_directory(tmp_path):
33
+ file_path = tmp_path / "file.txt"
34
+ file_path.write_text("data")
35
+
36
+ with pytest.raises(GHPSValidationError, match="`directory` must be a valid directory."):
37
+ GHPageServer(
38
+ directory=file_path,
39
+ port=8000,
40
+ base_path="",
41
+ strict=True,
42
+ no_cache=False,
43
+ threaded=True,
44
+ )
45
+
46
+
47
+ def test_invalid_port_type(tmp_path):
48
+ with pytest.raises(GHPSValidationError, match="`port` must be int."):
49
+ GHPageServer(
50
+ directory=tmp_path,
51
+ port="8000",
52
+ base_path="",
53
+ strict=True,
54
+ no_cache=False,
55
+ threaded=True,
56
+ )
57
+
58
+
59
+ def test_invalid_port_range(tmp_path):
60
+ with pytest.raises(GHPSValidationError, match="`port` must be between 1 and 65535."):
61
+ GHPageServer(
62
+ directory=tmp_path,
63
+ port=70000,
64
+ base_path="",
65
+ strict=True,
66
+ no_cache=False,
67
+ threaded=True,
68
+ )
69
+
70
+
71
+ def test_invalid_base_path_type(tmp_path):
72
+ with pytest.raises(GHPSValidationError, match="`base_path` must be str."):
73
+ GHPageServer(
74
+ directory=tmp_path,
75
+ port=8000,
76
+ base_path=123,
77
+ strict=True,
78
+ no_cache=False,
79
+ threaded=True,
80
+ )
81
+
82
+
83
+ def test_invalid_base_path_format(tmp_path):
84
+ with pytest.raises(GHPSValidationError, match="`base_path` must start with '/' or be empty."):
85
+ GHPageServer(
86
+ directory=tmp_path,
87
+ port=8000,
88
+ base_path="docs",
89
+ strict=True,
90
+ no_cache=False,
91
+ threaded=True,
92
+ )
93
+
94
+
95
+ def test_invalid_strict_type(tmp_path):
96
+ with pytest.raises(GHPSValidationError, match="`strict` must be bool."):
97
+ GHPageServer(
98
+ directory=tmp_path,
99
+ port=8000,
100
+ base_path="",
101
+ strict="yes",
102
+ no_cache=False,
103
+ threaded=True,
104
+ )
105
+
106
+
107
+ def test_invalid_no_cache_type(tmp_path):
108
+ with pytest.raises(GHPSValidationError, match="`no_cache` must be bool."):
109
+ GHPageServer(
110
+ directory=tmp_path,
111
+ port=8000,
112
+ base_path="",
113
+ strict=True,
114
+ no_cache="false",
115
+ threaded=True,
116
+ )
117
+
118
+
119
+ def test_invalid_threaded_type(tmp_path):
120
+ with pytest.raises(GHPSValidationError, match="`threaded` must be bool."):
121
+ GHPageServer(
122
+ directory=tmp_path,
123
+ port=8000,
124
+ base_path="",
125
+ strict=True,
126
+ no_cache=False,
127
+ threaded="true",
128
+ )
129
+
130
+
131
+ def test_invalid_auto_open_type(tmp_path):
132
+ with pytest.raises(GHPSValidationError, match="`auto_open` must be bool."):
133
+ GHPageServer(
134
+ directory=tmp_path,
135
+ port=8000,
136
+ base_path="",
137
+ strict=True,
138
+ no_cache=False,
139
+ threaded=True,
140
+ auto_open="true",
141
+ )
142
+
143
+
144
+ @patch("ghps.server.webbrowser.open", side_effect=Exception("boom"))
145
+ @patch("ghps.server._ThreadedTCPServer")
146
+ @patch("builtins.print")
147
+ def test_auto_open_failure_logs_error(mock_print, mock_server_cls, mock_web_open, tmp_path):
148
+ mock_server = mock_server_cls.return_value
149
+ mock_server.server_address = ("127.0.0.1", 8000)
150
+ mock_server.serve_forever.side_effect = KeyboardInterrupt
151
+ server = GHPageServer(
152
+ directory=tmp_path,
153
+ port=8000,
154
+ auto_open=True,
155
+ )
156
+ server.start()
157
+ mock_web_open.assert_called_once_with("http://localhost:8000")
158
+ assert any(
159
+ "[GHPS ERROR]: Failed to open browser automatically" in str(call)
160
+ for call in mock_print.call_args_list
161
+ )
162
+
163
+
164
+ @patch("ghps.server._ThreadedTCPServer")
165
+ def test_port_in_use_error(mock_server_cls, tmp_path):
166
+ mock_server_cls.side_effect = OSError(errno.EADDRINUSE, "in use")
167
+
168
+ server = GHPageServer(directory=tmp_path, port=8000)
169
+
170
+ with pytest.raises(GHPSRuntimeError) as exc:
171
+ server.start()
172
+
173
+ assert "`port` is already in use. Try another port or use 0." in str(exc.value)
174
+
175
+
176
+ @patch("ghps.server._ThreadedTCPServer")
177
+ def test_port_access_denied_error(mock_server_cls, tmp_path):
178
+ mock_server_cls.side_effect = OSError(errno.EACCES, "denied")
179
+
180
+ server = GHPageServer(directory=tmp_path, port=80)
181
+
182
+ with pytest.raises(GHPSRuntimeError) as exc:
183
+ server.start()
184
+
185
+ assert "`port` access denied. Try a different port or check permissions." in str(exc.value)
186
+
187
+
188
+ @patch("ghps.server._ThreadedTCPServer")
189
+ def test_port_generic_error(mock_server_cls, tmp_path):
190
+ mock_server_cls.side_effect = OSError(9999, "boom")
191
+
192
+ server = GHPageServer(directory=tmp_path, port=8000)
193
+
194
+ with pytest.raises(GHPSRuntimeError) as exc:
195
+ server.start()
196
+
197
+ assert "Failed to start server on port" in str(exc.value)
198
+
199
+
200
+ @patch("ghps.server._ThreadedTCPServer")
201
+ def test_port_address_not_available_error(mock_server_cls, tmp_path):
202
+ mock_server_cls.side_effect = OSError(errno.EADDRNOTAVAIL, "not-available")
203
+
204
+ server = GHPageServer(directory=tmp_path, port=80)
205
+
206
+ with pytest.raises(GHPSRuntimeError) as exc:
207
+ server.start()
208
+
209
+ assert "`port` address is not available. Check network configuration." in str(exc.value)
@@ -128,3 +128,52 @@ def test_keyboard_interrupt(capsys):
128
128
 
129
129
  captured = capsys.readouterr()
130
130
  assert "Server stopped." in captured.out
131
+
132
+
133
+ @patch("ghps.server.webbrowser.open")
134
+ @patch("ghps.server._ThreadedTCPServer")
135
+ def test_auto_open_enabled(mock_server_cls, mock_web_open, tmp_path):
136
+ mock_server = mock_server_cls.return_value
137
+ mock_server.server_address = ("127.0.0.1", 8000)
138
+ mock_server.serve_forever.side_effect = KeyboardInterrupt
139
+
140
+ server = GHPageServer(
141
+ directory=tmp_path,
142
+ port=8000,
143
+ auto_open=True,
144
+ )
145
+
146
+ server.start()
147
+
148
+ mock_web_open.assert_called_once_with("http://localhost:8000")
149
+
150
+
151
+ @patch("ghps.server.webbrowser.open")
152
+ @patch("ghps.server._ThreadedTCPServer")
153
+ def test_auto_open_disabled(mock_server_cls, mock_web_open, tmp_path):
154
+ mock_server = mock_server_cls.return_value
155
+ mock_server.server_address = ("127.0.0.1", 8000)
156
+ mock_server.serve_forever.side_effect = KeyboardInterrupt
157
+
158
+ server = GHPageServer(
159
+ directory=tmp_path,
160
+ port=8000,
161
+ auto_open=False,
162
+ )
163
+
164
+ server.start()
165
+
166
+ mock_web_open.assert_not_called()
167
+
168
+
169
+ @patch("ghps.server._ThreadedTCPServer")
170
+ def test_random_port_resolution(mock_server_cls, tmp_path):
171
+ mock_server = mock_server_cls.return_value
172
+ mock_server.server_address = ("127.0.0.1", 54321)
173
+ mock_server.serve_forever.side_effect = KeyboardInterrupt
174
+ server = GHPageServer(
175
+ directory=tmp_path,
176
+ port=0,
177
+ )
178
+ server.start()
179
+ assert server._port == 54321
ghps-0.2/ghps/__init__.py DELETED
@@ -1,7 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- """ghps modules."""
3
- from .params import GHPS_VERSION
4
- from .server import GHPageServer
5
- __version__ = GHPS_VERSION
6
-
7
- __all__ = ["GHPageServer"]
@@ -1,126 +0,0 @@
1
- # -*- coding: utf-8 -*-
2
- import pytest
3
- from ghps import GHPageServer
4
-
5
-
6
- def test_invalid_directory_type():
7
- with pytest.raises(ValueError, match="`directory` must be str or pathlib.Path."):
8
- GHPageServer(
9
- directory=123,
10
- port=8000,
11
- base_path="",
12
- strict=True,
13
- no_cache=False,
14
- threaded=True,
15
- )
16
-
17
-
18
- def test_directory_not_found():
19
- with pytest.raises(ValueError, match="`directory` does not exist."):
20
- GHPageServer(
21
- directory="__not_existing_dir__",
22
- port=8000,
23
- base_path="",
24
- strict=True,
25
- no_cache=False,
26
- threaded=True,
27
- )
28
-
29
-
30
- def test_directory_not_directory(tmp_path):
31
- file_path = tmp_path / "file.txt"
32
- file_path.write_text("data")
33
-
34
- with pytest.raises(ValueError, match="`directory` must be a valid directory."):
35
- GHPageServer(
36
- directory=file_path,
37
- port=8000,
38
- base_path="",
39
- strict=True,
40
- no_cache=False,
41
- threaded=True,
42
- )
43
-
44
-
45
- def test_invalid_port_type(tmp_path):
46
- with pytest.raises(ValueError, match="`port` must be int."):
47
- GHPageServer(
48
- directory=tmp_path,
49
- port="8000",
50
- base_path="",
51
- strict=True,
52
- no_cache=False,
53
- threaded=True,
54
- )
55
-
56
-
57
- def test_invalid_port_range(tmp_path):
58
- with pytest.raises(ValueError, match="`port` must be between 1 and 65535."):
59
- GHPageServer(
60
- directory=tmp_path,
61
- port=70000,
62
- base_path="",
63
- strict=True,
64
- no_cache=False,
65
- threaded=True,
66
- )
67
-
68
-
69
- def test_invalid_base_path_type(tmp_path):
70
- with pytest.raises(ValueError, match="`base_path` must be str."):
71
- GHPageServer(
72
- directory=tmp_path,
73
- port=8000,
74
- base_path=123,
75
- strict=True,
76
- no_cache=False,
77
- threaded=True,
78
- )
79
-
80
-
81
- def test_invalid_base_path_format(tmp_path):
82
- with pytest.raises(ValueError, match="`base_path` must start with '/' or be empty."):
83
- GHPageServer(
84
- directory=tmp_path,
85
- port=8000,
86
- base_path="docs",
87
- strict=True,
88
- no_cache=False,
89
- threaded=True,
90
- )
91
-
92
-
93
- def test_invalid_strict_type(tmp_path):
94
- with pytest.raises(ValueError, match="`strict` must be bool."):
95
- GHPageServer(
96
- directory=tmp_path,
97
- port=8000,
98
- base_path="",
99
- strict="yes",
100
- no_cache=False,
101
- threaded=True,
102
- )
103
-
104
-
105
- def test_invalid_no_cache_type(tmp_path):
106
- with pytest.raises(ValueError, match="`no_cache` must be bool."):
107
- GHPageServer(
108
- directory=tmp_path,
109
- port=8000,
110
- base_path="",
111
- strict=True,
112
- no_cache="false",
113
- threaded=True,
114
- )
115
-
116
-
117
- def test_invalid_threaded_type(tmp_path):
118
- with pytest.raises(ValueError, match="`threaded` must be bool."):
119
- GHPageServer(
120
- directory=tmp_path,
121
- port=8000,
122
- base_path="",
123
- strict=True,
124
- no_cache=False,
125
- threaded="true",
126
- )
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