diff options
author | Haibo Huang <hhb@google.com> | 2020-07-13 17:53:01 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2020-07-13 17:53:01 +0000 |
commit | cb290e283dc4f8b0b9b83963507831d11ba7b92c (patch) | |
tree | d03f477e6aca73e0795c2bfc64026a3cc5af19f4 | |
parent | a089bf3939184223f72ec5c8e02b18b5e0251ea9 (diff) | |
parent | 1c12faf75d74e933af10361bb5d264f28dad4747 (diff) | |
download | google-benchmark-cb290e283dc4f8b0b9b83963507831d11ba7b92c.tar.gz |
Upgrade google-benchmark to 37177a84b7e8d33696ea1e1854513cb0de3b4dc3 am: 1c12faf75d
Original change: https://android-review.googlesource.com/c/platform/external/google-benchmark/+/1361218
Change-Id: I93b588e5cb8f35b4a64e9083557b47c4c6b1a9f9
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | .travis.yml | 4 | ||||
-rw-r--r-- | METADATA | 6 | ||||
-rw-r--r-- | WORKSPACE | 7 | ||||
-rw-r--r-- | bindings/python/google_benchmark/BUILD (renamed from bindings/python/benchmark/BUILD) | 4 | ||||
-rw-r--r-- | bindings/python/google_benchmark/__init__.py (renamed from bindings/python/benchmark/__init__.py) | 9 | ||||
-rw-r--r-- | bindings/python/google_benchmark/benchmark.cc (renamed from bindings/python/benchmark/benchmark.cc) | 3 | ||||
-rw-r--r-- | bindings/python/google_benchmark/example.py (renamed from bindings/python/benchmark/example.py) | 20 | ||||
-rw-r--r-- | docs/releasing.md (renamed from releasing.md) | 0 | ||||
-rw-r--r-- | docs/tools.md | 6 | ||||
-rw-r--r-- | mingw.py | 320 | ||||
-rw-r--r-- | setup.py | 7 | ||||
-rw-r--r-- | src/timers.cc | 69 | ||||
-rw-r--r-- | test/reporter_output_test.cc | 2 | ||||
-rwxr-xr-x | tools/compare.py | 10 | ||||
-rw-r--r-- | tools/requirements.txt | 1 |
16 files changed, 113 insertions, 359 deletions
@@ -60,3 +60,7 @@ CMakeSettings.json # Visual Studio Code cache/options directory .vscode/ + +# Python build stuff +dist/ +*.egg-info* diff --git a/.travis.yml b/.travis.yml index f220a7d..36e343d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -211,11 +211,11 @@ install: - if [ "${TRAVIS_OS_NAME}" == "linux" ]; then sudo apt-get update -qq; sudo apt-get install -qq unzip cmake3; - wget https://github.com/bazelbuild/bazel/releases/download/0.10.1/bazel-0.10.1-installer-linux-x86_64.sh --output-document bazel-installer.sh; + wget https://github.com/bazelbuild/bazel/releases/download/3.2.0/bazel-3.2.0-installer-linux-x86_64.sh --output-document bazel-installer.sh; travis_wait sudo bash bazel-installer.sh; fi - if [ "${TRAVIS_OS_NAME}" == "osx" ]; then - curl -L -o bazel-installer.sh https://github.com/bazelbuild/bazel/releases/download/0.10.1/bazel-0.10.1-installer-darwin-x86_64.sh; + curl -L -o bazel-installer.sh https://github.com/bazelbuild/bazel/releases/download/3.2.0/bazel-3.2.0-installer-darwin-x86_64.sh; travis_wait sudo bash bazel-installer.sh; fi @@ -9,11 +9,11 @@ third_party { type: GIT value: "https://github.com/google/benchmark.git" } - version: "d3ad0b9d11c190cb58de5fb17c3555def61fdc96" + version: "37177a84b7e8d33696ea1e1854513cb0de3b4dc3" license_type: NOTICE last_upgrade_date { year: 2020 - month: 5 - day: 13 + month: 7 + day: 10 } } @@ -9,6 +9,13 @@ http_archive( ) http_archive( + name = "com_google_absl", + sha256 = "f41868f7a938605c92936230081175d1eae87f6ea2c248f41077c8f88316f111", + strip_prefix = "abseil-cpp-20200225.2", + urls = ["https://github.com/abseil/abseil-cpp/archive/20200225.2.tar.gz"], +) + +http_archive( name = "com_google_googletest", strip_prefix = "googletest-3f0cf6b62ad1eb50d8736538363d3580dd640c3e", urls = ["https://github.com/google/googletest/archive/3f0cf6b62ad1eb50d8736538363d3580dd640c3e.zip"], diff --git a/bindings/python/benchmark/BUILD b/bindings/python/google_benchmark/BUILD index 49f536e..3c1561f 100644 --- a/bindings/python/benchmark/BUILD +++ b/bindings/python/google_benchmark/BUILD @@ -1,7 +1,7 @@ load("//bindings/python:build_defs.bzl", "py_extension") py_library( - name = "benchmark", + name = "google_benchmark", srcs = ["__init__.py"], visibility = ["//visibility:public"], deps = [ @@ -32,7 +32,7 @@ py_test( srcs_version = "PY3", visibility = ["//visibility:public"], deps = [ - ":benchmark", + ":google_benchmark", ], ) diff --git a/bindings/python/benchmark/__init__.py b/bindings/python/google_benchmark/__init__.py index 27f76e0..c3a93bf 100644 --- a/bindings/python/benchmark/__init__.py +++ b/bindings/python/google_benchmark/__init__.py @@ -14,7 +14,7 @@ """Python benchmarking utilities. Example usage: - import benchmark + import google_benchmark as benchmark @benchmark.register def my_benchmark(state): @@ -28,7 +28,7 @@ Example usage: """ from absl import app -from benchmark import _benchmark +from google_benchmark import _benchmark __all__ = [ "register", @@ -60,3 +60,8 @@ def _run_benchmarks(argv): def main(argv=None): return app.run(_run_benchmarks, argv=argv, flags_parser=_flags_parser) + + +# Methods for use with custom main function. +initialize = _benchmark.Initialize +run_benchmarks = _benchmark.RunSpecifiedBenchmarks diff --git a/bindings/python/benchmark/benchmark.cc b/bindings/python/google_benchmark/benchmark.cc index ef95559..374bf54 100644 --- a/bindings/python/benchmark/benchmark.cc +++ b/bindings/python/google_benchmark/benchmark.cc @@ -42,6 +42,7 @@ PYBIND11_MODULE(_benchmark, m) { py::class_<benchmark::State>(m, "State") .def("__bool__", &benchmark::State::KeepRunning) - .def_property_readonly("keep_running", &benchmark::State::KeepRunning); + .def_property_readonly("keep_running", &benchmark::State::KeepRunning) + .def("skip_with_error", &benchmark::State::SkipWithError); }; } // namespace diff --git a/bindings/python/benchmark/example.py b/bindings/python/google_benchmark/example.py index 24da127..e968462 100644 --- a/bindings/python/benchmark/example.py +++ b/bindings/python/google_benchmark/example.py @@ -11,9 +11,16 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Example of Python using C++ benchmark framework.""" +"""Example of Python using C++ benchmark framework. -import benchmark +To run this example, you must first install the `google_benchmark` Python package. + +To install using `setup.py`, download and extract the `google_benchmark` source. +In the extracted directory, execute: + python setup.py install +""" + +import google_benchmark as benchmark @benchmark.register @@ -28,5 +35,14 @@ def sum_million(state): sum(range(1_000_000)) +@benchmark.register +def skipped(state): + if True: # Test some predicate here. + state.skip_with_error('some error') + return # NOTE: You must explicitly return, or benchmark will continue. + + ... # Benchmark code would be here. + + if __name__ == '__main__': benchmark.main() diff --git a/releasing.md b/docs/releasing.md index f0cd701..f0cd701 100644 --- a/releasing.md +++ b/docs/releasing.md diff --git a/docs/tools.md b/docs/tools.md index 4a3b2e9..f2d0c49 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -4,7 +4,11 @@ The `compare.py` can be used to compare the result of benchmarks. -**NOTE**: the utility relies on the scipy package which can be installed using [these instructions](https://www.scipy.org/install.html). +### Dependencies +The utility relies on the [scipy](https://www.scipy.org) package which can be installed using pip: +```bash +pip3 install -r requirements.txt +``` ### Displaying aggregates only diff --git a/mingw.py b/mingw.py deleted file mode 100644 index 65cf4b8..0000000 --- a/mingw.py +++ /dev/null @@ -1,320 +0,0 @@ -#! /usr/bin/env python -# encoding: utf-8 - -import argparse -import errno -import logging -import os -import platform -import re -import sys -import subprocess -import tempfile - -try: - import winreg -except ImportError: - import _winreg as winreg -try: - import urllib.request as request -except ImportError: - import urllib as request -try: - import urllib.parse as parse -except ImportError: - import urlparse as parse - -class EmptyLogger(object): - ''' - Provides an implementation that performs no logging - ''' - def debug(self, *k, **kw): - pass - def info(self, *k, **kw): - pass - def warn(self, *k, **kw): - pass - def error(self, *k, **kw): - pass - def critical(self, *k, **kw): - pass - def setLevel(self, *k, **kw): - pass - -urls = ( - 'http://downloads.sourceforge.net/project/mingw-w64/Toolchains%20' - 'targetting%20Win32/Personal%20Builds/mingw-builds/installer/' - 'repository.txt', - 'http://downloads.sourceforge.net/project/mingwbuilds/host-windows/' - 'repository.txt' -) -''' -A list of mingw-build repositories -''' - -def repository(urls = urls, log = EmptyLogger()): - ''' - Downloads and parse mingw-build repository files and parses them - ''' - log.info('getting mingw-builds repository') - versions = {} - re_sourceforge = re.compile(r'http://sourceforge.net/projects/([^/]+)/files') - re_sub = r'http://downloads.sourceforge.net/project/\1' - for url in urls: - log.debug(' - requesting: %s', url) - socket = request.urlopen(url) - repo = socket.read() - if not isinstance(repo, str): - repo = repo.decode(); - socket.close() - for entry in repo.split('\n')[:-1]: - value = entry.split('|') - version = tuple([int(n) for n in value[0].strip().split('.')]) - version = versions.setdefault(version, {}) - arch = value[1].strip() - if arch == 'x32': - arch = 'i686' - elif arch == 'x64': - arch = 'x86_64' - arch = version.setdefault(arch, {}) - threading = arch.setdefault(value[2].strip(), {}) - exceptions = threading.setdefault(value[3].strip(), {}) - revision = exceptions.setdefault(int(value[4].strip()[3:]), - re_sourceforge.sub(re_sub, value[5].strip())) - return versions - -def find_in_path(file, path=None): - ''' - Attempts to find an executable in the path - ''' - if platform.system() == 'Windows': - file += '.exe' - if path is None: - path = os.environ.get('PATH', '') - if type(path) is type(''): - path = path.split(os.pathsep) - return list(filter(os.path.exists, - map(lambda dir, file=file: os.path.join(dir, file), path))) - -def find_7zip(log = EmptyLogger()): - ''' - Attempts to find 7zip for unpacking the mingw-build archives - ''' - log.info('finding 7zip') - path = find_in_path('7z') - if not path: - key = winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, r'SOFTWARE\7-Zip') - path, _ = winreg.QueryValueEx(key, 'Path') - path = [os.path.join(path, '7z.exe')] - log.debug('found \'%s\'', path[0]) - return path[0] - -find_7zip() - -def unpack(archive, location, log = EmptyLogger()): - ''' - Unpacks a mingw-builds archive - ''' - sevenzip = find_7zip(log) - log.info('unpacking %s', os.path.basename(archive)) - cmd = [sevenzip, 'x', archive, '-o' + location, '-y'] - log.debug(' - %r', cmd) - with open(os.devnull, 'w') as devnull: - subprocess.check_call(cmd, stdout = devnull) - -def download(url, location, log = EmptyLogger()): - ''' - Downloads and unpacks a mingw-builds archive - ''' - log.info('downloading MinGW') - log.debug(' - url: %s', url) - log.debug(' - location: %s', location) - - re_content = re.compile(r'attachment;[ \t]*filename=(")?([^"]*)(")?[\r\n]*') - - stream = request.urlopen(url) - try: - content = stream.getheader('Content-Disposition') or '' - except AttributeError: - content = stream.headers.getheader('Content-Disposition') or '' - matches = re_content.match(content) - if matches: - filename = matches.group(2) - else: - parsed = parse.urlparse(stream.geturl()) - filename = os.path.basename(parsed.path) - - try: - os.makedirs(location) - except OSError as e: - if e.errno == errno.EEXIST and os.path.isdir(location): - pass - else: - raise - - archive = os.path.join(location, filename) - with open(archive, 'wb') as out: - while True: - buf = stream.read(1024) - if not buf: - break - out.write(buf) - unpack(archive, location, log = log) - os.remove(archive) - - possible = os.path.join(location, 'mingw64') - if not os.path.exists(possible): - possible = os.path.join(location, 'mingw32') - if not os.path.exists(possible): - raise ValueError('Failed to find unpacked MinGW: ' + possible) - return possible - -def root(location = None, arch = None, version = None, threading = None, - exceptions = None, revision = None, log = EmptyLogger()): - ''' - Returns the root folder of a specific version of the mingw-builds variant - of gcc. Will download the compiler if needed - ''' - - # Get the repository if we don't have all the information - if not (arch and version and threading and exceptions and revision): - versions = repository(log = log) - - # Determine some defaults - version = version or max(versions.keys()) - if not arch: - arch = platform.machine().lower() - if arch == 'x86': - arch = 'i686' - elif arch == 'amd64': - arch = 'x86_64' - if not threading: - keys = versions[version][arch].keys() - if 'posix' in keys: - threading = 'posix' - elif 'win32' in keys: - threading = 'win32' - else: - threading = keys[0] - if not exceptions: - keys = versions[version][arch][threading].keys() - if 'seh' in keys: - exceptions = 'seh' - elif 'sjlj' in keys: - exceptions = 'sjlj' - else: - exceptions = keys[0] - if revision is None: - revision = max(versions[version][arch][threading][exceptions].keys()) - if not location: - location = os.path.join(tempfile.gettempdir(), 'mingw-builds') - - # Get the download url - url = versions[version][arch][threading][exceptions][revision] - - # Tell the user whatzzup - log.info('finding MinGW %s', '.'.join(str(v) for v in version)) - log.debug(' - arch: %s', arch) - log.debug(' - threading: %s', threading) - log.debug(' - exceptions: %s', exceptions) - log.debug(' - revision: %s', revision) - log.debug(' - url: %s', url) - - # Store each specific revision differently - slug = '{version}-{arch}-{threading}-{exceptions}-rev{revision}' - slug = slug.format( - version = '.'.join(str(v) for v in version), - arch = arch, - threading = threading, - exceptions = exceptions, - revision = revision - ) - if arch == 'x86_64': - root_dir = os.path.join(location, slug, 'mingw64') - elif arch == 'i686': - root_dir = os.path.join(location, slug, 'mingw32') - else: - raise ValueError('Unknown MinGW arch: ' + arch) - - # Download if needed - if not os.path.exists(root_dir): - downloaded = download(url, os.path.join(location, slug), log = log) - if downloaded != root_dir: - raise ValueError('The location of mingw did not match\n%s\n%s' - % (downloaded, root_dir)) - - return root_dir - -def str2ver(string): - ''' - Converts a version string into a tuple - ''' - try: - version = tuple(int(v) for v in string.split('.')) - if len(version) is not 3: - raise ValueError() - except ValueError: - raise argparse.ArgumentTypeError( - 'please provide a three digit version string') - return version - -def main(): - ''' - Invoked when the script is run directly by the python interpreter - ''' - parser = argparse.ArgumentParser( - description = 'Downloads a specific version of MinGW', - formatter_class = argparse.ArgumentDefaultsHelpFormatter - ) - parser.add_argument('--location', - help = 'the location to download the compiler to', - default = os.path.join(tempfile.gettempdir(), 'mingw-builds')) - parser.add_argument('--arch', required = True, choices = ['i686', 'x86_64'], - help = 'the target MinGW architecture string') - parser.add_argument('--version', type = str2ver, - help = 'the version of GCC to download') - parser.add_argument('--threading', choices = ['posix', 'win32'], - help = 'the threading type of the compiler') - parser.add_argument('--exceptions', choices = ['sjlj', 'seh', 'dwarf'], - help = 'the method to throw exceptions') - parser.add_argument('--revision', type=int, - help = 'the revision of the MinGW release') - group = parser.add_mutually_exclusive_group() - group.add_argument('-v', '--verbose', action='store_true', - help='increase the script output verbosity') - group.add_argument('-q', '--quiet', action='store_true', - help='only print errors and warning') - args = parser.parse_args() - - # Create the logger - logger = logging.getLogger('mingw') - handler = logging.StreamHandler() - formatter = logging.Formatter('%(message)s') - handler.setFormatter(formatter) - logger.addHandler(handler) - logger.setLevel(logging.INFO) - if args.quiet: - logger.setLevel(logging.WARN) - if args.verbose: - logger.setLevel(logging.DEBUG) - - # Get MinGW - root_dir = root(location = args.location, arch = args.arch, - version = args.version, threading = args.threading, - exceptions = args.exceptions, revision = args.revision, - log = logger) - - sys.stdout.write('%s\n' % os.path.join(root_dir, 'bin')) - -if __name__ == '__main__': - try: - main() - except IOError as e: - sys.stderr.write('IO error: %s\n' % e) - sys.exit(1) - except OSError as e: - sys.stderr.write('OS error: %s\n' % e) - sys.exit(1) - except KeyboardInterrupt as e: - sys.stderr.write('Killed\n') - sys.exit(1) @@ -17,7 +17,7 @@ IS_WINDOWS = sys.platform.startswith('win') def _get_version(): """Parse the version string from __init__.py.""" - with open(os.path.join(here, 'bindings', 'python', 'benchmark', '__init__.py')) as f: + with open(os.path.join(here, 'bindings', 'python', 'google_benchmark', '__init__.py')) as f: try: version_line = next( line for line in f if line.startswith('__version__')) @@ -95,7 +95,7 @@ class BuildBazelExtension(build_ext.build_ext): setuptools.setup( - name='google-benchmark', + name='google_benchmark', version=_get_version(), url='https://github.com/google/benchmark', description='A library to benchmark code snippets.', @@ -106,7 +106,7 @@ setuptools.setup( packages=setuptools.find_packages('bindings/python'), install_requires=_parse_requirements('bindings/python/requirements.txt'), cmdclass=dict(build_ext=BuildBazelExtension), - ext_modules=[BazelExtension('benchmark._benchmark', '//bindings/python/benchmark:_benchmark')], + ext_modules=[BazelExtension('google_benchmark._benchmark', '//bindings/python/google_benchmark:_benchmark')], zip_safe=False, # PyPI package information. classifiers=[ @@ -116,6 +116,7 @@ setuptools.setup( 'License :: OSI Approved :: Apache Software License', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Software Development :: Testing', 'Topic :: System :: Benchmark', ], diff --git a/src/timers.cc b/src/timers.cc index 7613ff9..4f76edd 100644 --- a/src/timers.cc +++ b/src/timers.cc @@ -178,40 +178,67 @@ double ThreadCPUUsage() { #endif } -namespace { - -std::string DateTimeString(bool local) { +std::string LocalDateTimeString() { + // Write the local time in RFC3339 format yyyy-mm-ddTHH:MM:SS+/-HH:MM. typedef std::chrono::system_clock Clock; std::time_t now = Clock::to_time_t(Clock::now()); - const std::size_t kStorageSize = 128; - char storage[kStorageSize]; - std::size_t written; + const std::size_t kTzOffsetLen = 6; + const std::size_t kTimestampLen = 19; + + std::size_t tz_len; + std::size_t timestamp_len; + long int offset_minutes; + char tz_offset_sign = '+'; + // Long enough buffers to avoid format-overflow warnings + char tz_offset[128]; + char storage[128]; - if (local) { #if defined(BENCHMARK_OS_WINDOWS) - written = - std::strftime(storage, sizeof(storage), "%x %X", ::localtime(&now)); + std::tm *timeinfo_p = ::localtime(&now); #else - std::tm timeinfo; - ::localtime_r(&now, &timeinfo); - written = std::strftime(storage, sizeof(storage), "%F %T", &timeinfo); + std::tm timeinfo; + std::tm *timeinfo_p = &timeinfo; + ::localtime_r(&now, &timeinfo); #endif + + tz_len = std::strftime(tz_offset, sizeof(tz_offset), "%z", timeinfo_p); + + if (tz_len < kTzOffsetLen && tz_len > 1) { + // Timezone offset was written. strftime writes offset as +HHMM or -HHMM, + // RFC3339 specifies an offset as +HH:MM or -HH:MM. To convert, we parse + // the offset as an integer, then reprint it to a string. + + offset_minutes = ::strtol(tz_offset, NULL, 10); + if (offset_minutes < 0) { + offset_minutes *= -1; + tz_offset_sign = '-'; + } + + tz_len = ::snprintf(tz_offset, sizeof(tz_offset), "%c%02li:%02li", + tz_offset_sign, offset_minutes / 100, offset_minutes % 100); + CHECK(tz_len == kTzOffsetLen); + ((void)tz_len); // Prevent unused variable warning in optimized build. } else { + // Unknown offset. RFC3339 specifies that unknown local offsets should be + // written as UTC time with -00:00 timezone. #if defined(BENCHMARK_OS_WINDOWS) - written = std::strftime(storage, sizeof(storage), "%x %X", ::gmtime(&now)); + // Potential race condition if another thread calls localtime or gmtime. + timeinfo_p = ::gmtime(&now); #else - std::tm timeinfo; ::gmtime_r(&now, &timeinfo); - written = std::strftime(storage, sizeof(storage), "%F %T", &timeinfo); #endif + + strncpy(tz_offset, "-00:00", kTzOffsetLen + 1); } - CHECK(written < kStorageSize); - ((void)written); // prevent unused variable in optimized mode. - return std::string(storage); -} -} // end namespace + timestamp_len = std::strftime(storage, sizeof(storage), "%Y-%m-%dT%H:%M:%S", + timeinfo_p); + CHECK(timestamp_len == kTimestampLen); + // Prevent unused variable warning in optimized build. + ((void)kTimestampLen); -std::string LocalDateTimeString() { return DateTimeString(true); } + std::strncat(storage, tz_offset, sizeof(storage) - timestamp_len - 1); + return std::string(storage); +} } // end namespace benchmark diff --git a/test/reporter_output_test.cc b/test/reporter_output_test.cc index 1a96b5f..d806a4e 100644 --- a/test/reporter_output_test.cc +++ b/test/reporter_output_test.cc @@ -15,7 +15,7 @@ ADD_CASES(TC_ConsoleOut, {{"^[-]+$", MR_Next}, static int AddContextCases() { AddCases(TC_ConsoleErr, { - {"%int[-/]%int[-/]%int %int:%int:%int$", MR_Default}, + {"^%int-%int-%intT%int:%int:%int[-+]%int:%int$", MR_Default}, {"Running .*/reporter_output_test(\\.exe)?$", MR_Next}, {"Run on \\(%int X %float MHz CPU s?\\)", MR_Next}, }); diff --git a/tools/compare.py b/tools/compare.py index 539ace6..bd01be5 100755 --- a/tools/compare.py +++ b/tools/compare.py @@ -48,6 +48,14 @@ def create_parser(): "of repetitions. Do note that only the display is affected. " "Internally, all the actual runs are still used, e.g. for U test.") + parser.add_argument( + '--no-color', + dest='color', + default=True, + action="store_false", + help="Do not use colors in the terminal output" + ) + utest = parser.add_argument_group() utest.add_argument( '--no-utest', @@ -239,7 +247,7 @@ def main(): # Diff and output output_lines = gbench.report.generate_difference_report( json1, json2, args.display_aggregates_only, - args.utest, args.utest_alpha) + args.utest, args.utest_alpha, args.color) print(description) for ln in output_lines: print(ln) diff --git a/tools/requirements.txt b/tools/requirements.txt new file mode 100644 index 0000000..3b3331b --- /dev/null +++ b/tools/requirements.txt @@ -0,0 +1 @@ +scipy>=1.5.0
\ No newline at end of file |