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.
- {ghps-0.7 → ghps-0.8}/CHANGELOG.md +13 -1
- {ghps-0.7 → ghps-0.8}/PKG-INFO +52 -11
- {ghps-0.7 → ghps-0.8}/README.md +35 -7
- {ghps-0.7 → ghps-0.8}/SECURITY.md +2 -2
- {ghps-0.7 → ghps-0.8}/ghps/cli.py +29 -7
- {ghps-0.7 → ghps-0.8}/ghps/params.py +6 -1
- {ghps-0.7 → ghps-0.8}/ghps/server.py +125 -5
- {ghps-0.7 → ghps-0.8}/ghps.egg-info/PKG-INFO +52 -11
- {ghps-0.7 → ghps-0.8}/ghps.egg-info/SOURCES.txt +1 -0
- ghps-0.8/ghps.egg-info/requires.txt +2 -0
- ghps-0.8/pyproject.toml +3 -0
- {ghps-0.7 → ghps-0.8}/requirements-dev.txt +1 -1
- ghps-0.8/requirements-latest.txt +2 -0
- {ghps-0.7 → ghps-0.8}/setup.py +5 -5
- {ghps-0.7 → ghps-0.8}/tests/test_cli.py +89 -23
- {ghps-0.7 → ghps-0.8}/tests/test_error.py +40 -0
- {ghps-0.7 → ghps-0.8}/tests/test_server.py +89 -0
- ghps-0.7/ghps.egg-info/requires.txt +0 -1
- ghps-0.7/requirements-latest.txt +0 -1
- {ghps-0.7 → ghps-0.8}/AUTHORS.md +0 -0
- {ghps-0.7 → ghps-0.8}/LICENSE +0 -0
- {ghps-0.7 → ghps-0.8}/MANIFEST.in +0 -0
- {ghps-0.7 → ghps-0.8}/ghps/__init__.py +0 -0
- {ghps-0.7 → ghps-0.8}/ghps/__main__.py +0 -0
- {ghps-0.7 → ghps-0.8}/ghps/errors.py +0 -0
- {ghps-0.7 → ghps-0.8}/ghps.egg-info/dependency_links.txt +0 -0
- {ghps-0.7 → ghps-0.8}/ghps.egg-info/entry_points.txt +0 -0
- {ghps-0.7 → ghps-0.8}/ghps.egg-info/top_level.txt +0 -0
- {ghps-0.7 → ghps-0.8}/setup.cfg +0 -0
|
@@ -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.
|
|
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
|
{ghps-0.7 → ghps-0.8}/PKG-INFO
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ghps
|
|
3
|
-
Version: 0.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
155
|
+
server1 = GHPageServer(
|
|
150
156
|
directory="dist",
|
|
151
|
-
|
|
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
|
-
|
|
163
|
+
auto_open=True,
|
|
164
|
+
quiet=False,
|
|
158
165
|
)
|
|
159
166
|
|
|
160
|
-
|
|
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.
|
|
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
|
{ghps-0.7 → ghps-0.8}/README.md
RENAMED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
## Overview
|
|
12
12
|
|
|
13
13
|
<p align="justify">
|
|
14
|
-
<b>Ghps</b> is a minimal
|
|
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.
|
|
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.
|
|
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
|
-
|
|
103
|
+
server1 = GHPageServer(
|
|
99
104
|
directory="dist",
|
|
100
|
-
|
|
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
|
-
|
|
111
|
+
auto_open=True,
|
|
112
|
+
quiet=False,
|
|
107
113
|
)
|
|
108
114
|
|
|
109
|
-
|
|
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!
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
126
|
+
strict=False if args.no_strict else None,
|
|
108
127
|
no_cache=args.no_cache,
|
|
109
|
-
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.
|
|
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.
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
155
|
+
server1 = GHPageServer(
|
|
150
156
|
directory="dist",
|
|
151
|
-
|
|
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
|
-
|
|
163
|
+
auto_open=True,
|
|
164
|
+
quiet=False,
|
|
158
165
|
)
|
|
159
166
|
|
|
160
|
-
|
|
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.
|
|
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
|
ghps-0.8/pyproject.toml
ADDED
{ghps-0.7 → ghps-0.8}/setup.py
RENAMED
|
@@ -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
|
|
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.
|
|
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.
|
|
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
|
|
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
|
-
"
|
|
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"]
|
|
40
|
-
assert captured["port"]
|
|
41
|
-
assert captured["strict"] is
|
|
42
|
-
assert captured["no_cache"] is
|
|
43
|
-
assert captured["threaded"] is
|
|
44
|
-
assert captured["directory_listing"] is
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
|
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
|
|
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
|
-
"
|
|
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
|
ghps-0.7/requirements-latest.txt
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
art==6.5
|
{ghps-0.7 → ghps-0.8}/AUTHORS.md
RENAMED
|
File without changes
|
{ghps-0.7 → ghps-0.8}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ghps-0.7 → ghps-0.8}/setup.cfg
RENAMED
|
File without changes
|