portable-python 1.8.4__tar.gz → 1.8.5__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.
Files changed (39) hide show
  1. {portable-python-1.8.4/src/portable_python.egg-info → portable-python-1.8.5}/PKG-INFO +1 -1
  2. portable-python-1.8.5/pyproject.toml +76 -0
  3. {portable-python-1.8.4 → portable-python-1.8.5}/setup.py +0 -1
  4. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/__init__.py +51 -36
  5. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/cli.py +7 -7
  6. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/config.py +68 -42
  7. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/cpython.py +41 -31
  8. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/external/_inspect.py +10 -11
  9. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/external/tkinter.py +3 -2
  10. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/external/xcpython.py +29 -12
  11. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/inspector.py +21 -17
  12. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/tracking.py +0 -3
  13. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/versions.py +31 -9
  14. {portable-python-1.8.4 → portable-python-1.8.5/src/portable_python.egg-info}/PKG-INFO +1 -1
  15. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_cleanup.py +1 -1
  16. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_inspector.py +1 -1
  17. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_list.py +6 -5
  18. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_setup.py +7 -6
  19. portable-python-1.8.4/pyproject.toml +0 -2
  20. {portable-python-1.8.4 → portable-python-1.8.5}/DEVELOP.md +0 -0
  21. {portable-python-1.8.4 → portable-python-1.8.5}/LICENSE +0 -0
  22. {portable-python-1.8.4 → portable-python-1.8.5}/MANIFEST.in +0 -0
  23. {portable-python-1.8.4 → portable-python-1.8.5}/README.rst +0 -0
  24. {portable-python-1.8.4 → portable-python-1.8.5}/SECURITY.md +0 -0
  25. {portable-python-1.8.4 → portable-python-1.8.5}/requirements.txt +0 -0
  26. {portable-python-1.8.4 → portable-python-1.8.5}/setup.cfg +0 -0
  27. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/__main__.py +0 -0
  28. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python/external/__init__.py +0 -0
  29. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python.egg-info/SOURCES.txt +0 -0
  30. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python.egg-info/dependency_links.txt +0 -0
  31. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python.egg-info/entry_points.txt +0 -0
  32. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python.egg-info/requires.txt +0 -0
  33. {portable-python-1.8.4 → portable-python-1.8.5}/src/portable_python.egg-info/top_level.txt +0 -0
  34. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_build.py +0 -0
  35. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_failed.py +0 -0
  36. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_invoker.py +0 -0
  37. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_prefix.py +0 -0
  38. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_recompress.py +0 -0
  39. {portable-python-1.8.4 → portable-python-1.8.5}/tests/test_report.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portable-python
3
- Version: 1.8.4
3
+ Version: 1.8.5
4
4
  Summary: Portable python binaries
5
5
  Home-page: https://github.com/codrsquad/portable-python
6
6
  Author: Zoran Simic
@@ -0,0 +1,76 @@
1
+ [build-system]
2
+ requires = ["setuptools", "wheel"]
3
+
4
+ [tool.ruff]
5
+ cache-dir = ".tox/.ruff_cache"
6
+ line-length = 140
7
+ src = ["src", "tests"]
8
+
9
+ [tool.ruff.lint]
10
+ extend-select = [
11
+ "A", # flake8-builtins
12
+ # "ARG", # flake8-unused-arguments
13
+ "B", # flake8-bugbear
14
+ "C4", # flake8-comprehensions
15
+ "C90", # mccabe
16
+ "D", # pydocstyle
17
+ "DTZ", # flake8-datetimez
18
+ "E", # pycodestyle errors
19
+ "EM", # flake8-errmsg
20
+ "ERA", # eradicate
21
+ "EXE", # flake8-executable
22
+ "F", # pyflakes
23
+ "FLY", # flynt
24
+ "G", # flake8-logging-format
25
+ "I", # isort
26
+ "INT", # flake8-gettext
27
+ "PGH", # pygrep-hooks
28
+ "PIE", # flake8-pie
29
+ "PT", # flake8-pytest
30
+ "PYI", # flake8-pyi
31
+ "Q", # flake8-quotes
32
+ "RSE", # flake8-raise
33
+ "RET", # flake8-return
34
+ "RUF", # ruff-specific
35
+ "S", # flake8-bandit
36
+ # "SIM", # flake8-simplify
37
+ "SLF", # flake8-self
38
+ "SLOT", # flake8-slots
39
+ "T10", # flake8-debugger
40
+ "TID", # flake8-tidy-imports
41
+ "TCH", # flake8-type-checking
42
+ # "TD", # flake8-todos
43
+ "TRY", # tryceratops
44
+ "W", # pycodestyle warnings
45
+ ]
46
+ ignore = [
47
+ # TODO: gradually remove these (document all the things)
48
+ "D100", # Missing docstring in public module
49
+ "D101", # Missing docstring in public class
50
+ "D102", # Missing docstring in public method
51
+ "D103", # Missing docstring in public function
52
+ "D104", # Missing docstring in public package
53
+ "D105", # Missing docstring in magic method
54
+ "D200", # One-line docstring should fit on one line with quotes
55
+ "D205", # 1 blank line required between summary line and description
56
+ "D400", # First line should end with a period
57
+ # Not useful:
58
+ "RET503", # Missing explicit `return` at the end of function able to return non-`None` value
59
+ ]
60
+
61
+ [tool.ruff.lint.flake8-builtins]
62
+ builtins-ignorelist = ["bin", "compile"]
63
+
64
+ [tool.ruff.lint.isort]
65
+ order-by-type = false
66
+
67
+ [tool.ruff.lint.mccabe]
68
+ max-complexity = 14
69
+
70
+ [tool.ruff.lint.pydocstyle]
71
+ convention = "numpy"
72
+
73
+ [tool.ruff.per-file-ignores]
74
+ "tests/*" = [
75
+ "S", # No security checks for tests
76
+ ]
@@ -1,6 +1,5 @@
1
1
  from setuptools import setup
2
2
 
3
-
4
3
  setup(
5
4
  name="portable-python",
6
5
  setup_requires="setupmeta",
@@ -15,7 +15,7 @@ import multiprocessing
15
15
  import os
16
16
  import pathlib
17
17
  import re
18
- from typing import List
18
+ from typing import ClassVar, List
19
19
 
20
20
  import runez
21
21
  from runez.http import RestClient
@@ -24,7 +24,6 @@ from runez.render import Header, PrettyTable
24
24
 
25
25
  from portable_python.versions import PPG
26
26
 
27
-
28
27
  LOG = logging.getLogger(__name__)
29
28
  RX_BINARY = re.compile(r"^.*\.(dylib|gmo|icns|ico|nib|prof.*|tar)$")
30
29
 
@@ -36,11 +35,16 @@ def is_binary_file(path):
36
35
  def patch_folder(folder, regex, replacement, ignore=None):
37
36
  """Replace all occurrences of 'old_text' by 'new_text' in all files in 'folder'
38
37
 
39
- Args:
40
- folder (pathlib.Path): Folder to scan
41
- regex: Regex to replace
42
- replacement (str): Replacement text
43
- ignore: Regex stating what to ignore
38
+ Parameters
39
+ ----------
40
+ folder : pathlib.Path
41
+ Folder to scan
42
+ regex : str
43
+ Regex to replace
44
+ replacement : str
45
+ Replacement text
46
+ ignore : re.Pattern | None
47
+ Regex stating what to ignore
44
48
  """
45
49
  for path in runez.ls_dir(folder):
46
50
  if not path.is_symlink() and (not ignore or not ignore.match(path.name)):
@@ -53,21 +57,21 @@ def patch_folder(folder, regex, replacement, ignore=None):
53
57
 
54
58
  def patch_file(path, regex, replacement):
55
59
  try:
56
- with open(path, "rt") as fh:
60
+ with open(path) as fh:
57
61
  text = fh.read()
58
62
 
59
63
  new_text = re.sub(regex, replacement, text, flags=re.MULTILINE)
60
64
  if text != new_text:
61
- with open(path, "wt") as fh:
65
+ with open(path, "w") as fh:
62
66
  fh.write(new_text)
63
67
 
64
- LOG.info("Patched '%s' in %s" % (regex, runez.short(path)))
68
+ LOG.info("Patched '%s' in %s", regex, runez.short(path))
65
69
 
66
70
  except Exception as e:
67
- with open(path, "rt", errors="ignore") as fh:
71
+ with open(path, errors="ignore") as fh:
68
72
  text = fh.read()
69
73
  if re.search(regex, text):
70
- LOG.warning("Can't patch '%s': %s" % (runez.short(path), e))
74
+ LOG.warning("Can't patch '%s': %s", runez.short(path), e)
71
75
 
72
76
 
73
77
  class FolderMask:
@@ -81,7 +85,7 @@ class FolderMask:
81
85
  """
82
86
 
83
87
  def __init__(self, target_folder):
84
- LOG.info("Applying isolation hack/mask to %s" % target_folder)
88
+ LOG.info("Applying isolation hack/mask to %s", target_folder)
85
89
  self.target_folder = target_folder
86
90
  r = runez.run("hdiutil", "attach", "-nomount", "ram://2048", fatal=Exception)
87
91
  self.ram_disk = r.output.strip()
@@ -93,7 +97,7 @@ class FolderMask:
93
97
  self.mounted = True
94
98
 
95
99
  def cleanup(self):
96
- LOG.info("Cleaning up isolation hack/mask for %s" % self.target_folder)
100
+ LOG.info("Cleaning up isolation hack/mask for %s", self.target_folder)
97
101
  if self.mounted:
98
102
  runez.run("umount", self.target_folder, fatal=False)
99
103
 
@@ -118,8 +122,12 @@ class BuildContext:
118
122
 
119
123
  def _resolved_isolation(self):
120
124
  """
121
- Returns:
122
- (str | None): What strategy to use to work around the fact that python's ./configure script looks at /usr/local
125
+ Isolation setting currently configured.
126
+
127
+ Returns
128
+ -------
129
+ str | None
130
+ What strategy to use to work around the fact that python's ./configure script looks at /usr/local
123
131
  """
124
132
  v = PPG.config.get_value("isolate-usr-local")
125
133
  if v == "auto":
@@ -182,7 +190,7 @@ class BuildContext:
182
190
 
183
191
  class BuildSetup:
184
192
  """
185
- This class drives the compilation, external modules first, then the target python itself.
193
+ Drives the compilation, external modules first, then the target python itself.
186
194
  All modules are compiled in the same manner, follow the same conventional build layout.
187
195
  """
188
196
 
@@ -191,10 +199,14 @@ class BuildSetup:
191
199
 
192
200
  def __init__(self, python_spec=None, modules=None, prefix=None):
193
201
  """
194
- Args:
195
- python_spec (str | PythonSpec | None): Python to build (family and version)
196
- modules (str | None): Modules to build (default: from config)
197
- prefix (str | None): --prefix to use
202
+ Parameters
203
+ ----------
204
+ python_spec : str | PythonSpec | None
205
+ Python to build (family and version)
206
+ modules : str | None
207
+ Modules to build (default: from config)
208
+ prefix : str | None
209
+ --prefix to use
198
210
  """
199
211
  if not python_spec or python_spec == "latest":
200
212
  python_spec = PPG.cpython.latest
@@ -267,11 +279,11 @@ class BuildSetup:
267
279
  with BuildContext(self) as build_context:
268
280
  self.build_context = build_context
269
281
  modules = self.python_builder.modules
270
- LOG.info("portable-python v%s, current folder: %s" % (runez.get_version(__name__), os.getcwd()))
282
+ LOG.info("portable-python v%s, current folder: %s", runez.get_version(__name__), os.getcwd())
271
283
  LOG.info(runez.joined(modules, list(modules)))
272
284
  LOG.info(PPG.config.config_files_report())
273
- LOG.info("Platform: %s" % PPG.target)
274
- LOG.info("Build report:\n%s" % self.python_builder.modules.report())
285
+ LOG.info("Platform: %s", PPG.target)
286
+ LOG.info("Build report:\n%s", self.python_builder.modules.report())
275
287
  self.validate_module_selection(fatal=not runez.DRYRUN and not self.x_debug)
276
288
  self.ensure_clean_folder(self.folders.components)
277
289
  self.ensure_clean_folder(self.folders.deps)
@@ -375,7 +387,6 @@ class ModuleCollection:
375
387
 
376
388
 
377
389
  class LinkerOutcome(enum.Enum):
378
-
379
390
  absent = "orange"
380
391
  failed = "red"
381
392
  shared = "blue"
@@ -389,6 +400,7 @@ class ModuleBuilder:
389
400
  m_build_cwd: str = None # Optional: relative (to unpacked source) folder where to run configure/make from
390
401
  m_debian = None
391
402
  m_include: str = None # Optional: subfolder to automatically list in CPATH when this module is active
403
+ m_telltale: ClassVar[list] # Optional: list of files that, if present, indicate this module is installed
392
404
 
393
405
  setup: BuildSetup
394
406
  parent_module: "ModuleBuilder" = None
@@ -396,8 +408,10 @@ class ModuleBuilder:
396
408
 
397
409
  def __init__(self, parent_module):
398
410
  """
399
- Args:
400
- parent_module (BuildSetup | ModuleBuilder): Associated parent
411
+ Parameters
412
+ ----------
413
+ parent_module : BuildSetup | ModuleBuilder
414
+ Associated parent
401
415
  """
402
416
  self.m_name = ModuleCollection.get_module_name(self.__class__)
403
417
  if isinstance(parent_module, BuildSetup):
@@ -458,7 +472,7 @@ class ModuleBuilder:
458
472
  if self.resolved_telltale:
459
473
  return "has %s" % self.resolved_telltale
460
474
 
461
- return "no %s" % getattr(self, "m_telltale")
475
+ return "no %s" % self.m_telltale
462
476
 
463
477
  def _find_telltale(self):
464
478
  telltales = getattr(self, "m_telltale", runez.UNSET)
@@ -525,8 +539,7 @@ class ModuleBuilder:
525
539
 
526
540
  def run_configure(self, program, *args, prefix=None):
527
541
  """
528
- Calling ./configure is similar across all components.
529
- This allows to have descendants customize each part relatively elegantly
542
+ Run ./configure script for this module.
530
543
  """
531
544
  if prefix is None:
532
545
  prefix = self.deps
@@ -564,7 +577,8 @@ class ModuleBuilder:
564
577
  yield
565
578
 
566
579
  except Exception as e:
567
- LOG.error("Error while compiling %r: %r", self, e)
580
+ overview = repr(e)
581
+ LOG.exception("Error while compiling %s: %s", self, overview)
568
582
  raise
569
583
 
570
584
  finally:
@@ -598,7 +612,7 @@ class ModuleBuilder:
598
612
  env_vars = self._get_env_vars()
599
613
  prev_env_vars = {}
600
614
  for var_name, value in env_vars.items():
601
- LOG.info("env %s=%s" % (var_name, runez.short(value, size=2048)))
615
+ LOG.info("env %s=%s", var_name, runez.short(value, size=2048))
602
616
  prev_env_vars[var_name] = os.environ.get(var_name)
603
617
  os.environ[var_name] = value
604
618
 
@@ -672,7 +686,6 @@ class ModuleBuilder:
672
686
 
673
687
 
674
688
  class PythonBuilder(ModuleBuilder):
675
-
676
689
  _bin_python: pathlib.Path = None
677
690
 
678
691
  def __init__(self, parent_module):
@@ -692,8 +705,10 @@ class PythonBuilder(ModuleBuilder):
692
705
  @property
693
706
  def bin_python(self):
694
707
  """
695
- Returns:
696
- (pathlib.Path | None): Path to freshly compiled bin/python, real file (not symlink)
708
+ Returns
709
+ -------
710
+ pathlib.Path | None
711
+ Path to freshly compiled bin/python, real file (not symlink)
697
712
  """
698
713
  if self._bin_python is None:
699
714
  self._bin_python = PPG.config.find_main_file(self.bin_folder / "python", self.version)
@@ -724,5 +739,5 @@ class PythonBuilder(ModuleBuilder):
724
739
  expected = 0o755 if path.is_dir() else 0o644
725
740
  current = path.stat().st_mode & 0o777
726
741
  if current != expected:
727
- LOG.info("Corrected permissions for %s (was %s)" % (runez.short(path), oct(current)))
742
+ LOG.info("Corrected permissions for %s (was %s)", runez.short(path), oct(current))
728
743
  path.chmod(expected)
@@ -8,7 +8,6 @@ from runez.render import PrettyTable
8
8
  from portable_python import BuildSetup, PPG
9
9
  from portable_python.inspector import LibAutoCorrect, PythonInspector
10
10
 
11
-
12
11
  LOG = logging.getLogger(__name__)
13
12
 
14
13
 
@@ -60,10 +59,6 @@ def build_report(modules, python_spec):
60
59
  def diagnostics():
61
60
  """Show diagnostics info"""
62
61
  with runez.Anchored("."):
63
- def _diagnostics():
64
- yield "invoker python", runez.SYS_INFO.invoker_python
65
- yield from runez.SYS_INFO.diagnostics()
66
-
67
62
  config = PPG.config.represented()
68
63
  print(PrettyTable.two_column_diagnostics(_diagnostics(), config))
69
64
 
@@ -106,6 +101,11 @@ def list_cmd(json, family):
106
101
  print(" %s: %s" % (runez.bold(mm), v))
107
102
 
108
103
 
104
+ def _diagnostics():
105
+ yield "invoker python", runez.SYS_INFO.invoker_python
106
+ yield from runez.SYS_INFO.diagnostics()
107
+
108
+
109
109
  def _find_recompress_source(folders, path):
110
110
  candidate = runez.to_path(path)
111
111
  if candidate.exists() or candidate.is_absolute():
@@ -158,7 +158,7 @@ def recompress(path, ext):
158
158
 
159
159
  \b
160
160
  Mildly useful for comparing sizes from different compressions
161
- """
161
+ """ # noqa: D301
162
162
  extension = runez.SYS_INFO.platform_id.canonical_compress_extension(ext)
163
163
  pspec = PythonSpec.from_text(path)
164
164
  folders = PPG.get_folders(base=".", family=pspec and pspec.family, version=pspec and pspec.version)
@@ -203,6 +203,6 @@ def lib_auto_correct(commit, prefix, path):
203
203
 
204
204
 
205
205
  if __name__ == "__main__":
206
- from portable_python.cli import main # noqa, re-import with proper package
206
+ from portable_python.cli import main
207
207
 
208
208
  main()
@@ -9,7 +9,6 @@ import runez
9
9
  import yaml
10
10
  from runez.pyenv import Version
11
11
 
12
-
13
12
  LOG = logging.getLogger(__name__)
14
13
 
15
14
  DEFAULT_CONFIG = """
@@ -79,9 +78,12 @@ class Config:
79
78
 
80
79
  def __init__(self, paths=None, target=None):
81
80
  """
82
- Args:
83
- paths (str | list | None): Path(s) to config file(s)
84
- target (str | runez.system.PlatformId | None): Target platform (for testing, defaults to current platform)
81
+ Parameters
82
+ ----------
83
+ paths : str | list | None
84
+ Path(s) to config file(s)
85
+ target : str | runez.system.PlatformId | None
86
+ Target platform (for testing, defaults to current platform)
85
87
  """
86
88
  self.paths = runez.flattened(paths, split=",")
87
89
  if not isinstance(target, runez.system.PlatformId):
@@ -98,17 +100,21 @@ class Config:
98
100
  return "%s [%s]" % (runez.plural(self._sources, "config source"), self.target)
99
101
 
100
102
  def completions(self, **given):
101
- res = dict(arch=self.target.arch, platform=self.target.platform, subsystem=self.target.subsystem, target=str(self.target))
103
+ res = {"arch": self.target.arch, "platform": self.target.platform, "subsystem": self.target.subsystem, "target": str(self.target)}
102
104
  res.update(given)
103
105
  return res
104
106
 
105
107
  def get_value(self, *key, by_platform=True):
106
108
  """
107
- Args:
108
- key (str | tuple): Key to look up, tuple represents hierarchy, ie: a/b -> (a, b)
109
- by_platform (bool): If True, value can be configured by platform
110
-
111
- Returns:
109
+ Parameters
110
+ ----------
111
+ key : strzz | tuple
112
+ Key to look up, tuple represents hierarchy, ie: a/b -> (a, b)
113
+ by_platform : bool
114
+ If True, value can be configured by platform
115
+
116
+ Returns
117
+ -------
112
118
  Associated value, if any
113
119
  """
114
120
  value, _ = self.get_entry(*key, by_platform=by_platform)
@@ -116,18 +122,23 @@ class Config:
116
122
 
117
123
  def get_entry(self, *key, by_platform=True):
118
124
  """
119
- Args:
120
- key (str | tuple): Key to look up, tuple represents hierarchy, ie: a/b -> (a, b)
121
- by_platform (bool): If True, value can be configured by platform
122
-
123
- Returns:
124
- Associated value, if any
125
+ Parameters
126
+ ----------
127
+ key : str | tuple
128
+ Key to look up, tuple represents hierarchy, ie: a/b -> (a, b)
129
+ by_platform : bool
130
+ If True, value can be configured by platform
131
+
132
+ Returns
133
+ -------
134
+ (str | int | float | bool | dict | list | None, ConfigSource | None)
135
+ Associated value (if any), together with the source that defined it
125
136
  """
126
137
  if by_platform:
127
138
  keys = (self.target.platform, self.target.arch, *key), (self.target.platform, *key), key
128
139
 
129
140
  else:
130
- keys = (key, )
141
+ keys = (key,)
131
142
 
132
143
  for k in keys:
133
144
  for source in self._sources:
@@ -150,7 +161,7 @@ class Config:
150
161
  return value
151
162
 
152
163
  def config_files_report(self):
153
- """One liner describing which config files are used, if any"""
164
+ """One-liner describing which config files are used, if any"""
154
165
  if len(self._sources) > 1:
155
166
  return "Config files: %s" % runez.joined(self._sources[:-1], delimiter=", ")
156
167
 
@@ -174,7 +185,7 @@ class Config:
174
185
  def delete(path):
175
186
  size = runez.filesize(path)
176
187
  runez.delete(path, logger=None)
177
- LOG.info("Deleted %s (%s)" % (runez.short(path), runez.represented_bytesize(size)))
188
+ LOG.info("Deleted %s (%s)", runez.short(path), runez.represented_bytesize(size))
178
189
  return size
179
190
 
180
191
  @staticmethod
@@ -187,10 +198,14 @@ class Config:
187
198
 
188
199
  def cleanup_configured_globs(self, title, module, *keys):
189
200
  """
190
- Args:
191
- title (str): Title to use in log messages
192
- module (portable_python.PythonBuilder): Associated python builder module
193
- *keys (str): Config keys to lookup
201
+ Parameters
202
+ ----------
203
+ title : str
204
+ Title to use in log messages
205
+ module : portable_python.PythonBuilder
206
+ Associated python builder module
207
+ *keys : str
208
+ Config keys to lookup
194
209
  """
195
210
  globs = [(x, f"{x}-{self.target.platform}") for x in keys]
196
211
  globs = runez.flattened(globs, transform=self.get_value)
@@ -199,16 +214,20 @@ class Config:
199
214
 
200
215
  def cleanup_globs(self, title, module, *globs):
201
216
  """
202
- Args:
203
- title (str): Title to use in log messages
204
- module (portable_python.PythonBuilder): Associated python builder module
205
- *globs (str): Glob patterns to clean up
217
+ Parameters
218
+ ----------
219
+ title : str
220
+ Title to use in log messages
221
+ module : portable_python.PythonBuilder
222
+ Associated python builder module
223
+ *globs : str
224
+ Glob patterns to clean up
206
225
  """
207
226
  if globs:
208
227
  spec = [module.setup.folders.formatted(x) for x in globs]
209
228
  deleted_size = 0
210
229
  matcher = FileMatcher(spec)
211
- LOG.info("Applying clean-up spec: %s" % matcher)
230
+ LOG.info("Applying clean-up spec: %s", matcher)
212
231
  cleaned = []
213
232
  for dirpath, dirnames, filenames in os.walk(module.install_folder):
214
233
  removed = []
@@ -241,7 +260,7 @@ class Config:
241
260
  _find_file_duplicates(seen, folder)
242
261
  duplicates = {k: v for k, v in seen.items() if len(v) > 1}
243
262
  for dupes in duplicates.values():
244
- LOG.info("Found duplicates: %s" % runez.joined(dupes, delimiter=", "))
263
+ LOG.info("Found duplicates: %s", runez.joined(dupes, delimiter=", "))
245
264
  dupes = sorted(dupes, key=lambda x: len(str(x)))
246
265
  if len(dupes) == 2:
247
266
  shorter, longer = dupes
@@ -261,14 +280,19 @@ class Config:
261
280
  return basename, "%s%s" % (basename, version.major), "%s%s" % (basename, version.mm)
262
281
 
263
282
  @staticmethod
264
- def find_main_file(desired: pathlib.Path, version: Version):
283
+ def find_main_file(desired, version):
265
284
  """
266
- Args:
267
- desired: Desired path (base name considered if path not directly available)
268
- version: Associated version
269
-
270
- Returns:
271
- (pathlib.Path | None): Path to associated real file (not symlink)
285
+ Parameters
286
+ ----------
287
+ desired : pathlib.Path
288
+ Desired path (base name considered if path not directly available)
289
+ version : Version
290
+ Associated version
291
+
292
+ Returns
293
+ -------
294
+ pathlib.Path | None
295
+ Path to associated real file (not symlink)
272
296
  """
273
297
  p = Config.real_path(desired)
274
298
  if p:
@@ -335,10 +359,14 @@ class ConfigSource:
335
359
 
336
360
  def get_value(self, key):
337
361
  """
338
- Args:
339
- key (str | tuple): Key to look up, tuple represents hierarchy, ie: a/b -> (a, b)
340
-
341
- Returns:
362
+ Parameters
363
+ ----------
364
+ key : str | tuple
365
+ Key to look up, tuple represents hierarchy, ie: a/b -> (a, b)
366
+
367
+ Returns
368
+ -------
369
+ str | int | float | bool | dict | list | None
342
370
  Associated value, if any
343
371
  """
344
372
  return self._deep_get(self.data, key)
@@ -360,7 +388,6 @@ class ConfigSource:
360
388
 
361
389
 
362
390
  class FileMatcher:
363
-
364
391
  def __init__(self, clean_spec):
365
392
  self.matches = []
366
393
  for spec in clean_spec:
@@ -376,7 +403,6 @@ class FileMatcher:
376
403
 
377
404
 
378
405
  class SingleFileMatch:
379
-
380
406
  _on_folder = False
381
407
  _rx_basename = None
382
408
  _rx_path = None
@@ -12,7 +12,6 @@ from portable_python.external.tkinter import TkInter
12
12
  from portable_python.external.xcpython import Bdb, Bzip2, Gdbm, LibFFI, Openssl, Readline, Sqlite, Uuid, Xz, Zlib
13
13
  from portable_python.inspector import LibAutoCorrect, PythonInspector
14
14
 
15
-
16
15
  # https://github.com/docker-library/python/issues/160
17
16
  PGO_TESTS = """
18
17
  -m test.regrtest --pgo test_array test_base64 test_binascii test_binhex test_binop test_bytes test_c_locale_coercion
@@ -24,12 +23,17 @@ test_struct test_threading test_time test_traceback test_unicode
24
23
 
25
24
  def represented_yaml(key_value_pairs):
26
25
  """
27
- Represented yaml for given key/value pairs
28
- Args:
29
- key_value_pairs (generator): Key/value pairs to represent
26
+ Yaml representation for given key/value pairs
27
+
28
+ Parameters
29
+ ----------
30
+ key_value_pairs : iterable
31
+ Key/value pairs to represent
30
32
 
31
- Returns:
32
- (str): Yaml representation
33
+ Returns
34
+ -------
35
+ str
36
+ Yaml representation
33
37
  """
34
38
  content = [yaml.safe_dump(runez.serialize.json_sanitized({x: y}), width=140) for x, y in key_value_pairs]
35
39
  return runez.joined(content, delimiter="\n")
@@ -46,28 +50,38 @@ class Cpython(PythonBuilder):
46
50
 
47
51
  def build_information(self):
48
52
  """
49
- Yields key/value pairs to store as build information.
53
+ Build information to store in manifest.
50
54
  `None` or empty values are "naturally" omitted by the fact we use `runez.joined()` and `runez.flattened()`,
51
- which have a default parameter `keep_empty=False`
55
+ which have a default parameter `keep_empty=False`.
56
+
57
+ Yields
58
+ ------
59
+ (str, dict)
52
60
  """
53
- yield "cpython", {
54
- "prefix": self.setup.prefix,
55
- "source": self.url,
56
- "static": runez.joined(self.modules.selected) or None,
57
- "target": PPG.target,
58
- "version": self.version,
59
- }
61
+ yield (
62
+ "cpython",
63
+ {
64
+ "prefix": self.setup.prefix,
65
+ "source": self.url,
66
+ "static": runez.joined(self.modules.selected) or None,
67
+ "target": PPG.target,
68
+ "version": self.version,
69
+ },
70
+ )
60
71
  yield "configure-args", runez.joined(runez.short(x) for x in self.c_configure_args())
61
72
  compiled_by = os.environ.get("PP_ORIGIN") or PPG.config.get_value("compiled-by")
62
73
  bc = self.setup.build_context
63
- yield "compilation-info", {
64
- "compiled-by": compiled_by or "https://pypi.org/project/portable-python/",
65
- "date": datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"),
66
- "host-platform": runez.SYS_INFO.platform_info,
67
- "ldd-version": PythonInspector.tool_version("ldd"),
68
- "portable-python-version": runez.get_version(__package__),
69
- "special-context": bc.isolate_usr_local and bc,
70
- }
74
+ yield (
75
+ "compilation-info",
76
+ {
77
+ "compiled-by": compiled_by or "https://pypi.org/project/portable-python/",
78
+ "date": datetime.datetime.now(tz=datetime.timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"),
79
+ "host-platform": runez.SYS_INFO.platform_info,
80
+ "ldd-version": PythonInspector.tool_version("ldd"),
81
+ "portable-python-version": runez.get_version(__package__),
82
+ "special-context": bc.isolate_usr_local and bc,
83
+ },
84
+ )
71
85
  additional = PPG.config.get_value("manifest", "additional-info")
72
86
  if additional:
73
87
  res = {}
@@ -174,7 +188,7 @@ class Cpython(PythonBuilder):
174
188
  # Special edge case in macosx_sdk_specified() where /usr/local is fine...
175
189
  x = "startswith({q}/usr/{q}) and not path.startswith({q}{p}{q})"
176
190
  special_case = x.replace("(", r"\(").replace(")", r"\)").format(q="['\"]", p=self.deps)
177
- restored = x.format(q="'", p='/usr/local')
191
+ restored = x.format(q="'", p="/usr/local")
178
192
  patch_file(setup_py, special_case, restored)
179
193
 
180
194
  # Only doable on macOS: patch -install_name so produced exes/libs use a relative path
@@ -286,11 +300,7 @@ class Cpython(PythonBuilder):
286
300
  Autocorrect pkgconfig and sysconfig to use relative paths
287
301
  See https://manpages.debian.org/stretch/pkg-config/pkg-config.1.en.html#PKG-CONFIG_DERIVED_VARIABLES
288
302
  """
289
- patch_folder(
290
- self.install_folder / "lib/pkgconfig",
291
- f"prefix={self.c_configure_prefix}",
292
- "prefix=${pcfiledir}/../.."
293
- )
303
+ patch_folder(self.install_folder / "lib/pkgconfig", f"prefix={self.c_configure_prefix}", "prefix=${pcfiledir}/../..")
294
304
  sys_cfg = self._find_sys_cfg()
295
305
  if sys_cfg:
296
306
  rs = RelSysConf(sys_cfg, self.c_configure_prefix)
@@ -347,7 +357,7 @@ class Cpython(PythonBuilder):
347
357
 
348
358
  if lines:
349
359
  LOG.info("Auto-corrected shebang for %s" % runez.short(path))
350
- with open(path, "wt") as fh:
360
+ with open(path, "w") as fh:
351
361
  for line in lines:
352
362
  fh.write(line)
353
363
 
@@ -375,7 +385,7 @@ class RelSysConf:
375
385
  def _relativize(self, line):
376
386
  start = 0
377
387
  for m in self.rx_strings.finditer(line):
378
- yield line[start:m.start(0)]
388
+ yield line[start : m.start(0)]
379
389
  start = m.end(0)
380
390
  content = m.group(2)
381
391
  if self.prefix in content:
@@ -4,7 +4,6 @@ import re
4
4
  import sys
5
5
  import sysconfig
6
6
 
7
-
8
7
  RX_VERSION = re.compile(r"\d\.\d")
9
8
  INSIGHTS = {
10
9
  "_gdbm": "_GDBM_VERSION",
@@ -38,7 +37,7 @@ def get_version(text):
38
37
  def pymodule_version_info(key, value, pymodule):
39
38
  version = get_version(value)
40
39
  if version:
41
- result = dict(version_field=key, version=version)
40
+ result = {"version_field": key, "version": version}
42
41
  if hasattr(pymodule, "__file__"):
43
42
  result["path"] = pymodule.__file__
44
43
 
@@ -54,14 +53,14 @@ def pymodule_info(module_name, pymodule):
54
53
  return v
55
54
 
56
55
  if hasattr(pymodule, "__file__"):
57
- return dict(path=pymodule.__file__)
56
+ return {"path": pymodule.__file__}
58
57
 
59
58
  if hasattr(pymodule, "__spec__"):
60
- v = getattr(pymodule.__spec__, "origin")
59
+ v = getattr(pymodule.__spec__, "origin", None)
61
60
  if v == "built-in":
62
- return dict(version=v)
61
+ return {"version": v}
63
62
 
64
- return dict(note=str(dir(pymodule)))
63
+ return {"note": str(dir(pymodule))}
65
64
 
66
65
 
67
66
  def module_report(module_name):
@@ -71,9 +70,9 @@ def module_report(module_name):
71
70
  except Exception as e:
72
71
  note = str(e)
73
72
  if "No module named" in note:
74
- return dict(version="*absent*")
73
+ return {"version": "*absent*"}
75
74
 
76
- return dict(version="*absent*", note=note)
75
+ return {"version": "*absent*", "note": note}
77
76
 
78
77
 
79
78
  def get_srcdir():
@@ -92,7 +91,7 @@ def get_simplified_dirs(path):
92
91
  if path.startswith("/private"):
93
92
  result.append(path[8:]) # whoever compiled didn't use realpath(tmp)
94
93
 
95
- elif not path.startswith("/tmp"): # nosec, just simplifying paths
94
+ elif not path.startswith("/tmp"): # noqa: S108, just simplifying path representation
96
95
  result.append(os.path.dirname(result[0]))
97
96
 
98
97
  return result
@@ -121,8 +120,8 @@ def main(arg):
121
120
  names.remove("pip")
122
121
  names.append("pip")
123
122
 
124
- report = dict((k, module_report(k)) for k in names)
125
- report = dict(report=report, srcdir=get_srcdir(), prefix=sysconfig.get_config_var("prefix"))
123
+ report = dict((k, module_report(k)) for k in names) # noqa: C402, works on py2 as well (can inspect py2)
124
+ report = {"report": report, "srcdir": get_srcdir(), "prefix": sysconfig.get_config_var("prefix")}
126
125
  print(json.dumps(report, indent=2, sort_keys=True))
127
126
 
128
127
 
@@ -5,6 +5,8 @@ It is also unclear what version to use, which build docs to follow etc.
5
5
  Contributions are welcome!
6
6
  """
7
7
 
8
+ from typing import ClassVar
9
+
8
10
  import runez
9
11
 
10
12
  from portable_python import ModuleBuilder, PPG
@@ -37,7 +39,6 @@ class Tcl(ModuleBuilder):
37
39
 
38
40
 
39
41
  class Tk(ModuleBuilder):
40
-
41
42
  m_build_cwd = "unix"
42
43
 
43
44
  @property
@@ -107,7 +108,7 @@ class TkInter(ModuleBuilder):
107
108
  """
108
109
 
109
110
  m_debian = "-tk-dev"
110
- m_telltale = ["{include}/tk", "{include}/tk.h"]
111
+ m_telltale: ClassVar[list] = ["{include}/tk", "{include}/tk.h"]
111
112
 
112
113
  @classmethod
113
114
  def candidate_modules(cls):
@@ -1,3 +1,5 @@
1
+ from typing import ClassVar
2
+
1
3
  import runez
2
4
 
3
5
  from portable_python import LinkerOutcome, ModuleBuilder, PPG
@@ -64,7 +66,7 @@ class Gdbm(ModuleBuilder):
64
66
  """
65
67
 
66
68
  m_debian = "libgdbm-dev"
67
- m_telltale = ["{include}/gdbm.h"]
69
+ m_telltale: ClassVar[list] = ["{include}/gdbm.h"]
68
70
 
69
71
  @property
70
72
  def url(self):
@@ -102,7 +104,7 @@ class LibFFI(ModuleBuilder):
102
104
  """
103
105
 
104
106
  m_debian = "!libffi-dev"
105
- m_telltale = ["{include}/ffi.h", "{include}/ffi/ffi.h"]
107
+ m_telltale: ClassVar[list] = ["{include}/ffi.h", "{include}/ffi/ffi.h"]
106
108
 
107
109
  xenv_CFLAGS = "-fPIC"
108
110
 
@@ -163,7 +165,6 @@ class Openssl(ModuleBuilder):
163
165
 
164
166
 
165
167
  class Ncurses(ModuleBuilder):
166
-
167
168
  m_include = "ncursesw"
168
169
 
169
170
  xenv_CFLAGS = "-fPIC"
@@ -231,8 +232,14 @@ class Readline(ModuleBuilder):
231
232
 
232
233
  def _do_linux_compile(self):
233
234
  self.run_configure(
234
- "./configure", "--disable-shared", "--enable-static", "--with-curses", "--enable-multibyte",
235
- "--disable-install-examples", "--disable-docs", "--enable-portable-binary",
235
+ "./configure",
236
+ "--disable-shared",
237
+ "--enable-static",
238
+ "--with-curses",
239
+ "--enable-multibyte",
240
+ "--disable-install-examples",
241
+ "--disable-docs",
242
+ "--enable-portable-binary",
236
243
  )
237
244
  self.run_make(cpu_count=0)
238
245
  self.run_make("install", cpu_count=0)
@@ -245,7 +252,7 @@ class Sqlite(ModuleBuilder):
245
252
  """
246
253
 
247
254
  m_debian = "+libsqlite3-dev"
248
- m_telltale = ["{include}/sqlite3.h"]
255
+ m_telltale: ClassVar[list] = ["{include}/sqlite3.h"]
249
256
 
250
257
  xenv_CFLAGS = "-fPIC"
251
258
 
@@ -265,7 +272,12 @@ class Sqlite(ModuleBuilder):
265
272
 
266
273
  def _do_linux_compile(self):
267
274
  self.run_configure(
268
- "./configure", "--enable-shared=no", "--enable-static=yes", "--disable-tcl", "--disable-readline", "--with-pic=yes"
275
+ "./configure",
276
+ "--enable-shared=no",
277
+ "--enable-static=yes",
278
+ "--disable-tcl",
279
+ "--disable-readline",
280
+ "--with-pic=yes",
269
281
  )
270
282
  self.run_make()
271
283
  self.run_make("install")
@@ -279,7 +291,7 @@ class Uuid(ModuleBuilder):
279
291
 
280
292
  m_debian = "+uuid-dev"
281
293
  m_include = "uuid"
282
- m_telltale = ["{include}/uuid/uuid.h"]
294
+ m_telltale: ClassVar[list] = ["{include}/uuid/uuid.h"]
283
295
 
284
296
  xenv_CFLAGS = "-fPIC"
285
297
 
@@ -298,7 +310,6 @@ class Uuid(ModuleBuilder):
298
310
 
299
311
 
300
312
  class Xz(ModuleBuilder):
301
-
302
313
  m_telltale = "{include}/lzma.h"
303
314
 
304
315
  def auto_select_reason(self):
@@ -316,8 +327,14 @@ class Xz(ModuleBuilder):
316
327
  def _do_linux_compile(self):
317
328
  self.run_configure(
318
329
  "./configure",
319
- "--enable-shared=no", "--enable-static=yes", "--with-pic=yes", "--disable-rpath",
320
- "--disable-dependency-tracking", "--disable-doc", "--disable-nls", "--without-libintl-prefix",
330
+ "--enable-shared=no",
331
+ "--enable-static=yes",
332
+ "--with-pic=yes",
333
+ "--disable-rpath",
334
+ "--disable-dependency-tracking",
335
+ "--disable-doc",
336
+ "--disable-nls",
337
+ "--without-libintl-prefix",
321
338
  )
322
339
  self.run_make()
323
340
  self.run_make("install")
@@ -340,7 +357,7 @@ class Zlib(ModuleBuilder):
340
357
 
341
358
  @property
342
359
  def url(self):
343
- return f"https://zlib.net/zlib-{self.version}.tar.gz"
360
+ return f"https://zlib.net/fossils/zlib-{self.version}.tar.gz"
344
361
 
345
362
  @property
346
363
  def version(self):
@@ -11,7 +11,6 @@ from runez.render import PrettyTable
11
11
  from portable_python.tracking import Trackable, Tracker
12
12
  from portable_python.versions import PPG
13
13
 
14
-
15
14
  LOG = logging.getLogger(__name__)
16
15
  RX_DYNLIB = re.compile(r"^.*\.(so(\.[0-9.]+)?|dylib)$")
17
16
 
@@ -57,10 +56,14 @@ class LibAutoCorrect:
57
56
 
58
57
  def __init__(self, prefix, install_folder, ppp_marker=None):
59
58
  """
60
- Args:
61
- prefix (str): Prefix used in ./configure
62
- install_folder (pathlib.Path): Installation folder to scan (all paths will be relative to this)
63
- ppp_marker (str | None): Associated ppp-marker, if any
59
+ Parameters
60
+ ----------
61
+ prefix: str
62
+ Prefix used in ./configure
63
+ install_folder : pathlib.Path
64
+ Installation folder to scan (all paths will be relative to this)
65
+ ppp_marker : str | None
66
+ Path to ppp-marker, if any
64
67
  """
65
68
  self.prefix = prefix
66
69
  self.install_folder = install_folder
@@ -103,6 +106,7 @@ class LibAutoCorrect:
103
106
  def _auto_correct_macos(self, path):
104
107
  """
105
108
  On macos, we use install_name_tool, example:
109
+
106
110
  install_name_tool -add_rpath @executable_path/../lib .../bin/python
107
111
  install_name_tool -change /<prefix>/lib/libpython3.9.dylib @rpath/libpython3.9.dylib .../bin/python
108
112
 
@@ -146,7 +150,6 @@ class LibAutoCorrect:
146
150
 
147
151
 
148
152
  class ModuleInfo:
149
-
150
153
  _regex = re.compile(r"^(.*?)\s*(\S+/(lib(64)?/.*))$")
151
154
 
152
155
  def __init__(self, inspector: "PythonInspector", name: str, payload: dict):
@@ -166,8 +169,7 @@ class ModuleInfo:
166
169
  path = self.filepath
167
170
  if path:
168
171
  if is_dyn_lib(path):
169
- info = SoInfo(self.inspector, path)
170
- return info
172
+ return SoInfo(self.inspector, path)
171
173
 
172
174
  if path.name.startswith("__init__."):
173
175
  path = path.parent
@@ -200,7 +202,6 @@ class ModuleInfo:
200
202
 
201
203
 
202
204
  class CLibInfo(Trackable):
203
-
204
205
  def __init__(self, inspector: "PythonInspector", path: str, version: str, basename: str):
205
206
  self.inspector = inspector
206
207
  if not basename:
@@ -249,7 +250,6 @@ class CLibInfo(Trackable):
249
250
 
250
251
 
251
252
  class SoInfo(Trackable):
252
-
253
253
  def __init__(self, inspector: "PythonInspector", path):
254
254
  self.inspector = inspector
255
255
  self.path = runez.to_path(path)
@@ -286,7 +286,7 @@ class SoInfo(Trackable):
286
286
  if runez.which(program):
287
287
  r = runez.run(*cmd, path, fatal=False, logger=None)
288
288
  if not r.succeeded:
289
- logging.warning("%s exited with code %s for %s: %s" % (program, r.exit_code, path, r.full_output))
289
+ logging.warning("%s exited with code %s for %s: %s", program, r.exit_code, path, r.full_output)
290
290
  return program, None
291
291
 
292
292
  return program, r.output
@@ -397,7 +397,6 @@ def get_lib_type(install_folder, path, basename):
397
397
 
398
398
 
399
399
  class PythonInspector:
400
-
401
400
  default = "_bz2,_ctypes,_curses,_decimal,_dbm,_gdbm,_lzma,_tkinter,_sqlite3,_ssl,_uuid,pip,readline,pyexpat,setuptools,zlib"
402
401
  additional = "_asyncio,_functools,_tracemalloc,dbm.gnu,ensurepip,ossaudiodev,spwd,sys,tkinter,venv,wheel"
403
402
 
@@ -500,11 +499,17 @@ class PythonInspector:
500
499
  @staticmethod
501
500
  def parsed_version(output):
502
501
  """
503
- Args:
504
- output (str | None): Output to parse
502
+ Version mentioned in `output`
505
503
 
506
- Returns:
507
- (str | None): Simplified version number reported
504
+ Parameters
505
+ ----------
506
+ output : str | None
507
+ Output to parse
508
+
509
+ Returns
510
+ -------
511
+ str | None
512
+ Simplified version number reported
508
513
  """
509
514
  if output:
510
515
  for line in output.splitlines():
@@ -535,7 +540,6 @@ def _find_parent_subfolder(folder, basename):
535
540
 
536
541
 
537
542
  class FullSoReport:
538
-
539
543
  def __init__(self, inspector: PythonInspector):
540
544
  self.inspector = inspector
541
545
  self.size = 0
@@ -2,7 +2,6 @@ import runez
2
2
 
3
3
 
4
4
  class Trackable:
5
-
6
5
  tracked_category = None
7
6
 
8
7
  def __eq__(self, other):
@@ -16,7 +15,6 @@ class Trackable:
16
15
 
17
16
 
18
17
  class TrackedCollection:
19
-
20
18
  def __init__(self, name):
21
19
  self.name = name
22
20
  self.items = []
@@ -38,7 +36,6 @@ class TrackedCollection:
38
36
 
39
37
 
40
38
  class Tracker(TrackedCollection):
41
-
42
39
  def __init__(self, enum, name=None):
43
40
  self.kind = enum.__name__.replace("Type", "").lower()
44
41
  super().__init__(name or self.kind)
@@ -1,11 +1,13 @@
1
1
  """
2
- Tracking only a handful of most recent (and non-EOL) versions by design
3
- Not trying to do historical stuff here, older (or EOL-ed) versions will be removed from the list without notice
2
+ Tracking only a handful of most recent (and non-EOL) versions by design.
3
+
4
+ Not trying to do historical stuff here, older (or EOL-ed) versions will be removed from the list without notice.
4
5
  """
5
6
 
6
7
  import logging
7
8
  import os
8
9
  import re
10
+ from typing import ClassVar
9
11
 
10
12
  import runez
11
13
  from runez.http import RestClient
@@ -49,13 +51,23 @@ class VersionFamily:
49
51
  self._fetch_versions()
50
52
  return self._versions
51
53
 
52
- def get_available_versions(self) -> list:
53
- """Implementation supplied by descendant: iterable of available versions, can be strings"""
54
+ def get_available_versions(self):
55
+ """
56
+ Available versions.
57
+
58
+ Returns
59
+ -------
60
+ list[Version]
61
+ """
54
62
 
55
63
  def get_builder(self):
56
64
  """
57
- Returns:
58
- (portable_python.PythonBuilder)
65
+ Builder implementation for this family.
66
+
67
+ Returns
68
+ -------
69
+ portable_python.PythonBuilder
70
+ Builder implementation for this family
59
71
  """
60
72
 
61
73
 
@@ -105,7 +117,6 @@ class CPythonFamily(VersionFamily):
105
117
 
106
118
 
107
119
  class Folders:
108
-
109
120
  def __init__(self, config: Config, base=None, family=None, version=None):
110
121
  self.config = config
111
122
  self.base_folder = runez.resolved_path(base)
@@ -160,10 +171,21 @@ class Folders:
160
171
 
161
172
 
162
173
  class PPG:
163
- """Globals"""
174
+ """
175
+ Global settings for portable-python
176
+
177
+ Attributes
178
+ ----------
179
+ families : dict[str, VersionFamily]
180
+ Mapping of family names to implementations
181
+ config : portable_python.config.Config
182
+ Global configuration
183
+ target : runez.system.PlatformId
184
+ Target platform
185
+ """
164
186
 
165
187
  cpython = CPythonFamily()
166
- families = dict(cpython=cpython)
188
+ families: ClassVar[dict] = {"cpython": cpython}
167
189
  config = Config()
168
190
  target = config.target
169
191
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: portable-python
3
- Version: 1.8.4
3
+ Version: 1.8.5
4
4
  Summary: Portable python binaries
5
5
  Home-page: https://github.com/codrsquad/portable-python
6
6
  Author: Zoran Simic
@@ -48,7 +48,7 @@ def test_cleanup(cli, monkeypatch):
48
48
  assert "MACOSX_DEPLOYMENT_TARGET" not in cli.logged
49
49
  assert "selected: all" in cli.logged
50
50
  assert f"Would symlink {install_dir}/bin/pip{f.mm} <- {install_dir}/bin/pip" in cli.logged
51
- assert f"Would symlink {lib}/python{f.mm}/config-{f.mm}-darwin/libpython{f.mm}.a <- {lib}/libpython{f.mm}.a"
51
+ assert f"Would symlink {lib}/python{f.mm}/config-{f.mm}-darwin/libpython{f.mm}.a <- {lib}/libpython{f.mm}.a" in cli.logged
52
52
  assert f"Would tar {install_dir} -> dist/cpython-{f.version}-linux-x86_64.tar.gz" in cli.logged
53
53
 
54
54
  runez.touch("bin/brew", logger=None)
@@ -133,7 +133,7 @@ class MockSharedExeRun:
133
133
  if args[1] == "--print-rpath":
134
134
  return self.prefix
135
135
 
136
- def _macos_run(self, program, *args):
136
+ def _macos_run(self, program, *_):
137
137
  if program == "otool":
138
138
  return f"foo/bin/python:\n {self.prefix}/lib/libpython3.9.dylib (...)\n /usr/lib/... (...)"
139
139
 
@@ -3,7 +3,6 @@ from runez.http import RestClient
3
3
  from portable_python import BuildSetup
4
4
  from portable_python.versions import CPythonFamily, PPG
5
5
 
6
-
7
6
  REST_CLIENT = RestClient()
8
7
  GH_CPYTHON_SAMPLE = """
9
8
  [
@@ -21,10 +20,12 @@ PYTHON_ORG_SAMPLE = """
21
20
  """
22
21
 
23
22
 
24
- @REST_CLIENT.mock({
25
- "https://www.python.org/ftp/python/": PYTHON_ORG_SAMPLE,
26
- "https://api.github.com/repos/python/cpython/git/matching-refs/tags/v3.": GH_CPYTHON_SAMPLE,
27
- })
23
+ @REST_CLIENT.mock(
24
+ {
25
+ "https://www.python.org/ftp/python/": PYTHON_ORG_SAMPLE,
26
+ "https://api.github.com/repos/python/cpython/git/matching-refs/tags/v3.": GH_CPYTHON_SAMPLE,
27
+ },
28
+ )
28
29
  def test_list(cli, monkeypatch):
29
30
  # Edge cases
30
31
  monkeypatch.setattr(PPG, "config", None)
@@ -5,8 +5,8 @@ from portable_python import BuildSetup, ModuleBuilder
5
5
  from portable_python.versions import PPG
6
6
 
7
7
 
8
- def test_config(cli, monkeypatch):
9
- with pytest.raises(BaseException):
8
+ def test_config(cli):
9
+ with pytest.raises(runez.system.AbortException):
10
10
  PPG.config.parsed_yaml("a: b\ninvalid line", "testing")
11
11
 
12
12
  cli.run("-ntmacos-arm64", "-c", cli.tests_path("sample-config1.yml"), "build", "3.9.7", "-mnone")
@@ -34,8 +34,8 @@ def test_diagnostics(cli):
34
34
 
35
35
  def test_edge_cases(temp_folder, monkeypatch, logged):
36
36
  monkeypatch.setattr(PPG, "config", None)
37
- with pytest.raises(BaseException):
38
- PPG.grab_config(runez.DEV.tests_path("sample-incomplete.yml"))
37
+ PPG.grab_config(runez.DEV.tests_path("sample-incomplete.yml"))
38
+ with pytest.raises(runez.system.AbortException):
39
39
  PPG.get_folders()
40
40
  assert "Folder 'destdir' must be configured" in logged.pop()
41
41
 
@@ -56,9 +56,10 @@ def test_edge_cases(temp_folder, monkeypatch, logged):
56
56
  assert outcome.name == "failed"
57
57
  assert reason == "broken, can't compile statically with foo present"
58
58
 
59
- PPG.config._sources[0].data = dict(ext="foo")
59
+ sources = getattr(PPG.config, "_sources", None)
60
+ monkeypatch.setattr(sources[0], "data", {"ext": "foo"})
60
61
  assert not logged
61
- with pytest.raises(BaseException):
62
+ with pytest.raises(runez.system.AbortException):
62
63
  _ = BuildSetup("3.9.6")
63
64
  assert "Invalid extension 'foo'" in logged.pop()
64
65
 
@@ -1,2 +0,0 @@
1
- [build-system]
2
- requires = ["setuptools", "wheel"]
File without changes