ghps 0.7__tar.gz → 0.8__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,17 @@ 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.8] - 2026-11-08
9
+ ### Added
10
+ - `quiet` parameter
11
+ - `--quiet` CLI argument
12
+ - `--config` CLI argument
13
+ - `from_config` method
14
+ ### Changed
15
+ - Test system modified
16
+ - Build system modified
17
+ - `Python 3.15` added to `test.yml`
18
+ - `README.md` updated
8
19
  ## [0.7] - 2026-09-22
9
20
  ### Added
10
21
  - `host` parameter
@@ -59,7 +70,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
59
70
  - Command line interface
60
71
  - Test system
61
72
 
62
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.7...dev
73
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.8...dev
74
+ [0.8]: https://github.com/sepandhaghighi/ghps/compare/v0.7...v0.8
63
75
  [0.7]: https://github.com/sepandhaghighi/ghps/compare/v0.6...v0.7
64
76
  [0.6]: https://github.com/sepandhaghighi/ghps/compare/v0.5...v0.6
65
77
  [0.5]: https://github.com/sepandhaghighi/ghps/compare/v0.4...v0.5
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghps
3
- Version: 0.7
3
+ Version: 0.8
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.7
6
+ Download-URL: https://github.com/sepandhaghighi/ghps/tarball/v0.8
7
7
  Author: Sepand Haghighi
8
8
  Author-email: me@sepand.tech
9
9
  License: MIT
@@ -11,7 +11,6 @@ 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
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Natural Language :: English
14
- Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Operating System :: OS Independent
16
15
  Classifier: Environment :: Console
17
16
  Classifier: Programming Language :: Python :: 3.7
@@ -22,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
22
  Classifier: Programming Language :: Python :: 3.13
24
23
  Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
25
25
  Classifier: Intended Audience :: Developers
26
26
  Classifier: Intended Audience :: Education
27
27
  Classifier: Topic :: Utilities
@@ -33,6 +33,7 @@ Description-Content-Type: text/markdown
33
33
  License-File: LICENSE
34
34
  License-File: AUTHORS.md
35
35
  Requires-Dist: art>=5.3
36
+ Requires-Dist: tomli>=2.0.2
36
37
  Dynamic: author
37
38
  Dynamic: author-email
38
39
  Dynamic: classifier
@@ -62,7 +63,7 @@ Dynamic: summary
62
63
  ## Overview
63
64
 
64
65
  <p align="justify">
65
- <b>Ghps</b> is a minimal, zero-dependency GitHub Pages simulator written in pure Python for local development and testing. It accurately replicates GitHub Pages' static hosting behavior, including proper <code>404.html</code> handling, project base path simulation, strict routing mode, and optional cache control - all while remaining lightweight, fast, and usable both as a CLI tool and as an importable Python library.
66
+ <b>Ghps</b> is a minimal GitHub Pages simulator written in pure Python for local development and testing. It accurately replicates GitHub Pages' static hosting behavior, including proper <code>404.html</code> handling, project base path simulation, strict routing mode, and optional cache control - all while remaining lightweight, fast, and usable both as a CLI tool and as an importable Python library.
66
67
  </p>
67
68
 
68
69
  <table>
@@ -101,13 +102,13 @@ Dynamic: summary
101
102
  ## Installation
102
103
 
103
104
  ### Source Code
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
+ - Download [Version 0.8](https://github.com/sepandhaghighi/ghps/archive/v0.8.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
105
106
  - `pip install .`
106
107
 
107
108
  ### PyPI
108
109
 
109
110
  - Check [Python Packaging User Guide](https://packaging.python.org/installing/)
110
- - `pip install ghps==0.7`
111
+ - `pip install ghps==0.8`
111
112
 
112
113
 
113
114
  ## Usage
@@ -124,6 +125,7 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
124
125
 
125
126
  | Option | Description | Default |
126
127
  |--------|-------------|----------|
128
+ | `-c, --config` | Path to configuration file | Auto discovers `.ghps` |
127
129
  | `--host` | Host address to bind to | `localhost` |
128
130
  | `-p, --port` | Port to serve on | `8000` |
129
131
  | `-d, --directory` | Directory to serve | Current directory (`.`) |
@@ -133,6 +135,7 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
133
135
  | `--no-threaded` | Disable threaded server | Threaded enabled |
134
136
  | `--auto-open` | Automatically open the server URL | Disable |
135
137
  | `--directory-listing` | Enable directory listing | Disable |
138
+ | `--quiet` | Suppress informational server output | Disable |
136
139
 
137
140
  > [!NOTE]
138
141
  > You can set `--port=0` to let the OS automatically select an available port.
@@ -140,24 +143,32 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
140
143
  > [!NOTE]
141
144
  > 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`.
142
145
 
146
+ > [!NOTE]
147
+ > Ghps automatically looks for a `.ghps` configuration file in the served directory if available, or you can specify a custom configuration file path using `-c` / `--config`.
148
+
143
149
 
144
150
  ### Library
145
151
 
146
152
  ```python
147
153
  from ghps import GHPageServer
148
154
 
149
- server = GHPageServer(
155
+ server1 = GHPageServer(
150
156
  directory="dist",
151
- host="localhost",
157
+ host="localhost",
152
158
  port=5000,
153
159
  base_path="/repo-name",
154
160
  strict=True,
155
161
  no_cache=False,
156
162
  threaded=True,
157
- auto_open=True,
163
+ auto_open=True,
164
+ quiet=False,
158
165
  )
159
166
 
160
- server.start()
167
+ server1.start()
168
+
169
+ server2 = GHPageServer.from_config(config="config.toml")
170
+
171
+ server2.start()
161
172
  ```
162
173
 
163
174
  #### Parameters
@@ -173,6 +184,7 @@ server.start()
173
184
  | `threaded` | `bool` | Enable threaded request handling | `True` |
174
185
  | `auto_open` | `bool` | Automatically open the server URL | `False` |
175
186
  | `directory_listing` | `bool` | Enable directory listing | `False` |
187
+ | `quiet` | `bool` | Suppress informational server output | `False` |
176
188
 
177
189
  > [!NOTE]
178
190
  > You can set `port=0` to let the OS automatically select an available port.
@@ -180,6 +192,23 @@ server.start()
180
192
  > [!NOTE]
181
193
  > The `host` parameter controls the network interface used for server binding. The default is `"localhost"`.
182
194
 
195
+ ### Configuration File
196
+
197
+ You can define your server settings in a **TOML** configuration file (typically named `.ghps`):
198
+
199
+ ```toml
200
+ directory = "dist"
201
+ host = "localhost"
202
+ port = 8080
203
+ base_path = "/repo-name"
204
+ strict = true
205
+ no_cache = false
206
+ threaded = true
207
+ auto_open = false
208
+ directory_listing = false
209
+ quiet = false
210
+ ```
211
+
183
212
  ## Issues & Bug Reports
184
213
 
185
214
  Just fill an issue and describe it. We'll check it ASAP!
@@ -229,6 +258,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
229
258
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
230
259
 
231
260
  ## [Unreleased]
261
+ ## [0.8] - 2026-11-08
262
+ ### Added
263
+ - `quiet` parameter
264
+ - `--quiet` CLI argument
265
+ - `--config` CLI argument
266
+ - `from_config` method
267
+ ### Changed
268
+ - Test system modified
269
+ - Build system modified
270
+ - `Python 3.15` added to `test.yml`
271
+ - `README.md` updated
232
272
  ## [0.7] - 2026-09-22
233
273
  ### Added
234
274
  - `host` parameter
@@ -283,7 +323,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
283
323
  - Command line interface
284
324
  - Test system
285
325
 
286
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.7...dev
326
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.8...dev
327
+ [0.8]: https://github.com/sepandhaghighi/ghps/compare/v0.7...v0.8
287
328
  [0.7]: https://github.com/sepandhaghighi/ghps/compare/v0.6...v0.7
288
329
  [0.6]: https://github.com/sepandhaghighi/ghps/compare/v0.5...v0.6
289
330
  [0.5]: https://github.com/sepandhaghighi/ghps/compare/v0.4...v0.5
@@ -11,7 +11,7 @@
11
11
  ## Overview
12
12
 
13
13
  <p align="justify">
14
- <b>Ghps</b> is a minimal, zero-dependency GitHub Pages simulator written in pure Python for local development and testing. It accurately replicates GitHub Pages' static hosting behavior, including proper <code>404.html</code> handling, project base path simulation, strict routing mode, and optional cache control - all while remaining lightweight, fast, and usable both as a CLI tool and as an importable Python library.
14
+ <b>Ghps</b> is a minimal GitHub Pages simulator written in pure Python for local development and testing. It accurately replicates GitHub Pages' static hosting behavior, including proper <code>404.html</code> handling, project base path simulation, strict routing mode, and optional cache control - all while remaining lightweight, fast, and usable both as a CLI tool and as an importable Python library.
15
15
  </p>
16
16
 
17
17
  <table>
@@ -50,13 +50,13 @@
50
50
  ## Installation
51
51
 
52
52
  ### Source Code
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)
53
+ - Download [Version 0.8](https://github.com/sepandhaghighi/ghps/archive/v0.8.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.7`
59
+ - `pip install ghps==0.8`
60
60
 
61
61
 
62
62
  ## Usage
@@ -73,6 +73,7 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
73
73
 
74
74
  | Option | Description | Default |
75
75
  |--------|-------------|----------|
76
+ | `-c, --config` | Path to configuration file | Auto discovers `.ghps` |
76
77
  | `--host` | Host address to bind to | `localhost` |
77
78
  | `-p, --port` | Port to serve on | `8000` |
78
79
  | `-d, --directory` | Directory to serve | Current directory (`.`) |
@@ -82,6 +83,7 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
82
83
  | `--no-threaded` | Disable threaded server | Threaded enabled |
83
84
  | `--auto-open` | Automatically open the server URL | Disable |
84
85
  | `--directory-listing` | Enable directory listing | Disable |
86
+ | `--quiet` | Suppress informational server output | Disable |
85
87
 
86
88
  > [!NOTE]
87
89
  > You can set `--port=0` to let the OS automatically select an available port.
@@ -89,24 +91,32 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
89
91
  > [!NOTE]
90
92
  > 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`.
91
93
 
94
+ > [!NOTE]
95
+ > Ghps automatically looks for a `.ghps` configuration file in the served directory if available, or you can specify a custom configuration file path using `-c` / `--config`.
96
+
92
97
 
93
98
  ### Library
94
99
 
95
100
  ```python
96
101
  from ghps import GHPageServer
97
102
 
98
- server = GHPageServer(
103
+ server1 = GHPageServer(
99
104
  directory="dist",
100
- host="localhost",
105
+ host="localhost",
101
106
  port=5000,
102
107
  base_path="/repo-name",
103
108
  strict=True,
104
109
  no_cache=False,
105
110
  threaded=True,
106
- auto_open=True,
111
+ auto_open=True,
112
+ quiet=False,
107
113
  )
108
114
 
109
- server.start()
115
+ server1.start()
116
+
117
+ server2 = GHPageServer.from_config(config="config.toml")
118
+
119
+ server2.start()
110
120
  ```
111
121
 
112
122
  #### Parameters
@@ -122,6 +132,7 @@ server.start()
122
132
  | `threaded` | `bool` | Enable threaded request handling | `True` |
123
133
  | `auto_open` | `bool` | Automatically open the server URL | `False` |
124
134
  | `directory_listing` | `bool` | Enable directory listing | `False` |
135
+ | `quiet` | `bool` | Suppress informational server output | `False` |
125
136
 
126
137
  > [!NOTE]
127
138
  > You can set `port=0` to let the OS automatically select an available port.
@@ -129,6 +140,23 @@ server.start()
129
140
  > [!NOTE]
130
141
  > The `host` parameter controls the network interface used for server binding. The default is `"localhost"`.
131
142
 
143
+ ### Configuration File
144
+
145
+ You can define your server settings in a **TOML** configuration file (typically named `.ghps`):
146
+
147
+ ```toml
148
+ directory = "dist"
149
+ host = "localhost"
150
+ port = 8080
151
+ base_path = "/repo-name"
152
+ strict = true
153
+ no_cache = false
154
+ threaded = true
155
+ auto_open = false
156
+ directory_listing = false
157
+ quiet = false
158
+ ```
159
+
132
160
  ## Issues & Bug Reports
133
161
 
134
162
  Just fill an issue and describe it. We'll check it ASAP!
@@ -4,8 +4,8 @@
4
4
 
5
5
  | Version | Supported |
6
6
  | ------------- | ------------------ |
7
- | 0.7 | :white_check_mark: |
8
- | < 0.7 | :x: |
7
+ | 0.8 | :white_check_mark: |
8
+ | < 0.8 | :x: |
9
9
 
10
10
  ## Reporting a Vulnerability
11
11
 
@@ -32,62 +32,81 @@ def _parse_args() -> argparse.Namespace:
32
32
  help="Version"
33
33
  )
34
34
 
35
+ parser.add_argument(
36
+ "-c", "--config",
37
+ type=str,
38
+ default=None,
39
+ help="Path to configuration file"
40
+ )
41
+
35
42
  parser.add_argument(
36
43
  "--host",
37
44
  type=str,
38
- default="localhost",
45
+ default=None,
39
46
  help="Host address to bind to (default: localhost)"
40
47
  )
41
48
 
42
49
  parser.add_argument(
43
50
  "-p", "--port",
44
51
  type=int,
45
- default=8000,
52
+ default=None,
46
53
  help="Port to serve on (default: 8000, use 0 for automatic allocation)"
47
54
  )
48
55
 
49
56
  parser.add_argument(
50
57
  "-d", "--directory",
51
- default=".",
58
+ default=None,
52
59
  help="Directory to serve (default: current directory)"
53
60
  )
54
61
 
55
62
  parser.add_argument(
56
63
  "-b", "--base-path",
57
- default="",
64
+ default=None,
58
65
  help="Base path for project pages (e.g. /repo-name)"
59
66
  )
60
67
 
61
68
  parser.add_argument(
62
69
  "--no-strict",
63
70
  action="store_true",
71
+ default=None,
64
72
  help="Enable .html fallback"
65
73
  )
66
74
 
67
75
  parser.add_argument(
68
76
  "--no-cache",
69
77
  action="store_true",
78
+ default=None,
70
79
  help="Disable HTTP caching headers"
71
80
  )
72
81
 
73
82
  parser.add_argument(
74
83
  "--no-threaded",
75
84
  action="store_true",
85
+ default=None,
76
86
  help="Disable threaded server"
77
87
  )
78
88
 
79
89
  parser.add_argument(
80
90
  "--auto-open",
81
91
  action="store_true",
92
+ default=None,
82
93
  help="Automatically open the server URL"
83
94
  )
84
95
 
85
96
  parser.add_argument(
86
97
  "--directory-listing",
87
98
  action="store_true",
99
+ default=None,
88
100
  help="Enable directory listing"
89
101
  )
90
102
 
103
+ parser.add_argument(
104
+ "--quiet",
105
+ action="store_true",
106
+ default=None,
107
+ help="Suppress informational server output"
108
+ )
109
+
91
110
  args = parser.parse_args()
92
111
  return args
93
112
 
@@ -99,16 +118,19 @@ def main() -> None:
99
118
  if args.version:
100
119
  print(GHPS_VERSION)
101
120
  else:
102
- server = GHPageServer(
121
+ server = GHPageServer.from_config(
103
122
  directory=args.directory,
104
123
  host=args.host,
105
124
  port=args.port,
106
125
  base_path=args.base_path,
107
- strict=not args.no_strict,
126
+ strict=False if args.no_strict else None,
108
127
  no_cache=args.no_cache,
109
- threaded=not args.no_threaded,
128
+ threaded=False if args.no_threaded else None,
110
129
  auto_open=args.auto_open,
111
130
  directory_listing=args.directory_listing,
131
+ quiet=args.quiet,
132
+ config=args.config,
133
+ auto_discover=True,
112
134
  )
113
135
  server.start()
114
136
  except GHPSError as e:
@@ -1,7 +1,7 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  """ghps params."""
3
3
 
4
- GHPS_VERSION = "0.7"
4
+ GHPS_VERSION = "0.8"
5
5
  GHPS_REPO = "https://github.com/sepandhaghighi/ghps"
6
6
 
7
7
  INVALID_DIRECTORY_TYPE_ERROR = "`directory` must be str or pathlib.Path."
@@ -20,6 +20,11 @@ INVALID_NO_CACHE_TYPE_ERROR = "`no_cache` must be bool."
20
20
  INVALID_THREADED_TYPE_ERROR = "`threaded` must be bool."
21
21
  INVALID_AUTO_OPEN_TYPE_ERROR = "`auto_open` must be bool."
22
22
  INVALID_DIRECTORY_LISTING_TYPE_ERROR = "`directory_listing` must be bool."
23
+ INVALID_QUIET_TYPE_ERROR = "`quiet` must be bool."
24
+
25
+ INVALID_CONFIG_TYPE_ERROR = "`config` must be str, pathlib.Path, or dict."
26
+ INVALID_CONFIG_FILE_ERROR = "Failed to load config file: {error}"
27
+ INVALID_CONFIG_FORMAT_ERROR = "Config file content must be a valid TOML format."
23
28
 
24
29
  PORT_IN_USE_ERROR = "`port` is already in use. Try another port or use `0` for automatic allocation."
25
30
  PORT_ACCESS_DENIED_ERROR = "`port` access denied. Try a different port or check permissions."
@@ -7,9 +7,10 @@ import socketserver
7
7
  import webbrowser
8
8
  import errno
9
9
  from functools import partial
10
- from typing import Optional, Any
10
+ from typing import Optional, Any, Union
11
11
  from pathlib import Path
12
12
  from urllib.parse import unquote
13
+ import tomli
13
14
  from .params import (
14
15
  GHPS_VERSION,
15
16
  GHPS_REPO,
@@ -26,7 +27,11 @@ from .params import (
26
27
  INVALID_NO_CACHE_TYPE_ERROR,
27
28
  INVALID_THREADED_TYPE_ERROR,
28
29
  INVALID_AUTO_OPEN_TYPE_ERROR,
29
- INVALID_DIRECTORY_LISTING_TYPE_ERROR
30
+ INVALID_DIRECTORY_LISTING_TYPE_ERROR,
31
+ INVALID_QUIET_TYPE_ERROR,
32
+ INVALID_CONFIG_TYPE_ERROR,
33
+ INVALID_CONFIG_FILE_ERROR,
34
+ INVALID_CONFIG_FORMAT_ERROR
30
35
  )
31
36
  from .params import (
32
37
  PORT_IN_USE_ERROR,
@@ -46,7 +51,8 @@ def _validate_inputs(
46
51
  no_cache: Any,
47
52
  threaded: Any,
48
53
  auto_open: Any,
49
- directory_listing: Any
54
+ directory_listing: Any,
55
+ quiet: Any
50
56
  ):
51
57
  """
52
58
  Validate GHPageServer inputs.
@@ -60,6 +66,7 @@ def _validate_inputs(
60
66
  :param threaded: If True, handles requests using threads.
61
67
  :param auto_open: If True, automatically opens the server URL in the default web browser.
62
68
  :param directory_listing: If True, enables directory listing.
69
+ :param quiet: If True, suppresses informational server output.
63
70
  """
64
71
  if not isinstance(directory, (str, Path)):
65
72
  raise GHPSValidationError(INVALID_DIRECTORY_TYPE_ERROR)
@@ -101,6 +108,52 @@ def _validate_inputs(
101
108
  if not isinstance(directory_listing, bool):
102
109
  raise GHPSValidationError(INVALID_DIRECTORY_LISTING_TYPE_ERROR)
103
110
 
111
+ if not isinstance(quiet, bool):
112
+ raise GHPSValidationError(INVALID_QUIET_TYPE_ERROR)
113
+
114
+
115
+ def _load_config(directory: Union[str, Path], config_source: Optional[Union[str,
116
+ Path, dict]] = None, auto_discover: bool = False) -> dict:
117
+ """
118
+ Load config.
119
+
120
+ :param directory: Root directory.
121
+ :param config_source: Config source.
122
+ :param auto_discover: Auto discover flag.
123
+ """
124
+ if config_source is None:
125
+ if not auto_discover:
126
+ return {}
127
+ dir_path = Path(directory)
128
+ if dir_path.exists() and dir_path.is_dir():
129
+ candidate = dir_path / ".ghps"
130
+ if candidate.exists() and candidate.is_file():
131
+ config_path = candidate
132
+ else:
133
+ return {}
134
+ else:
135
+ return {}
136
+ elif isinstance(config_source, dict):
137
+ return config_source
138
+ elif isinstance(config_source, (str, Path)):
139
+ config_path = Path(config_source)
140
+ else:
141
+ raise GHPSValidationError(INVALID_CONFIG_TYPE_ERROR)
142
+
143
+ if not config_path.exists():
144
+ return {}
145
+
146
+ try:
147
+ with open(config_path, "rb") as f:
148
+ data = tomli.load(f)
149
+ if not isinstance(data, dict):
150
+ raise GHPSValidationError(INVALID_CONFIG_FORMAT_ERROR)
151
+ return data
152
+ except Exception as e:
153
+ if isinstance(e, GHPSValidationError):
154
+ raise e
155
+ raise GHPSValidationError(INVALID_CONFIG_FILE_ERROR.format(error=e))
156
+
104
157
 
105
158
  def _handle_bind_error(port: int, e: OSError) -> None:
106
159
  """
@@ -262,7 +315,8 @@ class GHPageServer:
262
315
  no_cache: bool = False,
263
316
  threaded: bool = True,
264
317
  auto_open: bool = False,
265
- directory_listing: bool = False
318
+ directory_listing: bool = False,
319
+ quiet: bool = False
266
320
  ):
267
321
  """
268
322
  Initialize the server.
@@ -276,6 +330,7 @@ class GHPageServer:
276
330
  :param threaded: If True, handles requests using threads.
277
331
  :param auto_open: If True, automatically opens the server URL in the default web browser.
278
332
  :param directory_listing: If True, enables directory listing.
333
+ :param quiet: If True, suppresses informational server output.
279
334
  """
280
335
  _validate_inputs(
281
336
  directory=directory,
@@ -286,7 +341,8 @@ class GHPageServer:
286
341
  no_cache=no_cache,
287
342
  threaded=threaded,
288
343
  auto_open=auto_open,
289
- directory_listing=directory_listing
344
+ directory_listing=directory_listing,
345
+ quiet=quiet
290
346
  )
291
347
  self._directory = str(Path(directory).resolve())
292
348
  self._host = host
@@ -298,10 +354,14 @@ class GHPageServer:
298
354
  self._threaded = threaded
299
355
  self._auto_open = auto_open
300
356
  self._directory_listing = directory_listing
357
+ self._quiet = quiet
301
358
  self._httpd = None
302
359
 
303
360
  def _print_server_info(self) -> None:
304
361
  """Print the current server configuration and access URL."""
362
+ if self._quiet:
363
+ return
364
+
305
365
  print(f"Serving at {self._url}")
306
366
  print(f"Directory: {self._directory}")
307
367
  print(f"Strict mode: {'ON' if self._strict else 'OFF'}")
@@ -348,3 +408,63 @@ class GHPageServer:
348
408
  self._httpd.shutdown()
349
409
  self._httpd.server_close()
350
410
  print("Server stopped.")
411
+
412
+ @classmethod
413
+ def from_config(
414
+ cls,
415
+ config: Optional[Union[str, Path, dict]] = None,
416
+ directory: Optional[str] = None,
417
+ host: Optional[str] = None,
418
+ port: Optional[int] = None,
419
+ base_path: Optional[str] = None,
420
+ strict: Optional[bool] = None,
421
+ no_cache: Optional[bool] = None,
422
+ threaded: Optional[bool] = None,
423
+ auto_open: Optional[bool] = None,
424
+ directory_listing: Optional[bool] = None,
425
+ quiet: Optional[bool] = None,
426
+ auto_discover: bool = False,
427
+ ):
428
+ """
429
+ Create a GHPageServer instance from a configuration file or dictionary, with explicit parameter overrides.
430
+
431
+ :param config: Config input.
432
+ :param directory: Root directory to serve files from.
433
+ :param host: Host address to bind the server to.
434
+ :param port: Port number to bind the server to.
435
+ :param base_path: URL base path prefix for serving content.
436
+ :param strict: If False, enables automatic ".html" resolution.
437
+ :param no_cache: If True, disables client-side caching.
438
+ :param threaded: If True, handles requests using threads.
439
+ :param auto_open: If True, automatically opens the server URL in the default web browser.
440
+ :param directory_listing: If True, enables directory listing.
441
+ :param quiet: If True, suppresses informational server output.
442
+ :param auto_discover: Auto discover flag.
443
+ """
444
+ init_directory = directory if directory is not None else "."
445
+ loaded_config = _load_config(init_directory, config, auto_discover=auto_discover)
446
+
447
+ resolved_directory = directory if directory is not None else loaded_config.get("directory", ".")
448
+ resolved_host = host if host is not None else loaded_config.get("host", "localhost")
449
+ resolved_port = port if port is not None else loaded_config.get("port", 8000)
450
+ resolved_base_path = base_path if base_path is not None else loaded_config.get("base_path", "")
451
+ resolved_strict = strict if strict is not None else loaded_config.get("strict", True)
452
+ resolved_no_cache = no_cache if no_cache is not None else loaded_config.get("no_cache", False)
453
+ resolved_threaded = threaded if threaded is not None else loaded_config.get("threaded", True)
454
+ resolved_auto_open = auto_open if auto_open is not None else loaded_config.get("auto_open", False)
455
+ resolved_directory_listing = directory_listing if directory_listing is not None else loaded_config.get(
456
+ "directory_listing", False)
457
+ resolved_quiet = quiet if quiet is not None else loaded_config.get("quiet", False)
458
+
459
+ return cls(
460
+ directory=resolved_directory,
461
+ host=resolved_host,
462
+ port=resolved_port,
463
+ base_path=resolved_base_path,
464
+ strict=resolved_strict,
465
+ no_cache=resolved_no_cache,
466
+ threaded=resolved_threaded,
467
+ auto_open=resolved_auto_open,
468
+ directory_listing=resolved_directory_listing,
469
+ quiet=resolved_quiet,
470
+ )
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ghps
3
- Version: 0.7
3
+ Version: 0.8
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.7
6
+ Download-URL: https://github.com/sepandhaghighi/ghps/tarball/v0.8
7
7
  Author: Sepand Haghighi
8
8
  Author-email: me@sepand.tech
9
9
  License: MIT
@@ -11,7 +11,6 @@ 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
12
  Classifier: Development Status :: 4 - Beta
13
13
  Classifier: Natural Language :: English
14
- Classifier: License :: OSI Approved :: MIT License
15
14
  Classifier: Operating System :: OS Independent
16
15
  Classifier: Environment :: Console
17
16
  Classifier: Programming Language :: Python :: 3.7
@@ -22,6 +21,7 @@ Classifier: Programming Language :: Python :: 3.11
22
21
  Classifier: Programming Language :: Python :: 3.12
23
22
  Classifier: Programming Language :: Python :: 3.13
24
23
  Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Programming Language :: Python :: 3.15
25
25
  Classifier: Intended Audience :: Developers
26
26
  Classifier: Intended Audience :: Education
27
27
  Classifier: Topic :: Utilities
@@ -33,6 +33,7 @@ Description-Content-Type: text/markdown
33
33
  License-File: LICENSE
34
34
  License-File: AUTHORS.md
35
35
  Requires-Dist: art>=5.3
36
+ Requires-Dist: tomli>=2.0.2
36
37
  Dynamic: author
37
38
  Dynamic: author-email
38
39
  Dynamic: classifier
@@ -62,7 +63,7 @@ Dynamic: summary
62
63
  ## Overview
63
64
 
64
65
  <p align="justify">
65
- <b>Ghps</b> is a minimal, zero-dependency GitHub Pages simulator written in pure Python for local development and testing. It accurately replicates GitHub Pages' static hosting behavior, including proper <code>404.html</code> handling, project base path simulation, strict routing mode, and optional cache control - all while remaining lightweight, fast, and usable both as a CLI tool and as an importable Python library.
66
+ <b>Ghps</b> is a minimal GitHub Pages simulator written in pure Python for local development and testing. It accurately replicates GitHub Pages' static hosting behavior, including proper <code>404.html</code> handling, project base path simulation, strict routing mode, and optional cache control - all while remaining lightweight, fast, and usable both as a CLI tool and as an importable Python library.
66
67
  </p>
67
68
 
68
69
  <table>
@@ -101,13 +102,13 @@ Dynamic: summary
101
102
  ## Installation
102
103
 
103
104
  ### Source Code
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
+ - Download [Version 0.8](https://github.com/sepandhaghighi/ghps/archive/v0.8.zip) or [Latest Source](https://github.com/sepandhaghighi/ghps/archive/dev.zip)
105
106
  - `pip install .`
106
107
 
107
108
  ### PyPI
108
109
 
109
110
  - Check [Python Packaging User Guide](https://packaging.python.org/installing/)
110
- - `pip install ghps==0.7`
111
+ - `pip install ghps==0.8`
111
112
 
112
113
 
113
114
  ## Usage
@@ -124,6 +125,7 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
124
125
 
125
126
  | Option | Description | Default |
126
127
  |--------|-------------|----------|
128
+ | `-c, --config` | Path to configuration file | Auto discovers `.ghps` |
127
129
  | `--host` | Host address to bind to | `localhost` |
128
130
  | `-p, --port` | Port to serve on | `8000` |
129
131
  | `-d, --directory` | Directory to serve | Current directory (`.`) |
@@ -133,6 +135,7 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
133
135
  | `--no-threaded` | Disable threaded server | Threaded enabled |
134
136
  | `--auto-open` | Automatically open the server URL | Disable |
135
137
  | `--directory-listing` | Enable directory listing | Disable |
138
+ | `--quiet` | Suppress informational server output | Disable |
136
139
 
137
140
  > [!NOTE]
138
141
  > You can set `--port=0` to let the OS automatically select an available port.
@@ -140,24 +143,32 @@ ghps --host=127.0.0.1 --port=8080 --no-cache --no-threaded
140
143
  > [!NOTE]
141
144
  > 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`.
142
145
 
146
+ > [!NOTE]
147
+ > Ghps automatically looks for a `.ghps` configuration file in the served directory if available, or you can specify a custom configuration file path using `-c` / `--config`.
148
+
143
149
 
144
150
  ### Library
145
151
 
146
152
  ```python
147
153
  from ghps import GHPageServer
148
154
 
149
- server = GHPageServer(
155
+ server1 = GHPageServer(
150
156
  directory="dist",
151
- host="localhost",
157
+ host="localhost",
152
158
  port=5000,
153
159
  base_path="/repo-name",
154
160
  strict=True,
155
161
  no_cache=False,
156
162
  threaded=True,
157
- auto_open=True,
163
+ auto_open=True,
164
+ quiet=False,
158
165
  )
159
166
 
160
- server.start()
167
+ server1.start()
168
+
169
+ server2 = GHPageServer.from_config(config="config.toml")
170
+
171
+ server2.start()
161
172
  ```
162
173
 
163
174
  #### Parameters
@@ -173,6 +184,7 @@ server.start()
173
184
  | `threaded` | `bool` | Enable threaded request handling | `True` |
174
185
  | `auto_open` | `bool` | Automatically open the server URL | `False` |
175
186
  | `directory_listing` | `bool` | Enable directory listing | `False` |
187
+ | `quiet` | `bool` | Suppress informational server output | `False` |
176
188
 
177
189
  > [!NOTE]
178
190
  > You can set `port=0` to let the OS automatically select an available port.
@@ -180,6 +192,23 @@ server.start()
180
192
  > [!NOTE]
181
193
  > The `host` parameter controls the network interface used for server binding. The default is `"localhost"`.
182
194
 
195
+ ### Configuration File
196
+
197
+ You can define your server settings in a **TOML** configuration file (typically named `.ghps`):
198
+
199
+ ```toml
200
+ directory = "dist"
201
+ host = "localhost"
202
+ port = 8080
203
+ base_path = "/repo-name"
204
+ strict = true
205
+ no_cache = false
206
+ threaded = true
207
+ auto_open = false
208
+ directory_listing = false
209
+ quiet = false
210
+ ```
211
+
183
212
  ## Issues & Bug Reports
184
213
 
185
214
  Just fill an issue and describe it. We'll check it ASAP!
@@ -229,6 +258,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
229
258
  and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
230
259
 
231
260
  ## [Unreleased]
261
+ ## [0.8] - 2026-11-08
262
+ ### Added
263
+ - `quiet` parameter
264
+ - `--quiet` CLI argument
265
+ - `--config` CLI argument
266
+ - `from_config` method
267
+ ### Changed
268
+ - Test system modified
269
+ - Build system modified
270
+ - `Python 3.15` added to `test.yml`
271
+ - `README.md` updated
232
272
  ## [0.7] - 2026-09-22
233
273
  ### Added
234
274
  - `host` parameter
@@ -283,7 +323,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
283
323
  - Command line interface
284
324
  - Test system
285
325
 
286
- [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.7...dev
326
+ [Unreleased]: https://github.com/sepandhaghighi/ghps/compare/v0.8...dev
327
+ [0.8]: https://github.com/sepandhaghighi/ghps/compare/v0.7...v0.8
287
328
  [0.7]: https://github.com/sepandhaghighi/ghps/compare/v0.6...v0.7
288
329
  [0.6]: https://github.com/sepandhaghighi/ghps/compare/v0.5...v0.6
289
330
  [0.5]: https://github.com/sepandhaghighi/ghps/compare/v0.4...v0.5
@@ -4,6 +4,7 @@ LICENSE
4
4
  MANIFEST.in
5
5
  README.md
6
6
  SECURITY.md
7
+ pyproject.toml
7
8
  requirements-dev.txt
8
9
  requirements-latest.txt
9
10
  setup.py
@@ -0,0 +1,2 @@
1
+ art>=5.3
2
+ tomli>=2.0.2
@@ -0,0 +1,3 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0.0", "wheel>=0.37.0"]
3
+ build-backend = "setuptools.build_meta"
@@ -1,7 +1,7 @@
1
1
  requests>=2.20.0
2
2
  pytest>=4.3.1
3
3
  pytest-cov>=2.6.1
4
- setuptools>=40.8.0
4
+ setuptools>=61.0.0
5
5
  vulture>=1.0
6
6
  bandit>=1.5.1
7
7
  pydocstyle>=6.3.0
@@ -0,0 +1,2 @@
1
+ art==6.5
2
+ tomli==2.5.0
@@ -14,7 +14,7 @@ def read_description() -> str:
14
14
  description += c.read()
15
15
  return description
16
16
  except Exception:
17
- return '''Ghps is a minimal, zero-dependency GitHub Pages simulator written in pure Python for local development and testing.
17
+ return '''Ghps is a minimal GitHub Pages simulator written in pure Python for local development and testing.
18
18
  It accurately replicates GitHub Pages' static hosting behavior, including proper 404.html handling, project base path simulation,
19
19
  strict routing mode, and optional cache control - all while remaining lightweight, fast, and usable both as a CLI tool and as an importable Python library.'''
20
20
 
@@ -22,7 +22,7 @@ def read_description() -> str:
22
22
  setup(
23
23
  name='ghps',
24
24
  packages=['ghps'],
25
- version='0.7',
25
+ version='0.8',
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,17 +30,16 @@ 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.7',
33
+ download_url='https://github.com/sepandhaghighi/ghps/tarball/v0.8',
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'
37
37
  },
38
- install_requires=['art>=5.3'],
38
+ install_requires=['art>=5.3', 'tomli>=2.0.2'],
39
39
  python_requires='>=3.7',
40
40
  classifiers=[
41
41
  'Development Status :: 4 - Beta',
42
42
  'Natural Language :: English',
43
- 'License :: OSI Approved :: MIT License',
44
43
  'Operating System :: OS Independent',
45
44
  'Environment :: Console',
46
45
  'Programming Language :: Python :: 3.7',
@@ -51,6 +50,7 @@ setup(
51
50
  'Programming Language :: Python :: 3.12',
52
51
  'Programming Language :: Python :: 3.13',
53
52
  'Programming Language :: Python :: 3.14',
53
+ 'Programming Language :: Python :: 3.15',
54
54
  'Intended Audience :: Developers',
55
55
  'Intended Audience :: Education',
56
56
  'Topic :: Utilities',
@@ -1,6 +1,6 @@
1
1
  # -*- coding: utf-8 -*-
2
2
  import sys
3
- from unittest.mock import patch
3
+ from unittest.mock import patch, MagicMock
4
4
  import pytest
5
5
  from ghps.params import GHPS_VERSION
6
6
  import ghps.cli
@@ -8,7 +8,6 @@ import ghps.cli
8
8
 
9
9
  def run_cli(monkeypatch, args):
10
10
  monkeypatch.setattr(sys, "argv", ["ghps"] + args)
11
- monkeypatch.setattr(ghps.cli.GHPageServer, "start", lambda self: None)
12
11
  ghps.cli.main()
13
12
 
14
13
 
@@ -22,39 +21,46 @@ def test_version_flag(capsys):
22
21
  def test_cli_default_arguments(monkeypatch):
23
22
  captured = {}
24
23
 
25
- def fake_init(self, **kwargs):
24
+ def fake_from_config(cls, **kwargs):
26
25
  captured.update(kwargs)
26
+ mock_server = MagicMock()
27
+ mock_server.start = lambda: None
28
+ return mock_server
27
29
 
28
30
  monkeypatch.setattr(ghps.cli, "GHPageServer", type(
29
31
  "MockServer",
30
32
  (),
31
33
  {
32
- "__init__": fake_init,
34
+ "from_config": classmethod(fake_from_config),
33
35
  "start": lambda self: None,
34
36
  },
35
37
  ))
36
38
 
37
39
  run_cli(monkeypatch, [])
38
40
 
39
- assert captured["host"] == "localhost"
40
- assert captured["port"] == 8000
41
- assert captured["strict"] is True
42
- assert captured["no_cache"] is False
43
- assert captured["threaded"] is True
44
- assert captured["directory_listing"] is False
41
+ assert captured["host"] is None
42
+ assert captured["port"] is None
43
+ assert captured["strict"] is None
44
+ assert captured["no_cache"] is None
45
+ assert captured["threaded"] is None
46
+ assert captured["directory_listing"] is None
47
+ assert captured["quiet"] is None
45
48
 
46
49
 
47
50
  def test_cli_custom_port_and_directory(monkeypatch, tmp_path):
48
51
  captured = {}
49
52
 
50
- def fake_init(self, **kwargs):
53
+ def fake_from_config(cls, **kwargs):
51
54
  captured.update(kwargs)
55
+ mock_server = MagicMock()
56
+ mock_server.start = lambda: None
57
+ return mock_server
52
58
 
53
59
  monkeypatch.setattr(ghps.cli, "GHPageServer", type(
54
60
  "MockServer",
55
61
  (),
56
62
  {
57
- "__init__": fake_init,
63
+ "from_config": classmethod(fake_from_config),
58
64
  "start": lambda self: None,
59
65
  },
60
66
  ))
@@ -68,14 +74,17 @@ def test_cli_custom_port_and_directory(monkeypatch, tmp_path):
68
74
  def test_cli_host(monkeypatch):
69
75
  captured = {}
70
76
 
71
- def fake_init(self, **kwargs):
77
+ def fake_from_config(cls, **kwargs):
72
78
  captured.update(kwargs)
79
+ mock_server = MagicMock()
80
+ mock_server.start = lambda: None
81
+ return mock_server
73
82
 
74
83
  monkeypatch.setattr(ghps.cli, "GHPageServer", type(
75
84
  "MockServer",
76
85
  (),
77
86
  {
78
- "__init__": fake_init,
87
+ "from_config": classmethod(fake_from_config),
79
88
  "start": lambda self: None,
80
89
  },
81
90
  ))
@@ -88,14 +97,17 @@ def test_cli_host(monkeypatch):
88
97
  def test_cli_base_path(monkeypatch):
89
98
  captured = {}
90
99
 
91
- def fake_init(self, **kwargs):
100
+ def fake_from_config(cls, **kwargs):
92
101
  captured.update(kwargs)
102
+ mock_server = MagicMock()
103
+ mock_server.start = lambda: None
104
+ return mock_server
93
105
 
94
106
  monkeypatch.setattr(ghps.cli, "GHPageServer", type(
95
107
  "MockServer",
96
108
  (),
97
109
  {
98
- "__init__": fake_init,
110
+ "from_config": classmethod(fake_from_config),
99
111
  "start": lambda self: None,
100
112
  },
101
113
  ))
@@ -108,24 +120,28 @@ def test_cli_base_path(monkeypatch):
108
120
  def test_cli_flags(monkeypatch):
109
121
  captured = {}
110
122
 
111
- def fake_init(self, **kwargs):
123
+ def fake_from_config(cls, **kwargs):
112
124
  captured.update(kwargs)
125
+ mock_server = MagicMock()
126
+ mock_server.start = lambda: None
127
+ return mock_server
113
128
 
114
129
  monkeypatch.setattr(ghps.cli, "GHPageServer", type(
115
130
  "MockServer",
116
131
  (),
117
132
  {
118
- "__init__": fake_init,
133
+ "from_config": classmethod(fake_from_config),
119
134
  "start": lambda self: None,
120
135
  },
121
136
  ))
122
137
 
123
- run_cli(monkeypatch, ["--no-strict", "--no-cache", "--no-threaded"])
138
+ run_cli(monkeypatch, ["--no-strict", "--no-cache", "--no-threaded", "--quiet"])
124
139
 
125
140
  assert captured["strict"] is False
126
141
  assert captured["no_cache"] is True
127
142
  assert captured["threaded"] is False
128
- assert captured["directory_listing"] is False
143
+ assert captured["directory_listing"] is None
144
+ assert captured["quiet"] is True
129
145
 
130
146
 
131
147
  def test_cli_invalid_directory(monkeypatch):
@@ -138,7 +154,7 @@ def test_cli_invalid_directory(monkeypatch):
138
154
  @patch("ghps.cli.GHPageServer")
139
155
  @patch("sys.stderr")
140
156
  def test_cli_unexpected_error(mock_stderr, mock_server_cls):
141
- mock_server = mock_server_cls.return_value
157
+ mock_server = mock_server_cls.from_config.return_value
142
158
  mock_server.start.side_effect = Exception("boom")
143
159
  test_args = ["ghps"]
144
160
  with patch("sys.argv", test_args):
@@ -152,14 +168,17 @@ def test_cli_unexpected_error(mock_stderr, mock_server_cls):
152
168
  def test_cli_directory_listing(monkeypatch):
153
169
  captured = {}
154
170
 
155
- def fake_init(self, **kwargs):
171
+ def fake_from_config(cls, **kwargs):
156
172
  captured.update(kwargs)
173
+ mock_server = MagicMock()
174
+ mock_server.start = lambda: None
175
+ return mock_server
157
176
 
158
177
  monkeypatch.setattr(ghps.cli, "GHPageServer", type(
159
178
  "MockServer",
160
179
  (),
161
180
  {
162
- "__init__": fake_init,
181
+ "from_config": classmethod(fake_from_config),
163
182
  "start": lambda self: None,
164
183
  },
165
184
  ))
@@ -167,3 +186,50 @@ def test_cli_directory_listing(monkeypatch):
167
186
  run_cli(monkeypatch, ["--directory-listing"])
168
187
 
169
188
  assert captured["directory_listing"] is True
189
+
190
+
191
+ def test_cli_quiet(monkeypatch):
192
+ captured = {}
193
+
194
+ def fake_from_config(cls, **kwargs):
195
+ captured.update(kwargs)
196
+ mock_server = MagicMock()
197
+ mock_server.start = lambda: None
198
+ return mock_server
199
+
200
+ monkeypatch.setattr(ghps.cli, "GHPageServer", type(
201
+ "MockServer",
202
+ (),
203
+ {
204
+ "from_config": classmethod(fake_from_config),
205
+ "start": lambda self: None,
206
+ },
207
+ ))
208
+
209
+ run_cli(monkeypatch, ["--quiet"])
210
+
211
+ assert captured["quiet"] is True
212
+
213
+
214
+ def test_cli_config_flag(monkeypatch, tmp_path):
215
+ captured = {}
216
+
217
+ def fake_from_config(cls, **kwargs):
218
+ captured.update(kwargs)
219
+ mock_server = MagicMock()
220
+ mock_server.start = lambda: None
221
+ return mock_server
222
+
223
+ monkeypatch.setattr(ghps.cli, "GHPageServer", type(
224
+ "MockServer",
225
+ (),
226
+ {
227
+ "from_config": classmethod(fake_from_config),
228
+ },
229
+ ))
230
+
231
+ config_path = str(tmp_path / "myconfig.toml")
232
+ run_cli(monkeypatch, ["-c", config_path])
233
+
234
+ assert captured["config"] == config_path
235
+ assert captured["auto_discover"] is True
@@ -196,6 +196,24 @@ def test_invalid_directory_listing_type(tmp_path):
196
196
  )
197
197
 
198
198
 
199
+ def test_invalid_quiet_type(tmp_path):
200
+ with pytest.raises(
201
+ GHPSValidationError,
202
+ match="`quiet` must be bool."
203
+ ):
204
+ GHPageServer(
205
+ directory=tmp_path,
206
+ port=8000,
207
+ base_path="",
208
+ strict=True,
209
+ no_cache=False,
210
+ threaded=True,
211
+ auto_open=False,
212
+ directory_listing=False,
213
+ quiet="true",
214
+ )
215
+
216
+
199
217
  @patch("ghps.server.webbrowser.open", side_effect=Exception("boom"))
200
218
  @patch("ghps.server._ThreadedTCPServer")
201
219
  @patch("builtins.print")
@@ -262,3 +280,25 @@ def test_port_address_not_available_error(mock_server_cls, tmp_path):
262
280
  server.start()
263
281
 
264
282
  assert "`port` address is not available. Check network configuration." in str(exc.value)
283
+
284
+
285
+ def test_invalid_config_type(tmp_path):
286
+ with pytest.raises(GHPSValidationError, match="`config` must be str, pathlib.Path, or dict."):
287
+ GHPageServer.from_config(config=123, directory=tmp_path)
288
+
289
+
290
+ def test_invalid_config_syntax(tmp_path):
291
+ config_file = tmp_path / ".ghps"
292
+ config_file.write_text('port = [invalid_syntax\n')
293
+
294
+ with pytest.raises(GHPSValidationError, match="Failed to load config file:"):
295
+ GHPageServer.from_config(config=config_file, directory=tmp_path)
296
+
297
+
298
+ def test_invalid_config_value(tmp_path):
299
+ config_file = tmp_path / "invalid.toml"
300
+ config_file.write_text("invalid = true\n")
301
+
302
+ with patch("ghps.server.tomli.load", return_value=[]):
303
+ with pytest.raises(GHPSValidationError, match="Config file content must be a valid TOML format."):
304
+ GHPageServer.from_config(config=config_file, directory=tmp_path)
@@ -346,3 +346,92 @@ def test_directory_listing_disabled():
346
346
  assert response.status_code == 404
347
347
 
348
348
  server.stop()
349
+
350
+
351
+ def test_quiet_mode(capsys, tmp_path):
352
+ server = GHPageServer(
353
+ directory=tmp_path,
354
+ port=9011,
355
+ quiet=True,
356
+ )
357
+
358
+ server._url = "http://localhost:9011"
359
+ server._print_server_info()
360
+
361
+ captured = capsys.readouterr()
362
+
363
+ assert captured.out == ""
364
+
365
+
366
+ def test_quiet_mode_disabled(capsys, tmp_path):
367
+ server = GHPageServer(
368
+ directory=tmp_path,
369
+ port=9012,
370
+ quiet=False,
371
+ )
372
+
373
+ server._url = "http://localhost:9012"
374
+ server._print_server_info()
375
+
376
+ captured = capsys.readouterr()
377
+
378
+ assert "Serving at http://localhost:9012" in captured.out
379
+ assert f"Directory: {tmp_path.resolve()}" in captured.out
380
+ assert "Strict mode: ON" in captured.out
381
+ assert "Cache disabled: NO" in captured.out
382
+
383
+
384
+ def test_server_from_config1():
385
+ with tempfile.TemporaryDirectory() as tmpdir:
386
+ config = {"port": 9015, "strict": False, "no_cache": True}
387
+ server = GHPageServer.from_config(config=config, directory=tmpdir)
388
+ assert server._port == 9015
389
+ assert server._strict is False
390
+ assert server._no_cache is True
391
+
392
+
393
+ def test_server_from_config2(tmp_path):
394
+ config_file = tmp_path / "custom.toml"
395
+ config_file.write_text('port = 9016\nhost = "127.0.0.1"\n')
396
+
397
+ server = GHPageServer.from_config(config=config_file, directory=tmp_path)
398
+ assert server._port == 9016
399
+ assert server._host == "127.0.0.1"
400
+
401
+
402
+ def test_server_from_config3():
403
+ server1 = GHPageServer.from_config(auto_discover=False)
404
+ server2 = GHPageServer()
405
+ assert server1._port == server2._port
406
+ assert server1._host == server2._host
407
+
408
+
409
+ def test_server_from_config4():
410
+ server1 = GHPageServer.from_config(auto_discover=True)
411
+ server2 = GHPageServer()
412
+ assert server1._port == server2._port
413
+ assert server1._host == server2._host
414
+
415
+
416
+ def test_server_from_config5():
417
+ server1 = GHPageServer.from_config(config="custom2.toml")
418
+ server2 = GHPageServer()
419
+ assert server1._port == server2._port
420
+ assert server1._host == server2._host
421
+
422
+
423
+ def test_server_from_config6(tmp_path):
424
+ config_file = tmp_path / ".ghps"
425
+ config_file.write_text('port = 9017\n')
426
+
427
+ server = GHPageServer.from_config(directory=tmp_path, auto_discover=True)
428
+ assert server._port == 9017
429
+
430
+
431
+ def test_server_from_config7(tmp_path):
432
+ config_file = tmp_path / ".ghps"
433
+ config_file.write_text('port = 9017\nstrict = false\n')
434
+
435
+ server = GHPageServer.from_config(directory=tmp_path, port=9018, auto_discover=True)
436
+ assert server._port == 9018
437
+ assert server._strict is False
@@ -1 +0,0 @@
1
- art>=5.3
@@ -1 +0,0 @@
1
- art==6.5
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