Skip to content

Commit 33401cb

Browse files
committed
[GR-23618] Improve packages patching: support separate ginstall/pip patches and versioning of the pip patches.
PullRequest: graalpython/976
2 parents 79597ad + 562226a commit 33401cb

File tree

12 files changed

+220
-36
lines changed

12 files changed

+220
-36
lines changed

docs/contributor/IMPLEMENTATION_DETAILS.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,3 +144,58 @@ as needed, we use the same mechanism to also pass the currently handled
144144
exception. Unlike CPython we do not use a stack of currently handled exceptions,
145145
instead we utilize the call stack of Java by always passing the current exception
146146
and holding on to the last (if any) in a local variable.
147+
148+
## Patching of Packages
149+
150+
Some PyPI packages contain code that is not compatible with GraalPython.
151+
To overcome this limitation and support such packages, GraalPython contains
152+
patches for some popular packages. The patches are applied to packages
153+
installed via GraalPython specific utility `ginstall` and also to packages
154+
installed via `pip`. This is achieved by patching `pip` code during the loading
155+
of the `pip` module in `pip_hook.py`.
156+
157+
The patches are regular POSIX `patch` command compatible diffs located
158+
in `lib-graalpython/patches`. The directory structure is following:
159+
160+
```
161+
patches
162+
├── atomicwrites
163+
│   └── sdist
164+
│   ├── atomicwrites.patch
165+
│   └── atomicwrites-5.3.1.patch
166+
└── pytest
167+
└── whl
168+
   ├── pytest.dir
169+
   ├── pytest.patch
170+
   ├── pytest-5.dir
171+
   ├── pytest-5.patch
172+
   ├── pytest-5.2.dir
173+
   ├── pytest-5.2.patch
174+
   ├── pytest-5.2.3.dir
175+
   └── pytest-5.2.3.patch
176+
```
177+
178+
The directory names are names of the Python packages.
179+
180+
Patches in the `sdist` subdirectory apply to the source distribution tarballs,
181+
i.e., such patches may patch, for example, the `setup.py` file.
182+
183+
Patches in the `whl` subdirectory apply to both the wheels binary distributions
184+
and to appropriate subdirectory of the source distributions. For example, in `pytest`
185+
the Python source files that are deployed into the top-level in the wheels binary
186+
archive are located in the `src` subdirectory inside the source distribution. To reuse
187+
one patch for both binary and source distributions, we need to also record the
188+
subdirectory where the sources live in the source distribution - path to that directory
189+
(relative to the root of the source distribution) is inside the `*.dir` files.
190+
191+
All the `*.patch` and `*.dir` files may contain version string. The search for
192+
an appropriate `*.patch` file for a package `P` of version `X.Y.Z` happens in
193+
this order:
194+
195+
* `P-X.Y.Z.patch`
196+
* `P-X.Y.patch`
197+
* `P-X.patch`
198+
* `P.patch`
199+
200+
The same applies to `*.dir` files, and the search is independent of the search
201+
for the `*.patch` file, i.e., multiple versions of `*.patch` may share one `*.dir`.

graalpython/lib-graalpython/modules/ginstall.py

Lines changed: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
3737
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
3838
# SOFTWARE.
39-
39+
import re
4040
from pathlib import Path
4141
import argparse
4242
import json
@@ -46,8 +46,8 @@
4646
import subprocess
4747
import tempfile
4848
import importlib
49-
import sys
5049

50+
import sys
5151

5252
WARNING = '\033[93m'
5353
FAIL = '\033[91m'
@@ -365,7 +365,7 @@ def xit(msg, status=-1):
365365
exit(-1)
366366

367367

368-
def _install_from_url(url, package, extra_opts=[], add_cflags="", ignore_errors=False, env={}):
368+
def _install_from_url(url, package, extra_opts=[], add_cflags="", ignore_errors=False, env={}, version=None):
369369
name = url[url.rfind("/")+1:]
370370
tempdir = tempfile.mkdtemp()
371371

@@ -407,11 +407,25 @@ def _install_from_url(url, package, extra_opts=[], add_cflags="", ignore_errors=
407407
else:
408408
xit("Unknown file type: %s" % name)
409409

410-
patch_file_path = get_patch(package)
410+
file_realpath = os.path.dirname(os.path.realpath(__file__))
411+
patches_dir = os.path.join(Path(file_realpath).parent, 'patches', package)
412+
# empty match group to have the same groups range as in pip_hook
413+
# unlike with pip, the version number may not be available at all
414+
versions = re.search("()(\\d+)?(.\\d+)?(.\\d+)?", "" if version is None else version)
415+
416+
patch_file_path = first_existing(package, versions, os.path.join(patches_dir, "sdist"), ".patch")
411417
if patch_file_path:
412418
system("patch -d %s/%s/ -p1 < %s" %
413419
(tempdir, bare_name, patch_file_path))
414420

421+
whl_patches_dir = os.path.join(patches_dir, "whl")
422+
patch_file_path = first_existing(package, versions, whl_patches_dir, ".patch")
423+
subdir = read_first_existing(package, versions, whl_patches_dir, ".dir")
424+
subdir = "" if subdir is None else "/" + subdir
425+
if patch_file_path:
426+
system("patch -d %s/%s%s -p1 < %s" %
427+
(tempdir, bare_name, subdir, patch_file_path))
428+
415429
if "--prefix" not in extra_opts and site.ENABLE_USER_SITE:
416430
user_arg = "--user"
417431
else:
@@ -420,19 +434,41 @@ def _install_from_url(url, package, extra_opts=[], add_cflags="", ignore_errors=
420434
if status != 0 and not ignore_errors:
421435
xit("An error occurred trying to run `setup.py install %s %s'" % (user_arg, " ".join(extra_opts)))
422436

423-
def get_patch(package):
424-
dir_path = os.path.dirname(os.path.realpath(__file__))
425-
parent_folder = Path(dir_path).parent
426-
patch_file_path = os.path.join(
427-
parent_folder, 'patches', "%s.patch" % package
428-
)
429-
if os.path.exists(patch_file_path):
430-
return patch_file_path
437+
# NOTE: Following 3 functions are duplicated in pip_hook.py:
438+
# creates a search list of a versioned file:
439+
# {name}-X.Y.Z.{suffix}, {name}-X.Y.{suffix}, {name}-X.{suffix}, {name}.{suffix}
440+
# 'versions' is a result of re.search
441+
def list_versioned(pkg_name, versions, dir, suffix):
442+
acc = ""
443+
res = []
444+
for i in range(2,5):
445+
v = versions.group(i)
446+
if v is not None:
447+
acc = acc + v
448+
res.append(acc)
449+
res.reverse()
450+
res = [os.path.join(dir, pkg_name + "-" + ver + suffix) for ver in res]
451+
res.append(os.path.join(dir, pkg_name + suffix))
452+
return res
453+
454+
def first_existing(pkg_name, versions, dir, suffix):
455+
for filename in list_versioned(pkg_name, versions, dir, suffix):
456+
if os.path.exists(filename):
457+
return filename
458+
459+
def read_first_existing(pkg_name, versions, dir, suffix):
460+
filename = first_existing(pkg_name, versions, dir, suffix)
461+
if filename:
462+
with open(filename, "r") as f:
463+
return f.read()
464+
465+
# end of code duplicated in pip_hook.py
431466

432467
def install_from_pypi(package, extra_opts=[], add_cflags="", ignore_errors=True, env={}):
433468
package_pattern = os.environ.get("GINSTALL_PACKAGE_PATTERN", "https://pypi.org/pypi/%s/json")
434469
package_version_pattern = os.environ.get("GINSTALL_PACKAGE_VERSION_PATTERN", "https://pypi.org/pypi/%s/%s/json")
435470

471+
version = None
436472
if "==" in package:
437473
package, _, version = package.rpartition("==")
438474
url = package_version_pattern % (package, version)
@@ -456,7 +492,8 @@ def install_from_pypi(package, extra_opts=[], add_cflags="", ignore_errors=True,
456492
break
457493

458494
if url:
459-
_install_from_url(url, package=package, extra_opts=extra_opts, add_cflags=add_cflags, ignore_errors=ignore_errors, env=env)
495+
_install_from_url(url, package=package, extra_opts=extra_opts, add_cflags=add_cflags,
496+
ignore_errors=ignore_errors, env=env, version=version)
460497
else:
461498
xit("Package not found: '%s'" % package)
462499

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
src

graalpython/lib-graalpython/patches/pytest.patch renamed to graalpython/lib-graalpython/patches/pytest/whl/pytest.patch

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
2-
diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py
3-
index aa4dcff..029e659 100644
4-
--- a/src/_pytest/_code/code.py
5-
+++ b/src/_pytest/_code/code.py
6-
@@ -211,15 +211,17 @@ class TracebackEntry:
1+
diff --git a/_pytest/_code/code.py b/_pytest/_code/code.py
2+
index 7d72234e7..1f0ca7f7e 100644
3+
--- a/_pytest/_code/code.py
4+
+++ b/_pytest/_code/code.py
5+
@@ -222,15 +222,17 @@ class TracebackEntry:
76
if key is not None:
87
astnode = astcache.get(key, None)
98
start = self.getfirstlinesource()
@@ -30,11 +29,24 @@ index aa4dcff..029e659 100644
3029
return source[start:end]
3130

3231
source = property(getsource)
33-
diff --git a/src/_pytest/python.py b/src/_pytest/python.py
34-
index 66d8530..8bb2ab6 100644
35-
--- a/src/_pytest/python.py
36-
+++ b/src/_pytest/python.py
37-
@@ -494,8 +494,10 @@ class Module(nodes.File, PyCollector):
32+
diff --git a/_pytest/assertion/__init__.py b/_pytest/assertion/__init__.py
33+
index 126929b6a..8ece37d23 100644
34+
--- a/_pytest/assertion/__init__.py
35+
+++ b/_pytest/assertion/__init__.py
36+
@@ -15,7 +15,7 @@ def pytest_addoption(parser):
37+
action="store",
38+
dest="assertmode",
39+
choices=("rewrite", "plain"),
40+
- default="rewrite",
41+
+ default="plain",
42+
metavar="MODE",
43+
help="""Control assertion debugging tools. 'plain'
44+
performs no assertion debugging. 'rewrite'
45+
diff --git a/_pytest/python.py b/_pytest/python.py
46+
index 913a93bc0..0b6d75dd8 100644
47+
--- a/_pytest/python.py
48+
+++ b/_pytest/python.py
49+
@@ -497,8 +497,10 @@ class Module(nodes.File, PyCollector):
3850
def _importtestmodule(self):
3951
# we assume we are only called once per module
4052
importmode = self.config.getoption("--import-mode")
@@ -45,7 +57,7 @@ index 66d8530..8bb2ab6 100644
4557
except SyntaxError:
4658
raise self.CollectError(
4759
_pytest._code.ExceptionInfo.from_current().getrepr(style="short")
48-
@@ -538,6 +540,10 @@ class Module(nodes.File, PyCollector):
60+
@@ -541,6 +543,10 @@ class Module(nodes.File, PyCollector):
4961
"or @pytest.mark.skipif decorators instead, and to skip a "
5062
"module use `pytestmark = pytest.mark.{skip,skipif}."
5163
)
@@ -56,4 +68,3 @@ index 66d8530..8bb2ab6 100644
5668
self.config.pluginmanager.consider_module(mod)
5769
return mod
5870

59-

0 commit comments

Comments
 (0)