From 12fb32f0d17922cd3abd1bfa2dd6f871e454c1fc Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 15 Mar 2022 17:04:51 -0600 Subject: [PATCH 1/8] Add dev.py. --- .gitignore | 1 + dev.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 dev.py diff --git a/.gitignore b/.gitignore index 44077e3d..342e412e 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ pyperformance.egg-info/ # Created by the pyperformance script venv/ +.venvs/ # Created by the tox program .tox/ diff --git a/dev.py b/dev.py new file mode 100644 index 00000000..7b28eb42 --- /dev/null +++ b/dev.py @@ -0,0 +1,62 @@ +# A script for running pyperformance out of the repo in dev-mode. + +import os.path +import sys + + +REPO_ROOT = os.path.dirname(os.path.abspath(__file__)) +VENV = os.path.join(REPO_ROOT, '.venvs', 'dev') + + +def main(venvroot=None): + if sys.prefix != sys.base_prefix: # already in a venv + assert os.path.exists(os.path.join(sys.prefix, 'pyvenv.cfg')) + # Make sure the venv has pyperformance installed. + ready = os.path.join(sys.prefix, 'READY') + if not os.path.exists(ready): + import subprocess + relroot = os.path.relpath(sys.prefix) + print(f'venv {relroot} not ready, installing dependencies...') + proc = subprocess.run( + [sys.executable, '-m', 'pip', 'install', + '--upgrade', + '--editable', REPO_ROOT], + ) + if proc.returncode != 0: + sys.exit('ERROR: install failed') + with open(ready, 'w'): + pass + print('...venv {relroot} ready!') + # Now run pyperformance. + import pyperformance.cli + pyperformance.cli.main() + else: + import venv + if not venvroot: + import sysconfig + if sysconfig.is_python_build(): + sys.exit('please install your built Python first (or pass it using --python)') + # XXX Handle other implementations too? + major, minor = sys.version_info[:2] + pyloc = ((os.path.abspath(sys.executable) + ).partition(os.path.sep)[2].lstrip(os.path.sep) + ).replace(os.path.sep, '-') + venvroot = f'{VENV}-{major}.{minor}-{pyloc}' + # Make sure the venv exists. + ready = os.path.join(venvroot, 'READY') + if not os.path.exists(ready): + relroot = os.path.relpath(venvroot) + if not os.path.exists(venvroot): + print(f'creating venv at {relroot}...') + else: + print(f'venv {relroot} not ready, re-creating...') + venv.create(venvroot, with_pip=True, clear=True) + # Now re-run dev.py using the venv. + binname = 'Scripts' if os.name == 'nt' else 'bin' + exename = os.path.basename(sys.executable) + python = os.path.join(venvroot, binname, exename) + os.execv(python, [python, *sys.argv]) + + +if __name__ == '__main__': + main() From 6d744f4f097dbcf0b989971852146e4512dfdcd8 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 15 Mar 2022 17:21:59 -0600 Subject: [PATCH 2/8] Always require that pyperformance be installed before running. --- pyperformance/__init__.py | 8 +++++-- pyperformance/cli.py | 49 +++++++++++++-------------------------- pyperformance/venv.py | 23 ------------------ 3 files changed, 22 insertions(+), 58 deletions(-) diff --git a/pyperformance/__init__.py b/pyperformance/__init__.py index 5e38c2c5..fd076fd6 100644 --- a/pyperformance/__init__.py +++ b/pyperformance/__init__.py @@ -11,14 +11,18 @@ def is_installed(): - parent = os.path.dirname(PKG_ROOT) - if not os.path.exists(os.path.join(parent, 'setup.py')): + if not is_dev(): return True if _is_venv(): return True return _is_devel_install() +def is_dev(): + parent = os.path.dirname(PKG_ROOT) + return os.path.exists(os.path.join(parent, 'setup.py')) + + def _is_venv(): if sys.base_prefix == sys.prefix: return False diff --git a/pyperformance/cli.py b/pyperformance/cli.py index 95f78429..fa920dff 100644 --- a/pyperformance/cli.py +++ b/pyperformance/cli.py @@ -1,11 +1,10 @@ import argparse -import contextlib import logging import os.path import sys -from pyperformance import _utils, is_installed -from pyperformance.venv import exec_in_virtualenv, cmd_venv +from pyperformance import _utils, is_installed, is_dev +from pyperformance.venv import cmd_venv def comma_separated(values): @@ -158,10 +157,6 @@ def parse_args(): "names that are inherited from the parent " "environment when running benchmarking " "subprocesses.")) - cmd.add_argument("--inside-venv", action="store_true", - help=("Option for internal usage only, don't use " - "it directly. Notice that we are already " - "inside the virtual environment.")) cmd.add_argument("-p", "--python", help="Python executable (default: use running Python)", default=sys.executable) @@ -198,21 +193,6 @@ def parse_args(): return (parser, options) -@contextlib.contextmanager -def _might_need_venv(options): - try: - if not is_installed(): - # Always force a local checkout to be installed. - assert not options.inside_venv - raise ModuleNotFoundError - yield - except ModuleNotFoundError: - if not options.inside_venv: - print('switching to a venv.', flush=True) - exec_in_virtualenv(options) - raise # re-raise - - def _manifest_from_options(options): from pyperformance import _manifest return _manifest.load_manifest(options.manifest) @@ -253,6 +233,13 @@ def _select_benchmarks(raw, manifest): def _main(): + if not is_installed(): + # Always require a local checkout to be installed. + print('ERROR: pyperformance should not be run without installing first') + if is_dev(): + print('(consider using the dev.py script)') + sys.exit(1) + parser, options = parse_args() if options.action == 'venv': @@ -280,23 +267,19 @@ def _main(): cmd_show(options) sys.exit() elif options.action == 'run': - with _might_need_venv(options): - from pyperformance.cli_run import cmd_run - benchmarks = _benchmarks_from_options(options) + from pyperformance.cli_run import cmd_run + benchmarks = _benchmarks_from_options(options) cmd_run(options, benchmarks) elif options.action == 'compare': - with _might_need_venv(options): - from pyperformance.compare import cmd_compare + from pyperformance.compare import cmd_compare cmd_compare(options) elif options.action == 'list': - with _might_need_venv(options): - from pyperformance.cli_run import cmd_list - benchmarks = _benchmarks_from_options(options) + from pyperformance.cli_run import cmd_list + benchmarks = _benchmarks_from_options(options) cmd_list(options, benchmarks) elif options.action == 'list_groups': - with _might_need_venv(options): - from pyperformance.cli_run import cmd_list_groups - manifest = _manifest_from_options(options) + from pyperformance.cli_run import cmd_list_groups + manifest = _manifest_from_options(options) cmd_list_groups(manifest) else: parser.print_help() diff --git a/pyperformance/venv.py b/pyperformance/venv.py index b5d56954..5eb895fd 100644 --- a/pyperformance/venv.py +++ b/pyperformance/venv.py @@ -527,29 +527,6 @@ def install_reqs(self, requirements=None, *, exitonerror=False): return requirements -def exec_in_virtualenv(options): - venv = VirtualEnvironment( - options.python, - options.venv, - inherit_environ=options.inherit_environ, - ) - - venv.ensure() - venv_python = venv.get_python_program() - - args = [venv_python, "-m", "pyperformance"] + \ - sys.argv[1:] + ["--inside-venv"] - # os.execv() is buggy on windows, which is why we use run_cmd/subprocess - # on windows. - # * https://bugs.python.org/issue19124 - # * https://github.com/python/benchmarks/issues/5 - if os.name == "nt": - venv.run_cmd(args, verbose=False) - sys.exit(0) - else: - os.execv(args[0], args) - - def cmd_venv(options, benchmarks=None): venv = VirtualEnvironment( options.python, From b65c1dfc666820be58075404899e59caa0bd1155 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 15 Mar 2022 18:34:08 -0600 Subject: [PATCH 3/8] Fix cli.py. --- pyperformance/cli.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/pyperformance/cli.py b/pyperformance/cli.py index fa920dff..ae45c5c1 100644 --- a/pyperformance/cli.py +++ b/pyperformance/cli.py @@ -244,8 +244,7 @@ def _main(): if options.action == 'venv': if options.venv_action in ('create', 'recreate'): - with _might_need_venv(options): - benchmarks = _benchmarks_from_options(options) + benchmarks = _benchmarks_from_options(options) else: benchmarks = None cmd_venv(options, benchmarks) From be9098e0fb5a63aa95d9378859603322e2d6323e Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 15 Mar 2022 18:34:35 -0600 Subject: [PATCH 4/8] Factor out ensure_venv_ready(). --- dev.py | 70 ++++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 44 insertions(+), 26 deletions(-) diff --git a/dev.py b/dev.py index 7b28eb42..f869a13a 100644 --- a/dev.py +++ b/dev.py @@ -5,31 +5,16 @@ REPO_ROOT = os.path.dirname(os.path.abspath(__file__)) -VENV = os.path.join(REPO_ROOT, '.venvs', 'dev') +VENVS = os.path.join(REPO_ROOT, '.venvs') -def main(venvroot=None): - if sys.prefix != sys.base_prefix: # already in a venv +def ensure_venv_ready(venvroot=None, kind='dev'): + if sys.prefix != sys.base_prefix: assert os.path.exists(os.path.join(sys.prefix, 'pyvenv.cfg')) - # Make sure the venv has pyperformance installed. - ready = os.path.join(sys.prefix, 'READY') - if not os.path.exists(ready): - import subprocess - relroot = os.path.relpath(sys.prefix) - print(f'venv {relroot} not ready, installing dependencies...') - proc = subprocess.run( - [sys.executable, '-m', 'pip', 'install', - '--upgrade', - '--editable', REPO_ROOT], - ) - if proc.returncode != 0: - sys.exit('ERROR: install failed') - with open(ready, 'w'): - pass - print('...venv {relroot} ready!') - # Now run pyperformance. - import pyperformance.cli - pyperformance.cli.main() + venvroot = sys.prefix + python = sys.executable + readyfile = os.path.join(sys.prefix, 'READY') + isready = os.path.exists(readyfile) else: import venv if not venvroot: @@ -37,25 +22,58 @@ def main(venvroot=None): if sysconfig.is_python_build(): sys.exit('please install your built Python first (or pass it using --python)') # XXX Handle other implementations too? + base = os.path.join(VENVS, kind or 'dev') major, minor = sys.version_info[:2] pyloc = ((os.path.abspath(sys.executable) ).partition(os.path.sep)[2].lstrip(os.path.sep) ).replace(os.path.sep, '-') - venvroot = f'{VENV}-{major}.{minor}-{pyloc}' + venvroot = f'{base}-{major}.{minor}-{pyloc}' # Make sure the venv exists. - ready = os.path.join(venvroot, 'READY') - if not os.path.exists(ready): + readyfile = os.path.join(venvroot, 'READY') + isready = os.path.exists(readyfile) + if not isready: relroot = os.path.relpath(venvroot) if not os.path.exists(venvroot): print(f'creating venv at {relroot}...') else: print(f'venv {relroot} not ready, re-creating...') venv.create(venvroot, with_pip=True, clear=True) - # Now re-run dev.py using the venv. + else: + assert os.path.exists(os.path.join(venvroot, 'pyvenv.cfg')) + # Return the venv's Python executable. binname = 'Scripts' if os.name == 'nt' else 'bin' exename = os.path.basename(sys.executable) python = os.path.join(venvroot, binname, exename) + + # Now make sure the venv has pyperformance installed. + if not isready: + import subprocess + relroot = os.path.relpath(venvroot) + print(f'venv {relroot} not ready, installing dependencies...') + proc = subprocess.run( + [python, '-m', 'pip', 'install', + '--upgrade', + '--editable', REPO_ROOT], + ) + if proc.returncode != 0: + sys.exit('ERROR: install failed') + with open(readyfile, 'w'): + pass + print('...venv {relroot} ready!') + + return python + + +def main(venvroot=None): + python = ensure_venv_ready(venvroot) + if python != sys.executable: + # Now re-run using the venv. os.execv(python, [python, *sys.argv]) + # + + # Now run pyperformance. + import pyperformance.cli + pyperformance.cli.main() if __name__ == '__main__': From e61f69a234192ca997d2dd14a2bc904ecb3963e0 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 15 Mar 2022 18:34:51 -0600 Subject: [PATCH 5/8] Use ensure_venv_ready() in runtests.py. --- runtests.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/runtests.py b/runtests.py index 535fd1ce..e1632002 100755 --- a/runtests.py +++ b/runtests.py @@ -3,8 +3,17 @@ import subprocess import sys +from dev import ensure_venv_ready + def main(): + python = ensure_venv_ready(kind='tests') + if python != sys.executable: + # Now re-run using the venv. + os.execv(python, [python, *sys.argv]) + # + + # Now run the tests. subprocess.run( [sys.executable, '-u', '-m', 'pyperformance.tests'], cwd=os.path.dirname(__file__) or None, From fc71e46454adeb34efaaa717f9d792ab73d39150 Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 15 Mar 2022 17:46:33 -0600 Subject: [PATCH 6/8] Do not ship test-related files. --- MANIFEST.in | 2 -- 1 file changed, 2 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index aefbfa73..9f89da35 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -5,8 +5,6 @@ include README.rst include TODO.rst include requirements.in include requirements.txt -include runtests.py -include tox.ini include doc/*.rst doc/images/*.png doc/images/*.jpg include doc/conf.py doc/Makefile doc/make.bat From 520a4e52183741fd21fcec36a6ad18a57661baba Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 15 Mar 2022 19:20:57 -0600 Subject: [PATCH 7/8] Drop the --venv option. --- pyperformance/cli.py | 14 +++++++------- pyperformance/compile.py | 26 ++++++++++++++------------ pyperformance/run.py | 13 ++++--------- pyperformance/tests/__init__.py | 5 +---- pyperformance/tests/test_show.py | 2 +- 5 files changed, 27 insertions(+), 33 deletions(-) diff --git a/pyperformance/cli.py b/pyperformance/cli.py index ae45c5c1..07168068 100644 --- a/pyperformance/cli.py +++ b/pyperformance/cli.py @@ -135,19 +135,21 @@ def parse_args(): cmds.append(cmd) # venv - cmd = subparsers.add_parser('venv', + venv_common = argparse.ArgumentParser(add_help=False) + venv_common.add_argument("--venv", help="Path to the virtual environment") + cmd = subparsers.add_parser('venv', parents=[venv_common], help='Actions on the virtual environment') cmd.set_defaults(venv_action='show') venvsubs = cmd.add_subparsers(dest="venv_action") - cmd = venvsubs.add_parser('show') + cmd = venvsubs.add_parser('show', parents=[venv_common]) cmds.append(cmd) - cmd = venvsubs.add_parser('create') + cmd = venvsubs.add_parser('create', parents=[venv_common]) filter_opts(cmd, allow_no_benchmarks=True) cmds.append(cmd) - cmd = venvsubs.add_parser('recreate') + cmd = venvsubs.add_parser('recreate', parents=[venv_common]) filter_opts(cmd, allow_no_benchmarks=True) cmds.append(cmd) - cmd = venvsubs.add_parser('remove') + cmd = venvsubs.add_parser('remove', parents=[venv_common]) cmds.append(cmd) for cmd in cmds: @@ -160,8 +162,6 @@ def parse_args(): cmd.add_argument("-p", "--python", help="Python executable (default: use running Python)", default=sys.executable) - cmd.add_argument("--venv", - help="Path to the virtual environment") options = parser.parse_args() diff --git a/pyperformance/compile.py b/pyperformance/compile.py index 537b9c96..c682db49 100644 --- a/pyperformance/compile.py +++ b/pyperformance/compile.py @@ -506,7 +506,6 @@ def compile_install(self): # First: remove everything self.safe_rmdir(self.conf.build_dir) self.safe_rmdir(self.conf.prefix) - self.safe_rmdir(self.conf.venv) self.python.patch(self.patch) self.python.compile_install() @@ -522,18 +521,22 @@ def create_venv(self): if not python or not exists: python = sys.executable cmd = [python, '-u', '-m', 'pyperformance', 'venv', 'recreate', - '--benchmarks', ''] - if self.conf.venv: - cmd.extend(('--venv', self.conf.venv)) + '--venv', self.conf.venv, + '--benchmarks', '', + ] if self.options.inherit_environ: cmd.append('--inherit-environ=%s' % ','.join(self.options.inherit_environ)) exitcode = self.run_nocheck(*cmd) if exitcode: sys.exit(EXIT_VENV_ERROR) + binname = 'Scripts' if os.name == 'nt' else 'bin' + base = os.path.basename(python) + return os.path.join(self.conf.venv, binname, base) - def run_benchmark(self): + def run_benchmark(self, python=None): self.safe_makedirs(os.path.dirname(self.filename)) - python = self.python.program + if not python: + python = self.python.program if self._dryrun: python = sys.executable cmd = [python, '-u', @@ -549,8 +552,6 @@ def run_benchmark(self): cmd.append('--benchmarks=%s' % self.conf.benchmarks) if self.conf.affinity: cmd.extend(('--affinity', self.conf.affinity)) - if self.conf.venv: - cmd.extend(('--venv', self.conf.venv)) if self.conf.debug: cmd.append('--debug-single-value') exitcode = self.run_nocheck(*cmd) @@ -709,9 +710,12 @@ def compile_bench(self): except SystemExit: sys.exit(EXIT_COMPILE_ERROR) - self.create_venv() + if self.conf.venv: + python = self.create_venv() + else: + python = None - failed = self.run_benchmark() + failed = self.run_benchmark(python) if not failed and self.conf.upload: self.upload() return failed @@ -992,8 +996,6 @@ def cmd_compile(options): conf.update = False if options.no_tune: conf.system_tune = False - if options.venv: - conf.venv = options.venv bench = BenchmarkRevision(conf, options.revision, options.branch, patch=options.patch, options=options) bench.main() diff --git a/pyperformance/run.py b/pyperformance/run.py index 8e196547..d5719e12 100644 --- a/pyperformance/run.py +++ b/pyperformance/run.py @@ -60,20 +60,15 @@ def run_benchmarks(should_run, python, options): benchmarks = {} venvs = set() - if options.venv: - venv = _venv.VirtualEnvironment( - options.python, - options.venv, - inherit_environ=options.inherit_environ, - ) - venv.ensure(refresh=False) - venvs.add(venv.get_path()) + if sys.prefix != sys.base_prefix: + venvs.add(sys.prefix) + common_venv = None # XXX Add the ability to combine venvs. for i, bench in enumerate(to_run): bench_runid = runid._replace(bench=bench) assert bench_runid.name, (bench, bench_runid) venv = _venv.VirtualEnvironment( options.python, - options.venv, + common_venv, inherit_environ=options.inherit_environ, name=bench_runid.name, usebase=True, diff --git a/pyperformance/tests/__init__.py b/pyperformance/tests/__init__.py index 21a8ffbd..85eba80c 100644 --- a/pyperformance/tests/__init__.py +++ b/pyperformance/tests/__init__.py @@ -116,10 +116,7 @@ def tearDownClass(cls): def venv_python(self): return resolve_venv_python(self._VENV) - def run_pyperformance(self, cmd, *args, invenv=True): - if invenv: - assert self._VENV - args += ('--venv', self._VENV) + def run_pyperformance(self, cmd, *args): run_cmd( sys.executable, '-u', '-m', 'pyperformance', cmd, *args, diff --git a/pyperformance/tests/test_show.py b/pyperformance/tests/test_show.py index a2c2dbe8..2c125ac2 100644 --- a/pyperformance/tests/test_show.py +++ b/pyperformance/tests/test_show.py @@ -12,7 +12,7 @@ def test_show(self): os.path.join(tests.DATA_DIR, 'mem1.json'), ): with self.subTest(filename): - self.run_pyperformance('show', filename, invenv=False) + self.run_pyperformance('show', filename) if __name__ == "__main__": From 9df3e4b5651e43e9ddcca6aa8461d3adfc33409f Mon Sep 17 00:00:00 2001 From: Eric Snow Date: Tue, 15 Mar 2022 20:13:34 -0600 Subject: [PATCH 8/8] Fix the pythoninfo tests. --- pyperformance/tests/test_pythoninfo.py | 68 ++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 9 deletions(-) diff --git a/pyperformance/tests/test_pythoninfo.py b/pyperformance/tests/test_pythoninfo.py index 38936991..1ac0f7b0 100644 --- a/pyperformance/tests/test_pythoninfo.py +++ b/pyperformance/tests/test_pythoninfo.py @@ -40,11 +40,14 @@ def test_current_python(self): def test_venv(self): self.maxDiff = 80 * 100 - venv, python = self.venv() expected = dict(INFO) - expected['executable'] = python - expected['prefix'] = venv - expected['exec_prefix'] = venv + if sys.prefix != sys.base_prefix: + python = sys.executable + else: + venv, python = self.venv() + expected['executable'] = python + expected['prefix'] = venv + expected['exec_prefix'] = venv info = _pythoninfo.get_python_info(python) @@ -80,12 +83,39 @@ def test_given_prefix(self): self.assertEqual(pyid, f'spam-{self.ID}') +def read_venv_config(venv): + filename = os.path.join(venv, 'pyvenv.cfg') + with open(filename, encoding='utf-8') as infile: + text = infile.read() + cfg = {} + for line in text.splitlines(): + name, sep, value = line.partition(' = ') + if sep: + cfg[name.strip()] = value.strip() + return cfg + + +def get_venv_base(venv): + cfg = read_venv_config(sys.prefix) + if 'executable' in cfg: + return cfg['executable'] + elif 'home' in cfg: + major, minor = cfg['version'].split('.')[:2] + base = f'python{major}.{minor}' + return os.path.join(cfg['home'], base) + else: + return None + + class InspectPythonInstallTests(tests.Resources, unittest.TestCase): BASE = getattr(sys, '_base_executable', None) def test_info(self): - info = INFO + info = dict(INFO) + if sys.prefix != sys.base_prefix: + info['prefix'] = info['base_prefix'] + info['exec_prefix'] = info['base_exec_prefix'] (base, isdev, isvenv, ) = _pythoninfo.inspect_python_install(info) @@ -96,16 +126,36 @@ def test_info(self): self.assertFalse(isvenv) def test_normal(self): + if sys.prefix != sys.base_prefix: + try: + python = sys._base_executable + except AttributeError: + python = sys.executable + if python == sys.executable: + python = get_venv_base(sys.prefix) + assert python + else: + python = sys.executable (base, isdev, isvenv, - ) = _pythoninfo.inspect_python_install() + ) = _pythoninfo.inspect_python_install(python) - self.assertEqual(base, sys.executable) + self.assertEqual(base, python) self.assertFalse(isdev) self.assertFalse(isvenv) def test_venv(self): - base_expected = sys.executable - _, python = self.venv(base_expected) + if sys.prefix != sys.base_prefix: + python = sys.executable + try: + base_expected = sys._base_executable + except AttributeError: + base_expected = sys.executable + if base_expected == sys.executable: + base_expected = get_venv_base(sys.prefix) + assert base_expected + else: + base_expected = sys.executable + _, python = self.venv(base_expected) (base, isdev, isvenv, ) = _pythoninfo.inspect_python_install(python)