Fix ci pipeline for python3.9 and python3.13 (#435)

This commit is contained in:
ye11owSub
2025-04-05 20:47:18 +04:00
committed by GitHub
parent 8efe994820
commit 17c6cbd96d
6 changed files with 222 additions and 181 deletions

View File

@@ -7,28 +7,30 @@ jobs:
runs-on: ubuntu-22.04
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4.0.0
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install system dependencies
run: >
sudo apt-get update;
sudo apt-get --no-install-recommends install
catch2
cmake
libfreetype6-dev
libglew-dev
libglm-dev
libmsgpack-dev
libnetcdf-dev
libpng-dev
libxml2-dev
python-is-python3
python3-biopython
python3-dev
python3-setuptools
python3-numpy
python3-pip
run: |
sudo apt-get update
sudo apt-get --no-install-recommends install \
catch2 \
libfreetype6-dev \
libglew-dev \
libglm-dev \
libmsgpack-dev \
libnetcdf-dev \
libpng-dev \
libxml2-dev
- name: Install collada2gltf
run: |
@@ -38,7 +40,7 @@ jobs:
- name: Get additional sources
run: |
git clone --depth 1 https://github.com/rcsb/mmtf-cpp.git
cp -R mmtf-cpp/include/mmtf* include/
cp -R mmtf-cpp/include/* include/
- name: Build
run: |
@@ -51,87 +53,120 @@ jobs:
run: |
pymol -ckqy testing/testing.py --run all
build-Windows:
runs-on: windows-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
env:
CONDA_ROOT: ${{github.workspace}}\..\tmp\miniforge
MINIFORGE_EXEC: ${{github.workspace}}\..\tmp\miniforge.exe
CONDA_ENV_NAME: "testing_env"
steps:
- uses: actions/checkout@v4.0.0
- name: Download Miniforge
shell: cmd
run: |-
if not exist %CONDA_ROOT% mkdir %CONDA_ROOT%
curl -L -o %MINIFORGE_EXEC% https://github.com/conda-forge/miniforge/releases/download/24.11.0-0/Miniforge3-24.11.0-0-Windows-x86_64.exe
start /wait %MINIFORGE_EXEC% /S /D=%CONDA_ROOT%
- uses: actions/checkout@v4
- name: Install Miniforge
run: |
choco install miniforge3
- name: Add conda to PATH
run: |
echo "$env:CONDA" | Out-File -Append -FilePath $env:GITHUB_PATH
echo "$env:CONDA\Scripts" | Out-File -Append -FilePath $env:GITHUB_PATH
- name: Set up Miniforge
shell: cmd
run: |-
CALL %CONDA_ROOT%\\Scripts\\activate.bat
conda install -y -c conda-forge -c schrodinger python cmake libpng freetype pyside6 glew libxml2 catch2=2.13.3 glm libnetcdf collada2gltf biopython msgpack-python pip python-build
- name: Conda info
shell: cmd
run: |-
CALL %CONDA_ROOT%\\Scripts\\activate.bat
conda info
run: |
conda init powershell
conda create --name $env:CONDA_ENV_NAME -c conda-forge -c schrodinger `
python=${{ matrix.python-version }} `
pip `
catch2=2.13.3 `
collada2gltf `
freetype `
glew `
glm `
libpng `
libxml2 `
libnetcdf `
- name: Get additional sources
shell: cmd
run: |
conda activate $env:CONDA_ENV_NAME
git clone --depth 1 https://github.com/rcsb/mmtf-cpp.git
cp -R mmtf-cpp/include/mmtf* %CONDA_ROOT%/Library/include/
Copy-Item -Recurse -Path mmtf-cpp/include\* -Destination "$env:CONDA_PREFIX\Library\include"
git clone --depth 1 --single-branch --branch cpp_master https://github.com/msgpack/msgpack-c.git
cp -R msgpack-c/include/msgpack* %CONDA_ROOT%/Library/include/
Copy-Item -Recurse -Path msgpack-c/include\* -Destination "$env:CONDA_PREFIX\Library\include"
dir $env:CONDA_PREFIX\Library\include
- name: Build PyMOL
shell: cmd
run: |
CALL %CONDA_ROOT%\\Scripts\\activate.bat
conda activate $env:CONDA_ENV_NAME
pip install -v --config-settings testing=True .[dev]
- name: Test
shell: cmd
run: |
CALL %CONDA_ROOT%\\Scripts\\activate.bat
pymol -ckqy testing\\testing.py --run all
conda activate $env:CONDA_ENV_NAME
pymol -ckqy testing\testing.py --run all
build-MacOS:
runs-on: macos-latest
strategy:
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
env:
CONDA_ROOT: "/tmp/miniforge"
CONDA_ENV_NAME: "testing_env"
steps:
- uses: actions/checkout@v4.0.0
- name: Set up Miniforge and Build
run: |-
- name: Download Miniforge
run: |
curl -L -o $CONDA_ROOT.sh https://github.com/conda-forge/miniforge/releases/download/24.11.0-0/Miniforge3-MacOSX-x86_64.sh
bash $CONDA_ROOT.sh -b -p $CONDA_ROOT
export PATH="$CONDA_ROOT/bin:$PATH"
conda config --set quiet yes
conda install -y -c conda-forge -c schrodinger python cmake libpng freetype pyside6 glew libxml2 catch2=2.13.3 glm libnetcdf collada2gltf biopython msgpack-python pip python-build
conda info
- name: Add conda to PATH
run: |
echo "${CONDA_ROOT}/bin" >> "$GITHUB_PATH"
- name: Set up Miniforge
run: |
conda create --name $CONDA_ENV_NAME -c conda-forge -c schrodinger \
python=${{ matrix.python-version }} \
pip \
catch2=2.13.3 \
collada2gltf \
freetype \
glew \
glm \
libpng \
libxml2 \
libnetcdf
- name: Get additional sources
run: |
source activate $CONDA_ENV_NAME
git clone --depth 1 https://github.com/rcsb/mmtf-cpp.git
cp -R mmtf-cpp/include/mmtf* ${CONDA_ROOT}/include/
cp -R mmtf-cpp/include/* ${CONDA_PREFIX}/include/
git clone --depth 1 --single-branch --branch cpp_master https://github.com/msgpack/msgpack-c.git
cp -R msgpack-c/include/msgpack* ${CONDA_ROOT}/include/
cp -R msgpack-c/include/* ${CONDA_PREFIX}/include/
- name: Build PyMOL
run: |-
run: |
source activate $CONDA_ENV_NAME
export MACOSX_DEPLOYMENT_TARGET=12.0
export PATH="$CONDA_ROOT/bin:$PATH"
pip install -v --config-settings testing=True '.[dev]'
- name: Test
run: |-
export PATH="$CONDA_ROOT/bin:$PATH"
run: |
source activate $CONDA_ENV_NAME
pymol -ckqy testing/testing.py --run all

View File

@@ -9,27 +9,17 @@
# we make extensive use of Python's build-in in web infrastructure
import sys
if True:
import http.server as BaseHTTPServer
import io as StringIO
import urllib.parse as urlparse
from urllib.request import urlopen
import cgi
import os
import socket
import sys
import threading
import traceback
import json
import io as StringIO
from http.server import BaseHTTPRequestHandler, HTTPServer
from urllib import parse
from urllib.request import urlopen
# we also rely upon Python's json infrastructure
try:
import simplejson as json
except:
import json
# standard Python dependencies
import types, os, sys, traceback, threading
# NOTE: Let's attempt to follow Python PEP 8 for coding style for this
# source code file. URL: http://www.python.org/de/peps/pep-0008
@@ -42,7 +32,7 @@ import types, os, sys, traceback, threading
_json_mime_types = [ 'text/json', 'application/json' ]
class _PymolHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
class _PymolHTTPRequestHandler(BaseHTTPRequestHandler):
# for now, we're using a single-threaded server
@@ -62,7 +52,7 @@ class _PymolHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def log_message(self, format, *args):
if self.server.pymol_logging:
BaseHTTPServer.BaseHTTPRequestHandler.log_message(self,format,
BaseHTTPRequestHandler.log_message(self,format,
*args)
def process_request(self):
"""
@@ -88,26 +78,22 @@ class _PymolHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def parse_args(self):
"""
parses URL arguments into a urlpath (before the ?)
and a cgiFieldStorage object (args after the ?).
for example:
http://localhost:8080/apply/pymol.cmd.color?color=blue&selection=benz
would yield self.fs.getvalue("color") as "blue"
and self.fs.getvalue("selection") as "benz"
Example:
http://localhost:8080/apply/pymol.cmd.color?color=blue&selection=benz
qsl == "color=blue&selection=benz"
parse.parse_qs(qs) == {'color': ['blue'], 'selection': ['benz']}
self.urlpath would be "/apply/pymol.cmd.color"
"""
if (self.command == "POST"):
self.fs = cgi.FieldStorage(fp=self.rfile, headers=self.headers,
environ = {'REQUEST_METHOD':'POST'},
keep_blank_values = 1)
self.fs = {}
if self.command == "POST":
self.fs = self.headers
self.urlpath = self.path
elif (self.command == "GET"):
scheme,netloc,path,params,qs,fragment = urlparse.urlparse(self.path)
self.fs = cgi.FieldStorage(environ = {'REQUEST_METHOD':'GET',
'QUERY_STRING':qs},
keep_blank_values = 1)
elif self.command == "GET":
_, _, path, _, qs, _ = parse.urlparse(self.path)
self.fs = parse.parse_qs(qs, keep_blank_values=True)
self.urlpath = path
else:
self.fs = None
def process_urlpath(self):
"""
@@ -230,26 +216,39 @@ class _PymolHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
query_kwds = {}
send_multi_result_list = False
for k in self.fs.keys():
if k[0:1] == '_': # leading-underscore argument (special handling)
if k == '_callback':
self.callback = self.fs.getfirst(k)
elif k == '_json': # main path for Javascript API
method = json.loads(self.fs.getfirst(k))
for key, value in self.fs.items():
first_value = value[0]
# leading-underscore argument (special handling)
if key[0:1] == "_":
if key == "_callback":
self.callback = first_value
# main path for Javascript API
elif key == "_json":
# [ "my_method", [ arg1, ... ] , { 'key1' : 'val1, ... } ]
# or
# [ [ "my_met1", [ arg1, ... ], { 'key1' : 'val1, ... } ],
# [ "my_met2", [ arg1, ... ], { 'key1' : 'val1, ... } ] ]
elif k == '_method': # tentative, not in spec -- may disappear
method = json.loads(first_value)
# tentative, not in spec -- may disappear
elif key == "_method":
# a method name "my_method"
method = json.loads(self.fs.getfirst(k))
elif k == '_args': # tentative, not in spec -- may disappear
args = json.loads(self.fs.getfirst(k))
elif k == '_kwds': # tentative, not in spec -- may disappear
kwds = json.loads(self.fs.getfirst(k))
# other underscore arguments are ignored (not passed on)
elif k[0:1] != '_':
query_kwds[k] = self.fs.getfirst(k)
method = json.loads(first_value)
# tentative, not in spec -- may disappear
elif key == "_args":
args = json.loads(first_value)
# tentative, not in spec -- may disappear
elif key == "_kwds":
kwds = json.loads(first_value)
# other underscore arguments are ignored (not passed on)
elif key[0:1] != "_":
query_kwds[key] = value[0]
blocks = []
if isinstance(method, str):
@@ -413,32 +412,30 @@ class _PymolHTTPRequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
for debugging requests
"""
self.wfile_write("%s\n" % self.command)
if (self.fs):
for k in self.fs.keys():
self.wfile_write("%s = " % k)
# key can have multiple values, as with checkboxes,
# but also arbitrarily
if (isinstance(self.fs[k], list)):
self.wfile_write("%s\n" % self.fs.getlist(k))
else:
# key can be uploaded file
if (self.fs[k].filename):
self.wfile_write("%s\n" % self.fs[k].filename)
fp = self.fs[k].file
#self.wfile.write("FILE %s" % cgi.escape(repr(fp)))
#self.wfile.write("%s\n" % fp.name)
# fails for StringIO instances
self.wfile_write("%s\n" % repr(fp))
# two ways to get file contents
#file_contents = self.fs.getvalue(k)
#file_contents = fp.read()
#self.wfile.write("%s" % file_contents)
else:
#plain-old key/value
self.wfile_write("%s\n" % self.fs.getvalue(k))
else:
if not self.fs:
self.wfile_write("No args\n")
for key, value in self.fs.items():
self.wfile_write("%s = " % key)
# key can have multiple values, as with checkboxes,
# but also arbitrarily
if (isinstance(value, list)):
self.wfile_write(f"{value}\n")
continue
# key can be uploaded file
if value.filename:
self.wfile_write(f"{self.fs[key].filename}\n")
fp = value.file
self.wfile_write(f"{repr(fp)}\n")
# two ways to get file contents
#file_contents = self.fs.getvalue(k)
#file_contents = fp.read()
#self.wfile.write("%s" % file_contents)
else:
self.wfile_write(f"{value}\n")
# this is the public class we're exposing to PyMOL consortium members
class PymolHttpd:
@@ -475,7 +472,7 @@ class PymolHttpd:
session['pymol.cmd.label'] = self_cmd.label2 # no-eval version
self.server = BaseHTTPServer.HTTPServer(('', self.port),
self.server = HTTPServer(('', self.port),
_PymolHTTPRequestHandler)
self.server.wrap_natives = wrap_natives
self.server.custom_headers = headers

View File

@@ -12,7 +12,7 @@
# -*
# Z* -------------------------------------------------------------------
from typing import Iterable, Optional
from typing import Iterable, Optional, Union
from collections import defaultdict
from pymol import parsing
@@ -30,7 +30,7 @@ class Shortcut:
if filter_leading_underscore
else keywords
)
self.shortcut: dict[str, str | int] = {}
self.shortcut: dict[str, Union[str, int]] = {}
self.abbreviation_dict = defaultdict(list)
for keyword in self.keywords:
@@ -41,7 +41,7 @@ class Shortcut:
def __contains__(self, keyword: str) -> bool:
return keyword in self.shortcut
def __getitem__(self, keyword: str) -> Optional[int | str]:
def __getitem__(self, keyword: str) -> Optional[Union[int, str]]:
return self.shortcut.get(keyword)
def __delitem__(self, keyword: str) -> None:
@@ -128,7 +128,7 @@ class Shortcut:
def interpret(
self, keyword: str, mode: bool = False
) -> Optional[int | str | list[str]]:
) -> Optional[Union[int, str, list[str]]]:
"""
Returns None (no hit), str (one hit) or list (multiple hits)
@@ -172,7 +172,7 @@ class Shortcut:
def auto_err(
self, keyword: str, descrip: Optional[str] = None
) -> Optional[int | str | list[str]]:
) -> Optional[Union[int, str, list[str]]]:
"""
Automatically raises an error if a keyword is unknown or ambiguous.

View File

@@ -21,14 +21,19 @@ dependencies = [
build-backend = "backend"
backend-path = ["_custom_build"]
requires = [
"cmake>=3.13.3",
"numpy>=2.0",
"setuptools>=69.2.0",
]
[project.optional-dependencies]
dev = [
"pillow==10.3.0",
"biopython>=1.80",
"msgpack==1.0.8",
"pillow==11.1.0",
"PySide6==6.8.1",
"pytest==8.2.2",
"requests==2.32.3",
]
[project.urls]

View File

@@ -1,15 +1,22 @@
import os
import sys
import pymol
from pymol import cmd, testing, stored
import socket
from pathlib import Path
from tempfile import TemporaryDirectory
pdbstr = '''ATOM 1 N GLY 22 -1.195 0.201 -0.206 1.00 0.00 N
ATOM 2 CA GLY 22 0.230 0.318 -0.502 1.00 0.00 C
ATOM 3 C GLY 22 1.059 -0.390 0.542 1.00 0.00 C
ATOM 4 O GLY 22 0.545 -0.975 1.499 1.00 0.00 O
ATOM 5 H GLY 22 -1.558 -0.333 0.660 1.00 0.00 H
ATOM 6 3HA GLY 22 0.482 1.337 -0.514 0.00 0.00 H
ATOM 7 HA GLY 22 0.434 -0.159 -1.479 1.00 0.00 H
import requests
import pymol
from pymol import cmd, testing
pdbstr = '''ATOM 1 N GLY 22 -1.195 0.201 -0.206 1.00 0.00 N
ATOM 2 CA GLY 22 0.230 0.318 -0.502 1.00 0.00 C
ATOM 3 C GLY 22 1.059 -0.390 0.542 1.00 0.00 C
ATOM 4 O GLY 22 0.545 -0.975 1.499 1.00 0.00 O
ATOM 5 H GLY 22 -1.558 -0.333 0.660 1.00 0.00 H
ATOM 6 3HA GLY 22 0.482 1.337 -0.514 0.00 0.00 H
ATOM 7 HA GLY 22 0.434 -0.159 -1.479 1.00 0.00 H
END
'''
@@ -45,7 +52,6 @@ mmodstr = ''' 7 gly
def _get_free_port() -> int:
import socket
with socket.socket() as s:
s.bind(("", 0))
return s.getsockname()[1]
@@ -608,43 +614,39 @@ class TestImporting(testing.PyMOLTestCase):
'network'
) # doesn't use external network, but is very slow while being connected to VPN
def testLoadPWG(self):
if sys.version_info[0] < 3:
import urllib2
else:
import urllib.request as urllib2
content_index = b'Hello World\n'
content_index = b"Hello World\n"
port = _get_free_port()
baseurl = 'http://localhost:%d' % port
base_url = f"http://localhost:{port}"
def urlread(url):
handle = urllib2.urlopen(url, timeout=3.0)
try:
return handle.info(), handle.read()
finally:
handle.close()
with TemporaryDirectory() as root_dir:
root_dir = Path(root_dir)
index_file = root_dir / "index.html"
start_pwg_file = root_dir / "start.pwg"
with testing.mkdtemp() as rootdir:
filename = os.path.join(rootdir, 'index.html')
with open(filename, 'wb') as handle:
handle.write(content_index)
index_file.write_bytes(content_index)
filename = os.path.join(rootdir, 'start.pwg')
with open(filename, 'w') as handle:
handle.write('root %s\n' % rootdir)
handle.write('port %d\n' % port)
handle.write('header add Test-Key Test-Value\n') # new in 2.4
cmd.load(filename)
pwg_content = (
f"root {root_dir}\n"
f"port {port}\n"
"header add Test-Key Test-Value\n"
)
header, content = urlread(baseurl)
self.assertEqual(header['Test-Key'], 'Test-Value')
self.assertEqual(content, content_index)
start_pwg_file.write_text(pwg_content)
cmd.load(str(start_pwg_file))
response = requests.get(url=base_url, timeout=5)
response.raise_for_status()
self.assertEqual(response.headers["Test-Key"], "Test-Value")
self.assertEqual(response.content, content_index)
# Warning: can't call locking functions here, will dead-lock
# get_version is non-locking since 1.7.4
header, content = urlread(baseurl + '/apply/pymol.cmd.get_version')
content = content.decode('ascii', errors='ignore')
response = requests.get(f"{base_url}/apply/pymol.cmd.get_version", timeout=5)
response.raise_for_status()
content = response.content.decode("ascii", errors="ignore")
self.assertTrue(content, cmd.get_version()[0] in content)
@testing.requires_version('2.1')

View File

@@ -1,3 +1,5 @@
from typing import Union
import pytest
from pymol.shortcut import Shortcut
@@ -48,7 +50,7 @@ def test_all_keywords(sc: Shortcut):
],
)
def test_full_prefix_hits(
sc: Shortcut, prefixs: list[str], expected_result: str | list[str]
sc: Shortcut, prefixs: list[str], expected_result: Union[str, list[str]]
):
for prefix in prefixs:
result = sc.interpret(prefix)