From e06541652a6528bf7544ea3757ceaa5c1ef44050 Mon Sep 17 00:00:00 2001 From: Toby Roseman Date: Tue, 25 Aug 2020 16:46:06 -0700 Subject: [PATCH] Python 3.8 Support (#3295) * In Python 3.8 docstrings are constants, not strings * Update build requirements * Updates sckit-learn API usage * Update unit tests for new coremltools version * Update dependencies in setup.py * Add Python 3.8 to GitLab config * Code review feedback * Added instructions for pushing new docker images * More requirements tweaking * Be explicit about which Python version documentation build should use * More requirements tweaking * Only depend on beta version of coremltools when necessary * Make unit tests agnostic of coremltool version; newer versions of coremltools add a key/value to the user metadata * Sound Classifier fails on Linux with TensorFlow 2.3 and 2.2 * Try the first coremltools 4.0 beta * And more dependency version tweaking * More dependency expirementation * Scenerio tests should not use minimal wheel. It doesn't have all required functionality * Go back to using beta3 for coremltools; they have fixed some things. Also 'and' vs 'or' bug fix. * Skip another test on Linux for Python 3.8 * Run 3.8 mac unit tests on 10.15; CoreMLTool beta is broken on 10.14 * Add Python 3.8 support to README * Update unsupported_python/setup.py to include Python 3.8 * Add Python 3.8 classifier tags to setup.py files --- .gitlab-ci.yml | 102 +++++++++++++++++- README.md | 2 +- scripts/Dockerfile-CentOS-6 | 15 ++- scripts/Dockerfile-Ubuntu-18.04 | 7 ++ scripts/create_docker_images.sh | 4 + scripts/get_docker_image.sh | 2 +- scripts/install_python_toolchain.sh | 5 +- scripts/make_documentation.sh | 2 +- scripts/make_wheel.sh | 5 +- scripts/requirements-minimal.txt | 9 +- scripts/requirements.txt | 21 ++-- scripts/test_wheel.sh | 5 +- src/python/setup.py | 18 +++- src/python/turicreate/_cython/cy_variant.pyx | 2 +- src/python/turicreate/extensions.py | 1 - .../test/test_activity_classifier.py | 27 +++-- .../test/test_audio_functionality.py | 43 ++++---- .../turicreate/test/test_coreml_export.py | 13 +-- .../test/test_drawing_classifier.py | 24 ++--- .../turicreate/test/test_image_classifier.py | 21 ++-- .../turicreate/test/test_image_similarity.py | 26 ++--- .../turicreate/test/test_object_detector.py | 30 +++--- .../test/test_one_shot_object_detector.py | 35 +++--- .../turicreate/test/test_recommender.py | 14 +-- .../turicreate/test/test_style_transfer.py | 28 +++-- .../turicreate/test/test_svm_classifier.py | 12 +-- .../turicreate/test/test_text_classifier.py | 20 ++-- .../turicreate/util/lambda_closure_capture.py | 12 ++- src/unsupported_python/setup.py | 3 +- 29 files changed, 317 insertions(+), 191 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index c00048fd0f..9ad5acc4c1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -108,6 +108,26 @@ build_wheel_linux_python37: paths: - target/ +build_wheel_linux_python38: + tags: + - docker + services: + - docker:19.03-dind + image: docker:19.03.8 + stage: build + variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_DRIVER: overlay2 + DOCKER_TLS_CERTDIR: "" + script: + - apk add bash + - mkdir -p /etc/ssl/certs/ + - bash -e scripts/make_wheel.sh --skip_test --skip_cpp_test --build_number=$CI_PIPELINE_ID --num_procs=4 --docker-python3.8 + artifacts: + expire_in: 1 day + paths: + - target/ + build_wheel_mac_10_15_python27: tags: - macos10.15 @@ -172,6 +192,22 @@ build_wheel_mac_10_15_python37: paths: - target/ +build_wheel_mac_10_15_python38: + tags: + - macos10.15 + only: + variables: + - $TC_HAS_MACOS_RUNNERS == "1" + stage: build + script: + - export VIRTUALENV="/Library/Frameworks/Python.framework/Versions/3.8/bin/virtualenv" + - source scripts/use_ccache.sh + - bash -e scripts/make_wheel.sh --skip_test --skip_cpp_test --build_number=$CI_PIPELINE_ID --num_procs=4 + artifacts: + expire_in: 1 day + paths: + - target/ + build_dylib_macos_10_15: tags: - macos10.15 @@ -445,6 +481,28 @@ test_python_linux_python37: paths: - pytest.* +test_python_linux_python38: + tags: + - docker + services: + - docker:19.03-dind + image: docker:19.03.8 + stage: test + variables: + DOCKER_HOST: tcp://docker:2375 + DOCKER_DRIVER: overlay2 + DOCKER_TLS_CERTDIR: "" + dependencies: + - build_wheel_linux_python38 + script: + - apk add bash + - bash -e scripts/test_wheel.sh --docker-python3.8 + artifacts: + when: always + expire_in: 2 weeks + paths: + - pytest.* + test_python_mac_python27_10_13: tags: - macos10.13 @@ -553,6 +611,42 @@ test_python_mac_python37_10_14_minimal: paths: - pytest.* +test_python_mac_python38_10_15: + tags: + - macos10.15 + only: + variables: + - $TC_HAS_MACOS_RUNNERS == "1" + stage: test + dependencies: + - build_wheel_mac_10_15_python38 + script: + - export VIRTUALENV="/Library/Frameworks/Python.framework/Versions/3.8/bin/virtualenv" + - bash -e scripts/test_wheel.sh + artifacts: + when: always + expire_in: 2 weeks + paths: + - pytest.* + +test_python_mac_python38_10_14_minimal: + tags: + - macos10.14 + only: + variables: + - $TC_HAS_MACOS_RUNNERS == "1" + stage: test + dependencies: + - build_wheel_mac_10_15_python38 + script: + - export VIRTUALENV="/Library/Frameworks/Python.framework/Versions/3.8/bin/virtualenv" + - bash -e scripts/test_wheel.sh --minimal + artifacts: + when: always + expire_in: 2 weeks + paths: + - pytest.* + scenario_tests_linux_python27: tags: - docker @@ -570,7 +664,7 @@ scenario_tests_linux_python27: - apk add bash - cd scenario-tests - ./run_scenario_tests.sh --docker-python2.7 \ - ../target/turicreate-*+minimal*.whl + "$(ls -1 ../target/turicreate-*.whl | grep -v minimal)" scenario_tests_linux_python35: tags: @@ -589,7 +683,7 @@ scenario_tests_linux_python35: - apk add bash - cd scenario-tests - ./run_scenario_tests.sh --docker-python3.5 \ - ../target/turicreate-*+minimal*.whl + "$(ls -1 ../target/turicreate-*.whl | grep -v minimal)" scenario_tests_linux_python36: tags: @@ -608,7 +702,7 @@ scenario_tests_linux_python36: - apk add bash - cd scenario-tests - ./run_scenario_tests.sh --docker-python3.6 \ - ../target/turicreate-*+minimal*.whl + "$(ls -1 ../target/turicreate-*.whl | grep -v minimal)" scenario_tests_mac_python27_10_13: tags: @@ -650,10 +744,12 @@ collect_artifacts: - build_wheel_linux_python35 - build_wheel_linux_python36 - build_wheel_linux_python37 + - build_wheel_linux_python38 - build_wheel_mac_10_15_python27 - build_wheel_mac_10_15_python35 - build_wheel_mac_10_15_python36 - build_wheel_mac_10_15_python37 + - build_wheel_mac_10_15_python38 - build_documentation script: - find release/src/deployment/ -name '*.dylib' -exec mv {} target/ \; diff --git a/README.md b/README.md index 096ea415e5..40d2db7306 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ System Requirements Turi Create requires: -* Python 2.7, 3.5, 3.6, 3.7 +* Python 2.7, 3.5, 3.6, 3.7, 3.8 * x86\_64 architecture * At least 4 GB of RAM diff --git a/scripts/Dockerfile-CentOS-6 b/scripts/Dockerfile-CentOS-6 index 874ddae544..d6e3c35585 100644 --- a/scripts/Dockerfile-CentOS-6 +++ b/scripts/Dockerfile-CentOS-6 @@ -7,7 +7,7 @@ # Builds against libstdc++ from GCC 4.8 for compatibility with older libstdc++ # runtime versions. -FROM quay.io/pypa/manylinux2010_x86_64@sha256:191e8bc35089ce4ceda48224289b7475322ee6853faa4bf7d4b8e0d9c3b0358e +FROM quay.io/pypa/manylinux2010_x86_64 # Set env variables for tools to pick up ENV CC="gcc" @@ -133,6 +133,17 @@ RUN ./configure --prefix=/usr/local --enable-unicode=ucs4 --enable-shared --enab ldconfig && \ rm -rf /src/Python-3.7.3* +# Install Python 3.8 from source +WORKDIR /src +RUN curl -O https://www.python.org/ftp/python/3.8.5/Python-3.8.5.tgz +RUN tar xf Python-3.8.5.tgz +WORKDIR /src/Python-3.8.5 +RUN ./configure --prefix=/usr/local --enable-unicode=ucs4 --enable-shared --enable-loadable-sqlite-extensions && \ + make -j4 --quiet && \ + make install && \ + ldconfig && \ + rm -rf /src/Python-3.8.5* + ENV CC="/usr/local/bin/gcc" ENV CXX="/usr/local/bin/g++" @@ -147,6 +158,8 @@ RUN python3.6 get-pip.py RUN pip3.6 install virtualenv RUN python3.7 get-pip.py RUN pip3.7 install virtualenv +RUN python3.8 get-pip.py +RUN pip3.8 install virtualenv RUN rm -rf /src/get-pip.py ENV LD="/usr/local/lib64" diff --git a/scripts/Dockerfile-Ubuntu-18.04 b/scripts/Dockerfile-Ubuntu-18.04 index 1259f822fb..b474177891 100644 --- a/scripts/Dockerfile-Ubuntu-18.04 +++ b/scripts/Dockerfile-Ubuntu-18.04 @@ -17,6 +17,11 @@ RUN apt-get -y upgrade # turicreate and upstream dependencies RUN apt-get -y install python3.6 python3.6-distutils python3.7 python3.7-distutils libgomp1 lsb-release npm nodejs doxygen zip +# Install Python 3.8 +RUN apt-get -y install software-properties-common +RUN add-apt-repository -y ppa:deadsnakes/ppa +RUN apt-get -y install python3.8 + # Install build-essential (needed by numpy, which tries to # build itself from source on 3.6 and above) RUN apt-get -y install build-essential libpython3.6-dev libpython3.7-dev @@ -29,3 +34,5 @@ RUN python3.6 get-pip.py RUN pip3.6 install virtualenv RUN python3.7 get-pip.py RUN pip3.7 install virtualenv +RUN python3.8 get-pip.py +RUN pip3.8 install virtualenv diff --git a/scripts/create_docker_images.sh b/scripts/create_docker_images.sh index 0d4a216a5c..bcd0ba6a5f 100755 --- a/scripts/create_docker_images.sh +++ b/scripts/create_docker_images.sh @@ -3,6 +3,10 @@ set -e set -x +# Once new docker images have been verified, you should manually push them to the GitLab registry. +# To do this, first log: registry.gitlab.com`. +# Then for each docker image: `docker push `. + SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) WORKSPACE=${SCRIPT_DIR}/.. cd ${WORKSPACE} diff --git a/scripts/get_docker_image.sh b/scripts/get_docker_image.sh index 84edecf08c..0fc8666564 100755 --- a/scripts/get_docker_image.sh +++ b/scripts/get_docker_image.sh @@ -37,7 +37,7 @@ if [[ -z "${UBUNTU_VERSION}" && -z "${CENTOS_VERSION}" ]]; then fi # The build image version that will be used for building -TC_BUILD_IMAGE_VERSION="1.0.6" +TC_BUILD_IMAGE_VERSION="1.0.7" if [[ -z "${CENTOS_VERSION}" ]]; then # The base image name - using Gitlab CI registry diff --git a/scripts/install_python_toolchain.sh b/scripts/install_python_toolchain.sh index ebe5d6a54c..131ea19871 100755 --- a/scripts/install_python_toolchain.sh +++ b/scripts/install_python_toolchain.sh @@ -27,8 +27,9 @@ function linux_patch_sigfpe_handler { if [[ $OSTYPE == linux* ]]; then targfile=deps/local/include/pyfpe.h if [[ -f $targfile ]]; then - echo "#undef WANT_SIGFPE_HANDLER" | cat - $targfile > tmp - mv -f tmp $targfile + temp_file=/tmp/sigfpe_handler_temp.txt + echo "#undef WANT_SIGFPE_HANDLER" | cat - $targfile > $temp_file + mv -f $temp_file $targfile fi fi } diff --git a/scripts/make_documentation.sh b/scripts/make_documentation.sh index 3d5c21407e..782b86dff0 100755 --- a/scripts/make_documentation.sh +++ b/scripts/make_documentation.sh @@ -56,7 +56,7 @@ fi TARGET_DIR=${WORKSPACE}/target -virtualenv ./deps/env +virtualenv --python=python3.7 ./deps/env source deps/env/bin/activate # ignore minimal build diff --git a/scripts/make_wheel.sh b/scripts/make_wheel.sh index a9367a5bcb..ef9b2f1d24 100755 --- a/scripts/make_wheel.sh +++ b/scripts/make_wheel.sh @@ -44,11 +44,13 @@ print_help() { echo echo " --docker-python3.7 Use docker to build for Python 3.7 in CentOS 6 with Clang 8." echo + echo " --docker-python3.8 Use docker to build for Python 3.8 in CentOS 6 with Clang 8." + echo echo " --num_procs=n Specify the number of proceses to run in parallel." echo echo " --target-dir=[dir] The directory where the wheel and associated files are put." echo - echo "Produce a local wheel and skip test and doc generation" + echo "Produce a local wheel and skip test" echo "Example: ./make_wheel.sh --skip_test" exit 1 } # end of print help @@ -70,6 +72,7 @@ while [ $# -gt 0 ] --docker-python3.5) USE_DOCKER=1;DOCKER_PYTHON=3.5;; --docker-python3.6) USE_DOCKER=1;DOCKER_PYTHON=3.6;; --docker-python3.7) USE_DOCKER=1;DOCKER_PYTHON=3.7;; + --docker-python3.8) USE_DOCKER=1;DOCKER_PYTHON=3.8;; --help) print_help ;; *) unknown_option $1 ;; esac diff --git a/scripts/requirements-minimal.txt b/scripts/requirements-minimal.txt index ab17727243..ce76568983 100644 --- a/scripts/requirements-minimal.txt +++ b/scripts/requirements-minimal.txt @@ -1,7 +1,10 @@ -numpy==1.16.4 -cython==0.29.10 +numpy==1.16.4; python_version < '3.6' +numpy==1.19.1; python_version >= '3.6' + +cython==0.29.21 decorator==4.4.0 -pandas==0.24.2 +pandas==0.24.2; python_version <= '3.6' +pandas==1.0.5; python_version > '3.6' pillow==6.2.2 prettytable==0.7.2 pytz==2019.1 diff --git a/scripts/requirements.txt b/scripts/requirements.txt index 9bd5a5e4cb..0e80504b02 100644 --- a/scripts/requirements.txt +++ b/scripts/requirements.txt @@ -2,15 +2,22 @@ ### extra for full pkg ### -coremltools==3.3 -scipy==1.2.1 +coremltools==3.3; python_version < '3.8' +coremltools==4.0b3; python_version == '3.8' + +scipy==1.2.1; python_version < '3.8' +scipy==1.5.1; python_version == '3.8' + resampy==0.2.1 -scikit-learn==0.20.3 -statsmodels==0.9.0 -tensorflow==2.0.1; python_version > '2.7' -tensorflow==2.0.0; python_version < '3.5' # Last version to support Python 2.7 + +scikit-learn==0.20.3; python_version < '3.8' +scikit-learn==0.23.1; python_version == '3.8' + +statsmodels==0.9.0; python_version < '3.8' +statsmodels==0.11.1; python_version == '3.8' + UISoup==2.5.7 -pyobjc==5.2; sys_platform == 'darwin' + llvmlite==0.33.0; python_version >= '3.6' llvmlite==0.31.0; python_version < '3.6' diff --git a/scripts/test_wheel.sh b/scripts/test_wheel.sh index ddb4d6d05c..a978331f71 100755 --- a/scripts/test_wheel.sh +++ b/scripts/test_wheel.sh @@ -30,6 +30,8 @@ print_help() { echo echo " --docker-python3.7 Use docker to test on Python 3.7 in Ubuntu 18.04." echo + echo " --docker-python3.8 Use docker to test on Python 3.8 in Ubuntu 18.04." + echo echo " --minimal test the turicreate+minimal package" echo exit 1 @@ -45,6 +47,7 @@ while [ $# -gt 0 ] --docker-python3.5) USE_DOCKER=1;DOCKER_PYTHON=3.5;; --docker-python3.6) USE_DOCKER=1;DOCKER_PYTHON=3.6;; --docker-python3.7) USE_DOCKER=1;DOCKER_PYTHON=3.7;; + --docker-python3.8) USE_DOCKER=1;DOCKER_PYTHON=3.8;; --minimal) USE_MINIMAL=1;; --help) print_help ;; *) unknown_option "$1" ;; @@ -81,7 +84,7 @@ if [[ -n "${USE_DOCKER}" ]]; then --env-file "${envlist}" \ "${TC_BUILD_IMAGE_1404}" \ /build/scripts/test_wheel.sh ${docker_extra_args} - elif [[ "${DOCKER_PYTHON}" == "3.6" ]] || [[ "${DOCKER_PYTHON}" == "3.7" ]]; then + elif [[ "${DOCKER_PYTHON}" == "3.6" ]] || [[ "${DOCKER_PYTHON}" == "3.7" ]] || [[ "${DOCKER_PYTHON}" == "3.8" ]]; then docker run --rm -m=8g \ --mount type=bind,source="$WORKSPACE",target=/build,consistency=delegated \ --env-file "${envlist}" \ diff --git a/src/python/setup.py b/src/python/setup.py index 2fa0c096d3..51d4ed464e 100644 --- a/src/python/setup.py +++ b/src/python/setup.py @@ -136,6 +136,7 @@ def do_not_install(require): "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis", @@ -163,7 +164,6 @@ def do_not_install(require): long_description = f.read().decode("utf-8") install_requires = [ - "coremltools==3.3", "decorator >= 4.0.9", "numpy", "pandas >= 0.23.2", @@ -179,12 +179,26 @@ def do_not_install(require): ): install_requires.append("llvmlite == 0.31.0") + if sys.version_info[0] == 3 and sys.version_info[1] == 8: + # Only 4.0 betas support Python 3.8 + install_requires.append("coremltools==4.0b3") + else: + install_requires.append("coremltools==3.3") + if sys.platform == "darwin": install_requires.append("tensorflow >= 2.0.0") else: # ST, OD, AC and DC segfault on Linux with TensorFlow 2.1.0 and 2.1.1 # See: https://github.com/apple/turicreate/issues/3003 - install_requires.append("tensorflow >= 2.0.0,!= 2.1.0,!= 2.1.1") + + # SC errors out on Linux with TensorFlow 2.2 and 2.3 + # See: https://github.com/apple/turicreate/issues/3303 + + if sys.version_info[0] != 3 or sys.version_info[1] != 8: + install_requires.append("tensorflow >= 2.0.0,<2.1.0") + else: + # Only TensorFlow >= 2.2 supports Python 3.8 + install_requires.append("tensorflow >= 2.0.0") # numba 0.51 started using "manylinux2014" rather than "manylinux2010". # This breaks a lot of Linux installs. diff --git a/src/python/turicreate/_cython/cy_variant.pyx b/src/python/turicreate/_cython/cy_variant.pyx index 397b1c8452..3c919b330b 100644 --- a/src/python/turicreate/_cython/cy_variant.pyx +++ b/src/python/turicreate/_cython/cy_variant.pyx @@ -514,7 +514,7 @@ cdef inline bint _var_set_dict_internal(variant_map_type& ret_as_vm, flex_dict& writing_to_flex_dict = False pos += 1 - else: + else: # Not writing to a flex_dict tr_code = get_var_tr_code(v) _convert_to_variant_type(ret_as_vm[str_to_cpp(k)], v, tr_code) diff --git a/src/python/turicreate/extensions.py b/src/python/turicreate/extensions.py index b89aefa63a..fad9ae968f 100644 --- a/src/python/turicreate/extensions.py +++ b/src/python/turicreate/extensions.py @@ -710,7 +710,6 @@ def _build_native_function_call(fn): which can then be passed to C++. Returns a _Closure object on success, raises an exception on failure. - """ # See if fn is the native function itself native_function_name = _get_toolkit_function_name_from_function(fn) diff --git a/src/python/turicreate/test/test_activity_classifier.py b/src/python/turicreate/test/test_activity_classifier.py index 004a81c1f1..2541940495 100644 --- a/src/python/turicreate/test/test_activity_classifier.py +++ b/src/python/turicreate/test/test_activity_classifier.py @@ -486,6 +486,7 @@ def test_export_coreml(self): Check the export_coreml() function. """ import coremltools + import platform # Export the model as a CoreML model file filename = tempfile.NamedTemporaryFile(suffix=".mlmodel").name @@ -493,22 +494,18 @@ def test_export_coreml(self): # Load the model back from the CoreML model file coreml_model = coremltools.models.MLModel(filename) - import platform + metadata = coreml_model.user_defined_metadata + + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["target"], self.target) + self.assertEqual(metadata["type"], "activity_classifier") + self.assertEqual(metadata["prediction_window"], str(self.prediction_window)) + self.assertEqual(metadata["session_id"], self.session_id) + self.assertEqual(metadata["features"], ",".join(self.features)) + self.assertEqual(metadata["max_iterations"], "10") + self.assertEqual(metadata["version"], "2") - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "target": self.target, - "type": "activity_classifier", - "prediction_window": str(self.prediction_window), - "session_id": self.session_id, - "features": ",".join(self.features), - "max_iterations": "10", - "version": "2", - }, - dict(coreml_model.user_defined_metadata), - ) expected_result = "Activity classifier created by Turi Create (version %s)" % ( tc.__version__ ) diff --git a/src/python/turicreate/test/test_audio_functionality.py b/src/python/turicreate/test/test_audio_functionality.py index eb7e832efe..4abb7141b0 100644 --- a/src/python/turicreate/test/test_audio_functionality.py +++ b/src/python/turicreate/test/test_audio_functionality.py @@ -18,7 +18,7 @@ import pytest import numpy as np -import sys as _sys +import sys import turicreate as tc from turicreate.toolkits._internal_utils import _raise_error_if_not_sarray @@ -163,7 +163,9 @@ def generate_sine_wave(length, sample_rate): binary_test_data = _generate_binary_test_data() - +# Skip tests on Linux for Python 3.8 +@unittest.skipIf(sys.platform != "darwin" and sys.version_info[0] == 3 and sys.version_info[1] == 8, + "https://github.com/apple/turicreate/issues/3303") class ClassifierTestTwoClassesStringLabels(unittest.TestCase): @classmethod def setUpClass(self): @@ -342,20 +344,14 @@ def test_export_core_ml_no_prediction(self): core_ml_model = coremltools.models.MLModel(file_name) # Check metadata - metadata = core_ml_model.get_spec().description.metadata - self.assertTrue("sampleRate" in metadata.userDefined) - self.assertEqual(metadata.userDefined["sampleRate"], "16000") - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "type": "SoundClassifier", - "coremltoolsVersion": coremltools.__version__, - "sampleRate": "16000", - "version": "1", - }, - dict(core_ml_model.user_defined_metadata), - ) + metadata = core_ml_model.get_spec().description.metadata.userDefined + + self.assertEqual(metadata["sampleRate"], "16000") + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], "SoundClassifier") + self.assertEqual(metadata["version"], "1") + expected_result = "Sound classifier created by Turi Create (version %s)" % ( tc.__version__ ) @@ -442,7 +438,9 @@ def test_summary_invalid_input(self): with self.assertRaises(ToolkitError): model.summary(model.summary({})) - +# Skip tests on Linux for Python 3.8 +@unittest.skipIf(sys.platform != "darwin" and sys.version_info[0] == 3 and sys.version_info[1] == 8, + "https://github.com/apple/turicreate/issues/3303") class ClassifierTestTwoClassesIntLabels(ClassifierTestTwoClassesStringLabels): @classmethod def setUpClass(self): @@ -463,6 +461,9 @@ def setUpClass(self): assert self.model.custom_layer_sizes == layer_sizes +# Skip tests on Linux for Python 3.8 +@unittest.skipIf(sys.platform != "darwin" and sys.version_info[0] == 3 and sys.version_info[1] == 8, + "https://github.com/apple/turicreate/issues/3303") class ClassifierTestThreeClassesStringLabels(ClassifierTestTwoClassesStringLabels): @classmethod def setUpClass(self): @@ -498,7 +499,9 @@ def generate_constant_noise(length, sample_rate): def test_validation_set(self): self.assertTrue(self.model.validation_accuracy is not None) - +# Skip tests on Linux for Python 3.8 +@unittest.skipIf(sys.platform != "darwin" and sys.version_info[0] == 3 and sys.version_info[1] == 8, + "https://github.com/apple/turicreate/issues/3303") class ClassifierTestWithShortClip(unittest.TestCase): @classmethod def setUpClass(self): @@ -597,7 +600,9 @@ def test_case(self): self.assertEqual(y2.shape, (1, 96, 64)) self.assertTrue(np.isclose(y1, y2, atol=1e-04).all()) - +# Skip tests on Linux for Python 3.8 +@unittest.skipIf(sys.platform != "darwin" and sys.version_info[0] == 3 and sys.version_info[1] == 8, + "https://github.com/apple/turicreate/issues/3303") class ReuseDeepFeatures(unittest.TestCase): def test_simple_case(self): data = copy(binary_test_data) diff --git a/src/python/turicreate/test/test_coreml_export.py b/src/python/turicreate/test/test_coreml_export.py index 52036935ad..2e1bfa6860 100644 --- a/src/python/turicreate/test/test_coreml_export.py +++ b/src/python/turicreate/test/test_coreml_export.py @@ -81,14 +81,11 @@ def _test_coreml_export( import coremltools coreml_model = coremltools.models.MLModel(mlmodel_filename) - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "type": model.__class__.__name__, - }, - dict(coreml_model.user_defined_metadata), - ) + metadata = coreml_model.user_defined_metadata + + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], model.__class__.__name__) if _mac_ver() < (10, 13): print("Skipping export test; model not supported on this platform.") diff --git a/src/python/turicreate/test/test_drawing_classifier.py b/src/python/turicreate/test/test_drawing_classifier.py index 1fef40309d..918032026a 100644 --- a/src/python/turicreate/test/test_drawing_classifier.py +++ b/src/python/turicreate/test/test_drawing_classifier.py @@ -353,19 +353,17 @@ def test_export_coreml(self): # Load the model back from the CoreML model file coreml_model = coremltools.models.MLModel(filename) - self.assertDictEqual( - { - "com.github.apple.turicreate.version": _tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "target": self.target, - "feature": self.feature, - "type": "drawing_classifier", - "warm_start": warm_start_ans, - "max_iterations": max_iters_ans[i], - "version": "2", - }, - dict(coreml_model.user_defined_metadata), - ) + metadata = coreml_model.user_defined_metadata + + self.assertEqual(metadata["com.github.apple.turicreate.version"], _tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], "drawing_classifier") + self.assertEqual(metadata["version"], "2") + self.assertEqual(metadata["target"], self.target) + self.assertEqual(metadata["feature"], self.feature) + self.assertEqual(metadata["warm_start"], warm_start_ans) + self.assertEqual(metadata["max_iterations"], max_iters_ans[i]) + expected_result = ( "Drawing classifier created by Turi Create (version %s)" % (_tc.__version__) diff --git a/src/python/turicreate/test/test_image_classifier.py b/src/python/turicreate/test/test_image_classifier.py index a03386b4e1..75fe51f271 100644 --- a/src/python/turicreate/test/test_image_classifier.py +++ b/src/python/turicreate/test/test_image_classifier.py @@ -229,16 +229,13 @@ def test_export_coreml(self): self.model.export_coreml(filename) coreml_model = coremltools.models.MLModel(filename) - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "type": "ImageClassifier", - "coremltoolsVersion": coremltools.__version__, - "version": "1", - }, - dict(coreml_model.user_defined_metadata), - ) + + metadata = coreml_model.user_defined_metadata + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], "ImageClassifier") + self.assertEqual(metadata["version"], "1") + expected_result = ( "Image classifier (%s) created by Turi Create (version %s)" % (self.model.model.lower(), tc.__version__) @@ -254,6 +251,7 @@ def test_export_coreml_predict(self): coreml_model = coremltools.models.MLModel(filename) if self.feature == "awesome_image": + # tc.image_classifier.create(...) was not called with deep features img = data[0:1][self.feature][0] img_fixed = tc.image_analysis.resize(img, *reversed(self.input_image_shape)) from PIL import Image @@ -270,9 +268,6 @@ def test_export_coreml_predict(self): list(self.model.predict(img_fixed, output_type="probability_vector")), self.tolerance, ) - else: - # If the code came here that means the type of the feature used is deep_deatures and the predict fwature in coremltools doesn't work with deep_features yet so we will ignore this specific test case unitl the same is written. - pass def test_classify(self): model = self.model diff --git a/src/python/turicreate/test/test_image_similarity.py b/src/python/turicreate/test/test_image_similarity.py index 925723bd45..046cf16c52 100644 --- a/src/python/turicreate/test/test_image_similarity.py +++ b/src/python/turicreate/test/test_image_similarity.py @@ -228,6 +228,7 @@ def test_export_coreml(self): Check the export_coreml() function. """ import coremltools + import platform def get_psnr(x, y): # See: https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio @@ -242,26 +243,16 @@ def get_psnr(x, y): # Load the model back from the CoreML model file coreml_model = coremltools.models.MLModel(filename) - import platform + metadata = coreml_model.user_defined_metadata - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "type": "ImageSimilarityModel", - "coremltoolsVersion": coremltools.__version__, - "version": "1", - }, - dict(coreml_model.user_defined_metadata), - ) - - expected_result = ( - "Image similarity (%s) created by Turi Create (version %s)" - % (self.model.model, tc.__version__) - ) + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], "ImageSimilarityModel") + self.assertEqual(metadata["version"], "1") # Get model distances for comparison if self.feature == "awesome_image": + # tc.image_classifier.create(...) was not called with deep features img = data[0:1][self.feature][0] img_fixed = tc.image_analysis.resize(img, *reversed(self.input_image_shape)) tc_ret = self.model.query(img_fixed, k=data.num_rows()) @@ -277,9 +268,6 @@ def get_psnr(x, y): tc_distances = tc_ret.sort("reference_label")["distance"].to_numpy() psnr_value = get_psnr(coreml_distances, tc_distances) self.assertTrue(psnr_value > 50) - else: - # If the code came here that means the type of the feature used is deep_deatures and the predict fwature in coremltools doesn't work with deep_features yet so we will ignore this specific test case unitl the same is written. - pass def test_save_and_load(self): with test_util.TempDirectory() as filename: diff --git a/src/python/turicreate/test/test_object_detector.py b/src/python/turicreate/test/test_object_detector.py index f19af52822..194b1dd732 100644 --- a/src/python/turicreate/test/test_object_detector.py +++ b/src/python/turicreate/test/test_object_detector.py @@ -467,22 +467,20 @@ def test_export_coreml(self): self.model.export_coreml(filename, include_non_maximum_suppression=False) coreml_model = coremltools.models.MLModel(filename) - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "annotations": self.annotations, - "type": "object_detector", - "classes": ",".join(sorted(_CLASSES)), - "feature": self.feature, - "include_non_maximum_suppression": "False", - "max_iterations": "1", - "model": "YOLOv2", - "training_iterations": "1", - "version": "1", - }, - dict([(str(k), v) for k, v in coreml_model.user_defined_metadata.items()]), - ) + metadata = coreml_model.user_defined_metadata + + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], "object_detector") + self.assertEqual(metadata["version"], "1") + self.assertEqual(metadata["annotations"], self.annotations) + self.assertEqual(metadata["classes"], ",".join(sorted(_CLASSES))) + self.assertEqual(metadata["feature"], self.feature) + self.assertEqual(metadata["include_non_maximum_suppression"], "False") + self.assertEqual(metadata["max_iterations"], "1") + self.assertEqual(metadata["model"], "YOLOv2") + self.assertEqual(metadata["training_iterations"], "1") + expected_result = "Object detector created by Turi Create (version %s)" % ( tc.__version__ ) diff --git a/src/python/turicreate/test/test_one_shot_object_detector.py b/src/python/turicreate/test/test_one_shot_object_detector.py index 2ff1768b83..bb4eccf229 100644 --- a/src/python/turicreate/test/test_one_shot_object_detector.py +++ b/src/python/turicreate/test/test_one_shot_object_detector.py @@ -213,32 +213,29 @@ def test_export_coreml(self): filename = tempfile.NamedTemporaryFile(suffix=".mlmodel").name self.model.export_coreml(filename, include_non_maximum_suppression=False) - ## Test metadata + # Test metadata coreml_model = coremltools.models.MLModel(filename) - self.maxDiff = None - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "type": "object_detector", - "classes": ",".join(sorted(_CLASSES)), - "feature": self.feature, - "include_non_maximum_suppression": "False", - "annotations": "annotation", - "max_iterations": "1", - "model": "YOLOv2", - "training_iterations": "1", - "version": "1", - }, - dict([(str(k), v) for k, v in coreml_model.user_defined_metadata.items()]), - ) + metadata = coreml_model.user_defined_metadata + + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], "object_detector") + self.assertEqual(metadata["version"], "1") + self.assertEqual(metadata["classes"], ",".join(sorted(_CLASSES))) + self.assertEqual(metadata["feature"], self.feature) + self.assertEqual(metadata["include_non_maximum_suppression"], "False") + self.assertEqual(metadata["annotations"], "annotation") + self.assertEqual(metadata["max_iterations"], "1") + self.assertEqual(metadata["model"], "YOLOv2") + self.assertEqual(metadata["training_iterations"], "1") + expected_result = ( "One shot object detector created by Turi Create (version %s)" % (tc.__version__) ) self.assertEquals(expected_result, coreml_model.short_description) - ## Test prediction + # Test prediction img = self.train[0:1][self.feature][0] img_fixed = tc.image_analysis.resize(img, 416, 416, 3) pil_img = Image.fromarray(img_fixed.pixel_data) diff --git a/src/python/turicreate/test/test_recommender.py b/src/python/turicreate/test/test_recommender.py index 5e6038c715..f90aeaf615 100644 --- a/src/python/turicreate/test/test_recommender.py +++ b/src/python/turicreate/test/test_recommender.py @@ -75,6 +75,7 @@ def _coreml_to_tc(preds): class RecommenderTestBase(unittest.TestCase): def _test_coreml_export(self, m, item_ids, ratings=None): import coremltools as _coremltools + import platform temp_file_path = tempfile.NamedTemporaryFile().name if m.target and ratings: @@ -109,16 +110,11 @@ def _test_coreml_export(self, m, item_ids, ratings=None): # compare them self.assertEqual(predictions_tc_dict, _coreml_to_tc(predictions_coreml)) - # compare user defined data - import platform + # compare user defined metadata + metadata = coremlmodel.user_defined_metadata + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - }, - dict(coremlmodel.user_defined_metadata), - ) os.unlink(temp_file_path) def _get_trained_model( diff --git a/src/python/turicreate/test/test_style_transfer.py b/src/python/turicreate/test/test_style_transfer.py index bf9f9c4d76..8b7096d3a3 100644 --- a/src/python/turicreate/test/test_style_transfer.py +++ b/src/python/turicreate/test/test_style_transfer.py @@ -312,21 +312,19 @@ def test_export_coreml(self): ## Metadata test coreml_model = coremltools.models.MLModel(filename) - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "type": "style_transfer", - "content_feature": self.content_feature, - "style_feature": self.style_feature, - "model": self.pre_trained_model, - "max_iterations": "1", - "training_iterations": "1", - "num_styles": str(self.num_styles), - "version": "1", - }, - dict(coreml_model.user_defined_metadata), - ) + metadata = coreml_model.user_defined_metadata + + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], "style_transfer") + self.assertEqual(metadata["version"], "1") + self.assertEqual(metadata["content_feature"], self.content_feature) + self.assertEqual(metadata["style_feature"], self.style_feature) + self.assertEqual(metadata["model"], self.pre_trained_model) + self.assertEqual(metadata["max_iterations"], "1") + self.assertEqual(metadata["training_iterations"], "1") + self.assertEqual(metadata["num_styles"], str(self.num_styles)) + expected_result = "Style transfer created by Turi Create (version %s)" % ( tc.__version__ ) diff --git a/src/python/turicreate/test/test_svm_classifier.py b/src/python/turicreate/test/test_svm_classifier.py index fcce33aa53..1be4f63856 100644 --- a/src/python/turicreate/test/test_svm_classifier.py +++ b/src/python/turicreate/test/test_svm_classifier.py @@ -88,7 +88,7 @@ def setUpClass(self): feature_names = self.features X_train = list(self.sf.apply(lambda row: [row[k] for k in features])) y_train = list(self.sf[self.target]) - sm_model = svm.LinearSVC(C=1.0, loss="l1") + sm_model = svm.LinearSVC(C=1.0, loss="hinge") sm_model.fit(X_train, y_train) self.coef = list(sm_model.intercept_) + list(sm_model.coef_[0]) @@ -301,7 +301,7 @@ def setUpClass(self): feature_names = self.features X_train = list(self.sf.apply(lambda row: [row[k] for k in feature_names])) y_train = list(self.sf[self.target]) - sm_model = svm.LinearSVC(C=1.0, loss="l1") + sm_model = svm.LinearSVC(C=1.0, loss="hinge") sm_model.fit(X_train, y_train) self.coef = list(sm_model.intercept_) + list(sm_model.coef_[0]) @@ -441,7 +441,7 @@ def setUpClass(self): feature_names = ["species_0", "species_1", "X1", "X2", "X3"] X_train = list(self.sf.apply(lambda row: [row[k] for k in feature_names])) y_train = list(self.sf[self.target]) - sm_model = svm.LinearSVC(C=1.0, loss="l1") + sm_model = svm.LinearSVC(C=1.0, loss="hinge") sm_model.fit(X_train, y_train) self.coef = list(sm_model.intercept_) + list(sm_model.coef_[0]) self.sf["species"] = self.sf["species"].apply(lambda x: [x]) @@ -521,7 +521,7 @@ def setUpClass(self): feature_names = ["species_0", "species_1", "X1", "X2", "X3"] X_train = list(self.sf.apply(lambda row: [row[k] for k in feature_names])) y_train = list(self.sf[self.target]) - sm_model = svm.LinearSVC(C=1.0, loss="l1") + sm_model = svm.LinearSVC(C=1.0, loss="hinge") sm_model.fit(X_train, y_train) self.coef = list(sm_model.intercept_) + list(sm_model.coef_[0]) @@ -676,7 +676,7 @@ def setUpClass(self): feature_names = self.features X_train = list(self.sf["vec"]) y_train = list(self.sf[self.target]) - sm_model = svm.LinearSVC(C=1.0, loss="l1") + sm_model = svm.LinearSVC(C=1.0, loss="hinge") sm_model.fit(X_train, y_train) self.coef = list(sm_model.intercept_) + list(sm_model.coef_[0]) @@ -751,7 +751,7 @@ def setUpClass(self): self.sf["dict"].apply(lambda x: [x[k] for k in sorted(x.keys())]) ) y_train = list(self.sf[self.target]) - sm_model = svm.LinearSVC(C=1.0, loss="l1") + sm_model = svm.LinearSVC(C=1.0, loss="hinge") sm_model.fit(X_train, y_train) self.coef = list(sm_model.intercept_) + list(sm_model.coef_[0]) diff --git a/src/python/turicreate/test/test_text_classifier.py b/src/python/turicreate/test/test_text_classifier.py index 454204d1d8..a810a62b54 100644 --- a/src/python/turicreate/test/test_text_classifier.py +++ b/src/python/turicreate/test/test_text_classifier.py @@ -94,21 +94,19 @@ def test_evaluate(self): self.model.evaluate(self.docs) def test_export_coreml(self): - filename = tempfile.NamedTemporaryFile(suffix=".mlmodel").name - self.model.export_coreml(filename) - import platform import coremltools + filename = tempfile.NamedTemporaryFile(suffix=".mlmodel").name + self.model.export_coreml(filename) + coreml_model = coremltools.models.MLModel(filename) - self.assertDictEqual( - { - "com.github.apple.turicreate.version": tc.__version__, - "com.github.apple.os.platform": platform.platform(), - "type": self.model.__class__.__name__, - }, - dict(coreml_model.user_defined_metadata), - ) + metadata = coreml_model.user_defined_metadata + + self.assertEqual(metadata["com.github.apple.turicreate.version"], tc.__version__) + self.assertEqual(metadata["com.github.apple.os.platform"], platform.platform()) + self.assertEqual(metadata["type"], self.model.__class__.__name__) + expected_result = ( "Text classifier created by Turi Create (version %s)" % tc.__version__ ) diff --git a/src/python/turicreate/util/lambda_closure_capture.py b/src/python/turicreate/util/lambda_closure_capture.py index eabf9b5bf8..5047a61c9b 100644 --- a/src/python/turicreate/util/lambda_closure_capture.py +++ b/src/python/turicreate/util/lambda_closure_capture.py @@ -146,7 +146,7 @@ def __str__(self): def translate_ast(self, ast_node): # print(ast.dump(ast_node)) - t = self.visit(ast_node) + self.visit(ast_node) def visit_Module(self, node): if self.state != self.FUNCTION: @@ -244,6 +244,8 @@ def visit_Lambda(self, node): return self.visit_FunctionDef(node) def visit_FunctionDef(self, node): + from sys import version_info as python_version + if self.state != self.FUNCTION: raise NotImplementedError("Unexpected function") @@ -256,7 +258,13 @@ def visit_FunctionDef(self, node): # it actually shows up in the ast as a Expr.str # so we need to catch that and skip it try: - if type(next_node) is ast.Expr and type(next_node.value) is ast.Str: + # Docstrings are constants in Python 3.8 and strings in all other + # supported Python versions. + if python_version.major == 3 and python_version.minor >= 8: + if type(next_node) is ast.Expr and type(next_node.value) is ast.Constant: + # this is *probably* a doc string! + next_node = node.body[1] + elif type(next_node) is ast.Expr and type(next_node.value) is ast.Str: # this is *probably* a doc string! next_node = node.body[1] except: diff --git a/src/unsupported_python/setup.py b/src/unsupported_python/setup.py index b0f91e9b57..57b595cbeb 100644 --- a/src/unsupported_python/setup.py +++ b/src/unsupported_python/setup.py @@ -33,7 +33,7 @@ def run(self): * 3.5 * 3.6 * 3.7 - + * 3.8 Another possible cause of this error is an outdated pip version. Try: `pip install -U pip` @@ -64,6 +64,7 @@ def run(self): "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Topic :: Scientific/Engineering", "Topic :: Scientific/Engineering :: Information Analysis",