pyappify 1.0.6__tar.gz → 1.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.
- {pyappify-1.0.6 → pyappify-1.0.8}/PKG-INFO +12 -12
- {pyappify-1.0.6 → pyappify-1.0.8}/pyappify/__init__.py +235 -10
- pyappify-1.0.8/pyappify/main.py +2 -0
- {pyappify-1.0.6 → pyappify-1.0.8}/pyappify.egg-info/PKG-INFO +12 -12
- {pyappify-1.0.6 → pyappify-1.0.8}/pyappify.egg-info/SOURCES.txt +1 -7
- {pyappify-1.0.6 → pyappify-1.0.8}/pyproject.toml +24 -24
- {pyappify-1.0.6 → pyappify-1.0.8}/setup.cfg +4 -4
- pyappify-1.0.6/pyappify/main.py +0 -2
- pyappify-1.0.6/tests/TestAppConfig.py +0 -89
- pyappify-1.0.6/tests/TestProcessControls.py +0 -77
- pyappify-1.0.6/tests/TestUpdateEnv.py +0 -100
- pyappify-1.0.6/tests/TestUpgrade.py +0 -110
- pyappify-1.0.6/tests/TestUpgradeOnline.py +0 -21
- pyappify-1.0.6/tests/TestVersion.py +0 -33
- {pyappify-1.0.6 → pyappify-1.0.8}/pyappify/app_config.py +0 -0
- {pyappify-1.0.6 → pyappify-1.0.8}/pyappify.egg-info/dependency_links.txt +0 -0
- {pyappify-1.0.6 → pyappify-1.0.8}/pyappify.egg-info/entry_points.txt +0 -0
- {pyappify-1.0.6 → pyappify-1.0.8}/pyappify.egg-info/requires.txt +0 -0
- {pyappify-1.0.6 → pyappify-1.0.8}/pyappify.egg-info/top_level.txt +0 -0
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pyappify
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary: My awesome Python application
|
|
5
|
-
Classifier: Programming Language :: Python :: 3
|
|
6
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
-
Classifier: Operating System :: OS Independent
|
|
8
|
-
Requires-Python: >=3.7
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
Provides-Extra: dev
|
|
11
|
-
Requires-Dist: twine; extra == "dev"
|
|
12
|
-
Requires-Dist: build; extra == "dev"
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyappify
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: My awesome Python application
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: twine; extra == "dev"
|
|
12
|
+
Requires-Dist: build; extra == "dev"
|
|
@@ -10,6 +10,10 @@ import urllib.request
|
|
|
10
10
|
import zipfile
|
|
11
11
|
import threading
|
|
12
12
|
import time
|
|
13
|
+
import tempfile
|
|
14
|
+
import uuid
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
13
17
|
|
|
14
18
|
from .app_config import (
|
|
15
19
|
UPDATE_METHOD_AUTO,
|
|
@@ -35,7 +39,6 @@ update_note = os.environ.get("PYAPPIFY_UPDATE_NOTE")
|
|
|
35
39
|
app_profile = os.environ.get("PYAPPIFY_APP_PROFILE")
|
|
36
40
|
app_locale = os.environ.get("PYAPPIFY_LOCALE") or "en"
|
|
37
41
|
pyappify_version = os.environ.get("PYAPPIFY_VERSION")
|
|
38
|
-
pyappify_executable = os.environ.get("PYAPPIFY_EXECUTABLE")
|
|
39
42
|
app_json_path = configure_app_json(os.environ.get("PYAPPIFY_APP_JSON_PATH"))
|
|
40
43
|
|
|
41
44
|
pyappify_upgradeable = os.environ.get("PYAPPIFY_UPGRADEABLE") == '1'
|
|
@@ -47,14 +50,41 @@ try:
|
|
|
47
50
|
except (ValueError, TypeError):
|
|
48
51
|
pid = None
|
|
49
52
|
|
|
50
|
-
import sys
|
|
51
|
-
|
|
52
53
|
try:
|
|
53
54
|
import ctypes
|
|
54
55
|
except ImportError:
|
|
55
56
|
ctypes = None
|
|
56
57
|
|
|
57
58
|
|
|
59
|
+
def find_pyappify_executable(start_dir=None, environ=None):
|
|
60
|
+
"""Find the launcher configured by PyAppify or in a parent directory."""
|
|
61
|
+
environ = os.environ if environ is None else environ
|
|
62
|
+
configured = environ.get("PYAPPIFY_EXECUTABLE")
|
|
63
|
+
if configured:
|
|
64
|
+
configured_path = Path(configured).expanduser()
|
|
65
|
+
if configured_path.is_file():
|
|
66
|
+
return str(configured_path.resolve())
|
|
67
|
+
|
|
68
|
+
current = Path(start_dir or os.getcwd()).expanduser()
|
|
69
|
+
if current.is_file():
|
|
70
|
+
current = current.parent
|
|
71
|
+
try:
|
|
72
|
+
current = current.resolve()
|
|
73
|
+
except OSError:
|
|
74
|
+
current = current.absolute()
|
|
75
|
+
|
|
76
|
+
executable_names = ("pyappify.exe", "pyappify")
|
|
77
|
+
for directory in (current,) + tuple(current.parents):
|
|
78
|
+
for executable_name in executable_names:
|
|
79
|
+
candidate = directory / executable_name
|
|
80
|
+
if candidate.is_file():
|
|
81
|
+
return str(candidate.resolve())
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
pyappify_executable = find_pyappify_executable()
|
|
86
|
+
|
|
87
|
+
|
|
58
88
|
def _get_logger():
|
|
59
89
|
global _console_logger
|
|
60
90
|
|
|
@@ -209,16 +239,18 @@ def show_pyappify(args=None, cwd=None, env=None):
|
|
|
209
239
|
global pid
|
|
210
240
|
|
|
211
241
|
log = _get_logger()
|
|
212
|
-
|
|
242
|
+
already_running = _is_process_running(pid)
|
|
243
|
+
if already_running and not args:
|
|
213
244
|
log.info(f"PyAppify is already running with PID: {pid}")
|
|
214
245
|
bring_window_to_front_by_pid(pid)
|
|
215
246
|
return pid
|
|
216
247
|
|
|
217
|
-
|
|
218
|
-
|
|
248
|
+
executable = pyappify_executable or find_pyappify_executable()
|
|
249
|
+
if not executable:
|
|
250
|
+
log.error("PyAppify executable was not found.")
|
|
219
251
|
return None
|
|
220
252
|
|
|
221
|
-
command = [
|
|
253
|
+
command = [executable]
|
|
222
254
|
if args:
|
|
223
255
|
if isinstance(args, str):
|
|
224
256
|
command.append(args)
|
|
@@ -228,15 +260,208 @@ def show_pyappify(args=None, cwd=None, env=None):
|
|
|
228
260
|
try:
|
|
229
261
|
process = subprocess.Popen(
|
|
230
262
|
command,
|
|
231
|
-
cwd=cwd or os.path.dirname(
|
|
263
|
+
cwd=cwd or os.path.dirname(executable) or None,
|
|
232
264
|
env=env,
|
|
233
265
|
)
|
|
234
|
-
|
|
266
|
+
if not already_running:
|
|
267
|
+
pid = process.pid
|
|
235
268
|
return pid
|
|
236
269
|
except Exception as e:
|
|
237
|
-
log.error(f"Failed to start PyAppify executable {
|
|
270
|
+
log.error(f"Failed to start PyAppify executable {executable}: {e}")
|
|
271
|
+
return None
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def _require_pyappify_executable():
|
|
275
|
+
global pyappify_executable
|
|
276
|
+
|
|
277
|
+
if pyappify_executable and os.path.isfile(pyappify_executable):
|
|
278
|
+
return os.path.abspath(pyappify_executable)
|
|
279
|
+
pyappify_executable = find_pyappify_executable()
|
|
280
|
+
if not pyappify_executable:
|
|
281
|
+
raise FileNotFoundError(
|
|
282
|
+
"PyAppify executable was not found. Set PYAPPIFY_EXECUTABLE to an "
|
|
283
|
+
"existing executable or place pyappify.exe in this directory or a parent directory."
|
|
284
|
+
)
|
|
285
|
+
return pyappify_executable
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _run_launcher_api(arguments, timeout=300):
|
|
289
|
+
executable = _require_pyappify_executable()
|
|
290
|
+
response_path = os.path.join(
|
|
291
|
+
tempfile.gettempdir(),
|
|
292
|
+
"pyappify-response-{}-{}.json".format(os.getpid(), uuid.uuid4().hex),
|
|
293
|
+
)
|
|
294
|
+
command = list(arguments) + ["--response-file", response_path]
|
|
295
|
+
try:
|
|
296
|
+
subprocess.Popen(
|
|
297
|
+
[executable] + command,
|
|
298
|
+
cwd=os.path.dirname(executable) or None,
|
|
299
|
+
stdout=subprocess.DEVNULL,
|
|
300
|
+
stderr=subprocess.DEVNULL,
|
|
301
|
+
)
|
|
302
|
+
except OSError as error:
|
|
303
|
+
raise RuntimeError(
|
|
304
|
+
"Failed to execute PyAppify at '{}': {}".format(executable, error)
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
deadline = time.monotonic() + timeout
|
|
308
|
+
try:
|
|
309
|
+
while time.monotonic() < deadline:
|
|
310
|
+
try:
|
|
311
|
+
with open(response_path, "r", encoding="utf-8") as response_file:
|
|
312
|
+
response = json.load(response_file)
|
|
313
|
+
if isinstance(response, dict) and response.get("error"):
|
|
314
|
+
raise RuntimeError(response["error"])
|
|
315
|
+
return response
|
|
316
|
+
except FileNotFoundError:
|
|
317
|
+
pass
|
|
318
|
+
except json.JSONDecodeError:
|
|
319
|
+
# The running launcher may still be completing the response write.
|
|
320
|
+
pass
|
|
321
|
+
time.sleep(0.05)
|
|
322
|
+
finally:
|
|
323
|
+
try:
|
|
324
|
+
os.remove(response_path)
|
|
325
|
+
except FileNotFoundError:
|
|
326
|
+
pass
|
|
327
|
+
|
|
328
|
+
raise TimeoutError("Timed out waiting for a response from PyAppify")
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def get_version_list(number_versions=10, release_only=True, timeout=120):
|
|
332
|
+
"""Return the latest versions and each version's notes since its predecessor."""
|
|
333
|
+
if isinstance(number_versions, bool) or not isinstance(number_versions, int):
|
|
334
|
+
raise TypeError("number_versions must be an integer")
|
|
335
|
+
if number_versions <= 0:
|
|
336
|
+
raise ValueError("number_versions must be greater than zero")
|
|
337
|
+
if not isinstance(release_only, bool):
|
|
338
|
+
raise TypeError("release_only must be a boolean")
|
|
339
|
+
|
|
340
|
+
if "PYAPPIFY_PYTHON_TEST" in os.environ:
|
|
341
|
+
time.sleep(5)
|
|
342
|
+
return _get_mock_version_list(number_versions)
|
|
343
|
+
|
|
344
|
+
response = _run_launcher_api(
|
|
345
|
+
[
|
|
346
|
+
"--get-version-list",
|
|
347
|
+
"--number-versions",
|
|
348
|
+
str(number_versions),
|
|
349
|
+
"--release-only",
|
|
350
|
+
str(release_only).lower(),
|
|
351
|
+
],
|
|
352
|
+
timeout=timeout,
|
|
353
|
+
)
|
|
354
|
+
if not isinstance(response, list):
|
|
355
|
+
raise RuntimeError("PyAppify returned an invalid version-list response")
|
|
356
|
+
return response
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def get_versions(number_versions=10, release_only=True, timeout=120):
|
|
360
|
+
"""Alias for get_version_list."""
|
|
361
|
+
return get_version_list(number_versions, release_only, timeout)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
def calculate_update_notes(update_notes, current_version, target_version):
|
|
365
|
+
"""Return descending notes without including versions newer than the target."""
|
|
366
|
+
if not isinstance(update_notes, list):
|
|
367
|
+
raise TypeError("update_notes must be a list")
|
|
368
|
+
|
|
369
|
+
versions = [
|
|
370
|
+
item for item in update_notes
|
|
371
|
+
if isinstance(item, dict) and item.get("version")
|
|
372
|
+
]
|
|
373
|
+
|
|
374
|
+
def normalize(version):
|
|
375
|
+
return str(version or "").lstrip("v")
|
|
376
|
+
|
|
377
|
+
def find_index(version):
|
|
378
|
+
normalized = normalize(version)
|
|
379
|
+
return next(
|
|
380
|
+
(index for index, item in enumerate(versions)
|
|
381
|
+
if normalize(item["version"]) == normalized),
|
|
382
|
+
None,
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
target_index = find_index(target_version)
|
|
386
|
+
if target_index is None:
|
|
387
|
+
return []
|
|
388
|
+
|
|
389
|
+
current_index = find_index(current_version)
|
|
390
|
+
if current_index is None:
|
|
391
|
+
selected_versions = versions[target_index:]
|
|
392
|
+
else:
|
|
393
|
+
first = min(current_index, target_index)
|
|
394
|
+
last = max(current_index, target_index)
|
|
395
|
+
selected_versions = versions[first:last + 1]
|
|
396
|
+
|
|
397
|
+
notes = []
|
|
398
|
+
for item in selected_versions:
|
|
399
|
+
raw_notes = item.get("update_note") or []
|
|
400
|
+
notes.extend(raw_notes if isinstance(raw_notes, list) else [raw_notes])
|
|
401
|
+
return [str(note) for note in notes]
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def update_to_version(version, timeout=300):
|
|
405
|
+
"""Start PyAppify (or forward to it) and update the managed app to version."""
|
|
406
|
+
if not isinstance(version, str) or not version.strip():
|
|
407
|
+
raise ValueError("version must be a non-empty string")
|
|
408
|
+
if "PYAPPIFY_PYTHON_TEST" in os.environ:
|
|
409
|
+
return {"updated": True, "version": version, "mocked": True}
|
|
410
|
+
response = _run_launcher_api(
|
|
411
|
+
["--update-to-version", version],
|
|
412
|
+
timeout=timeout,
|
|
413
|
+
)
|
|
414
|
+
if not isinstance(response, dict) or not response.get("updated"):
|
|
415
|
+
raise RuntimeError("PyAppify returned an invalid update response")
|
|
416
|
+
return response
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _get_mock_version_list(number_versions):
|
|
420
|
+
"""Return deterministic launcher data for UI and integration tests."""
|
|
421
|
+
current = app_version or "v1.0.0"
|
|
422
|
+
prefix = "v" if current.startswith("v") else ""
|
|
423
|
+
try:
|
|
424
|
+
parts = [int(part) for part in current.lstrip("v").split(".")]
|
|
425
|
+
except (TypeError, ValueError):
|
|
426
|
+
parts = [1, 0, 0]
|
|
427
|
+
prefix = "v"
|
|
428
|
+
parts = (parts + [0, 0, 0])[:3]
|
|
429
|
+
|
|
430
|
+
def previous_parts(version_parts):
|
|
431
|
+
major, minor, patch = version_parts
|
|
432
|
+
if patch > 0:
|
|
433
|
+
return [major, minor, patch - 1]
|
|
434
|
+
if minor > 0:
|
|
435
|
+
return [major, minor - 1, 9]
|
|
436
|
+
if major > 0:
|
|
437
|
+
return [major - 1, 9, 9]
|
|
238
438
|
return None
|
|
239
439
|
|
|
440
|
+
version_parts = [[100, 1, 1], parts[:2] + [parts[2] + 2], parts[:2] + [parts[2] + 1], parts]
|
|
441
|
+
previous = previous_parts(parts)
|
|
442
|
+
while len(version_parts) < number_versions:
|
|
443
|
+
if previous is not None:
|
|
444
|
+
version_parts.append(previous)
|
|
445
|
+
previous = previous_parts(previous)
|
|
446
|
+
else:
|
|
447
|
+
break
|
|
448
|
+
|
|
449
|
+
versions = []
|
|
450
|
+
for item_parts in version_parts:
|
|
451
|
+
item_prefix = "v" if item_parts == [100, 1, 1] else prefix
|
|
452
|
+
version = item_prefix + ".".join(str(part) for part in item_parts)
|
|
453
|
+
previous_item = previous_parts(item_parts)
|
|
454
|
+
previous = item_prefix + ".".join(str(part) for part in previous_item) if previous_item else version
|
|
455
|
+
versions.append({
|
|
456
|
+
"version": version,
|
|
457
|
+
"previous_version": previous,
|
|
458
|
+
"update_note": [
|
|
459
|
+
f"Mock update note for {version}",
|
|
460
|
+
f"Changes since {previous}",
|
|
461
|
+
],
|
|
462
|
+
})
|
|
463
|
+
return versions[:number_versions]
|
|
464
|
+
|
|
240
465
|
|
|
241
466
|
def _replace_executable(source_path, target_path, timeout=30):
|
|
242
467
|
deadline = time.monotonic() + timeout
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: pyappify
|
|
3
|
-
Version: 1.0.
|
|
4
|
-
Summary: My awesome Python application
|
|
5
|
-
Classifier: Programming Language :: Python :: 3
|
|
6
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
-
Classifier: Operating System :: OS Independent
|
|
8
|
-
Requires-Python: >=3.7
|
|
9
|
-
Description-Content-Type: text/markdown
|
|
10
|
-
Provides-Extra: dev
|
|
11
|
-
Requires-Dist: twine; extra == "dev"
|
|
12
|
-
Requires-Dist: build; extra == "dev"
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pyappify
|
|
3
|
+
Version: 1.0.8
|
|
4
|
+
Summary: My awesome Python application
|
|
5
|
+
Classifier: Programming Language :: Python :: 3
|
|
6
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Requires-Python: >=3.7
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: twine; extra == "dev"
|
|
12
|
+
Requires-Dist: build; extra == "dev"
|
|
@@ -7,10 +7,4 @@ pyappify.egg-info/SOURCES.txt
|
|
|
7
7
|
pyappify.egg-info/dependency_links.txt
|
|
8
8
|
pyappify.egg-info/entry_points.txt
|
|
9
9
|
pyappify.egg-info/requires.txt
|
|
10
|
-
pyappify.egg-info/top_level.txt
|
|
11
|
-
tests/TestAppConfig.py
|
|
12
|
-
tests/TestProcessControls.py
|
|
13
|
-
tests/TestUpdateEnv.py
|
|
14
|
-
tests/TestUpgrade.py
|
|
15
|
-
tests/TestUpgradeOnline.py
|
|
16
|
-
tests/TestVersion.py
|
|
10
|
+
pyappify.egg-info/top_level.txt
|
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
[build-system]
|
|
2
|
-
requires = ["setuptools>=61.0"]
|
|
3
|
-
build-backend = "setuptools.build_meta"
|
|
4
|
-
|
|
5
|
-
[project]
|
|
6
|
-
name = "pyappify"
|
|
7
|
-
version = "1.0.
|
|
8
|
-
description = "My awesome Python application"
|
|
9
|
-
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.7"
|
|
11
|
-
classifiers = [
|
|
12
|
-
"Programming Language :: Python :: 3",
|
|
13
|
-
"License :: OSI Approved :: MIT License",
|
|
14
|
-
"Operating System :: OS Independent",
|
|
15
|
-
]
|
|
16
|
-
|
|
17
|
-
[project.scripts]
|
|
18
|
-
pyappify = "pyappify.main:main"
|
|
19
|
-
|
|
20
|
-
[project.optional-dependencies]
|
|
21
|
-
dev = ["twine", "build"]
|
|
22
|
-
|
|
23
|
-
[tool.setuptools.packages.find]
|
|
24
|
-
where = ["."]
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "pyappify"
|
|
7
|
+
version = "1.0.8"
|
|
8
|
+
description = "My awesome Python application"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Operating System :: OS Independent",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
pyappify = "pyappify.main:main"
|
|
19
|
+
|
|
20
|
+
[project.optional-dependencies]
|
|
21
|
+
dev = ["twine", "build"]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["."]
|
|
25
25
|
include = ["pyappify"]
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
[egg_info]
|
|
2
|
-
tag_build =
|
|
3
|
-
tag_date = 0
|
|
4
|
-
|
|
1
|
+
[egg_info]
|
|
2
|
+
tag_build =
|
|
3
|
+
tag_date = 0
|
|
4
|
+
|
pyappify-1.0.6/pyappify/main.py
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import os
|
|
3
|
-
import tempfile
|
|
4
|
-
import threading
|
|
5
|
-
import unittest
|
|
6
|
-
|
|
7
|
-
from pyappify.app_config import (
|
|
8
|
-
AppConfigAPI,
|
|
9
|
-
UPDATE_METHOD_AUTO_PRE_RELEASE,
|
|
10
|
-
UPDATE_METHOD_MANUAL,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class TestAppConfig(unittest.TestCase):
|
|
15
|
-
def setUp(self):
|
|
16
|
-
self.temporary_directory = tempfile.TemporaryDirectory()
|
|
17
|
-
self.config_path = os.path.join(self.temporary_directory.name, "app.json")
|
|
18
|
-
self._write_document(
|
|
19
|
-
{
|
|
20
|
-
"name": "example",
|
|
21
|
-
"installed": True,
|
|
22
|
-
"auto_start": False,
|
|
23
|
-
"update_method": "AUTO_UPDATE",
|
|
24
|
-
}
|
|
25
|
-
)
|
|
26
|
-
self.api = AppConfigAPI(self.config_path)
|
|
27
|
-
|
|
28
|
-
def tearDown(self):
|
|
29
|
-
self.api.stop_watcher()
|
|
30
|
-
self.temporary_directory.cleanup()
|
|
31
|
-
|
|
32
|
-
def _write_document(self, document):
|
|
33
|
-
with open(self.config_path, "w", encoding="utf-8") as config_file:
|
|
34
|
-
json.dump(document, config_file)
|
|
35
|
-
|
|
36
|
-
def test_reads_and_updates_preferences_without_losing_other_fields(self):
|
|
37
|
-
self.assertEqual(
|
|
38
|
-
{"auto_start": False, "update_method": "AUTO_UPDATE"},
|
|
39
|
-
self.api.get(),
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
self.api.update(
|
|
43
|
-
auto_start=True,
|
|
44
|
-
update_method=UPDATE_METHOD_AUTO_PRE_RELEASE,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
with open(self.config_path, "r", encoding="utf-8") as config_file:
|
|
48
|
-
saved = json.load(config_file)
|
|
49
|
-
self.assertEqual("example", saved["name"])
|
|
50
|
-
self.assertTrue(saved["installed"])
|
|
51
|
-
self.assertTrue(saved["auto_start"])
|
|
52
|
-
self.assertEqual(UPDATE_METHOD_AUTO_PRE_RELEASE, saved["update_method"])
|
|
53
|
-
|
|
54
|
-
def test_rejects_invalid_preferences(self):
|
|
55
|
-
with self.assertRaises(ValueError):
|
|
56
|
-
self.api.update(auto_start="yes")
|
|
57
|
-
with self.assertRaises(ValueError):
|
|
58
|
-
self.api.update(update_method="UNKNOWN")
|
|
59
|
-
with self.assertRaises(ValueError):
|
|
60
|
-
self.api.update(update_method=[])
|
|
61
|
-
|
|
62
|
-
def test_watcher_notifies_when_an_external_writer_changes_the_file(self):
|
|
63
|
-
changed = threading.Event()
|
|
64
|
-
received = []
|
|
65
|
-
|
|
66
|
-
def listener(config):
|
|
67
|
-
received.append(config)
|
|
68
|
-
changed.set()
|
|
69
|
-
|
|
70
|
-
self.api.add_listener(listener)
|
|
71
|
-
self.api.configure(self.config_path, watch=True, watch_interval=0.05)
|
|
72
|
-
self._write_document(
|
|
73
|
-
{
|
|
74
|
-
"name": "example",
|
|
75
|
-
"installed": True,
|
|
76
|
-
"auto_start": True,
|
|
77
|
-
"update_method": UPDATE_METHOD_MANUAL,
|
|
78
|
-
}
|
|
79
|
-
)
|
|
80
|
-
|
|
81
|
-
self.assertTrue(changed.wait(2), "watcher did not observe app.json change")
|
|
82
|
-
self.assertEqual(
|
|
83
|
-
{"auto_start": True, "update_method": UPDATE_METHOD_MANUAL},
|
|
84
|
-
received[-1],
|
|
85
|
-
)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if __name__ == "__main__":
|
|
89
|
-
unittest.main()
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import signal
|
|
3
|
-
import unittest
|
|
4
|
-
from unittest import mock
|
|
5
|
-
|
|
6
|
-
import pyappify
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
class TestProcessControls(unittest.TestCase):
|
|
10
|
-
def setUp(self):
|
|
11
|
-
self.old_pid = pyappify.pid
|
|
12
|
-
self.old_executable = pyappify.pyappify_executable
|
|
13
|
-
self.old_logger = pyappify.logger
|
|
14
|
-
pyappify.logger = mock.Mock()
|
|
15
|
-
|
|
16
|
-
def tearDown(self):
|
|
17
|
-
pyappify.pid = self.old_pid
|
|
18
|
-
pyappify.pyappify_executable = self.old_executable
|
|
19
|
-
pyappify.logger = self.old_logger
|
|
20
|
-
|
|
21
|
-
def test_kill_pyappify_exe_terminates_configured_pid(self):
|
|
22
|
-
pyappify.pid = 1234
|
|
23
|
-
|
|
24
|
-
with mock.patch.object(pyappify.os, "kill") as kill, mock.patch.object(
|
|
25
|
-
pyappify, "_wait_for_process_exit", return_value=True
|
|
26
|
-
) as wait:
|
|
27
|
-
self.assertTrue(pyappify.kill_pyappify_exe(timeout=5))
|
|
28
|
-
|
|
29
|
-
kill.assert_called_once_with(1234, signal.SIGTERM)
|
|
30
|
-
wait.assert_called_once_with(1234, 5)
|
|
31
|
-
|
|
32
|
-
def test_show_pyappify_brings_existing_window_to_front(self):
|
|
33
|
-
pyappify.pid = 1234
|
|
34
|
-
|
|
35
|
-
with mock.patch.object(
|
|
36
|
-
pyappify, "_is_process_running", return_value=True
|
|
37
|
-
) as is_running, mock.patch.object(
|
|
38
|
-
pyappify, "bring_window_to_front_by_pid", return_value=True
|
|
39
|
-
) as bring_to_front, mock.patch.object(pyappify.subprocess, "Popen") as popen:
|
|
40
|
-
self.assertEqual(1234, pyappify.show_pyappify())
|
|
41
|
-
|
|
42
|
-
is_running.assert_called_once_with(1234)
|
|
43
|
-
bring_to_front.assert_called_once_with(1234)
|
|
44
|
-
popen.assert_not_called()
|
|
45
|
-
|
|
46
|
-
def test_show_pyappify_starts_executable_when_not_running(self):
|
|
47
|
-
executable = os.path.abspath("pyappify.exe")
|
|
48
|
-
pyappify.pid = 1234
|
|
49
|
-
pyappify.pyappify_executable = executable
|
|
50
|
-
process = mock.Mock()
|
|
51
|
-
process.pid = 5678
|
|
52
|
-
|
|
53
|
-
with mock.patch.object(
|
|
54
|
-
pyappify, "_is_process_running", return_value=False
|
|
55
|
-
), mock.patch.object(pyappify.subprocess, "Popen", return_value=process) as popen:
|
|
56
|
-
self.assertEqual(
|
|
57
|
-
5678,
|
|
58
|
-
pyappify.show_pyappify(args=["--profile", "dev"]),
|
|
59
|
-
)
|
|
60
|
-
|
|
61
|
-
popen.assert_called_once_with(
|
|
62
|
-
[executable, "--profile", "dev"],
|
|
63
|
-
cwd=os.path.dirname(executable),
|
|
64
|
-
env=None,
|
|
65
|
-
)
|
|
66
|
-
self.assertEqual(5678, pyappify.pid)
|
|
67
|
-
|
|
68
|
-
def test_show_pyappify_returns_none_without_executable(self):
|
|
69
|
-
pyappify.pid = None
|
|
70
|
-
pyappify.pyappify_executable = None
|
|
71
|
-
|
|
72
|
-
with mock.patch.object(pyappify, "_is_process_running", return_value=False):
|
|
73
|
-
self.assertIsNone(pyappify.show_pyappify())
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if __name__ == "__main__":
|
|
77
|
-
unittest.main()
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
import importlib
|
|
2
|
-
import os
|
|
3
|
-
import unittest
|
|
4
|
-
|
|
5
|
-
import pyappify
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
ENV_KEYS = (
|
|
9
|
-
"PYAPPIFY_APP_VERSION",
|
|
10
|
-
"PYAPPIFY_APP_STARTING_VERSION",
|
|
11
|
-
"PYAPPIFY_UPDATE_NOTE",
|
|
12
|
-
"PYAPPIFY_APP_JSON_PATH",
|
|
13
|
-
"PYAPPIFY_LOCALE",
|
|
14
|
-
)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class TestUpdateEnv(unittest.TestCase):
|
|
18
|
-
def setUp(self):
|
|
19
|
-
self._saved_env = {key: os.environ.get(key) for key in ENV_KEYS}
|
|
20
|
-
|
|
21
|
-
def tearDown(self):
|
|
22
|
-
for key, value in self._saved_env.items():
|
|
23
|
-
if value is None:
|
|
24
|
-
os.environ.pop(key, None)
|
|
25
|
-
else:
|
|
26
|
-
os.environ[key] = value
|
|
27
|
-
importlib.reload(pyappify)
|
|
28
|
-
|
|
29
|
-
def _reload_with_env(self, **env):
|
|
30
|
-
for key in ENV_KEYS:
|
|
31
|
-
os.environ.pop(key, None)
|
|
32
|
-
for key, value in env.items():
|
|
33
|
-
os.environ[key] = value
|
|
34
|
-
return importlib.reload(pyappify)
|
|
35
|
-
|
|
36
|
-
def test_starting_version_falls_back_to_app_version(self):
|
|
37
|
-
module = self._reload_with_env(PYAPPIFY_APP_VERSION="1.2.3")
|
|
38
|
-
|
|
39
|
-
self.assertEqual("1.2.3", module.app_version)
|
|
40
|
-
self.assertEqual("1.2.3", module.app_starting_version)
|
|
41
|
-
self.assertFalse(module.is_app_updated())
|
|
42
|
-
self.assertFalse(module.is_updated())
|
|
43
|
-
self.assertFalse(module.is_app_downgraded())
|
|
44
|
-
self.assertFalse(module.is_downgrade())
|
|
45
|
-
|
|
46
|
-
def test_missing_versions_are_not_update_or_downgrade(self):
|
|
47
|
-
module = self._reload_with_env()
|
|
48
|
-
|
|
49
|
-
self.assertIsNone(module.app_version)
|
|
50
|
-
self.assertIsNone(module.app_starting_version)
|
|
51
|
-
self.assertFalse(module.is_app_updated())
|
|
52
|
-
self.assertFalse(module.is_app_downgraded())
|
|
53
|
-
|
|
54
|
-
def test_detects_update_and_downgrade(self):
|
|
55
|
-
module = self._reload_with_env(
|
|
56
|
-
PYAPPIFY_APP_VERSION="2.0.0",
|
|
57
|
-
PYAPPIFY_APP_STARTING_VERSION="1.0.0",
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
self.assertTrue(module.is_app_updated())
|
|
61
|
-
self.assertTrue(module.is_updated())
|
|
62
|
-
self.assertFalse(module.is_app_downgraded())
|
|
63
|
-
self.assertFalse(module.is_downgrade())
|
|
64
|
-
|
|
65
|
-
module = self._reload_with_env(
|
|
66
|
-
PYAPPIFY_APP_VERSION="1.0.0",
|
|
67
|
-
PYAPPIFY_APP_STARTING_VERSION="2.0.0",
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
self.assertFalse(module.is_app_updated())
|
|
71
|
-
self.assertFalse(module.is_updated())
|
|
72
|
-
self.assertTrue(module.is_app_downgraded())
|
|
73
|
-
self.assertTrue(module.is_downgrade())
|
|
74
|
-
|
|
75
|
-
def test_returns_update_notes_from_json_array(self):
|
|
76
|
-
module = self._reload_with_env(
|
|
77
|
-
PYAPPIFY_UPDATE_NOTE='["first change", "second change"]'
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
self.assertEqual(["first change", "second change"], module.get_update_notes())
|
|
81
|
-
self.assertEqual(["first change", "second change"], module.get_update_note())
|
|
82
|
-
|
|
83
|
-
def test_invalid_or_missing_update_notes_return_empty_list(self):
|
|
84
|
-
self.assertEqual([], self._reload_with_env().get_update_notes())
|
|
85
|
-
self.assertEqual(
|
|
86
|
-
[],
|
|
87
|
-
self._reload_with_env(PYAPPIFY_UPDATE_NOTE="not json").get_update_notes(),
|
|
88
|
-
)
|
|
89
|
-
self.assertEqual(
|
|
90
|
-
[],
|
|
91
|
-
self._reload_with_env(PYAPPIFY_UPDATE_NOTE='"single note"').get_update_notes(),
|
|
92
|
-
)
|
|
93
|
-
|
|
94
|
-
def test_reads_locale_with_english_fallback(self):
|
|
95
|
-
self.assertEqual("zh-CN", self._reload_with_env(PYAPPIFY_LOCALE="zh-CN").get_locale())
|
|
96
|
-
self.assertEqual("en", self._reload_with_env().get_locale())
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
if __name__ == "__main__":
|
|
100
|
-
unittest.main()
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import hashlib
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
import shutil
|
|
5
|
-
import subprocess
|
|
6
|
-
import tempfile
|
|
7
|
-
import time
|
|
8
|
-
import unittest
|
|
9
|
-
from pathlib import Path
|
|
10
|
-
|
|
11
|
-
import pyappify
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
EXECUTABLE_SHA256 = "50ef2557c193ca1f97c1e699bab3dad35b3966c9e8e8b860fb609bd5fb3861d1"
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
def _sha256(path):
|
|
18
|
-
digest = hashlib.sha256()
|
|
19
|
-
with open(path, "rb") as file:
|
|
20
|
-
for block in iter(lambda: file.read(1024 * 1024), b""):
|
|
21
|
-
digest.update(block)
|
|
22
|
-
return digest.hexdigest()
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
def assert_upgrade_kills_running_executable_and_replaces_it(
|
|
26
|
-
test_case, zip_url, timeout_seconds
|
|
27
|
-
):
|
|
28
|
-
source_executable = Path(os.environ["WINDIR"]) / "System32" / "PING.EXE"
|
|
29
|
-
test_case.assertTrue(source_executable.exists(), "Windows PING.EXE is required")
|
|
30
|
-
|
|
31
|
-
old_cwd = os.getcwd()
|
|
32
|
-
old_upgradeable = pyappify.pyappify_upgradeable
|
|
33
|
-
old_version = pyappify.pyappify_version
|
|
34
|
-
old_executable = pyappify.pyappify_executable
|
|
35
|
-
old_pid = pyappify.pid
|
|
36
|
-
old_logger = pyappify.logger
|
|
37
|
-
|
|
38
|
-
with tempfile.TemporaryDirectory() as temporary_directory:
|
|
39
|
-
executable = Path(temporary_directory) / "ok-ww.exe"
|
|
40
|
-
upgrade_temporary_directory = Path(temporary_directory) / "pyappify_tmp"
|
|
41
|
-
shutil.copy2(source_executable, executable)
|
|
42
|
-
test_case.assertNotEqual(_sha256(executable), EXECUTABLE_SHA256)
|
|
43
|
-
|
|
44
|
-
process = subprocess.Popen(
|
|
45
|
-
[str(executable), "-t", "127.0.0.1"],
|
|
46
|
-
cwd=temporary_directory,
|
|
47
|
-
stdout=subprocess.DEVNULL,
|
|
48
|
-
stderr=subprocess.DEVNULL,
|
|
49
|
-
)
|
|
50
|
-
try:
|
|
51
|
-
time.sleep(1)
|
|
52
|
-
test_case.assertIsNone(
|
|
53
|
-
process.poll(), "the current executable exited before upgrade began"
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
pyappify.pyappify_upgradeable = True
|
|
57
|
-
pyappify.pyappify_version = "v3.3.45"
|
|
58
|
-
pyappify.pyappify_executable = str(executable)
|
|
59
|
-
pyappify.pid = process.pid
|
|
60
|
-
pyappify.logger = logging.getLogger(__name__)
|
|
61
|
-
os.chdir(temporary_directory)
|
|
62
|
-
|
|
63
|
-
pyappify.upgrade("v3.3.46", EXECUTABLE_SHA256, [zip_url])
|
|
64
|
-
|
|
65
|
-
deadline = time.monotonic() + timeout_seconds
|
|
66
|
-
while time.monotonic() < deadline:
|
|
67
|
-
process_stopped = process.poll() is not None
|
|
68
|
-
upgraded = (
|
|
69
|
-
executable.exists()
|
|
70
|
-
and _sha256(executable) == EXECUTABLE_SHA256
|
|
71
|
-
)
|
|
72
|
-
update_finished = not upgrade_temporary_directory.exists()
|
|
73
|
-
if process_stopped and upgraded and update_finished:
|
|
74
|
-
break
|
|
75
|
-
time.sleep(0.1)
|
|
76
|
-
|
|
77
|
-
test_case.assertIsNotNone(
|
|
78
|
-
process.poll(), "upgrade did not stop the old process"
|
|
79
|
-
)
|
|
80
|
-
test_case.assertEqual(EXECUTABLE_SHA256, _sha256(executable))
|
|
81
|
-
finally:
|
|
82
|
-
os.chdir(old_cwd)
|
|
83
|
-
pyappify.pyappify_upgradeable = old_upgradeable
|
|
84
|
-
pyappify.pyappify_version = old_version
|
|
85
|
-
pyappify.pyappify_executable = old_executable
|
|
86
|
-
pyappify.pid = old_pid
|
|
87
|
-
pyappify.logger = old_logger
|
|
88
|
-
if process.poll() is None:
|
|
89
|
-
process.terminate()
|
|
90
|
-
try:
|
|
91
|
-
process.wait(timeout=5)
|
|
92
|
-
except subprocess.TimeoutExpired:
|
|
93
|
-
process.kill()
|
|
94
|
-
process.wait(timeout=5)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class TestUpgrade(unittest.TestCase):
|
|
98
|
-
LOCAL_ZIP = Path(__file__).with_name("ok-ww-win32.zip")
|
|
99
|
-
|
|
100
|
-
@unittest.skipUnless(os.name == "nt", "executable replacement test is Windows-only")
|
|
101
|
-
def test_upgrade_from_local_zip_kills_running_executable_and_replaces_it(self):
|
|
102
|
-
self.assertTrue(self.LOCAL_ZIP.exists(), "tests/ok-ww-win32.zip is required")
|
|
103
|
-
assert_upgrade_kills_running_executable_and_replaces_it(
|
|
104
|
-
self,
|
|
105
|
-
self.LOCAL_ZIP.as_uri(), timeout_seconds=30
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
if __name__ == "__main__":
|
|
110
|
-
unittest.main()
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import unittest
|
|
3
|
-
|
|
4
|
-
from tests.TestUpgrade import assert_upgrade_kills_running_executable_and_replaces_it
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
class TestUpgradeOnline(unittest.TestCase):
|
|
8
|
-
ZIP_URL = (
|
|
9
|
-
"https://github.com/ok-oldking/ok-wuthering-waves/"
|
|
10
|
-
"releases/download/v3.3.46/ok-ww-win32.zip"
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
@unittest.skipUnless(os.name == "nt", "executable replacement test is Windows-only")
|
|
14
|
-
def test_upgrade_from_online_zip_kills_running_executable_and_replaces_it(self):
|
|
15
|
-
assert_upgrade_kills_running_executable_and_replaces_it(
|
|
16
|
-
self, self.ZIP_URL, timeout_seconds=600
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if __name__ == "__main__":
|
|
21
|
-
unittest.main()
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
# filename: test_pyappify.is_greater_version.py
|
|
2
|
-
import unittest
|
|
3
|
-
import pyappify
|
|
4
|
-
|
|
5
|
-
class TestIsGreaterVersion(unittest.TestCase):
|
|
6
|
-
|
|
7
|
-
def test_version1_is_greater(self):
|
|
8
|
-
self.assertTrue(pyappify.is_greater_version("2.0.0", "1.9.9"))
|
|
9
|
-
self.assertTrue(pyappify.is_greater_version("1.2.0", "v1.1.9"))
|
|
10
|
-
self.assertTrue(pyappify.is_greater_version("v1.1.2", "1.1.1"))
|
|
11
|
-
self.assertTrue(pyappify.is_greater_version("1.1.0", "1.1"))
|
|
12
|
-
self.assertTrue(pyappify.is_greater_version("v10.0.0", "9.0.0"))
|
|
13
|
-
|
|
14
|
-
def test_version1_is_not_greater(self):
|
|
15
|
-
self.assertFalse(pyappify.is_greater_version("v1.0.0", "2.0.0"))
|
|
16
|
-
self.assertFalse(pyappify.is_greater_version("1.1.0", "v1.2.0"))
|
|
17
|
-
self.assertFalse(pyappify.is_greater_version("1.1.1", "1.1.2"))
|
|
18
|
-
self.assertFalse(pyappify.is_greater_version("1.1", "1.1.0"))
|
|
19
|
-
|
|
20
|
-
def test_versions_are_equal(self):
|
|
21
|
-
self.assertFalse(pyappify.is_greater_version("qwer", "1.0.0"))
|
|
22
|
-
self.assertFalse(pyappify.is_greater_version("1.2.3", "1.2.3"))
|
|
23
|
-
|
|
24
|
-
def test_invalid_or_edge_cases(self):
|
|
25
|
-
self.assertFalse(pyappify.is_greater_version(None, "1.0.0"))
|
|
26
|
-
self.assertFalse(pyappify.is_greater_version("1.0.0", None))
|
|
27
|
-
self.assertFalse(pyappify.is_greater_version(None, None))
|
|
28
|
-
self.assertFalse(pyappify.is_greater_version("v1.a.0", "1.0.0"))
|
|
29
|
-
self.assertFalse(pyappify.is_greater_version("1.0.0", "invalid"))
|
|
30
|
-
self.assertFalse(pyappify.is_greater_version("", "1.0.0"))
|
|
31
|
-
|
|
32
|
-
if __name__ == '__main__':
|
|
33
|
-
unittest.main(argv=['first-arg-is-ignored'], exit=False)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|