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.
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.4
2
- Name: pyappify
3
- Version: 1.0.6
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
- if _is_process_running(pid):
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
- if not pyappify_executable:
218
- log.error("PYAPPIFY_EXECUTABLE is not configured.")
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 = [pyappify_executable]
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(pyappify_executable) or None,
263
+ cwd=cwd or os.path.dirname(executable) or None,
232
264
  env=env,
233
265
  )
234
- pid = process.pid
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 {pyappify_executable}: {e}")
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
@@ -0,0 +1,2 @@
1
+ def main():
2
+ print("PyAppify Hello world")
@@ -1,12 +1,12 @@
1
- Metadata-Version: 2.4
2
- Name: pyappify
3
- Version: 1.0.6
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.6"
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
+
@@ -1,2 +0,0 @@
1
- def main():
2
- print("PyAppify Hello world")
@@ -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)